Blockly 类型化变量模态框插件 @blockly/plugin-typed-variable-modal 集成指南
【免费下载链接】blocklyThe web-based visual programming editor.项目地址: https://gitcode.com/gh_mirrors/bl/blockly
本指南围绕 Blockly 官方插件 @blockly/plugin-typed-variable-modal 展开,讲解如何为 Blockly 工作区打造一个"创建类型化变量"的模态对话框:用户点击自定义按钮后弹出对话框,输入变量名并从预设类型(如 PENGUIN、GIRAFFE)中单选一种,确认后由插件自动完成变量创建与重名冲突校验。读完本文,你将掌握该插件的安装、自定义动态 Flyout 集成、六类消息的国际化定制,以及其背后依赖 Blockly 动态变量系统的完整实现原理。
插件定位与依赖关系
Typed Variable Modal 是 Blockly 生态中的一个官方插件,核心能力是为"带类型的变量(typed variable)"提供创建入口。与 Blockly 内置的"创建变量"弹窗不同,它允许开发者预先定义一组业务类型(如动物、颜色、单位等),用户创建变量时必须从中选择一种类型,从而在编程积木层面实现类型约束。
从 package.json 可以看到它的依赖关系:
peerDependencies:blockly ^13.2.0,即要求宿主应用使用 Blockly 13.2 及以上版本;dependencies:@blockly/plugin-modal ^13.3.0,模态框的基础 UI 能力(遮罩、关闭按钮、焦点管理)由通用 Modal 插件提供;- 构建与测试基于
@blockly/dev-scripts、@blockly/dev-tools、mocha、sinon、jsdom等。
源码入口为 src/index.js,它只做了一件事——export * from './TypedVariableModal',核心类TypedVariableModal定义在 src/TypedVariableModal.js,该类直接extends Modal(来自@blockly/plugin-modal),并覆写了渲染、确认、清理等关键生命周期方法。
安装
通过 npm 安装到你的 Blockly 项目:
npm install @blockly/plugin-typed-variable-modal --save该命令会同时安装其依赖@blockly/plugin-modal,并自动校验 peer 依赖blockly版本是否满足^13.2.0。
快速集成:从工具箱到弹出模态框
插件的集成不是简单实例化一个类,而是需要配合Blockly 动态 Flyout(自定义工具箱分类)一起工作。核心思路是:在工具箱中声明一个custom分类,用回调动态填充该分类的 Flyout 内容(包含一个触发按钮和已有的变量积木),再把这个按钮回调与TypedVariableModal绑定。整个流程分为五步。
第一步:创建工作区
import * as Blockly from 'blockly'; import {TypedVariableModal} from '@blockly/plugin-typed-variable-modal'; workspace = Blockly.inject('blocklyDiv', { toolbox: toolbox, });第二步:在工具箱中添加自定义分类
在工具箱 XML(或 JSON)中声明一个带custom属性的分类,custom的值是一个自定义回调名,例如CREATE_TYPED_VARIABLE:
<category name="Colours" custom="CREATE_TYPED_VARIABLE"></category>custom分类的内容完全由注册的回调函数动态生成,这是 Blockly 官方文档所述"dynamic flyout category"的标准用法。
第三步:定义 Flyout 内容回调
回调负责组装该分类在 Flyout 中显示的元素:先推入一个按钮,再追加Blockly.VariablesDynamic.flyoutCategoryBlocks(workspace)返回的现有变量积木列表:
const createFlyout = function (workspace) { let xmlList = []; // Add your button and give it a callback name. const button = document.createElement('button'); button.setAttribute('text', 'Create Typed Variable'); button.setAttribute('callbackKey', 'callbackName'); xmlList.push(button); // This gets all the variables that the user creates and adds them to the // flyout. const blockList = Blockly.VariablesDynamic.flyoutCategoryBlocks(workspace); xmlList = xmlList.concat(blockList); return xmlList; };其中button的callbackKey就是后续要注册的按钮回调名(下文的'callbackName'),点击该按钮时工作区会调用这个回调名对应的函数。
Blockly.VariablesDynamic.flyoutCategoryBlocks定义在 packages/blockly/core/variables_dynamic.ts:它遍历workspace.getVariableMap().getAllVariables(),按名称排序后为每个变量生成variables_get_dynamic取积木,并额外生成一个带 24px 间距的variables_set_dynamic赋值积木。这意味着用户每次通过模态框创建的新变量都会自动出现在该分类的 Flyout 中,形成"创建即用"的闭环。
第四步:注册工具箱分类回调
workspace.registerToolboxCategoryCallback( 'CREATE_TYPED_VARIABLE', createFlyout, );这里把第二步中custom="CREATE_TYPED_VARIABLE"与第三步的createFlyout函数绑定。
第五步:创建并初始化 Typed Variable Modal
const typedVarModal = new TypedVariableModal(workspace, 'callbackName', [ ['PENGUIN', 'Penguin'], ['GIRAFFE', 'Giraffe'], ]); typedVarModal.init();构造函数三个参数的含义如下:
| 参数 | 类型 | 说明 |
|---|---|---|
workspace | Blockly.WorkspaceSvg | 模态框注册到的工作区 |
btnCallbackName | string | 第三步按钮callbackKey对应的回调名,init()会把它注册到工作区 |
types | Array<Array<string>> | 类型列表,每个元素是[显示名, 类型名],例如[['Penguin', 'PENGUIN']] |
init()的源码实现(见 TypedVariableModal.js)非常简洁:先调用父类super.init()完成模态框 DOM 初始化,再调用this.workspace_.registerButtonCallback(this.btnCallBackName_, () => this.show()),把按钮回调名绑定到show()。也就是说,用户点击 Flyout 里的 "Create Typed Variable" 按钮时,模态框即弹出。
核心 API 一览
README 中公开的实例 API 及其源码对应关系如下:
| 方法 | 作用 | 源码位置 |
|---|---|---|
init() | 初始化模态框并注册按钮回调 | TypedVariableModal.js |
dispose() | 销毁模态框并注销按钮回调 | TypedVariableModal.js |
show() | 显示模态框并聚焦第一个可聚焦元素(关闭按钮) | 继承自Modal |
hide() | 隐藏模态框 | 继承自Modal |
render() | 创建模态框全部 DOM 元素 | 内部由renderContent_()/renderFooter_()组成 |
setLocale(messages) | 替换模态框文案,支持多语言 | TypedVariableModal.js |
几个值得注意的实现细节:
dispose()除了调用super.dispose()释放父类资源外,还会调用workspace_.removeButtonCallback(this.btnCallBackName_)注销按钮回调,避免工作区残留悬挂引用。show()后焦点落在关闭按钮(blocklyModalBtn blocklyModalBtnClose),最后可聚焦元素是确认/取消按钮——这是Modal基类内置的焦点圈定逻辑,单测 typed_variable_modal_test.mocha.js 对此做了断言。- 构造函数将
this.shouldCloseOnOverlayClick = false置为false(源码 L101),即点击遮罩层不会关闭模态框,只能通过右上角X或Esc键关闭,防止用户误触丢失输入内容。
输入校验与重名冲突处理:源码级原理
用户点击 "Ok" 确认后,onConfirm_()(源码 L170-L200)依次执行三段逻辑:
- 名称合法性校验:
getValidInput_()先取输入框值,用newVar.replace(/[\s\xa0]+/g, ' ').trim()把连续空白(含不间断空格\xa0)折叠为单个空格并去除首尾空白;如果清洗后的名称恰好等于Blockly.Msg['RENAME_VARIABLE']或Blockly.Msg['NEW_VARIABLE'](即"重命名变量..."、"创建变量..."等系统占位文案),则判定为非法,返回null并弹出TYPED_VAR_MODAL_INVALID_NAME提示。 - 跨类型重名检查:调用
Blockly.Variables.nameUsedWithAnyType(text, workspace)做不区分大小写的全变量名搜索。该函数定义于 packages/blockly/core/variables.ts,遍历变量表把所有名称toLowerCase()后比对,返回第一个同名变量。 - 分支处理:
- 若同名变量类型相同:弹出
VARIABLE_ALREADY_EXISTS("A variable named '%1' already exists."); - 若同名变量类型不同:弹出
VARIABLE_ALREADY_EXISTS_FOR_ANOTHER_TYPE("A variable named '%1' already exists for another type: '%2'."),其中%2由getDisplayName_()根据[显示名, 类型名]配对反查得到; - 无冲突:调用
workspace.getVariableMap().createVariable(text, type)真正创建类型化变量并hide()关闭模态框。
- 若同名变量类型相同:弹出
单测 typed_variable_modal_test.mocha.js 用 sinon stub 覆盖了"空名称""合法名称""同类型已存在""不同类型已存在"四条路径,可作为理解该逻辑的参考。
类型列表types详解
types参数是Array<Array<string>>,每个子数组形如[displayName, typeName]:
displayName(索引 0)是展示给用户的类型名称,显示在单选按钮旁边的<label>中;typeName(索引 1)是写入变量模型的真实类型标识,同时用作单选按钮的id,即selectedType_的值。
源码createVariableTypeContainer_()(L314-L340)会为每个类型生成一个<li>,内含type="radio"、name="blocklyVariableType"的单选按钮及for指向该 id 的<label>;点击任一选项时,selectedType_被更新为该选项的 id。模态框每次打开时resetModalInputs_()会自动勾选第一个类型并清空变量名输入框,保证干净的输入状态。
由于单选按钮 id 直接使用类型名,建议类型名保持唯一且不含空格等特殊字符,避免 HTML id 冲突。
国际化与消息定制
插件目前不提供内置的多语言翻译,但 README 明确说明:可通过typedVarModal.setLocale(messages)传入翻译后的消息对象来实现多语言支持。setLocale()的实现是把每个 key 直接写入全局的Blockly.Msg(源码 L132-L136),因此后续渲染的按钮、标签、标题都会使用新文案;构造函数内部也有一份英文默认值并通过Object.assign(messages, optMessages)合并用户传入的第四参optMessages。
需要翻译的 6 个消息 key 及默认值如下:
| Key | 默认值(英文) | 用途 |
|---|---|---|
TYPED_VAR_MODAL_CONFIRM_BUTTON | Ok | 确认按钮文字 |
TYPED_VAR_MODAL_VARIABLE_NAME_LABEL | Variable Name: | 变量名输入框前的标签 |
TYPED_VAR_MODAL_TYPES_LABEL | Variable Types | 类型区块的标题 |
TYPED_VAR_MODAL_CANCEL_BUTTON | Cancel | 取消按钮文字 |
TYPED_VAR_MODAL_TITLE | Create Typed Variable | 模态框标题 |
TYPED_VAR_MODAL_INVALID_NAME | Name is not valid. Please choose a different name. | 非法名称提示(名称等于重命名/新建变量的系统文案、或为空字符串时触发) |
例如切换到中文:
typedVarModal.setLocale({ TYPED_VAR_MODAL_CONFIRM_BUTTON: '确定', TYPED_VAR_MODAL_CANCEL_BUTTON: '取消', TYPED_VAR_MODAL_TITLE: '创建类型化变量', TYPED_VAR_MODAL_VARIABLE_NAME_LABEL: '变量名称:', TYPED_VAR_MODAL_TYPES_LABEL: '变量类型', TYPED_VAR_MODAL_INVALID_NAME: '名称无效,请选择其他名称。', });单测 typed_variable_modal_test.mocha.js 验证了setLocale()会正确写入Blockly.Msg。
样式与 DOM 结构
插件通过Blockly.Css.register(...)在导入时注入内置样式(源码 L370-L391),因此无需额外引入 CSS 文件。关键样式类包括:
.typedModalTitle:模态框标题(加粗);.typedModalVariableInputContainer/.typedModalVariableLabel/.typedModalVariableNameInput:变量名输入区;.typedModalTypes:类型区块,使用display: flex; flex-wrap: wrap让类型单选列表横向换行排列;.typedModalList li:每个类型项,margin-right: 1em分隔。
DOM 结构由renderContent_()(变量名输入区 + 类型列表)与renderFooter_()(确认 + 取消两个按钮,分别带blocklyModalBtn blocklyModalBtnPrimary与blocklyModalBtn类)构成,单测 render 套件 断言了这些节点与按钮数量。
本地开发与测试
仓库内提供了完整的测试与演示环境:
- 测试入口:test/typed_variable_modal_test.mocha.js 使用 mocha + jsdom + sinon,覆盖
init、show焦点、setLocale、onConfirm_四分支、getDisplayName_、getValidInput_、render、按钮与容器创建等全部核心逻辑; - 演示页面:test/index.js 通过
@blockly/dev-tools的createPlayground搭建交互 playground,并把Typed Variables自定义分类注入toolboxCategories,运行npm test(内部调用blockly-scripts test,见 package.json)即可构建并打开 test/index.html 实际操作。
许可证
本插件采用 Apache 2.0 许可证(见 README 与源码文件头SPDX-License-Identifier: Apache-2.0),可自由用于商业与开源项目。
【免费下载链接】blocklyThe web-based visual programming editor.项目地址: https://gitcode.com/gh_mirrors/bl/blockly
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考