☰
Blockly入门:从可视化编程到自定义积木开发
2026/10/4 15:30:47 网站建设 项目流程

1. 这玩意到底是什么,为什么各个大厂都在用它

先给结论:Blockly 不是一款面向消费者的软件,而是一套开源的、用于构建“可视化编程界面”的前端工具库。也就是说,它是给“程序员”用的“积木编辑器生成器”。你用浏览器打开乐高官网搭积木、用 Scratch 做小游戏、在 App Inventor 里拖拖拽拽做安卓应用、甚至在某些机器人教育硬件配套软件里看到的图形化编程界面——它们的底层,很可能就是 Blockly 或它的衍生版本。

我第一次接触 Blockly 是在给一家教育公司做教学平台的时候。当时的需求很明确:孩子们不懂 JavaScript,但我们又希望他们能通过“拖动积木”来编写控制小车的逻辑,然后实时看到生成的代码。调研了一圈,发现 GitHub App Inventor 系列、micro:bit 官方编辑器、以及国内外大量少儿编程平台,核心骨架都指向 Google 开源的 Blockly。那一刻我就知道,与其自己从零去画拖拽交互、做积木拼接算法,不如把这个经过十余年验证的“轮子”直接用起来。

它能解决什么问题?简单说:通过图形化积木,让非专业人员也能构建逻辑,同时自动生成等价的专业代码。这句话拆分出来,解决的是两个痛点:

  1. 降低编程门槛:用户不用记语法,不用管分号括号,只需要关注“逻辑单元”的拼接。
  2. 保留代码能力:积木背后可以挂载 JavaScript、Python、PHP、Lua 等目标语言代码,拖出来的逻辑等于写出来的代码,方便迁移和学习。

适合谁读这篇教程?如果你是面向教育领域的前端工程师、STEAM 教育的创业者、企业内部低代码平台的开发者,或者纯粹对“拖拽式编程如何实现”感到好奇的同学,这篇“初识”就是为你准备的。我会尽量从“这东西是怎么设计出来的”角度讲,而不是上来就丢一堆 API。

2. 理解 Blockly 的灵魂:五个核心概念

2.1 工作区(Workspace):舞台不是凭空冒出来的

Blockly 的界面核心是Workspace。你可以把它理解成一张无限大的画布,积木就在这块画布上被拖进来、排列、嵌套、组合。在代码里,它对应于Blockly.Workspace类,而且区分一个隐藏概念:WorkspaceSvg(SVG 渲染的可见工作区)和Workspace(数据模型工作区)。刚开始学不用纠结这个区别,但你要知道:界面上的积木状态本质上是一份 JSON 结构数据,工作区做得再多,最终要保存或传输的就是这份 JSON。

实际项目里,我们一般通过Blockly.inject()方法把工作区“注入”到页面的某个div中:

const workspace = Blockly.inject('blocklyDiv', { toolbox: document.getElementById('toolbox'), scrollbars: true, trashcan: true, grid: { spacing: 20, length: 3, colour: '#ccc', snap: true } });

inject这个命名很有意思,说明 Blockly 不是把你整个网页变成编程环境,而是“嵌入”到你的页面里,成为一个组件。用过iframe嵌第三方编辑器的人会懂这个设计有多友好——它不会污染页面的全局 CSS、不会和你的路由体系冲突,你只管传递数据进去。

2.2 工具箱(Toolbox):积木从哪里来

工具箱就是工作区左侧那一列积木栏。积木按类别分组,比如“逻辑”“循环”“数学”“变量”“函数”等。工具箱的定义格式有两种:

  • XML 字符串格式(更常见,也更容易通过代码生成)
  • JSON 格式(较新版本支持,更结构化)

下面是一段 XML 格式的工具箱定义:

<xml id="toolbox" style="display: none"> <category name="逻辑" colour="#5C81A6"> <block type="controls_if"></block> <block type="logic_compare"></block> </category> <category name="数学" colour="#5CA65C"> <block type="math_number"></block> <block type="math_arithmetic"></block> </category> </xml>

注意两个细节:style="display: none"是必须的,因为工具箱本身不直接显示在页面上,而是被 Blockly 渲染成工作区左侧的面板;colour属性决定分类标题的颜色,方便用户快速区分。我记得第一次写的时候忘了加display: none,结果页面顶部凭空多出一块 XML 内容,排查了半天。

一个容易忽略的设置是categorystyle(分类样式)。在复杂项目中,我们会预定义若干种分类样式名,然后在多个工具箱里复用,这样能保证整个产品视觉统一,不会出现每个页面颜色都随心所欲的情况。

2.3 积木块(Block):拼接的是“形状”与“插口”

Blockly 里的积木块并不仅仅是一张漂亮的图。每个积木块有:

  • 类型(type):唯一标识,比如logic_compare。
  • 输入插口(inputs):可以连接其他积木的位置,分为“值输入”(value input)和“语句输入”(statement input)。
  • 字段(fields):可编辑的文本、下拉框、颜色选择器等。
  • 连接器(connections):上一个/下一个连接点,负责语句块的纵向拼接。

如果你只是想用现成积木,那不需要写任何块定义。但如果你要定制积木(几乎定制是必然的,否则和 Scatch 有什么区别),就绕不开“块定义”这一步。

用 JSON 格式定义一个最简单的“打印文本”积木:

{ "type": "custom_print", "message0": "打印 %1", "args0": [ { "type": "input_value", "name": "TEXT" } ], "previousStatement": null, "nextStatement": null, "colour": 160, "tooltip": "在控制台输出文本", "helpUrl": "" }

这段定义描述的是一个语句块,它有一个名为TEXT的值输入插口。previousStatement: null和nextStatement: null表示它可以在语句序列中被连接;colour: 160是色相值,Blockly 内部用 0-360 的角度值来标记颜色,而不是直接用十六进制色码。这一设计很巧妙,因为你在工具箱里可以直接用同一个色相统一整类积木的视觉风格。

2.4 代码生成器(Code Generator):拖拽积木是怎么变成代码的

这是 Blockly 最让我“哇”一声的设计。

Blockly 生成代码并不依赖运行时解释,而是基于字符串模板的拼接。以 JavaScript 生成器为例,每个积木类型都要注册一个生成函数,这个函数的职责是:读取当前积木的字段值、递归获取它的“子积木”生成的代码片段,然后把它们拼成一段目标代码字符串。

举个例子,上面定义的custom_print积木,对应生成器可以写成:

Blockly.JavaScript['custom_print'] = function(block) { const text = Blockly.JavaScript.valueToCode(block, 'TEXT', Blockly.JavaScript.ORDER_ATOMIC); const code = 'console.log(' + text + ');\n'; return code; };

valueToCode是这里的灵魂函数。它负责去取连接到TEXT插口上的那个积木所生成的代码,并指定运算优先级(ORDER_ATOMIC)。如果你接的是一个算术表达式块,它生成的字符串就能被正确地插入到console.log(...)的括号里,不会因为优先级问题出现括号错乱。

再强调一下:代码生成是“递归解析”,每一层积木只负责把自己那部分代码拼好,然后交还给父级。理解了这个模型,你之后写任何自定义块的生成器都会很顺手,因为你只需要思考“我这块的输入是什么、我要用什么语句包裹它们”。

2.5 渲染器(Renderer):积木不是好看的贴图

Blockly 对积木外观的实现方式很几何化:每个积木块是基于预先定义的路径模板绘制出来的 SVG 形状,不同积木的区别在于“插口数量”“连接位置”和“分支结构”的组合。

举个例子,一个controls_if积木带有一个“如果”插口和一个“如果为真则执行”的语句插口,呈现出的就是一个带“C”形槽的块。这个 C 形槽不是图片素材,它是通过矢量路径实时算出来的。所以即便你的积木宽度动态变化(比如字段文本变长),整个块的锯齿形底部、凸起的连接点也会跟着自动重绘,不会有拉伸失真问题。

这个机制带来的最大好处:你可以通过自定义渲染器彻底改变积木的视觉风格——圆角矩形、无边框、扁平化配色、更紧凑的间距,统统可以做。对于品牌定制要求高的教育产品来说,这是刚需。

3. 零代码体验:最快 10 分钟跑起一个积木应用

很多人一听到“开发”两个字,就以为要先搭工程、配编译环境。其实 Blockly 官方提供了一个非常友好的入口:Blockly Developer Tools(开发者工具)。它甚至不需要你写任何前端工程代码,直接在浏览器里就能完成积木的定义、预览、生成代码,然后导出成一个完整可运行的 HTML 文件。

3.1 使用官方开发者工具快速原型验证

打开 blockly-demo.appspot.com/static/demos/blockfactory/index.html ,你会看到左侧是“积木类型创建区”,中间是实时预览工作区,右侧是自动生成的代码。这个工具对于初学 Blockly 的人来说极其重要,它有三大用途:

  1. 通过可视化方式构建自定义积木,然后导出对应的 JSON 定义。
  2. 实时测试积木在工作区中的外观、连接是否合理。
  3. 自动生成积木的init函数或 JSON 定义、代码生成器示例、语言定义文件等。

我习惯的做法是:先在这个工具里把块的形状“搭”出来,确认交互手感没问题,再复制 JSON 定义回项目里使用。而不是直接写 JSON,因为 JSON 里每个字段的含义对新人不友好,一旦少了某个args0子项,块可能直接渲染不出来,排查起来很痛苦。

3.2 用官方 Code Lab 跑通完整流程

另一个新人友好型资源是Blockly Code Lab,它是一个交互式学习环境,界面左边是文字讲解,右边是真实可编辑的代码区,可以实时看到运行结果。建议把 01 到 08 的课程全部过一遍,因为它是目前少有的对“代码生成器(generator)”“自定义块定义”都做了细致演示的教学工具。

在 Code Lab 里,你会学到一个最简可运行的 HTML 模板,核心结构如下:

<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <title>Blockly Demo</title> <link rel="stylesheet" href="https://unpkg.com/blockly/css/blocks.css"> </head> <body> <div id="blocklyDiv" style="width: 100%; height: 600px;"></div> <pre id="generatedCode"></pre> <script src="https://unpkg.com/blockly/blockly_compressed.js"></script> <script src="https://unpkg.com/blockly/blocks_compressed.js"></script> <script src="https://unpkg.com/blockly/javascript_compressed.js"></script> <script src="https://unpkg.com/blockly/msg/zh-hans.js"></script> <script> const workspace = Blockly.inject('blocklyDiv', { toolbox: ` <xml> <category name="逻辑" colour="#5C81A6"> <block type="controls_if"></block> </category> <category name="文本" colour="#5CA65C"> <block type="text"></block> </category> </xml> `, toolboxPosition: 'start' }); workspace.addChangeListener(() => { const code = Blockly.JavaScript.workspaceToCode(workspace); document.getElementById('generatedCode').innerText = code; }); </script> </body> </html>

这段代码的工作流程非常好理:注入工作区 → 定义工具箱 → 监听内容变化 → 实时生成 JavaScript 代码并显示在页面底部。你拖入一个“如果”积木,然后在里面放一个“文本”积木,右边代码区就会立刻生成对应的控制流语句。这种“所见即所得”的反馈对于教初学者理解“图形化逻辑和代码的对应关系”简直是杀手级功能。

3.3 如何把工作区内容保存和加载

实际的商业化项目离不开“保存用户作品”。Blockly 工作区保存有标准方案:调用Blockly.serialization.workspaces.save(workspace)获得 JSON 对象,然后JSON.stringify后存到后端;加载时用Blockly.serialization.workspaces.load(json, workspace)还原。这是较新版本的推荐方式。

老版本用的是workspaceToDom/domToWorkspace的 XML 序列化方式,网上大量旧教程都在讲这个,但现在官方路线已经逐渐以 JSON 为主。我的建议是:新项目直接用 JSON 序列化,不要再踩 XML 的旧坑。JSON 可读性更强,和前端生态的融合度更高,而且在版本升级时兼容性维护更好。

4. 第一次动手:做一个“打招呼”的自定义积木

纸上得来终觉浅。我建议你用一节真实的自定义积木开发,把概念串起来。这一节我用一个最简单的“打招呼”积木,把块定义、语言文件、代码生成器、事件监听全部走一遍。

4.1 定义一个带下拉选项的积木

需求:积木形状是一个语句块,内容为“对 XX 说你好”,其中“XX”是从下拉框里选一个人物(如“老师”“同学”“爸爸”)。这么设计是为了同时演示“字段类型”和“值输入”的区别。

JSON 块定义:

{ "type": "say_hello", "message0": "对 %1 说 你好", "args0": [ { "type": "field_dropdown", "name": "PERSON", "options": [ ["老师", "teacher"], ["同学", "classmate"], ["爸爸", "father"] ] } ], "previousStatement": null, "nextStatement": null, "colour": 210, "tooltip": "向指定人物打招呼" }

这里field_dropdown的两个关键点:显示文本和实际取值可以不同,界面显示“老师”,底层代码生成时拿到的是teacher。这个设计很常见,比如界面显示中文,生成代码用英文,或者界面用单词简写、代码用完整变量名。

4.2 注册块定义与生成器

块定义注册如下:

Blockly.defineBlocksWithJsonArray([sayHelloJson]);

代码生成器:

Blockly.JavaScript['say_hello'] = function(block) { const person = block.getFieldValue('PERSON'); const code = `console.log('你好,' + '${person}');\n`; return code; };

getFieldValue('PERSON')从积木实例中读取当前选中的下拉值,然后拼到代码里。注意这里是字符串模板直接插值,所以最终生成的代码是console.log('你好,' + 'teacher');——很直白,适合教育场景。

4.3 加一点国际化:让积木显示中文但生成英文代码

不少做教育产品的同学一开始会把积木文本直接写死成中文,比如message0: "对 %1 说你好",生成代码时也直接把中文字符串输出,短期能跑,但后续如果要做多语言版本就会非常难改。Blockly 官方提供了msg系统:把积木里所有用户可见文本抽出来,定义为一个带 key 的存根。

Blockly.Msg.SAY_HELLO_MESSAGE0 = '对 %1 说 你好'; Blockly.Msg.SAY_HELLO_TOOLTIP = '向指定人物打招呼';

然后在块定义里引用:

{ "type": "say_hello", "message0": "%{BKY_SAY_HELLO_MESSAGE0}", "tooltip": "%{BKY_SAY_HELLO_TOOLTIP}" }

%{BKY_...}是一种延迟替换语法,Blockly 在渲染积木时会去Blockly.Msg表里找到真实的字符串。以后想翻译成日语、英语,只需要加载对应的 msg 文件,而不需要动块定义代码。这是被很多教程一笔带过但实际项目里特别重要的设计。

5. 踩坑经验:我从初学 Blockly 到生产落地的避坑笔记

这里记录几个我实际开发中真实踩过的坑,以及同行交流中频繁被提到的疑难杂症,统一梳理成速查表。这些问题单独拿出来都不难查,但组合起来足以劝退新手,所以值得单独开一节。

常见问题典型表现排查思路与解决方式
积木块显示为空白工具箱里能看到分类名,但里面是空白的块定义没有被正确加载。检查是否调用了Blockly.defineBlocksWithJsonArray,且 JSON 结构合法。
积木拖出来了,但无法连接语句块不能拼到另一个语句块下方缺previousStatement/nextStatement字段。值类型积木不能直接和语句类型插口连接,这是 Blockly 的类型检查机制。
代码生成报undefined值输入口没接积木,valueToCode返回了空字符串这是正常行为而非 Bug。需要自己处理“输入为空”的情况,返回默认值或提示用户补充逻辑。
工作区页面被其他 CSS 影响积木变形、工具箱定位错乱Blockly 的 SVG 渲染对继承样式敏感。检查全局是否设置了* { box-sizing: border-box }或 CSS 统一样式覆盖到了 Blockly 的 class。
中文输入法下数字块编辑卡顿用户在数字积木里输入中文后丢失焦点老版本 Blockly 的 field_input 对 IME 兼容不好。升级到较新版本,或在初始化时设置media路径和renderer选项。
工作区数据加载时积木重叠多块积木被恢复到了同一位置序列化 JSON 中的每个块都有x、y坐标字段,如果坐标丢失就会叠放。确认保存的数据完整,不能只存块列表。

关于工具链的另一个建议:不要用 CDN 的在线脚本做生产环境。本地开发时用 unpkg 或 jsdelivr 很方便,但生产环境最好把blockly_compressed.js、blocks_compressed.js、javascript_compressed.js、zh-hans.js下载下来做本地静态资源。因为 Blockly 体积较大(压缩后大约 600KB 级别),CDN 不稳定会影响首屏加载速度;而且离线教学场景(很多教室没有外网)要求必须本地化部署。

代码分割方面,标准压缩文件里已经内置了所有官方积木块的定义和英文语言包。如果你只用到少数几种积木,可以考虑用Blockly.defineBlocksWithJsonArray按需注册子集,配合自定义的语言文件,能有效削减打包体积。我做过一次极端优化,只保留逻辑、循环、数学三类积木,打包后比默认体积减少了约 40%。

另外提醒一下国内开发者:Blockly 官方文档站点访问速度不稳定,用镜像或本地文档会舒服很多。GitHub 上的google/blockly仓库里demos目录有大量可运行的示例,直接把整个仓库 clone 下来,跑一个本地静态服务器,比在线读 API 文档的效率高得多。

6. 通过一个综合示例串起整个流程

前面讲了概念、示例和避坑,这一节我建议你动手做一个更完整的示例:一个“小明的一天”小练习。整个页面分为左中右三栏:左侧工具箱,中间工作区,右侧展示生成代码。工具箱中包含“开始”积木、“行动”积木(自定义)、“循环”积木和“数字”积木。

6.1 设计需求

假设我们在做一个儿童教育游戏:让小朋友编排小明一天的行程,然后点击“执行”按钮,浏览器控制台依次输出行程。这个教学内容的核心是“顺序结构”和“循环结构”,所以积木设计要足够简单直观。

6.2 三个自定义积木的完整代码

定义“开始”积木:

{ "type": "start_block", "message0": "开始", "nextStatement": null, "colour": 300 }

定义“行动”积木:

{ "type": "action_block", "message0": "做 %1", "args0": [ { "type": "field_dropdown", "name": "ACTION", "options": [ ["起床", "get up"], ["吃饭", "eat"], ["学习", "study"], ["睡觉", "sleep"] ] } ], "previousStatement": null, "nextStatement": null, "colour": 60 }

定义“重复循环”积木:

{ "type": "repeat_times", "message0": "重复 %1 次 %2", "args0": [ { "type": "input_value", "name": "TIMES", "check": "Number" }, { "type": "input_statement", "name": "DO" } ], "previousStatement": null, "nextStatement": null, "colour": 120 }

对应 JavaScript 生成器:

Blockly.JavaScript['start_block'] = function() { return ''; }; Blockly.JavaScript['action_block'] = function(block) { const action = block.getFieldValue('ACTION'); return `console.log('${action}');\n`; }; Blockly.JavaScript['repeat_times'] = function(block) { const times = Blockly.JavaScript.valueToCode(block, 'TIMES', Blockly.JavaScript.ORDER_ATOMIC) || '1'; const branch = Blockly.JavaScript.statementToCode(block, 'DO'); const code = `for (let i = 0; i < ${times}; i++) {\n${branch}}\n`; return code; };

这里新增了statementToCode,它的作用是把嵌入到DO语句插口里的所有积木生成代码拼接成一段代码块,然后包进 for 循环的花括号里。你会发现它的返回值里自带换行和缩进,这是生成器规范的一部分——把缩进直接拼进代码字符串,而不是靠前端工具美化,因为输送出去的目标代码可能是在 Node.js 或浏览器控制台直接执行的,没有格式化环节。

6.3 执行代码的按钮逻辑

为了让示例更像一个完整应用,可以加一个按钮,点击时执行右侧生成的代码:

function runCode() { const code = Blockly.JavaScript.workspaceToCode(workspace); try { // 使用 Function 构造器,避免直接使用 eval const runnable = new Function(code); runnable(); } catch (e) { console.error('运行出错:', e); } }

用new Function而不是eval有几个好处:作用域隔离更好,不会直接污染当前作用域;在浏览器中性能略优;代码看起来也更清楚。但注意,这种动态执行代码的方式只适合受控场景,如果是教学平台、用户自己拖出的代码,那么冲突风险和注入风险都存在——你的积木集合本身限制了能生成代码的形式,所以风险是可控的;如果你是做开放编辑器,就得另外考虑沙箱方案。

7. 接下里往哪走:学习路线参考

看完这篇“初识”以后,你已经知道 Blockly 的整体框架了。下一步建议按这个顺序走:

  • 第一步:把官方 Code Lab 的所有关卡过一遍,尤其是自定义块的练习,至少亲手定义 5 种不同类型的积木。
  • 第二步:熟悉 Blockly 事件机制。会区分Blockly.Events.BLOCK_CREATE、BLOCK_MOVE、BLOCK_CHANGE,并理解为什么做在线协作编辑时需要用事件来同步工作区状态。
  • 第三步:学习自定义主题 Theme。了解如何通过主题配置改变工作区背景、块默认颜色、字体等。
  • 第四步:考虑将 Blockly 与你的后端语言结合。当前端工作区里的积木要真正运行在服务器上时,你需要后端也具备对应语言的代码生成器,或者把前端生成的代码 JSON 传到后端解释执行。

根据我个人实际经验,这里面最容易忽略的是“代码生成的执行环境”问题。前端生成了一段 JavaScript,可能在浏览器里能跑,但是如果你要把它转成 Python 发给用户,就要同时维护两套生成器。好在 Blockly 官方已经自带 JavaScript、Python、PHP、Ruby、Lua 等语言的生成器,你可以在同一个块定义下注册多个语言生成函数,一套积木,多语言输出,这是做教育出海产品的必杀技。

最后再分享一个技巧:开发自定义积木的时候,给块定义加一个helpUrl字段,指向你自己的文档页。这项工作看似不起眼,但你的积木数量一旦超过 30 个,使用者(学生或老师)就非常依赖这个入口去查看积木说明,能省下大量答疑的时间。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询