积木注册 API
本指南解释如何向 Scratch 的内置积木面板添加新积木。
概述
向 Scratch 添加新积木涉及多个组件协同工作:
- 积木定义 (scratch-blocks) - 定义视觉外观和结构
- 积木实现 (scratch-vm) - 定义运行时行为
- 编译器支持 (scratch-vm) - 支持编译模式
- 面板注册 (scratch-gui) - 使积木在工具箱中可见
- 本地化 (scratch-blocks) - 不同语言的文本字符串
步骤 1:定义积木结构 (scratch-blocks)
积木定义决定积木在编辑器中的外观和行为。需要添加到垂直和水平布局文件中。
垂直布局
文件:scratch-blocks/blocks_vertical/control.js
Blockly.Blocks['control_switch'] = {
init: function() {
this.jsonInit({
"message0": Blockly.Msg.CONTROL_SWITCH,
"message1": "%1", // 语句
"args0": [
{
"type": "input_value",
"name": "VALUE"
}
],
"args1": [
{
"type": "input_statement",
"name": "SUBSTACK"
}
],
"category": Blockly.Categories.control,
"extensions": ["colours_control", "shape_statement"]
});
}
};
水平布局
文件:scratch-blocks/blocks_horizontal/control.js
水平布局包括图标的额外样式和布局:
Blockly.Blocks['control_switch'] = {
init: function() {
this.jsonInit({
"message0": Blockly.Msg.CONTROL_SWITCH,
"message1": "%1",
"message2": "%1", // 图标
"lastDummyAlign2": "RIGHT",
"args0": [
{
"type": "input_value",
"name": "VALUE"
}
],
"args1": [
{
"type": "input_statement",
"name": "SUBSTACK"
}
],
"args2": [
{
"type": "field_image",
"src": Blockly.mainWorkspace.options.pathToMedia + "repeat.svg",
"width": 24,
"height": 24,
"alt": "*",
"flip_rtl": true
}
],
"category": Blockly.Categories.control,
"extensions": ["colours_control", "shape_statement"]
});
}
};
步骤 2:实现积木行为 (scratch-vm)
积木运行时行为在 scratch-vm 的积木包中实现。
注册积木原语
文件:scratch-vm/src/blocks/scratch3_control.js
getPrimitives() {
return {
// ... 现有积木
control_switch: this.switch,
control_case: this.case,
control_default: this.default,
control_break: this.break
};
}
实现积木方法
switch(args, util) {
// 获取 switch 值
const switchValue = args.VALUE;
// 存储在栈帧中供 case 积木访问
if (!util.stackFrame.switchValue) {
util.stackFrame.switchValue = switchValue;
util.stackFrame.switchMatched = false;
util.stackFrame.isBreakable = true;
}
// 执行包含 case 积木的子栈
util.startBranch(1, false);
}
case(args, util) {
const caseValue = args.VALUE;
const stackFrame = util.stackFrame;
// 找到父 switch 帧
const parentFrame = this.getParentSwitchFrame(util.thread);
if (!parentFrame) return;
// 检查此 case 是否匹配 switch 值
if (parentFrame.switchValue === caseValue || parentFrame.switchMatched) {
parentFrame.switchMatched = true;
util.startBranch(1, false);
}
}
break(args, util) {
// 找到最近的可中断帧并退出
const thread = util.thread;
for (let i = thread.stackFrames.length - 1; i >= 0; i--) {
const frame = thread.stackFrames[i];
if (frame.isBreakable || frame.isLoop) {
// 将栈清除回此帧
thread.stackFrames.length = i;
return;
}
}
}
步骤 3:添加编译器支持
为了积木能在编译模式下工作,需要向编译器的中间表示(IR)和 JavaScript 生成器添加支持。
IR 生成
文件:scratch-vm/src/compiler/irgen.js
case 'control_switch':
return {
kind: 'control.switch',
value: this.descendInput(block, 'VALUE'),
stack: this.descendSubstack(block, 'SUBSTACK')
};
case 'control_case':
return {
kind: 'control.case',
value: this.descendInput(block, 'VALUE'),
stack: this.descendSubstack(block, 'SUBSTACK')
};
case 'control_break':
return {
kind: 'control.break'
};
JavaScript 生成
文件:scratch-vm/src/compiler/jsgen.js
case 'control.switch': {
const switchValue = this.localVariables.next();
this.source += `var ${switchValue} = ${this.descendInput(node.value).asUnknown()};\n`;
this.source += `switch (${switchValue}) {\n`;
const switchFrame = new Frame(false);
switchFrame.isBreakable = true;
this.descendStack(node.stack, switchFrame);
this.source += `}\n`;
break;
}
case 'control.case': {
this.source += `case ${this.descendInput(node.value).asUnknown()}: {\n`;
const caseFrame = new Frame(false);
caseFrame.isBreakable = true;
this.descendStack(node.stack, caseFrame);
this.source += `}\n`;
break;
}
case 'control.break': {
let foundBreakable = false;
for (let i = this.frames.length - 1; i >= 0; i--) {
const frame = this.frames[i];
if (frame.isLoop || frame.isBreakable) {
foundBreakable = true;
break;
}
}
if (foundBreakable) {
this.source += `break;\n`;
}
break;
}
步骤 4:添加到积木面板 (scratch-gui)
要使积木在工具箱中可见,将它们添加到工具箱 XML 生成中。
文件:scratch-gui/src/lib/make-toolbox-xml.js
const control = function (isInitialSetup, isStage, targetId, colors) {
return `
<category
name="%{BKY_CATEGORY_CONTROL}"
id="control"
colour="${colors.primary}"
secondaryColour="${colors.tertiary}">
<!-- 现有积木 -->
${blockSeparator}
<block type="control_switch">
<value name="VALUE">
<shadow type="text">
<field name="TEXT">value</field>
</shadow>
</value>
</block>
<block type="control_case">
<value name="VALUE">
<shadow type="text">
<field name="TEXT">case</field>
</shadow>
</value>
</block>
<block type="control_default"/>
<block type="control_break"/>
</category>
`;
};
步骤 5:添加本地化
为新积木添加文本字符串以支持多种语言。
消息定义
文件:scratch-blocks/msg/messages.js
Blockly.Msg.CONTROL_SWITCH = 'switch %1';
Blockly.Msg.CONTROL_CASE = 'case %1';
Blockly.Msg.CONTROL_DEFAULT = 'default';
Blockly.Msg.CONTROL_BREAK = 'break';
英文本地化
文件:scratch-blocks/msg/js/en.js
Blockly.Msg["CONTROL_SWITCH"] = "switch %1";
Blockly.Msg["CONTROL_CASE"] = "case %1";
Blockly.Msg["CONTROL_DEFAULT"] = "default";
Blockly.Msg["CONTROL_BREAK"] = "break";
积木类型和属性
积木类型
BlockType.COMMAND- 执行动作的语句BlockType.REPORTER- 返回值的积木BlockType.BOOLEAN- 返回 true/false 的积木BlockType.HAT- 启动脚本的事件积木BlockType.CONDITIONAL- 带条件分支的积木
输入类型
input_value- 接受 reporter 积木input_statement- 接受命令积木(子栈)field_dropdown- 下拉菜单field_variable- 变量选择器
扩展
colours_control- 应用控制类别颜色shape_statement- 标准命令积木形状shape_hat- 帽子积木形状output_string- 字符串 reporter 形状
高级功能
栈帧管理
对于需要在 yield 之间维护状态的积木:
someBlock(args, util) {
// 首次运行时初始化
if (typeof util.stackFrame.counter === 'undefined') {
util.stackFrame.counter = 0;
}
// 使用状态
util.stackFrame.counter++;
if (util.stackFrame.counter < 10) {
util.startBranch(1, true); // 循环
}
}
Reporter 积木
对于返回值的积木:
getPrimitives() {
return {
my_reporter: this.myReporter
};
}
myReporter(args, util) {
return "some value";
}
线程管理
对于控制脚本执行的积木:
stopScript(args, util) {
util.thread.status = Thread.STATUS_DONE;
}
示例:Switch/Case 实现
Switch/case 积木演示了完整的实现:
- Switch 积木 - 将 switch 值存储在栈帧中
- Case 积木 - 将其值与 switch 值比较
- Default 积木 - 当没有 case 匹配时执行
- Break 积木 - 退出 switch 语句
这在 Scratch 的可视化环境中创建了一个熟悉的编程结构。
测试
实现新积木后:
- 在解释模式和编译模式下测试
- 验证积木出现在正确的类别中
- 测试边缘情况和错误条件
- 确保栈帧的正确清理
- 使用不同的项目类型和目标测试
最佳实践
- 遵循现有命名规范(
category_blockname) - 为积木的用途使用适当的积木形状
- 实现适当的错误处理
- 添加有意义的文档注释
- 考虑性能影响
- 使用真实项目测试,而不仅仅是孤立的案例