
从拖拽到可运行代码拆解 Blockly 可视化编程编辑器的完整架构【免费下载链接】blocklyThe web-based visual programming editor.项目地址: https://gitcode.com/gh_mirrors/bloc/blockly当你按住一块 if 积木拖到画布上的瞬间Blockly——一个基于 Web 的可视化编程编辑器——在几毫秒内已经完成了一连串状态流转手势识别接管指针、积木被挂到工作区Workspace树上、事件队列把整段变化压缩成一个可撤销的步骤、渲染管线重画 SVG 路径而当你点击运行生成器Generator再把积木树翻译成 JavaScript。这篇文章就顺着拖动并放下积木这一个动作把这套架构里各模块的协作关系走一遍再拆开三个最关键机制看源码。一次拖动如何贯穿事件、模型、渲染与产物先看整体链路建立一张协作地图。输入端指针落下时core/gesture.ts 里的手势Gesture类创建实例记录起点落在哪块积木上并区分点击和拖动。松手后具体拖拽逻辑由 core/dragging/block_drag_strategy.ts 执行更新积木位置和连接。数据模型端积木的逻辑本体是 core/block.ts 中的 Block 类它不碰任何 DOM。位置、父子关系、顶层积木列表由 core/workspace.ts 的 Workspace 维护连接是否合法交给 core/connection_checker.ts 检查最近的空插座由 core/connection_db.ts 的ConnectionDB.searchForClosest()在空间索引里查找。事件端拖动过程产生 BlockDrag、BlockMove 等事件core/events/ 目录但它们不立刻分发而是先进队列下一帧统一派发。渲染端模型变化通过 core/render_management.ts 的queueRender()打上脏标记当前帧内批量重画真正的画线工作由渲染器Renderer完成见下文。产物端demos/code/code.js 里演示了出口——generator.workspaceToCode(workspace)递归遍历积木树输出文本代码。五个环节各管一段互不越界这正是后面几个机制存在的原因。事件合并队列拖动一百次为什么只记一步撤销解决什么问题一次拖动在几百毫秒内能触发上百次中间状态变化。如果每个变化都立刻派发给监听器、还逐个压入撤销栈一次拖动就会污染整条操作历史监听器也会被刷屏。怎么实现core/events/utils.ts 中的fire()只做一件事——入队。第一个事件入队时才安排一次延迟派发fireNow()被调用时才批量过滤、合并、分发// core/events/utils.ts节选简化 function fireInternal(event: Abstract) { if (!FIRE_QUEUE.length) { // 队列第一个事件入队时把派发推迟到本帧渲染完成之后 requestAnimationFrame(() setTimeout(fireNow, 0)); } FIRE_QUEUE.push(event); // 同一帧内的所有变化都先进队列合并 }配合setGroup()给一组相关事件打上同一个组号。事件最终由 core/workspace.ts 的fireChangeListener()处理带recordUndo标记的事件压入undoStack_undo(redo)弹出时按组号把连续事件整体回滚所以一次拖动无论产生多少个事件撤销时是一步。代码在哪队列与合并在 core/events/utils.tsfire/fireInternal/fireNow/setGroup撤销栈在 core/workspace.tsfireChangeListener/undo/undoStack_。渲染管线脏标记加帧级批量重绘解决什么问题拖着一个由几十块积木组成的程序时相关区域每个鼠标位移都可能要重新布局重画。逐事件全量重绘会卡掉逐块独立重绘又会互相覆盖。怎么实现分两级。第一级是调度core/render_management.ts 用dirtyBlocks集合记录脏积木queueRender()用requestAnimationFrame把重绘延迟到帧边界同一帧内所有脏积木含祖先一次性重画完毕。第二级是单块绘制渲染器先由注册表创建core/renderers/common/block_rendering.ts 的register/init然后由 Drawer 把积木拆成外轮廓和内部结构两段 SVG 路径——core/renderers/common/drawer.ts 的Drawer.draw()生成outlinePath_和inlinePath_交给 core/renderers/common/path_object.ts 写进path元素。仓库内置 geras、thrasos、zelos 三套实现core/renderers/geras/ 等按名称即可切换。代码在哪调度在 core/render_management.ts绘制在 core/renderers/common/ 与 core/renderers/geras/尺寸颜色常量在各渲染器的constants.tsConstantProvider。表驱动的代码生成一块积木多门语言解决什么问题同一棵积木树要能输出 JavaScript、Python、Lua 等多种语言而每块积木的生成规则不能写死在积木定义里否则加一门语言就要改所有积木。怎么实现core/generator.ts 的 CodeGenerator 持有一张forBlock表积木类型名到生成函数的映射。生成时按顶层积木逐个递归// core/generator.ts节选简化 workspaceToCode(workspace?: Workspace): string { this.init(workspace); const blocks workspace.getTopBlocks(true); // 取顶层积木列表 for (let i 0, block; (block blocks[i]); i) { let line this.blockToCode(block); // 递归积木树查 forBlock 表 if (line) code.push(line); } // 拼接后经 finish() 补上辅助定义再清理空白返回 return this.finish(codeString); }值块提供结果的积木的生成函数返回[code, 优先级]元组生成器用语言自己的优先级表决定是否补括号——比如 generators/javascript/javascript_generator.ts 里的Order枚举。具体某块积木的规则放在对应语言目录如controls_if在 generators/javascript/logic.ts 中循环拼装 if / else if / else 字符串。代码在哪框架在 core/generator.ts各语言实现在 generators/javascript/、generators/python/ 等目录每块积木一个文件。设计取舍三个为什么这么做积木模型与视图分离。Block 是无头模型BlockSvgcore/block_svg.ts只是它在屏幕上的投影。代价是数据要双向流转、多一层间接直接让积木就是 DOM 节点会更省代码。但正是无头化让序列化、代码生成、键盘可访问性都不依赖浏览器环境就能跑——这是后面所有功能的底座。用 SVG 路径而非 Canvas 画积木。每块积木是真实 DOM 节点缩放任意倍都锐利屏幕读器和焦点导航ARIA 属性可以直接作用到积木上。代价是 DOM 节点开销大所以才有路径批量拼合、分层管理core/layer_manager.ts这些配套手段。Canvas 节点更少但可访问性和逐元素命中检测都得自己造。事件延后一帧而非即时派发。上面合并队列的代价是监听器看到的状态比实际变化晚一帧换来的是撤销粒度贴合用户觉得的一步操作。即时派发更直觉但撤销栈会被逐像素的拖动事件灌满。从读到用新增一块积木的最小路径内置积木blocks/ 目录和你自己的积木走同一条注册路径两步就够// 第一步JSON 声明积木外观注册进系统core/common.ts Blockly.defineBlocksWithJsonArray([{ type: sensor_read, // 类型名同时是生成器表的键 message0: 读取 %1 的值, args0: [{ type: field_dropdown, // 下拉字段 name: PORT, options: [[端口A, A], [端口B, B]], }], output: Number, // 可作为数值值连接 colour: 160, }]); // 第二步为目标语言注册生成函数generators/javascript/ javascript.forBlock[sensor_read] function(block) { const port block.getFieldValue(PORT); // 读下拉框当前值 return [readPort( port ), javascript.Order.ATOMIC]; };跑通这两步你的积木就能在画布里连接、撤销、序列化并被workspaceToCode()输出成目标语言——整条链路复用零额外胶水代码。Blockly 的全部设计围绕一个事实展开积木树是唯一真相渲染、事件、撤销、代码都只是它的派生视图。把这条边界守住新增语言、新增渲染风格、新增交互都退化成往对应注册表里加一项而不是重构核心。这正是它十几年间能支撑教育到工业多场景的原因。仓库地址clonehttps://gitcode.com/gh_mirrors/bloc/blockly【免费下载链接】blocklyThe web-based visual programming editor.项目地址: https://gitcode.com/gh_mirrors/bloc/blockly创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考