1. Scratch扩展开发简介

Scratch扩展允许开发者为Scratch添加自定义功能块,从而扩展其功能。通过JavaScript编写扩展,可以实现与外部API交互、硬件控制、复杂算法等功能。

扩展的优势

  • 增强Scratch功能,满足特定需求
  • 与外部服务集成(如天气API、翻译服务等)
  • 控制硬件设备(如micro:bit、Arduino等)
  • 实现复杂的数学或逻辑运算
  • 创建教育专用工具
  • 个性化定制学习体验
  • 连接真实世界数据

适用场景

  • 学校编程课程定制
  • 科学实验数据采集
  • 物联网项目开发
  • 游戏化学习工具
  • 艺术创作辅助
  • 数学建模与仿真
注意:本教程基于Scratch 3.0的扩展开发规范,适用于Scratch Desktop和在线版本。

2. 开发环境准备

必要工具

  • 文本编辑器(推荐VS Code)
  • 现代浏览器(Chrome、Firefox等)
  • 本地服务器(用于测试扩展)
  • Git(版本控制,可选)
  • Node.js(推荐LTS版本)
  • 代码调试工具

设置本地开发环境

  1. 安装Node.js(推荐LTS版本)
    • 访问 nodejs.org 下载安装包
    • 运行安装程序,按默认设置安装
    • 验证安装:打开终端输入 node --version
  2. 安装HTTP服务器
    • 全局安装http-server:npm install -g http-server
    • 或使用Live Server插件(VS Code推荐)
  3. 创建项目目录结构
    • 创建项目文件夹
    • 创建扩展主文件(如 extension.js)
    • 创建测试HTML文件
  4. 启动本地服务器
    • 进入项目目录
    • 运行命令:http-server
    • 访问 http://localhost:8080

浏览器开发者工具使用

  • 打开开发者工具(F12或右键检查)
  • 使用Console查看日志输出
  • 使用Network监控API请求
  • 使用Sources调试JavaScript代码
  • 使用Elements检查DOM结构
提示:使用Live Server插件可以更方便地进行实时预览,代码保存后自动刷新页面。

3. 扩展基本结构

一个标准的Scratch扩展包含以下核心组件:

扩展类结构详解

// 扩展主类 class MyExtension { constructor(runtime) { // 构造函数,接收运行时对象 this.runtime = runtime; // 国际化消息定义 this._formatMessage = runtime.getFormatMessage({ 'en': { 'myExtension.extensionName': 'My Extension', 'myExtension.blockName': 'do something with [PARAMETER]', }, 'zh-cn': { 'myExtension.extensionName': '我的扩展', 'myExtension.blockName': '用[PARAMETER]做某事', } }); // 初始化扩展状态 this.initializeState(); } // 初始化状态 initializeState() { this.counter = 0; this.isActive = false; this.dataCache = new Map(); } // 获取扩展信息 getInfo() { return { id: 'myExtension', // 扩展唯一标识 name: this._formatMessage('myExtension.extensionName'), // 扩展名称 blockIconURI: 'data:image/svg+xml;base64,...', // 扩展图标(可选) menuIconURI: 'image/svg+xml;base64,...', // 菜单图标(可选) // 积木定义 blocks: [ { opcode: 'myBlock', // 操作码,对应方法名 blockType: Scratch.BlockType.COMMAND, // 积木类型 text: this._formatMessage('myExtension.blockName'), // 积木文本 arguments: { PARAMETER: { type: Scratch.ArgumentType.STRING, // 参数类型 defaultValue: 'value' // 默认值 } } } ], // 菜单定义 menus: { languages: [ { text: '中文', value: 'chinese' }, { text: '英文', value: 'english' } ] } }; } // 积木功能实现 myBlock(args) { // 积木功能实现 console.log('执行积木:', args.PARAMETER); return '执行完成'; } // 扩展清理方法 _shutdown() { // 清理资源,如定时器、连接等 console.log('扩展关闭'); } // 扩展状态检查 _getStatus() { return { status: 2, // 2表示就绪,1表示警告,0表示错误 msg: '扩展已就绪' }; } }

关键方法说明

  • constructor(runtime): 构造函数,接收运行时对象,用于初始化扩展
  • getInfo(): 返回扩展信息和积木定义,这是扩展的核心配置
  • _shutdown(): 扩展关闭时调用,用于清理资源
  • _getStatus(): 返回扩展状态信息
  • 自定义方法: 实现具体功能的函数,名称与opcode对应

扩展注册方式

// 方式一:直接注册(适用于简单扩展) Scratch.extensions.register(new MyExtension(runtime)); // 方式二:模块化注册(适用于复杂扩展) (function(Scratch) { 'use strict'; class MyExtension { // 扩展实现... } // 注册扩展 Scratch.extensions.register(new MyExtension()); })(Scratch);

4. 积木类型详解

Scratch支持多种积木类型,每种类型有不同的用途和表现形式。

积木类型分类

类型 常量 用途 示例
命令积木 COMMAND 执行操作,无返回值 移动10步
报告积木 REPORTER 返回值,用于赋值或条件 获取鼠标X坐标
布尔积木 BOOLEAN 返回true/false 鼠标按下?
帽子积木 HAT 事件触发器 当绿旗被点击
条件积木 CONDITIONAL 条件语句 如果...那么

积木定义示例

blocks: [ // 命令积木 - 执行操作 { opcode: 'moveSteps', blockType: Scratch.BlockType.COMMAND, text: '移动 [STEPS] 步', arguments: { STEPS: { type: Scratch.ArgumentType.NUMBER, defaultValue: 10 } } }, // 报告积木 - 返回值 { opcode: 'getCurrentTime', blockType: Scratch.BlockType.REPORTER, text: '当前时间' }, // 布尔积木 - 返回布尔值 { opcode: 'isEven', blockType: Scratch.BlockType.BOOLEAN, text: '[NUMBER] 是偶数?', arguments: { NUMBER: { type: Scratch.ArgumentType.NUMBER, defaultValue: 2 } } }, // 帽子积木 - 事件触发 { opcode: 'whenKeyPressed', blockType: Scratch.BlockType.HAT, text: '当 [KEY] 键被按下', arguments: { KEY: { type: Scratch.ArgumentType.STRING, menu: 'keys', defaultValue: 'space' } } }, // 条件积木 - 条件语句 { opcode: 'ifCondition', blockType: Scratch.BlockType.CONDITIONAL, branchCount: 1, text: '如果 [CONDITION] 那么', arguments: { CONDITION: { type: Scratch.ArgumentType.BOOLEAN, defaultValue: true } } } ]

积木文本格式化

// 简单文本 text: '移动10步' // 带参数的文本 text: '移动 [STEPS] 步' // 多个参数 text: '在 [X] , [Y] 位置画点' // 国际化支持 text: this._formatMessage('myExtension.moveStepsText')

5. 参数处理与验证

正确处理和验证参数是扩展开发的重要环节。

参数类型详解

类型 常量 说明 示例值
数字 NUMBER 整数或小数 10, 3.14, -5
字符串 STRING 文本内容 "Hello", "Scratch"
布尔值 BOOLEAN true或false true, false
角度 ANGLE 0-360度 90, 180, 270
颜色 COLOR 十六进制颜色值 #FF0000, #00FF00
下拉菜单 NUMBER, STRING 配合menu属性使用 "选项1", "选项2"
广播消息 BROADCAST 广播消息选择器 "消息1", "开始"

参数验证示例

// 带参数验证的积木实现 calculatePower(args) { // 验证参数类型 const base = Number(args.BASE); const exponent = Number(args.EXPONENT); // 参数检查 if (isNaN(base) || isNaN(exponent)) { console.error('参数必须是数字'); return 0; } // 范围检查 if (Math.abs(base) > 1000 || Math.abs(exponent) > 100) { console.warn('参数值过大,可能影响性能'); } // 计算结果 return Math.pow(base, exponent); } // 字符串参数处理 processText(args) { const text = String(args.TEXT || ''); // 长度检查 if (text.length > 1000) { console.warn('文本过长,已截取前1000字符'); return text.substring(0, 1000); } return text; } // 下拉菜单参数 { opcode: 'setLanguage', blockType: Scratch.BlockType.COMMAND, text: '设置语言为 [LANGUAGE]', arguments: { LANGUAGE: { type: Scratch.ArgumentType.STRING, menu: 'languages', defaultValue: 'chinese' } } } // 菜单定义 get menus() { return { languages: [ { text: '中文', value: 'chinese' }, { text: '英文', value: 'english' }, { text: '日文', value: 'japanese' }, { text: '韩文', value: 'korean' } ], keys: [ { text: '空格键', value: 'space' }, { text: '回车键', value: 'enter' }, { text: '上箭头', value: 'up' }, { text: '下箭头', value: 'down' } ] }; }

参数默认值设置

arguments: { NUMBER: { type: Scratch.ArgumentType.NUMBER, defaultValue: 0 // 数字默认值 }, TEXT: { type: Scratch.ArgumentType.STRING, defaultValue: '默认文本' // 字符串默认值 }, ENABLED: { type: Scratch.ArgumentType.BOOLEAN, defaultValue: true // 布尔默认值 }, COLOR: { type: Scratch.ArgumentType.COLOR, defaultValue: '#ff0000' // 颜色默认值 } }

6. 外部API集成

通过集成外部API,可以让Scratch扩展具备更强大的功能。

HTTP请求处理

// 异步API调用示例 async getWeather(args) { const city = args.CITY; const apiKey = 'YOUR_API_KEY'; const url = `https://api.weather.com/v1/current?city=${encodeURIComponent(city)}&key=${apiKey}`; try { const response = await fetch(url); // 检查响应状态 if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const data = await response.json(); return `${data.temperature}°C, ${data.description}`; } catch (error) { console.error('获取天气信息失败:', error); return '无法获取天气信息'; } } // 在积木定义中使用 { opcode: 'getWeather', blockType: Scratch.BlockType.REPORTER, text: '城市 [CITY] 的天气', arguments: { CITY: { type: Scratch.ArgumentType.STRING, defaultValue: '北京' } } }

数据缓存优化

class MyExtension { constructor(runtime) { this.runtime = runtime; this.cache = new Map(); this.cacheTimeout = 5 * 60 * 1000; // 5分钟缓存 } async fetchDataWithCache(url) { const now = Date.now(); const cached = this.cache.get(url); // 检查缓存是否有效 if (cached && (now - cached.timestamp) < this.cacheTimeout) { console.log('使用缓存数据:', url); return cached.data; } try { const response = await fetch(url); const data = await response.json(); // 更新缓存 this.cache.set(url, { data, timestamp: now }); console.log('获取新数据并缓存:', url); return data; } catch (error) { console.error('数据获取失败:', error); // 如果有缓存数据,即使过期也返回 if (cached) { console.log('返回过期缓存数据:', url); return cached.data; } return null; } } // 清理过期缓存 cleanupCache() { const now = Date.now(); for (const [key, value] of this.cache.entries()) { if ((now - value.timestamp) >= this.cacheTimeout) { this.cache.delete(key); } } } }

API密钥安全处理

// 方式一:使用代理服务器(推荐) async secureApiCall(args) { const proxyUrl = 'https://your-proxy-server.com/api'; const requestData = { action: 'getWeather', city: args.CITY }; try { const response = await fetch(proxyUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(requestData) }); const data = await response.json(); return data.result; } catch (error) { console.error('API调用失败:', error); return '调用失败'; } } // 方式二:用户输入API密钥 { opcode: 'setApiKey', blockType: Scratch.BlockType.COMMAND, text: '设置API密钥为 [KEY]', arguments: { KEY: { type: Scratch.ArgumentType.STRING, defaultValue: '' } } } // 在扩展中存储API密钥 setApiKey(args) { this.apiKey = args.KEY; // 可以考虑加密存储 }
安全提醒:不要在客户端代码中暴露API密钥,建议通过代理服务器处理敏感请求。对于必须在客户端使用的API,应指导用户申请自己的API密钥。

7. 测试与调试

完善的测试流程能确保扩展的稳定性和可靠性。

本地测试方法

  1. 启动本地HTTP服务器
    • 确保扩展文件可通过HTTP访问
    • 检查文件路径是否正确
    • 验证服务器是否正常运行
  2. 在Scratch中加载扩展
    • 打开Scratch编辑器
    • 进入扩展管理界面
    • 输入扩展URL:http://localhost:8080/extension.js
    • 点击加载并测试功能
  3. 使用浏览器开发者工具
    • 打开Console查看日志输出
    • 使用Network监控API请求
    • 设置断点调试代码
  4. 测试各种边界条件
    • 测试空参数、无效参数
    • 测试网络异常情况
    • 测试并发调用
    • 测试长时间运行

调试技巧

// 添加调试日志 myBlock(args) { console.log('积木被调用:', args); try { // 执行主要逻辑 const result = this.performAction(args); console.log('执行结果:', result); return result; } catch (error) { console.error('积木执行出错:', error); // 返回默认值或错误信息 return '错误'; } } // 性能监控 async timeConsumingOperation(args) { const start = performance.now(); const result = await this.doSomething(args); const end = performance.now(); console.log(`操作耗时: ${end - start} 毫秒`); // 性能警告 if ((end - start) > 1000) { console.warn('操作耗时过长,可能影响用户体验'); } return result; } // 状态检查 checkExtensionStatus() { console.log('扩展状态检查:'); console.log('- 运行时对象:', this.runtime); console.log('- 缓存大小:', this.cache?.size || 0); console.log('- 当前计数器:', this.counter); }

单元测试框架

// 简单的测试函数 function runTests() { const extension = new MyExtension(); // 测试报告积木 const testResult = extension.addNumbers({A: 2, B: 3}); console.assert(testResult === 5, '加法测试失败'); // 测试布尔积木 const boolResult = extension.isEven({NUMBER: 4}); console.assert(boolResult === true, '偶数判断测试失败'); // 测试边界条件 const edgeResult = extension.divide({A: 10, B: 0}); console.assert(edgeResult === '除数不能为零', '除零测试失败'); console.log('所有测试完成'); } // 自动化测试示例 class ExtensionTester { constructor(extension) { this.extension = extension; this.testResults = []; } test(name, testFunction) { try { const result = testFunction(); this.testResults.push({name, passed: result, error: null}); } catch (error) { this.testResults.push({name, passed: false, error: error.message}); } } runAllTests() { this.test('加法运算', () => { return this.extension.add({A: 1, B: 2}) === 3; }); this.test('字符串处理', () => { return this.extension.toUpperCase({TEXT: 'hello'}) === 'HELLO'; }); // 输出测试结果 this.testResults.forEach(result => { if (result.passed) { console.log(`✓ ${result.name}`); } else { console.error(`✗ ${result.name}: ${result.error}`); } }); } }

8. 部署与发布

将完成的扩展部署到生产环境,供用户使用。

部署选项

  • GitHub Pages: 免费静态托管服务,适合开源项目
    • 创建GitHub仓库
    • 将扩展文件推送到仓库
    • 在仓库设置中启用GitHub Pages
    • 使用提供的URL加载扩展
  • Vercel/Netlify: 现代化部署平台,支持自动部署
    • 连接Git仓库
    • 配置自动构建和部署
    • 获得全球CDN加速
  • 自建服务器: 完全控制部署环境
    • 租用云服务器
    • 配置Web服务器(Nginx/Apache)
    • 设置SSL证书
    • 配置域名解析

GitHub Pages部署步骤

  1. 创建GitHub账户和仓库
    • 访问 github.com
    • 创建新仓库,命名为 my-scratch-extension
    • 初始化仓库(添加README)
  2. 上传扩展文件
    • 克隆仓库到本地
    • 将扩展文件放入仓库目录
    • 提交并推送更改
  3. 启用GitHub Pages
    • 进入仓库Settings
    • 找到Pages设置
    • 选择Source为main分支
    • 保存设置
  4. 获取扩展URL
    • Pages启用后会显示URL
    • 通常为 https://username.github.io/repository/extension.js
    • 在Scratch中使用此URL加载扩展

性能优化建议

  • 压缩JavaScript文件
    • 使用工具如UglifyJS或Terser
    • 移除调试代码和注释
    • 合并多个文件
  • 使用CDN加速资源加载
    • 选择可靠的CDN服务商
    • 配置缓存策略
    • 监控加载性能
  • 实现合理的缓存策略
    • 设置HTTP缓存头
    • 使用版本号管理更新
    • 避免缓存污染
  • 减少不必要的网络请求
    • 合并API调用
    • 使用数据缓存
    • 延迟加载非关键资源

9. 高级开发技巧

掌握高级技巧可以开发出更专业、更强大的扩展。

状态管理

class AdvancedExtension { constructor(runtime) { this.runtime = runtime; this.state = { counter: 0, isActive: false, userData: null, timers: new Map(), subscriptions: new Set() }; } // 状态更新方法 updateCounter(args) { this.state.counter += Number(args.VALUE); // 通知舞台更新 this.runtime.requestRedraw(); } // 获取状态 getCounter() { return this.state.counter; } // 状态持久化 saveState() { try { const stateString = JSON.stringify(this.state); localStorage.setItem('extensionState', stateString); } catch (error) { console.error('状态保存失败:', error); } } // 状态恢复 loadState() { try { const stateString = localStorage.getItem('extensionState'); if (stateString) { this.state = JSON.parse(stateString); } } catch (error) { console.error('状态恢复失败:', error); } } }

自定义积木外观

getInfo() { return { id: 'advancedExtension', name: '高级扩展', color1: '#a5d296', // 主色 color2: '#8bc34a', // 次色 color3: '#7cb342', // 边框色 blockIconURI: 'image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDAiIGhlaWdodD0iNDAiIHZpZXdCb3g9IjAgMCA0MCA0MCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHJlY3Qgd2lkdGg9IjQwIiBoZWlnaHQ9IjQwIiBmaWxsPSIjQTVEMjk2Ii8+CjxwYXRoIGQ9Ik0xNSAyMEgxNVYyMEgxNVpNMjAgMjVIMjBWMTVIMjBWMTVWMjVaTTI1IDIwSDI1VjIwSDI1WiIgc3Ryb2tlPSJ3aGl0ZSIgc3Ryb2tlLXdpZHRoPSI0Ii8+Cjwvc3ZnPgo=', // SVG图标 menuIconURI: 'image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAiIGhlaWdodD0iMjAiIHZpZXdCb3g9IjAgMCAyMCAyMCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHJlY3Qgd2lkdGg9IjIwIiBoZWlnaHQ9IjIwIiBmaWxsPSIjQTVEMjk2Ii8+CjxwYXRoIGQ9Ik03LjUgMTBINS41VjEwSDcuNVoiIHN0cm9rZT0id2hpdGUiIHN0cm9rZS13aWR0aD0iMiIvPgo8cGF0aCBkPSIxMCAxMi41SDEwVjcuNUgxMFY3LjVWMTIuNVoiIHN0cm9rZT0id2hpdGUiIHN0cm9rZS13aWR0aD0iMiIvPgo8cGF0aCBkPSIxMi41IDEwSDEyLjVWMTBIMTIuNVoiIHN0cm9rZT0id2hpdGUiIHN0cm9rZS13aWR0aD0iMiIvPgo8L3N2Zz4K', // 菜单图标 blocks: [ { opcode: 'customBlock', blockType: Scratch.BlockType.COMMAND, text: '自定义操作 [INPUT]', arguments: { INPUT: { type: Scratch.ArgumentType.STRING, defaultValue: '示例' } } } ] }; }

错误处理与用户反馈

async fetchData(args) { try { const response = await fetch(args.URL); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } return await response.text(); } catch (error) { // 向用户显示错误信息 this.runtime.startHats('extensionError', { ERROR_MESSAGE: error.message }); // 记录错误日志 console.error('数据获取失败:', error); return '获取失败'; } } // 自定义错误处理 handleError(error, context) { console.error(`[${context}] 错误:`, error); // 用户友好的错误信息 const userMessage = this.getUserFriendlyMessage(error); // 可选:显示通知 if (this.runtime && this.runtime.emit) { this.runtime.emit('EXTENSION_ERROR', { extension: 'MyExtension', message: userMessage, context: context }); } return userMessage; } getUserFriendlyMessage(error) { if (error.name === 'TypeError') { return '网络连接错误,请检查网络设置'; } if (error.name === 'SyntaxError') { return '数据格式错误,请联系开发者'; } return '操作失败,请重试'; }

异步操作管理

class AsyncExtension { constructor(runtime) { this.runtime = runtime; this.activeOperations = new Map(); } // 异步操作管理 async performAsyncOperation(args) { const operationId = this.generateId(); // 记录操作 this.activeOperations.set(operationId, { status: 'pending', startTime: Date.now() }); try { const result = await this.longRunningTask(args); // 更新操作状态 this.activeOperations.set(operationId, { status: 'completed', result: result, endTime: Date.now() }); return result; } catch (error) { this.activeOperations.set(operationId, { status: 'failed', error: error.message, endTime: Date.now() }); throw error; } } // 取消操作 cancelOperation(operationId) { const operation = this.activeOperations.get(operationId); if (operation && operation.status === 'pending') { // 取消逻辑 operation.status = 'cancelled'; this.activeOperations.delete(operationId); } } // 获取操作状态 getOperationStatus(operationId) { return this.activeOperations.get(operationId) || null; } generateId() { return Date.now().toString(36) + Math.random().toString(36).substr(2); } }

10. 实例教程

通过具体实例学习扩展开发的完整流程。

实例1:数学计算扩展

(function(ext) { ext._shutdown = function() {}; ext._getStatus = function() { return {status: 2, msg: 'Ready'}; }; ext.factorial = function(n) { if (n < 0) return '错误:负数无阶乘'; if (n === 0 || n === 1) return 1; let result = 1; for (let i = 2; i <= n; i++) { result *= i; } return result; }; ext.gcd = function(a, b) { a = Math.abs(a); b = Math.abs(b); while (b) { let t = b; b = a % b; a = t; } return a; }; ext.isPrime = function(n) { n = Math.abs(parseInt(n)); if (n < 2) return false; if (n === 2) return true; if (n % 2 === 0) return false; for (let i = 3; i <= Math.sqrt(n); i += 2) { if (n % i === 0) return false; } return true; }; var descriptor = { blocks: [ ['r', '%n 的阶乘', 'factorial', 5], ['r', '%n 和 %n 的最大公约数', 'gcd', 12, 8], ['b', '%n 是质数?', 'isPrime', 17] ] }; ScratchExtensions.register('数学扩展', descriptor, ext); })({});

实例2:天气查询扩展

class WeatherExtension { constructor(runtime) { this.runtime = runtime; this.cache = new Map(); this.cacheTimeout = 10 * 60 * 1000; // 10分钟缓存 } getInfo() { return { id: 'weatherExtension', name: '天气查询', color1: '#2196F3', color2: '#1976D2', color3: '#0D47A1', blocks: [ { opcode: 'getTemperature', blockType: Scratch.BlockType.REPORTER, text: '城市 [CITY] 的温度', arguments: { CITY: { type: Scratch.ArgumentType.STRING, defaultValue: '北京' } } }, { opcode: 'getWeatherDescription', blockType: Scratch.BlockType.REPORTER, text: '城市 [CITY] 的天气描述', arguments: { CITY: { type: Scratch.ArgumentType.STRING, defaultValue: '北京' } } } ] }; } async getTemperature(args) { const data = await this.fetchWeatherData(args.CITY); return data ? `${Math.round(data.main.temp)}°C` : '无法获取'; } async getWeatherDescription(args) { const data = await this.fetchWeatherData(args.CITY); return data ? data.weather[0].description : '无法获取'; } async fetchWeatherData(city) { const cacheKey = `weather_${city}`; const cached = this.cache.get(cacheKey); const now = Date.now(); if (cached && (now - cached.timestamp) < this.cacheTimeout) { return cached.data; } try { // 注意:实际使用时需要替换为有效的API密钥 const apiKey = 'YOUR_API_KEY'; const url = `https://api.openweathermap.org/data/2.5/weather?q=${encodeURIComponent(city)}&units=metric&appid=${apiKey}&lang=zh_cn`; const response = await fetch(url); if (!response.ok) { throw new Error(`HTTP ${response.status}`); } const data = await response.json(); this.cache.set(cacheKey, { data, timestamp: now }); return data; } catch (error) { console.error('天气数据获取失败:', error); return null; } } } // 注册扩展 (function() { const extensionClass = WeatherExtension; if (typeof window === 'undefined') { module.exports = extensionClass; } else { window.WeatherExtension = extensionClass; } })();

实例3:计时器扩展

class TimerExtension { constructor(runtime) { this.runtime = runtime; this.timers = new Map(); this.intervals = new Map(); } getInfo() { return { id: 'timerExtension', name: '计时器', color1: '#FF9800', color2: '#F57C00', color3: '#E65100', blocks: [ { opcode: 'startTimer', blockType: Scratch.BlockType.COMMAND, text: '开始计时器 [NAME]', arguments: { NAME: { type: Scratch.ArgumentType.STRING, defaultValue: 'myTimer' } } }, { opcode: 'stopTimer', blockType: Scratch.BlockType.COMMAND, text: '停止计时器 [NAME]', arguments: { NAME: { type: Scratch.ArgumentType.STRING, defaultValue: 'myTimer' } } }, { opcode: 'getTimerValue', blockType: Scratch.BlockType.REPORTER, text: '计时器 [NAME] 的值', arguments: { NAME: { type: Scratch.ArgumentType.STRING, defaultValue: 'myTimer' } } }, { opcode: 'resetTimer', blockType: Scratch.BlockType.COMMAND, text: '重置计时器 [NAME]', arguments: { NAME: { type: Scratch.ArgumentType.STRING, defaultValue: 'myTimer' } } } ] }; } startTimer(args) { const name = args.NAME; if (!this.timers.has(name)) { this.timers.set(name, { startTime: Date.now(), elapsed: 0, running: true }); } else { const timer = this.timers.get(name); if (!timer.running) { timer.startTime = Date.now() - timer.elapsed; timer.running = true; } } } stopTimer(args) { const name = args.NAME; const timer = this.timers.get(name); if (timer && timer.running) { timer.elapsed = Date.now() - timer.startTime; timer.running = false; } } getTimerValue(args) { const name = args.NAME; const timer = this.timers.get(name); if (!timer) return 0; if (timer.running) { return (Date.now() - timer.startTime) / 1000; // 返回秒数 } else { return timer.elapsed / 1000; // 返回秒数 } } resetTimer(args) { const name = args.NAME; this.timers.delete(name); } _shutdown() { // 清理所有定时器 this.timers.clear(); } }

实例4:文本处理扩展

class TextExtension { constructor(runtime) { this.runtime = runtime; } getInfo() { return { id: 'textExtension', name: '文本处理', color1: '#9C27B0', color2: '#7B1FA2', color3: '#4A148C', blocks: [ { opcode: 'toUpperCase', blockType: Scratch.BlockType.REPORTER, text: '[TEXT] 转大写', arguments: { TEXT: { type: Scratch.ArgumentType.STRING, defaultValue: 'hello world' } } }, { opcode: 'reverseString', blockType: Scratch.BlockType.REPORTER, text: '反转文本 [TEXT]', arguments: { TEXT: { type: Scratch.ArgumentType.STRING, defaultValue: 'hello' } } }, { opcode: 'countWords', blockType: Scratch.BlockType.REPORTER, text: '统计 [TEXT] 中的单词数', arguments: { TEXT: { type: Scratch.ArgumentType.STRING, defaultValue: 'hello world' } } }, { opcode: 'replaceText', blockType: Scratch.BlockType.REPORTER, text: '在 [TEXT] 中将 [OLD] 替换为 [NEW]', arguments: { TEXT: { type: Scratch.ArgumentType.STRING, defaultValue: 'hello world' }, OLD: { type: Scratch.ArgumentType.STRING, defaultValue: 'world' }, NEW: { type: Scratch.ArgumentType.STRING, defaultValue: 'Scratch' } } } ] }; } toUpperCase(args) { return String(args.TEXT || '').toUpperCase(); } reverseString(args) { return String(args.TEXT || '').split('').reverse().join(''); } countWords(args) { const text = String(args.TEXT || '').trim(); return text ? text.split(/\s+/).length : 0; } replaceText(args) { const text = String(args.TEXT || ''); const oldText = String(args.OLD || ''); const newText = String(args.NEW || ''); if (!oldText) return text; return text.split(oldText).join(newText); } }
开发建议:从简单功能开始,逐步增加复杂性。每个功能模块都要有对应的测试用例。建议先实现核心功能,再添加辅助功能。

11. 最佳实践

遵循最佳实践可以提高代码质量和用户体验。

代码组织规范

  • 模块化设计
    • 将功能分解为独立的模块
    • 每个模块职责单一
    • 提供清晰的接口
  • 命名规范
    • 使用有意义的变量和函数名
    • 遵循驼峰命名法
    • 常量使用大写字母
  • 注释和文档
    • 为复杂逻辑添加注释
    • 编写API文档
    • 提供使用示例

性能优化

  • 避免阻塞操作
    • 使用异步操作处理耗时任务
    • 避免在主线程执行复杂计算
    • 合理使用Web Workers
  • 内存管理
    • 及时清理不用的对象
    • 避免内存泄漏
    • 合理使用缓存
  • 网络优化
    • 合并HTTP请求
    • 使用缓存减少重复请求
    • 压缩传输数据

用户体验优化

  • 错误处理
    • 提供友好的错误信息
    • 优雅处理异常情况
    • 记录错误日志
  • 反馈机制
    • 及时响应用户操作
    • 提供加载状态提示
    • 显示操作结果
  • 国际化支持
    • 支持多语言
    • 适配不同地区习惯
    • 提供本地化内容

安全性考虑

  • 输入验证
    • 验证所有用户输入
    • 防止XSS攻击
    • 限制输入长度
  • API安全
    • 不要暴露敏感信息
    • 使用HTTPS协议
    • 实施访问控制
  • 权限管理
    • 最小权限原则
    • 用户授权机制
    • 隐私保护

12. 常见问题与解决方案

开发过程中可能遇到的问题及解决方法。

扩展加载问题

问题:扩展无法加载

可能原因:

  • URL地址错误
  • 服务器未启动
  • 文件路径问题
  • 浏览器安全限制

解决方案:

  1. 检查URL是否正确
  2. 确认本地服务器已启动
  3. 验证文件是否存在
  4. 使用HTTP服务器而非文件协议
  5. 检查浏览器控制台错误信息

积木功能异常

问题:积木不执行或返回错误结果

可能原因:

  • 方法名与opcode不匹配
  • 参数处理错误
  • 异步操作未正确处理
  • 语法错误

解决方案:

  1. 检查方法名是否与opcode一致
  2. 验证参数类型和默认值
  3. 确保异步方法正确使用async/await
  4. 使用开发者工具调试代码
  5. 添加console.log调试信息

API调用失败

问题:外部API无法访问

可能原因:

  • API密钥错误或过期
  • 网络连接问题
  • CORS跨域限制
  • API服务不可用

解决方案:

  1. 验证API密钥是否正确
  2. 检查网络连接状态
  3. 使用代理服务器解决跨域问题
  4. 查看API文档确认接口状态
  5. 实现错误重试机制

性能问题

问题:扩展运行缓慢

可能原因:

  • 频繁的网络请求
  • 复杂的计算操作
  • 内存泄漏
  • 未使用缓存机制

解决方案:

  1. 实现数据缓存
  2. 优化算法复杂度
  3. 及时清理不用的资源
  4. 使用防抖和节流技术
  5. 分析性能瓶颈

13. 学习资源与参考

丰富的学习资源帮助你深入掌握Scratch扩展开发。

官方文档

开源项目参考

学习教程

工具推荐

  • 开发工具
    • Visual Studio Code - 强大的代码编辑器
    • Chrome DevTools - 浏览器调试工具
    • Postman - API测试工具
  • 构建工具
    • Webpack - 模块打包工具
    • Babel - JavaScript编译器
    • ESLint - 代码质量检查工具
  • 部署平台
    • GitHub Pages - 免费静态托管
    • Vercel - 现代化部署平台
    • Netlify - 静态网站托管

社区资源