
我最早开始认真研究 Blockly并不是因为少儿编程而是因为一帮学硬件的学生。当时在带一个智能硬件工作坊有人连 Arduino 的分号都会漏但换成拖积木之后几乎每个人都写出了能跑的流水灯逻辑。这个反差让我对 Blockly 产生了很大的兴趣。后来发现它远比“给小孩玩的图形化编程”要深得多本质上一套可以嵌入任意网页的“可视化代码生成框架”。这篇是 Blockly 开发教程的第一篇我先不急着写一堆 API而是把 Blockly 到底是什么、它内部是怎么工作的、怎么在一个页面里快速跑起来讲透同时结合 ESP32-C5 这类新硬件开发场景说说为什么做硬件的人也应该重新认识它。1. 为什么一群五彩积木能撼动传统编程教育1.1 我的入坑经历代码写不动积木反而写通了我先讲个真实场景。去年我帮一个小学的科技社团设计课程校长要求“两周内让四年级学生能控制 LED 闪烁”。四年级学生连变量是什么都没概念直接教 C 语言基本是灾难。于是我临时把课程改成 Blockly先拖出一个“数字输出”块再拖一个“延时”块循环起来LED 就闪了。学生不理解什么叫digitalWrite但他们能理解“把引脚设为高电平”这块积木插进“无限循环”里是什么意思。那天放学后我突然意识到一件事Blockly 的价值不在“图形好看”而在于它把抽象语法树变成了人可以触摸的实体。代码对新手来说是二维文字而 Blockly 的每个块就是一个语法单元它有形状、有插槽、有颜色组合方式就是语法规则。这正是它能够降低编程门槛的根本原因。1.2 Blockly 被严重低估的真相它是“代码生成器”不是玩具很多人提起 Blockly 就想到 Scratch觉得是给孩子玩的玩具。但真正做过二次开发的人会告诉你Blockly 本质上是一个代码生成器框架。它本身不执行任何程序逻辑只负责把用户拖出来的图形块翻译成一段目标语言代码。你给它的目标语言可以是 JavaScript、Python、PHP、Lua也可以是 Arduino C甚至是你自己定义的某种 DSL。举个例子用户在工作区拖出一个“变量 x 1”的运算块Blockly 内部会生成一棵对应的语法树然后调用Blockly.JavaScript生成器把它翻译成x 1这段代码。这个过程对使用者是透明的但对开发者来说极其重要这意味着你可以用 Blockly 搭建一个完全属于自己的低代码平台业务逻辑由用户拖出来你想输出成什么都行。所以Blockly 不是一个完整产品而是一个 SDK。Scratch 是一个完整应用Blockly 是提供积木能力的底层库。这个定位决定了它的玩法非常多元。1.3 适合谁学学了能做什么这套教程适合三类人。第一类是想自己做少儿编程平台或教育产品的开发者你需要掌握 Blockly 的二次开发能力。第二类是搞物联网和硬件开发的工程师尤其是最近关注 ESP32C5 这类新芯片的人。用 Blockly 做教学原型或者快速验证控制逻辑比手写代码来得更快而且不容易漏掉细节。第三类是纯粹想搞明白“图形化编程原理”的编程爱好者Blockly 的源码和设计思路值得拆一拆。学习 Blockly 之后你能做的事情包括自定义自己的积木块、修改工具箱布局、定制代码生成规则、把 Blockly 接入 React/Vue 项目甚至做一个面向特定硬件平台的图形化编程工具。这些能力在现在的低代码趋势下相当吃香。2. Blockly 的工作台后面藏着三样东西工具箱、工作区、代码生成器2.1 工具箱定义你看到的所有积木都提前登记过打开任何一个 Blockly 页面左侧会有一排分类下面放着各种各样的积木。这些积木不是凭空出现的而是通过了“工具箱”配置。工具箱用 XML 或 JSON 定义告诉 Blockly 启动时要渲染哪些块。最基础的注册方式是给每个块定义一个 JSON 格式的块描述包括块的类型、颜色、所需参数以及块之间的连接规则。Blockly.defineBlocksWithJsonArray([ { type: my_led_on, message0: 打开 LED %1, args0: [ { type: field_dropdown, name: PIN, options: [[引脚2, 2], [引脚3, 3]] } ], colour: 160, tooltip: 点亮指定引脚上的 LED } ]);这个 JSON 描述完成后再用工具箱 XML 引用它xml xmlnshttps://developers.google.com/blockly/xml category name我的积木 colour160 block typemy_led_on/block /category /xml这里有一个很多人容易忽略的点工具箱里的block是一个模板用户每拖出一个块到工作区Blockly 就会根据这个模板创建一个全新的实例。你可以把工具箱理解成一个“零件仓库”而工作区是“组装台”。2.2 工作区与积木实例XML 就是积木们的存档文件工作区是用户拖拽积木的地方所有被拖出来的块都会以树形结构存在内存中。每个块的类型、字段值、坐标、甚至折叠状态都会被 Blockly 记录。当你需要保存项目时最稳妥的方式是把工作区序列化成 XML 字符串。const xml Blockly.Xml.workspaceToDom(workspace); const xmlText Blockly.Xml.domToText(xml); // 把这个 xmlText 存到 localStorage 或后端等用户再次打开页面时把这个 XML 解析回 DOM再放回工作区const dom Blockly.Xml.textToDom(xmlText); Blockly.Xml.domToWorkspace(dom, workspace);很多新手觉得 XML 序列化只是“保存/加载”功能用不到。但实际上它有两个非常实际的用途。第一项目迁移。Blockly 版本升级的时候某些块定义可能发生变化只要 XML 里的type不变旧项目就能恢复。第二与后端联动。你可以把用户拖出来的流程图存起来用于分享、评分、版本对比。大部分可视化编程平台的核心资产其实就是这些 XML。理解了这一点就不会再把 Blockly 当成只能离线拖一拖的玩具。2.3 代码生成器从可视化图形到真实语言的翻译官这是 Blockly 最核心的部分也是它区别于普通图形编辑器的关键。Blockly 本身不执行任何实际逻辑它只做“翻译”。翻译有两个步骤第一步把工作区里的块组织成语法树第二步调用对应语言的生成器将语法树拼接成代码字符串。Blockly.JavaScript.addReservedWords(my_led_on); Blockly.JavaScript.forBlock[my_led_on] function(block) { const pin block.getFieldValue(PIN); return digitalWrite(${pin}, HIGH);\n; };这里需要理解一个关键点代码生成器函数的返回字符串不是普通的输出语句而是“当前块生成的代码片段”。如果这个块有被子块嵌套比如循环块内部包含其他块那么循环块生成器内部会调用Blockly.JavaScript.statementToCode(block, DO)来递归拼接内侧代码。这种递归调用的设计让 Blockly 能应对任意复杂的嵌套结构。用一句话总结工具箱管“有哪些积木”工作区管“积木怎么组合”代码生成器管“组合后的积木变成什么代码”。这三件事的职责边界非常清楚所以 Blockly 才能在这么多年里保持极高的可扩展性。3. 手写一个最简单的 Blockly 页面从 HTML 到可执行 JS3.1 准备一个页面骨架引入 Blockly 核心文件先在本地建一个index.html把 Blockly 的核心包引进来。我这里使用 CDN 的方式方便你零配置跑起来。如果你用的是 Node 环境也可以直接npm install blockly然后在项目里import * as Blockly from blockly/core。不过第一次入门我建议直接用 CDN 版本省掉打包环节。!DOCTYPE html html head meta charsetutf-8 / title我的第一个 Blockly 页面/title script srchttps://unpkg.com/blockly/blockly.min.js/script style #blocklyDiv { width: 100%; height: 480px; border: 1px solid #ccc; position: relative; } #codeArea { width: 100%; height: 150px; margin-top: 10px; font-family: Consolas, monospace; } /style /head body div idblocklyDiv/div button idgenerateBtn生成代码/button button idrunBtn运行代码/button textarea idcodeArea readonly/textarea /body /html这里引入的是 Blockly 官方的全量包里面已经包含基础块、默认工具箱以及 JavaScript 代码生成器。如果你只想用核心功能不想要默认块需要按模块引入之后我会单独写一篇工程化的配置方式。初学阶段全量包最省心。3.2 定制第一组积木让页面输出“Hello Blockly”接下来我们不放默认块而是自定义几个最简单的块让整个流程更可控。在页面底部加入脚本先定义两个块一个是程序入口块一个是输出文本块。Blockly.defineBlocksWithJsonArray([ { type: print_text, message0: 打印文本 %1, args0: [ { type: field_input, name: TEXT, text: Hello Blockly } ], previousStatement: null, nextStatement: null, colour: 230, tooltip: 向控制台打印一段文本 } ]);这里previousStatement和nextStatement都设为null表示这个块可以被串在语句队列中。field_input表示这个块上有一个可编辑的文本输入框。接下来自定义工具箱把分类“我的文本”丢进去const toolbox { kind: categoryToolbox, contents: [ { kind: category, name: 我的文本, colour: 230, contents: [ { kind: block, type: print_text } ] } ] };然后用Blockly.inject把工作区挂载到页面上的blocklyDivconst workspace Blockly.inject(blocklyDiv, { toolbox });这样页面左上角会出现“我的文本”分类里面有一个“打印文本”块。拖出来双击可以修改文本内容。3.3 在浏览器里拖出逻辑并导出生成的 JavaScript现在给“生成代码”按钮绑定事件让它把工作区中的块翻译成 JavaScript 代码并显示在右侧文本框里。document.getElementById(generateBtn).addEventListener(click, () { const code Blockly.JavaScript.workspaceToCode(workspace); document.getElementById(codeArea).value code; });这里会遇到一个问题我们没有给print_text块定义代码生成器所以即使拖动块点击生成后也只会得到空字符串。先补上生成器Blockly.JavaScript.forBlock[print_text] function(block) { const text block.getFieldValue(TEXT); return console.log(${text});\n; };再次拖动块、点击生成textarea 里就会出现console.log(Hello Blockly);到这一步Blockly 的核心链路其实已经走通了。但很多新手在这里会卡住因为他们发现“生成的代码”和“ Blockly 页面本身”是相互独立的两个世界。块是拖出来了代码也生成了可它并没有被真正执行。要想让这个输出真正跑起来还需要把生成的代码接回浏览器的执行环境。3.4 把生成的代码真正跑起来连接自定义回调Blockly 的 JavaScript 生成器默认生成的代码是通过Generator.prototype.workspaceToCode生成的独立代码字符串。你完全可以把这段字符串交给eval或new Function去执行。虽然不推荐在生产环境乱用eval但在 Blockly 教学演示和快速原型验证时这是最直接的方式。document.getElementById(runBtn).addEventListener(click, () { const code document.getElementById(codeArea).value; try { new Function(code)(); } catch (e) { console.error(运行失败, e); } });这样点击“运行代码”浏览器控制台就会输出Hello Blockly。你可以继续扩展再定义一个“循环执行”块让它包裹“打印文本”块生成器里用递归调用来拼接循环体内的代码。这个动作就是 Blockly 能把“拖积木”变成“真实可运行程序”的完整链条。4. 初学 Blockly 时最常见的 5 个认知偏差以及我怎么纠正的4.1 误区一以为积木只是 Scratch 的换皮拖完就完事我见过不少开发者第一次看到 Blockly 之后说“这不就是网页版 Scratch 吗”。如果你也这么想会错过它最值钱的部分与自己的业务逻辑深度集成。Scratch 是封闭的积木生态大部分时候你不能随意自定义积木形状也不能控制代码生成的细节。但 Blockly 完全开放你可以写一个块让它打开摄像头可以生成一段 SQL也可以生成一段 C 代码烧到单片机上。换个角度看Scratch 适合教编程思维而 Blockly 适合构建“业务上的可视化开发工具”。把它当玩具天花板就被自己压低了。真正要做的是研究怎么用 Blockly 封装领域逻辑让你的用户能像拼积木一样配置业务规则。4.2 误区二忽略 XML 的作用导致项目迁移和版本升级时翻车有个学生曾经找我求助说他做的平台上线后用户保存的项目第二天无法打开。我拿到日志后发现他保存的不是工作区 XML而是只保存了代码生成结果。这样一来只要块定义稍有变化生成的代码就再也映射不回去了。更常见的问题是在 Blockly 版本升级时某些默认块的处理逻辑发生变化用户旧项目里引用的块类型新版本不再支持页面直接报错。正确做法是把 XML 作为主要存储对象代码生成结果只是“展示成果”。这样即使换了一套生成器你也能保留用户原始的拖拽结构。另外升级 Blockly 时一定要跑一遍回归测试重点关注旧的 XML 能否正常反序列化。项目越大这个测试的价值越高。4.3 误区三把 Blockly 生成的代码当成“最终代码”缺少抽象和封装有一段时间我在做内部工具时直接把 Blockly 生成的 JavaScript 塞进前端路由结果代码里全是重复的大段字符串拼接。比如用户拖了 50 个“设置LED”块生成器就会输出 50 行digitalWrite(2, HIGH); digitalWrite(3, HIGH);。这代码当然能跑但它是“展开式”代码没有任何抽象。更好的做法是让 Blockly 生成对业务函数的调用。比如这个块不是直接生成digitalWrite而是生成setLed(2, true)然后在外部平台代码里定义setLed函数负责处理引脚映射、状态管理和日志上报。这样既保留了拖拽可视化又不至于牺牲工程代码的可维护性。记住Blockly 生成器要追求“语义清晰”不是追求“一行对应一个动作”。4.4 误区四变量块让人看得懂但作用域理解反了Blockly 的变量机制非常接近真实编程语言但正因为太接近让很多人产生了错觉。一个“自定义变量”块可以在整个工作区被多个块引用看起来它就是全局变量。但当你把 Blockly 生成的代码放进一个函数内部时变量的定义可能被生成器提升到了函数顶部而引用它的地方在外层就出问题了。我踩过的最经典的一个坑是用户拖了变量x在“重复 N 次”块里给x赋值循环外打印x。生成器输出时如果变量声明位置和赋值位置不一致有时候会生成let x和x 0分离的代码在浏览器里不报错在严格模式下就报x is not defined。解决办法是检查生成器对variables_get和variables_set两个块的输出尤其是你自定义循环块时的statementToCode参数确保块生成的代码被正确包裹在同一个作用域内。4.5 误区五只会拖积木不会写自定义块很多教程告诉你 Blockly 内置了强大的默认块比如数学、逻辑、循环、文本拖出来就能用。但如果你只停在“使用内置块”那本质上还是把 Blockly 当高级 Scratch 玩没法发挥它的定制能力。实际项目里一个块应该对应你业务里的一个“动作”或“对象”。举个例子如果你在做智能家居控制面板你会希望工具箱里直接有“按名称开灯”“按温度调空调”“定时播报”这些块而不是让用户用一堆数学和逻辑块自己拼。自定义块虽然是 Blockly 二次开发的入门基本功但它决定了你的平台到底能不能让目标用户用起来。初学阶段就要敢于写defineBlocksWithJsonArray敢于写生成器和校验器这才是 Blockly 开发教程中最核心的转折点。5. 从浏览器积木到真实硬件Blockly 如何切入 ESP32-C5 这类开发5.1 为什么做硬件开发反而更需要可视化编程很多人做硬件开发的时候会有一个偏见觉得嵌入式代码那么底层可视化编程只会碍事。但我这几年带硬件项目发现一个扎心的现实硬件开发的大部分时间并不是在写复杂算法而是在调整引脚配置、初始化外设、设置延时、组合条件判断。这些东西逻辑简单却特别容易出错尤其是不同硬件平台之间 API 还经常不一致。拿 ESP32-C5 来说它是乐鑫最近发布的 RISC-V 架构 MCU主打 Wi-Fi 6 和低功耗。对于刚接触这款芯片的开发者先要用 Arduino 或 ESP-IDF 搭环境写初始化代码再看一堆数据手册。但如果你只是验证一个方案——温度高了就亮红灯连续五次超温就开启蜂鸣器——用 Blockly 拖出来再把生成的代码烧进去明显更直观而且方案改动时不用在源码里到处找逻辑。5.2 Blockly 生成 C/MicroPython 的组合方案先画图再调参Blockly 能做的硬件编程路线大概有三条。第一条是生成 Arduino C 代码。你可以为每个硬件操作定义块比如“设置引脚模式”“数字写入”“读取模拟值”然后让生成器输出对应的 Arduino API 调用。第二条是生成 MicroPython 代码适合跑在支持 MicroPython 的 ESP32 系列上包括现在的 ESP32-C5 生态也在逐步跟进。第三条是自己造一块“硬件抽象层”Blockly 生成的是平台无关的中间表示再由你自己的固件解析执行。我常用的方案是在开发阶段先在 Blockly 里把控制逻辑拖出来生成一份 C 或 MicroPython用来快速验证。然后在项目稳定后再把这套逻辑手工重构成一个结构清晰的固件模块。这样既有前端拖拽的效率又有嵌入式工程的品质。5.3 对于 ESP32-C5 开发者的建议别把 Blockly 当成唯一的救世主我特别想给搞硬件的人一句忠告Blockly 是很好的辅助工具但别指望它永远省心。生成的代码质量取决于你的块定义如果你定义块的时候输出的是Serial.println(hello)那出来的就是教学级别代码不适合上生产。如果你想用 Blockly 做商业级硬件开发工具就得花大量时间打磨代码生成器针对loop()、setup()这些结构做好代码片段拼接还要处理多文件工程和库依赖。以 ESP32-C5 为例它和传统 ESP32 的引脚定义、外设资源不尽相同你在 Blockly 里内置的“引脚选择”下拉框必须跟着目标芯片走。稍微成熟的做法是把芯片配置也做成一个特殊的块用户先在主工作区选择一个开发板型号再选引脚功能生成器根据这些信息动态映射到不同的代码。这些细节才是 Blockly 二次开发真正有意思的地方。从我的实际经验来看用 Blockly 做硬件原型教学或者内部快速验证效率提升非常明显。但进入量产级固件开发你还是得动手去写真正的 C 代码把可维护性和性能掌握在自己手里。把 Blockly 当成“思路草稿纸”而不是“最终编译器”反而更能在实践里找到它真正的价值。我自己在带项目时会刻意要求学生先在 Blockly 里把程序流程拖出来再对照流程图手写代码。这么做的好处是学生对于“分号该不该加”这样的细节越来越敏感而整体逻辑反而更不容易乱。等你真正跑通一个自定义 Blockly 页面又用它生成过硬件代码就会理解为什么这个可视化框架能在各种领域持续存在。下一篇文章我会进入 Blockly 的工程化配置聊聊怎么用 Node 项目管理块定义和代码生成器到时见。