Skip to content

修复适配 Blockly v12 过程中的关键兼容性问题与逻辑 Bug (变量名冲突、序列化及 UI 异常) #82

@hutufairy

Description

@hutufairy

概述

在使用 @mit-app-inventor/blockly-block-lexical-variables 插件集成到 Blockly v12 项目中时,发现了多个阻碍正常运行的 Bug 和兼容性问题。以下是详细的问题描述及已验证的修复方案。


1. 修复变量名冲突与保留字问题 (代码生成器)

问题现象:

  • 大小写不敏感: Blockly 默认的 nameDB_.getName 会强制将变量名转为小写来查重。这导致 Varvar 被视为同一个变量(其中一个会被重命名为 var2),但在 JavaScript 中它们本该是两个不同的变量。
  • 保留字安全: 局部变量声明块(local_declaration_statement)之前没有检查 JS 保留字,可能生成如 let let = 0; 这样的非法代码。

修复方案:

  1. 新增 checkVariableName 工具函数,仅对 JS 保留字进行重命名,对普通变量保留原始大小写。
  2. 重写 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;
}

应用范围与具体修改:

  1. 文件: 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')
        );
        // ...
      };
  2. 文件: 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 修复

问题现象:

  1. JSON 反序列化失败: 源码缺少 saveExtraStateloadExtraState 实现,导致在从 JSON 加载积木时,参数信息丢失或报错。
  2. 函数体丢失/顺序错乱: 在修复了序列化问题后,加载 procedures_defreturn(带返回值的函数定义)时,发现函数体(Statements/Do)直接消失了,或者 STACK 输入项跑到了 RETURN 输入项的后面。
    • 原因: procedures_defreturninit 方法未正确添加 STACK 输入,且复用了 procedures_defnoreturn.updateParams_ 方法。该基类方法只负责处理 this.bodyInputName。对于 defreturnbodyInputName'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 输入名称不匹配与模块加载失败

问题现象:

  1. 加载失败 (Missing Connection): 插件定义的 controls_for 使用了 FROM, TO, BY 作为输入名,但标准 Blockly 序列化数据和代码生成器通常期望 START, END, STEP。这导致加载时报错 missing END connection
  2. 代码生成错误: Generator 无法读取旧的字段名,导致生成的循环代码出错。
  3. 模块初始化崩溃: blocks/controls.js 引用了 blockly/core,导致无法加载标准块定义(如 controls_if 等),引起初始化崩溃。

修复方案:

  1. 修正 Import 路径。
  2. 统一修改输入名称为标准命名。

文件: 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)

问题现象:

  1. JSON 格式兼容性: local_declaration_statementloadExtraState 如果只检查 localNames(无下划线),当遇到带下划线 localNames_ 的数据时会失败。
  2. 加载崩溃: 在反序列化时,原有的 updateDeclarationInputs_ 逻辑通过 inputList.length - 1 计算输入数量。如果加载过程中输入结构不完整,它会试图移除不存在的输入(如 DECL1),导致抛出 Input not found 错误并中断加载。

修复方案:

  1. 增强 loadExtraState 的属性检查。
  2. 改用安全的遍历移除逻辑来清理旧输入。

文件: 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions