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

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions