概述
在使用 @mit-app-inventor/blockly-block-lexical-variables 插件集成到 Blockly v12 项目中时,发现了多个阻碍正常运行的 Bug 和兼容性问题。以下是详细的问题描述及已验证的修复方案。
1. 修复变量名冲突与保留字问题 (代码生成器)
问题现象:
- 大小写不敏感: Blockly 默认的
nameDB_.getName 会强制将变量名转为小写来查重。这导致 Var 和 var 被视为同一个变量(其中一个会被重命名为 var2),但在 JavaScript 中它们本该是两个不同的变量。
- 保留字安全: 局部变量声明块(
local_declaration_statement)之前没有检查 JS 保留字,可能生成如 let let = 0; 这样的非法代码。
修复方案:
- 新增
checkVariableName 工具函数,仅对 JS 保留字进行重命名,对普通变量保留原始大小写。
- 重写
generators/procedures.js 中的生成器逻辑,应用 checkVariableName 。
文件: generators/utils.js (新增或更新)
import * as Blockly from 'blockly/core';
import * as pkg from 'blockly/javascript';
const JS_RESERVED_WORDS = new Set([
'abstract',
'arguments',
'await',
'boolean',
'break',
'byte',
'case',
'catch',
'char',
'class',
'const',
'continue',
'debugger',
'default',
'delete',
'do',
'double',
'else',
'enum',
'eval',
'export',
'extends',
'false',
'final',
'finally',
'float',
'for',
'function',
'goto',
'if',
'implements',
'import',
'in',
'instanceof',
'int',
'interface',
'let',
'long',
'native',
'new',
'null',
'package',
'private',
'protected',
'public',
'return',
'short',
'static',
'super',
'switch',
'synchronized',
'this',
'throw',
'throws',
'transient',
'true',
'try',
'typeof',
'var',
'void',
'volatile',
'while',
'with',
'yield',
'Infinity',
'NaN',
'undefined',
]);
export function checkVariableName(v) {
// 1. 检查是否是 JS 保留字 (区分大小写)
if (JS_RESERVED_WORDS.has(v)) {
// 是保留字,交给 Blockly 生成一个安全的名字 (例如 var -> var2)
const generator = pkg ? pkg.javascriptGenerator : null;
if (generator && generator.nameDB_) {
return generator.nameDB_.getName(v, Blockly.Names.NameType.VARIABLE);
}
}
// 2. 如果不是保留字,直接返回原名。
// 因为 lexical-variables 使用 let 声明,具备块级作用域,
// 不需要像 Blockly 默认行为那样进行全局去重 (全局去重会将 NAME 和 name 视为冲突)。
return v;
}
应用范围与具体修改:
-
文件: generators/lexical-variables.js
import { checkVariableName } from './utils.js';
// ...
function getVariableName(name) {
const pair = Shared.unprefixName(name);
const prefix = pair[0];
const unprefixedName = pair[1];
if (
prefix === Blockly.Msg.LANG_VARIABLES_GLOBAL_PREFIX ||
prefix === Shared.GLOBAL_KEYWORD
) {
return checkVariableName(unprefixedName);
} else {
return checkVariableName(
Shared.possiblyPrefixGeneratedVarName(prefix)(unprefixedName)
);
}
}
// ...
function generateDeclarations(block, generator) {
let code = '{\n let ';
for (let i = 0; block.getFieldValue('VAR' + i); i++) {
code += checkVariableName(
(Shared.usePrefixInCode ? 'local_' : '') +
block.getFieldValue('VAR' + i)
);
// ...
}
// ...
}
// ... simple_local_declaration_statement 也类似修改
javascriptGenerator.forBlock['simple_local_declaration_statement'] =
function (block, generator) {
let code = '{\n let ';
code += checkVariableName(
(Shared.usePrefixInCode ? 'local_' : '') + block.getFieldValue('VAR')
);
// ...
};
-
文件: generators/procedures.js
import { checkVariableName } from './utils.js';
if (pkg) {
// ...
// 添加 procedures_defreturn 和 procedures_defnoreturn 的生成器,使用 checkVariableName 处理函数名和参数名
function generateProcedureDef(block, generator) {
const funcName = checkVariableName(block.getFieldValue('NAME'));
let xvar = block.getFieldValue('VAR');
if (xvar) {
xvar = checkVariableName(xvar);
}
let branch = generator.statementToCode(block, 'STACK');
let returnValue = '';
// 安全检查:只有存在 RETURN 输入时才尝试获取代码
if (block.getInput('RETURN')) {
returnValue = generator.valueToCode(block, 'RETURN', Order.NONE) || '';
}
let xfix1 = '';
if (returnValue) {
returnValue = generator.INDENT + 'return ' + returnValue + ';\n';
if (xvar) {
xfix1 = xvar + ' = ' + funcName + ';\n';
}
} else if (!branch) {
branch = '';
}
const args = [];
const variables = block.arguments_;
for (let i = 0; i < variables.length; i++) {
args[i] = checkVariableName(variables[i]);
}
let code =
'function ' +
funcName +
'(' +
args.join(', ') +
') {\n' +
branch +
returnValue +
'}';
code = generator.scrub_(block, code);
// Add to definitions
generator.definitions_['%' + funcName] = code;
return null;
}
javascriptGenerator.forBlock['procedures_defreturn'] =
generateProcedureDef;
javascriptGenerator.forBlock['procedures_defnoreturn'] =
generateProcedureDef;
javascriptGenerator.forBlock['procedures_callnoreturn'] = function (
block,
generator
) {
// Call a procedure with no return value.
const funcName = checkVariableName(block.getFieldValue('PROCNAME'));
const args = [];
const variables = block.arguments_;
for (let i = 0; i < variables.length; i++) {
args[i] =
generator.valueToCode(block, 'ARG' + i, Order.NONE) || 'null';
}
const code = funcName + '(' + args.join(', ') + ');\n';
return code;
};
// ...
javascriptGenerator.forBlock['procedures_callreturn'] = function (
block,
generator
) {
const funcName = checkVariableName(block.getFieldValue('PROCNAME'));
// ...
};
}
2. 增强过程块 (Procedure Blocks) 的序列化支持与 UI 修复
问题现象:
- JSON 反序列化失败: 源码缺少
saveExtraState 和 loadExtraState 实现,导致在从 JSON 加载积木时,参数信息丢失或报错。
- 函数体丢失/顺序错乱: 在修复了序列化问题后,加载
procedures_defreturn(带返回值的函数定义)时,发现函数体(Statements/Do)直接消失了,或者 STACK 输入项跑到了 RETURN 输入项的后面。
- 原因:
procedures_defreturn 的 init 方法未正确添加 STACK 输入,且复用了 procedures_defnoreturn.updateParams_ 方法。该基类方法只负责处理 this.bodyInputName。对于 defreturn,bodyInputName 是 'RETURN',因此 updateParams_ 只重置了 'RETURN' 的位置,完全忽略了 'STACK'(函数体),导致它在重绘时被遗漏或位置错误。
修复方案:
文件: blocks/procedures.js
// 1. 修正 Import (文件头部)
import * as Blockly from 'blockly'; // 原为 'blockly/core'
// ---------------------------------------------------------
// 修改 A: 为 procedures_defnoreturn 添加序列化支持
// ---------------------------------------------------------
// 在 procedures_defnoreturn 定义中添加:
saveExtraState: function () {
return {
arguments_: this.arguments_,
horizontalParameters: this.horizontalParameters,
};
},
loadExtraState: function (state) {
if (typeof state === 'string') {
const xmlElement = Blockly.utils.xml.textToDom(state);
this.domToMutation(xmlElement);
} else {
let params = [];
if (state.params && Array.isArray(state.params)) {
if (typeof state.params[0] === 'object' && state.params[0].name) {
params = state.params.map((p) => p.name);
} else {
params = state.params;
}
} else if (state.arguments_) {
params = state.arguments_;
}
this.horizontalParameters = state.horizontalParameters ?? true;
this.updateParams_(params);
}
},
// ---------------------------------------------------------
// 修改 B: 修复 procedures_defreturn 的 UI 和引用序列化
// ---------------------------------------------------------
// 在 procedures_defreturn 定义中修改/添加:
init: function () {
// ... (保留原有逻辑)
this.horizontalParameters = true; // horizontal by default
// 关键修复:显式添加 STACK (Do) 输入
this.appendStatementInput('STACK').appendField(
Blockly.Msg['LANG_PROCEDURES_DEFNORETURN_DO']
);
// ...
this.warnings = [{ name: 'checkEmptySockets', sockets: ['STACK', 'RETURN'] }];
},
// UI 修复:重写 updateParams_
updateParams_: function (opt_params) {
// 调用基类方法处理参数和 Header
Blockly.Blocks.procedures_defnoreturn.updateParams_.call(this, opt_params);
// 关键修复:确保 STACK (do) 存在并位于 RETURN (result) 之前
if (this.getInput('STACK') && this.getInput('RETURN')) {
this.moveInputBefore('STACK', 'RETURN');
}
},
// 引用 defnoreturn 的序列化逻辑 (或者复制实现)
saveExtraState: Blockly.Blocks.procedures_defnoreturn.saveExtraState,
loadExtraState: Blockly.Blocks.procedures_defnoreturn.loadExtraState,
// ---------------------------------------------------------
// 修改 C: 为 procedures_callnoreturn 添加序列化支持
// ---------------------------------------------------------
// 在 procedures_callnoreturn 定义中添加:
saveExtraState: function () {
return {
arguments_: this.arguments_,
};
},
loadExtraState: function (state) {
if (typeof state === 'string') {
const xmlElement = Blockly.utils.xml.textToDom(state);
this.domToMutation(xmlElement);
} else {
let params = [];
if (state.params && Array.isArray(state.params)) {
if (typeof state.params[0] === 'object' && state.params[0].name) {
params = state.params.map((p) => p.name);
} else {
params = state.params;
}
} else if (state.arguments_) {
params = state.arguments_;
}
this.arguments_ = params;
this.setProcedureParameters(this.arguments_, null, true);
}
},
// ---------------------------------------------------------
// 修改 D: 确保 procedures_callreturn 引用序列化
// ---------------------------------------------------------
// 在 procedures_callreturn 定义中添加 (或复用实现):
saveExtraState: Blockly.Blocks.procedures_callnoreturn.saveExtraState,
loadExtraState: Blockly.Blocks.procedures_callnoreturn.loadExtraState,
3. 修复 controls_for 输入名称不匹配与模块加载失败
问题现象:
- 加载失败 (Missing Connection): 插件定义的
controls_for 使用了 FROM, TO, BY 作为输入名,但标准 Blockly 序列化数据和代码生成器通常期望 START, END, STEP。这导致加载时报错 missing END connection。
- 代码生成错误: Generator 无法读取旧的字段名,导致生成的循环代码出错。
- 模块初始化崩溃:
blocks/controls.js 引用了 blockly/core,导致无法加载标准块定义(如 controls_if 等),引起初始化崩溃。
修复方案:
- 修正 Import 路径。
- 统一修改输入名称为标准命名。
文件: blocks/controls.js
// 1. 修正 Import
import * as Blockly from 'blockly'; // 原为 'blockly/core'
// ...
// 2. 修改输入名称:FROM -> START, TO -> END, BY -> STEP
this.appendValueInput('START') // 原为 'FROM'
.setCheck(Utilities.yailTypeToBlocklyType('number', Utilities.INPUT));
// ...
this.appendValueInput('END'); // 原为 'TO'
// ...
this.appendValueInput('STEP'); // 原为 'BY'
// ...
文件: generators/controls.js
// 同步修改 valueToCode 的读取字段
const argument0 =
generator.valueToCode(block, 'START', Order.ASSIGNMENT) || '0';
const argument1 = generator.valueToCode(block, 'END', Order.ASSIGNMENT) || '0';
const increment = generator.valueToCode(block, 'STEP', Order.ASSIGNMENT) || '1';
4. 修复与增强 JSON 序列化逻辑 (blocks/lexical-variables.js)
问题现象:
- JSON 格式兼容性:
local_declaration_statement 的 loadExtraState 如果只检查 localNames(无下划线),当遇到带下划线 localNames_ 的数据时会失败。
- 加载崩溃: 在反序列化时,原有的
updateDeclarationInputs_ 逻辑通过 inputList.length - 1 计算输入数量。如果加载过程中输入结构不完整,它会试图移除不存在的输入(如 DECL1),导致抛出 Input not found 错误并中断加载。
修复方案:
- 增强
loadExtraState 的属性检查。
- 改用安全的遍历移除逻辑来清理旧输入。
文件: blocks/lexical-variables.js
// 1. loadExtraState 兼容性
const localNames = state.localNames_ || state.localNames;
if (!localNames || localNames.length === 0) return;
this.localNames_ = localNames.slice();
// 2. updateDeclarationInputs_ 安全移除
// ...
// const numDecls = this.inputList.length - 1; // 删除
const thisBlock = this;
FieldParameterFlydown.withChangeHanderDisabled(function () {
const inputsToRemove = [];
// 安全地筛选以 DECL 开头的输入
for (const input of thisBlock.inputList) {
if (input.name.startsWith('DECL')) inputsToRemove.push(input.name);
}
for (const name of inputsToRemove) {
thisBlock.removeInput(name);
}
});
5. 修复 API 废弃与兼容性 (Blockly v10+)
问题现象:
Blockly.Xml 已废弃/移动。
replaceMessageReferences 路径变更。
修复方案:
-
API 替换:
Blockly.Xml -> Blockly.utils.xml
Blockly.utils.replaceMessageReferences -> Blockly.utils.parsing.replaceMessageReferences
概述
在使用
@mit-app-inventor/blockly-block-lexical-variables插件集成到 Blockly v12 项目中时,发现了多个阻碍正常运行的 Bug 和兼容性问题。以下是详细的问题描述及已验证的修复方案。1. 修复变量名冲突与保留字问题 (代码生成器)
问题现象:
nameDB_.getName会强制将变量名转为小写来查重。这导致Var和var被视为同一个变量(其中一个会被重命名为var2),但在 JavaScript 中它们本该是两个不同的变量。local_declaration_statement)之前没有检查 JS 保留字,可能生成如let let = 0;这样的非法代码。修复方案:
checkVariableName工具函数,仅对 JS 保留字进行重命名,对普通变量保留原始大小写。generators/procedures.js中的生成器逻辑,应用checkVariableName。文件:
generators/utils.js(新增或更新)应用范围与具体修改:
文件:
generators/lexical-variables.js文件:
generators/procedures.js2. 增强过程块 (Procedure Blocks) 的序列化支持与 UI 修复
问题现象:
saveExtraState和loadExtraState实现,导致在从 JSON 加载积木时,参数信息丢失或报错。procedures_defreturn(带返回值的函数定义)时,发现函数体(Statements/Do)直接消失了,或者STACK输入项跑到了RETURN输入项的后面。procedures_defreturn的init方法未正确添加STACK输入,且复用了procedures_defnoreturn.updateParams_方法。该基类方法只负责处理this.bodyInputName。对于defreturn,bodyInputName是'RETURN',因此updateParams_只重置了'RETURN'的位置,完全忽略了'STACK'(函数体),导致它在重绘时被遗漏或位置错误。修复方案:
文件:
blocks/procedures.js3. 修复
controls_for输入名称不匹配与模块加载失败问题现象:
controls_for使用了FROM,TO,BY作为输入名,但标准 Blockly 序列化数据和代码生成器通常期望START,END,STEP。这导致加载时报错missing END connection。blocks/controls.js引用了blockly/core,导致无法加载标准块定义(如controls_if等),引起初始化崩溃。修复方案:
文件:
blocks/controls.js文件:
generators/controls.js4. 修复与增强 JSON 序列化逻辑 (blocks/lexical-variables.js)
问题现象:
local_declaration_statement的loadExtraState如果只检查localNames(无下划线),当遇到带下划线localNames_的数据时会失败。updateDeclarationInputs_逻辑通过inputList.length - 1计算输入数量。如果加载过程中输入结构不完整,它会试图移除不存在的输入(如DECL1),导致抛出Input not found错误并中断加载。修复方案:
loadExtraState的属性检查。文件:
blocks/lexical-variables.js5. 修复 API 废弃与兼容性 (Blockly v10+)
问题现象:
Blockly.Xml已废弃/移动。replaceMessageReferences路径变更。修复方案:
API 替换:
Blockly.Xml->Blockly.utils.xmlBlockly.utils.replaceMessageReferences->Blockly.utils.parsing.replaceMessageReferences