可视化编程编辑器 Blockly 一文拆解:拖块如何生成五门语言代码
【免费下载链接】blocklyThe web-based visual programming editor.项目地址: https://gitcode.com/gh_mirrors/bloc/blockly
在网页里嵌一个编程界面,自己从零做,光处理积木吸附、撤销重做,就要耗掉一个团队几周。Blockly 把这套可视化编程编辑器做成了开源库,你拖一个积木,它能产出 JavaScript、Python 等五种语言的代码。本文从注入工作区讲起,拆它的数据流、渲染与代码生成,最后给一条能跑的最小集成路径。
从 inject 到可交互工作区:可视化编程编辑器的初始化
一切从Blockly.inject(container, options)开始,它把一个普通div变成能拖拽的主工作区。
入口函数在 core/inject.ts,接收一个容器和一个可配置对象,返回WorkspaceSvg。它先建一个带 class 的子div,再画 SVG 画布,最后初始化主工作区。
options里能控制rtl、grid、toolbox等,仓库自带的tests/playground.html里就有一份完整配置可抄。
这一段值得看的是分工:inject 只负责把东西摆上屏幕,积木的增删和状态全交给工作区对象。这样 UI 初始化和业务逻辑不耦合,换容器不用改逻辑。
工作区数据模型:顶层积木与撤销栈
工作区不直接存所有积木,只维护一份顶层积木列表,其余靠父子连接关系向下递归。
顶层管理就三行:addTopBlock入栈、removeTopBlock出栈、getTopBlocks取列表,都在 core/workspace.ts。真正的积木树由每个 block 的nextConnection与previousConnection串起来,递归即可遍历。
undo/redo 的底层是两条事件数组undoStack_和redoStack_,而不是存整块快照。这决定了它省内存,也决定了回滚必须靠重放。
undo 的分组回滚
下面这段是撤销的核心逻辑,来自同一文件:
core/workspace.ts的undo(redo)方法,展示撤销时如何按group把一批事件当成整体回滚。
const inputEvent = inputStack.pop(); if (!inputEvent) { return; } let events = [inputEvent]; // Do another undo/redo if the next one is of the same group. while ( inputStack.length && inputEvent.group && inputEvent.group === inputStack[inputStack.length - 1].group ) { const event = inputStack.pop();这段在做什么:弹出顶部事件后,顺着同group的事件一路弹空,攒成events数组。为什么值得看:一次粘贴或多块拖拽会拆成多条事件,靠 group 才能一键整体撤销,这是操作原子性的来源。
事件批处理:fire 队列为何延迟到下一帧
事件不是同步派发的,而是先入队,等浏览器画完当前帧再统一发出去。
核心在 core/events/utils.ts 的fireInternal,入队前会判断队列是否为空:
core/events/utils.ts的fireInternal,展示事件如何进FIRE_QUEUE并延迟触发。
if (!FIRE_QUEUE.length) { // First event added; schedule a firing of the event queue. try { requestAnimationFrame(() => { setTimeout(fireNow, 0); }); } catch (e) { setTimeout(fireNow, 0); } } FIRE_QUEUE.push(event);这段在做什么:只有第一条事件会调requestAnimationFrame预约一次fireNow,后续事件只往队列里堆。为什么值得看:同一帧内拖多个块会产生大量事件,延迟到渲染后再合并派发,既省掉重复渲染,又保证监听器拿到的坐标是最新的。撤销、协作同步也因此能复用同一条事件流。
三套渲染器:geras、thrasos 与 zelos 的注册
渲染被抽成可替换的策略,仓库里就有三套:geras、thrasos、zelos,注册点统一在core/renderers/下。
三套各用一个Renderer子类实现测量与画布,最后都调用blockRendering.register(name, Renderer)挂进注册表。你在inject的options里写renderer: 'geras'就能切换,业务代码一行不用改。
默认geras是为兼容旧版做的,源码注释也点明它 "backwards compatible with pre-2019"。想换视觉风格,改一个字符串即可,数据模型不动。
代码生成器 workspaceToCode:把积木树变成五种语言
图形到代码靠递归:workspaceToCode从顶层积木出发,逐块查对应类型的生成函数并拼字符串。
生成器实例与函数表在generators/javascript.ts,每种语言一个目录。真正干活的函数按类型分发,例如 if 积木:
generators/javascript/logic.ts的controls_if,展示一个积木如何递归拼出带 else if 的分支代码。
export function controls_if(block: Block, generator: JavascriptGenerator) { let n = 0; let code = ''; do { const conditionCode = generator.valueToCode(block, 'IF' + n, Order.NONE) || 'false'; let branchCode = generator.statementToCode(block, 'DO' + n); code += (n > 0 ? ' else ' : '') + 'if (' + conditionCode + ') {\n' + branchCode + '}'; n++; } while (block.getInput('IF' + n));这段在做什么:用do...while顺次吃IF0、IF1直到没有输入,每层把条件码与分支码拼成if/else if。为什么值得看:valueToCode和statementToCode是递归钩子,子积木就这样一层层展开成子串,这正是拖块生成代码的全部秘密。
一段可运行的最小集成:搭一个可视化编程界面
看完实现,最短的验证路径是跑仓库自带 playground,不用自己拼环境。
先克隆仓库,执行npm install再执行npm start:它会在本地起服务,并直接打开 tests/playground.html。你在里面拖积木、点代码生成按钮,就能实时看到五门语言输出。
想接进自己的项目,核心就三步:import的Blockly与某个生成器,调用inject建工作区,最后用generator.workspaceToCode(workspace)取代码。
设计取舍:它牺牲了什么,适合谁
它把通用当第一优先级,代价是包体和 API 面都不小,按需拆分是它exports分./core、./blocks、各语言子路径的原因。
事件走异步队列、撤销靠重放而非快照,让大场景更省,但实时性要求极高的场景要自己补时序。三套渲染并存,也说明它在兼容与现代表现间做了取舍。
适合谁:要给 web 或移动应用内嵌可视化编程界面、又要能产出真实代码的团队。不适合谁:想要完全自绘图形、或只做纯文本代码编辑的场景,用它偏重了。
上手建议:克隆后直接npm start打开 playground,从core/inject.ts与generators/javascript/两个文件顺读一遍,比看文档更快建立直觉。
仓库地址:https://gitcode.com/gh_mirrors/bloc/blockly
【免费下载链接】blocklyThe web-based visual programming editor.项目地址: https://gitcode.com/gh_mirrors/bloc/blockly
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考