-
Notifications
You must be signed in to change notification settings - Fork 18
Open
Description
概述
在使用 @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.jsimport { 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.jsimport { 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.xmlBlockly.utils.replaceMessageReferences->Blockly.utils.parsing.replaceMessageReferences
Metadata
Metadata
Assignees
Labels
No labels