1. 先想清楚:你要做的"扩展"到底是什么东西
1.1 积木块不够用的时候,我在琢磨什么
刚开始用 MakeCode 做东西的人,几乎都会经历同一个阶段:编辑器左边那一栏积木翻来覆去就那么几组,想把某个传感器的读数换算成实际物理量,或者想封装一套自己常用的动作序列,结果发现要么得拖十几个积木拼出来,要么干脆切到 JavaScript 手写。前者在课堂上没法讲清楚逻辑,后者对刚接触编程的人门槛又太高。
这个时候"扩展开发"就派上用场了。MakeCode 扩展开发,说白了就是把一段 TypeScript 代码,加上一组特殊的注释标记,交给编译器去生成图形化积木。你写的是代码,学生看到的是积木,中间那层转换由平台自动完成。自定义积木块解决的是"表达能力"问题——让不会写代码的人也能调用你的功能;功能模块解决的是"复用"问题——把一堆底层操作打包成一个语义清晰的 API,别人拖两下就能用。
我自己第一次做扩展,是因为一个模拟项目里需要反复读四路巡线传感器的状态,再转换成偏差值。手写 JavaScript 太啰嗦,纯拖积木又乱成一团,于是花了两个晚上啃文档,做出了第一个只有三个积木的扩展。那次之后我就明白了:扩展的价值不在于技术多复杂,而在于它能不能把"重复劳动"变成"一次定义、到处拖拽"。
这篇内容适合谁?三类人。第一类是做教学的人,想给学生提供一套贴合课程的自定义积木;第二类是做硬件的人,手里有一块板子或者一个模块,希望它能在图形编辑器里被直接调用;第三类是想把自己写的工具函数沉淀下来、跨项目复用的开发者。前两类占绝大多数,第三类往往是踩着前两类的坑走过来的。不管你是哪一类,下面这套流程都能直接照着走。
1.2 扩展的构成:一份带注解的 TypeScript 加一个清单文件
先把结构说透。一个最小可用的扩展,本质上就是两样东西:一个pxt.json清单文件,一个或多个.ts源码文件。清单文件告诉编辑器"我是谁、我要依赖什么、我包含哪些文件";源码文件里则是普通的 TypeScript 函数和类,唯一特殊的地方是函数上方那些//%开头的注解行。
这些注解不是装饰。它们是编译器的指令,决定了这个函数要不要生成积木、生成出来长什么样、叫什么名字、参数用什么控件、归到哪个分组、排在什么位置。换句话说,你在源码里写的是"功能",注解写的是"用户界面"。理解这一点很关键,因为初学者最容易犯的错误就是把这两件事混在一起,觉得写了函数就自动有积木——实际上没注解的函数在积木视图里是隐形的,只能被其他代码调用。
扩展可以只有一个文件,也可以有十几个文件加子目录。规模小的时候一个main.ts全塞进去完全没问题;一旦超过两三百行,就建议按功能拆文件,因为积木分组(group)是跨文件生效的,拆开之后每个文件的注解还能保持清晰。我在第三个扩展里吃过亏,所有东西写在同一个文件里,改到后面自己都找不到某个积木定义在哪一行。
1.3 先画积木,再写代码
这是我强烈推荐的一个习惯:动手写代码之前,先拿张纸或者白板,把你想要生成的积木"画"出来。画的时候只考虑三件事——这块积木叫什么名字(读起来像不像人话)、它需要用户填哪几个空、它归在哪个分组。
比如一个控制 RGB 灯带的扩展,我一开始想做"设置灯带第 N 颗灯的颜色"这种细粒度积木,画完发现用户实际想要的是"点亮第 N 颗灯为某颜色""整条灯带呼吸一次""从第 A 颗到第 B 颗渐变成某种颜色"这种更高层的表达。如果先写代码后想界面,很容易做出一堆参数又多、语义又碎的低价值积木,用起来比直接写代码还麻烦。
画积木的时候还要想清楚一件事:这个积木是"语句块"还是"值块"。语句块是那种自带凹槽、单独一行的,比如"设置引脚为高电平";值块是带返回值的、能塞到别的积木槽里的,比如"读取温度值"。这个区别在代码里就体现为函数有没有返回值,一旦画错了,后面改起来要动整个 API 设计。我的经验是:凡是会改变硬件状态或产生副作用的,一律做成语句块;凡是只读、只做计算的,一律做成值块。这条规则坚持下来,扩展的可用性会高一个档次。
2. 环境怎么搭:在线还是本地,差别比你想的大
2.1 两种开发路径的取舍
做 MakeCode 扩展有两条路。第一条是在线编辑器的"扩展"入口里直接粘贴一个 GitHub 仓库地址,编辑器会去拉取并编译,适合快速验证和小规模修改。第二条是在本地装工具链,用命令行把整套环境跑起来,适合正经开发——可以热更新、可以看编译错误、可以本地调试、可以管理多个文件。
我的建议很明确:只要你打算做的扩展超过二十行代码,就走本地。在线方式的反馈链路太长,改一行注释要在网页里刷新、重载、再找积木,折腾十几次一天就过去了。本地环境搭一次大概二十分钟,之后每次改动都是秒级生效,这笔投入太划算了。
本地环境的前提是 Node.js。装的时候注意版本别太旧,建议用 LTS 版本,太老的 Node 在安装命令行工具链时容易报一些莫名其妙的依赖错误。装完之后一条命令验证:
node -v npm -v两个都有版本号输出就说明基础环境 OK 了。
2.2 命令行工具链的搭建过程
全局安装开发工具:
npm install -g pxt这一步在国内网络环境下可能会慢,如果卡在下载环节,可以给 npm 配一个镜像源再装。装完之后执行pxt --version确认能正常输出。
接下来要拉一个"目标"下来。MakeCode 的架构是分层的,平台上有很多不同的目标(面向不同主控板或应用场景),扩展是挂在某个目标下面编译的,所以你得先选一个目标。以常见的 microbit 目标为例:
pxt target microbit这条命令会去拉取目标仓库并建立工作目录,第一次执行会比较久,因为它要装一大堆依赖。执行完之后你会得到一个完整的、可以在本地跑起来的目标工程。进到目录里,再跑一次:
pxt serve浏览器会自动打开本地编辑器。到这里,环境就算打通了。以后每次开发,只要进到目录执行pxt serve就行。
2.3 工程骨架:清单文件的每个字段都别瞎写
扩展放在目标的projects/目录下,自己建一个文件夹,名字用小写加短横线,比如my-sensor-kit。文件夹里第一件事是建pxt.json:
{ "name": "my-sensor-kit", "version": "1.0.0", "description": "一套用于读取模拟传感器的积木集合", "license": "MIT", "dependencies": { "core": "*" }, "files": [ "main.ts", "README.md" ], "testFiles": [ "test.ts" ], "public": true, "supportedTargets": [ "microbit" ], "preferredEditor": "tsprj" }几个字段值得单独说说。name必须和文件夹名一致,不一致会出现编译时的路径解析问题,这个坑我踩过一次,报错信息完全看不出根因。dependencies里的core是核心库,几乎所有扩展都要依赖它,写*表示跟随目标版本。files是会被编译进扩展的文件清单,注意README.md也要写进去,因为它就是用户在扩展页面看到的说明文档。testFiles里的文件只在测试时编译,不会进入最终产物,用来放一些验证逻辑。preferredEditor设成tsprj表示默认用 TypeScript 工程模式打开,调试时更直观。
提示:
files里漏写文件是新手最常见的错误之一。表现是代码明明写了、注解也加了,积木就是不出现。遇到这种情况第一反应应该是回头检查这个数组。
2.4 第一个能跑起来的扩展
建main.ts,写一段最小代码:
/** * 一个演示用的最小扩展 */ //% color="#2E7D32" weight=90 icon="\uf1b3" namespace demoKit { /** * 打招呼 */ //% blockId=demo_greet //% block="打招呼 $name" //% name.defl="世界" //% weight=100 //% group="基础" export function greet(name: string): void { // 实际项目里换成真实的业务逻辑 console.log("hello " + name) } }保存之后pxt serve会自动重新编译。在本地编辑器里新建一个项目,切到积木视图,左边应该多出一个绿色的"demoKit"分组,里面有一块"打招呼 [世界]"的积木。拖出来放到画布上,点一下,控制台会输出内容。
到这一步,很多人会松一口气然后直接开始堆功能。我的建议是先别急,把注解这一套吃透,因为后面 90% 的返工都和注解写得不合适有关。
3. 注解体系:让 TypeScript 长出积木的样子
3.1 三个核心注解撑起一块积木
一块积木能被生成出来,最少需要三样注解:blockId、block、以及所在命名空间上的color。
blockId是这块积木的全局唯一标识。它不显示给用户,但会出现在项目源码里,一旦发布就不能随便改——用户用旧版本搭的项目,加载时靠这个 ID 找回对应的积木,改了 ID 就意味着老项目里的这块积木会变成"未知积木"。所以我的做法是从一开始就定一套命名规则,比如模块名_动作,demo_greet、sensor_readTemp这样,宁可长一点也别图省事叫f1、b2。
block是显示文本。里面用$参数名来占位,占位的地方会自动变成可以填内容的槽。文本本身是自由的,可以写中文,可以写符号,但要注意别写得太长——积木在画布上是横向铺开的,超过一定宽度会把整个代码区撑得很难看。我一般控制在十二三个汉字以内,实在不够就把细节挪到参数里。
color决定这一组积木的颜色。它定义在命名空间上,所以同一个命名空间下的所有积木是同一个色系。选色有个小技巧:不要挑纯正的红绿蓝,挑带点灰度的深色,比如#2E7D32、#1565C0、#6A1B9A,在白色画布上更耐看,也不容易和平台自带的积木撞色。撞色会造成混淆,用户以为那是官方功能。
3.2 参数注解:把输入框调成用户顺手的样子
参数长什么样,完全由注解决定。这是最能体现"用心"的地方,也是区分一个扩展好用不好用的分水岭。
常用参数注解速查:
| 注解形式 | 作用 | 典型场景 |
|---|---|---|
$p.defl="文本" | 设置默认值 | 字符串、数字的初始值 |
$p.min=0$p.max=1023 | 限定数值范围 | 模拟量读数、PWM 值 |
$p.shadow="math_number_minmax" | 把输入框变成带上下限的滑块 | 亮度、音量、速度 |
$p.shadow="toggleOnOff" | 变成开关 | 使能、开关状态 |
$p.shadow="colorNumberPicker" | 变成颜色选择器 | 灯带颜色 |
$p.shadow="timePicker" | 变成时间选择器 | 延时、持续时间 |
$p.decompileLiterals=true | 允许把常量直接写在积木上 | 数字、字符串参数 |
举个实际的例子。做一个设置亮度的积木:
/** * 设置灯带亮度 * @param level 亮度百分比,范围 0 到 100 */ //% blockId=led_setBrightness //% block="设置亮度为 $level \\%" //% level.min=0 //% level.max=100 //% level.defl=50 //% level.shadow="math_number_minmax" //% weight=80 //% group="灯光" export function setBrightness(level: number): void { // 换算逻辑:百分比转成内部占空比 const duty = Math.round(level * 255 / 100) // 写入硬件寄存器 }这里有个细节值得注意:block字符串里的%是特殊字符,要用\\%转义,否则会解析出错。这个坑我第一次踩的时候,编辑器直接不显示积木,也没有明显报错,找了半天才发现是百分号的问题。
还有一个心得:min和max单独写不会让输入框变成滑块,必须配合shadow="math_number_minmax"。很多人只写了范围,发现还是普通输入框,以为注解没生效,其实是少了一行。
3.3 枚举与下拉菜单:把魔法数字变成人话
用户不该记住"0 表示低电平、1 表示高电平"这种东西。枚举就是干这个的,把数字包一层语义。
/** * 触发边沿 */ export enum Edge { //% block="上升沿" Rising, //% block="下降沿" Falling, //% block="任意变化" Any }枚举成员上的//% block="..."就是下拉里显示的文字。有了这个枚举,函数参数写成Edge类型,编辑器会自动渲染成下拉菜单:
//% blockId=pin_onEdge //% block="当引脚 $pin 出现 $edge 时执行 $handler" //% pin.defl=DigitalPin.P0 //% handlerStatement=1 //% draggableParameters="reporter" //% group="中断" export function onEdge(pin: DigitalPin, edge: Edge, handler: () => void): void { // 注册中断回调 }枚举的命名有个小建议:给每个枚举类型加上//% blockNamespace或者放在明确的命名空间里,避免和其他扩展的枚举重名。MakeCode 的积木系统在类型匹配上比较宽松,重名有时候不会报错,但会渲染出奇怪的默认值。
注意:枚举成员的顺序就是下拉的顺序,不要指望靠字母序自动排。想调整显示顺序,直接调整成员申明的先后即可。
3.4 事件型积木:C 形口是怎么来的
带回调参数的函数,会自动变成 C 形积木,用户可以往里面塞别的积木。这是图形化编程里最有表达力的形态,做硬件扩展几乎绕不开。
关键在两个注解:参数类型写成() => void,再加上//% handlerStatement=1。如果想让用户能把回调里的参数拖出来用,就补上//% draggableParameters="reporter",这样积木左侧会出现一个可拖拽的变量小圆点。
我做过一个按键扩展,最初没加draggableParameters,用户只能在回调里做无参操作,想说"按键按下时把按下的次数传出去"都做不到,体验很差。补上之后,用户可以拖出一个变量直接接进其他积木,整个流程顺畅很多。
还有一点值得提醒:C 形积木内部的代码是立即执行还是延迟执行,取决于你的实现。如果内部逻辑是异步的,一定要在文档里写清楚,否则用户会以为里面写的积木是"同时跑"的,出现问题很难排查。
3.5 视觉层级:分组、权重与折叠
累积到十几个积木之后,光靠颜色区分已经不够了,必须靠分组。
group决定积木归到哪个折叠区。分组名直接用中文,语义要清晰,比如"基础""传感器""灯光""高级"。分组之间的顺序由组内积木的最高weight决定,weight越大越靠前。我习惯按 100、90、80 这样十位递减来分配,中间留出空档,方便后面插入新积木不用全部重排。
advanced=true会把积木收进"更多"里,默认不显示。这个注解非常有用——把不常用但确实需要的积木(比如底层寄存器操作、调试开关)藏起来,主界面上只留高频积木,学习曲线会平缓得多。我做教学扩展的时候,基础版只放五个积木,进阶功能全部收进高级区,学生的反馈明显比一股脑全铺开要好。
blockGap控制积木之间的垂直间距,默认值一般够用,但如果某个积木特别高(比如带两个 C 形口的),适当加大一点会更透气。
icon用的是字体图标编码,可以给分组加一个小图标。这个不是必需的,但加上之后扩展在扩展列表里会显得更完整。图标编码要写成"\uf1b3"这种转义形式,直接贴字符有时候会出问题。
4. 功能模块:把硬件能力封装成能复用的 API
4.1 状态放哪里:初始化时机的选择
积木只是皮,真正的功夫在功能模块的设计上。第一个要面对的问题是:状态放哪、什么时候初始化。
常见做法是模块级变量加一个初始化函数:
namespace myKit { let inited = false let brightness = 50 //% blockId=my_init //% block="初始化模块" //% weight=110 //% group="基础" export function init(): void { if (inited) return // 硬件初始化:配置引脚、复位寄存器 inited = true } }这里有个设计取舍:要不要让用户必须手动调"初始化"?我的倾向是能自动就自动。在每个公开函数的入口做一次惰性初始化,用户不需要关心这件事,体验最好。但对于资源占用大、初始化慢的模块,手动初始化反而更可控,因为用户能决定什么时候付出这个代价。
不管选哪种,一定要处理重复初始化。上面代码里的if (inited) return就是干这个的。没有这行,用户多点几次初始化积木就可能把硬件配置搞乱,表现是偶发的、极难复现的故障,排查起来非常痛苦。
4.2 用 shim 对接底层能力
有些功能在纯 TypeScript 层做不了,比如直接操作寄存器、调用底层驱动。这时候用shim注解把函数映射到底层实现:
//% blockId=my_writeReg //% block="写入寄存器 $addr 值 $value" //% shim=myKit::writeReg //% group="高级" //% advanced=true export function writeReg(addr: number, value: number): void { // 这里的实现会被 shim 指向的底层函数覆盖 }shim后面的格式是命名空间::函数名。这个机制的好处是:积木的签名在 TypeScript 层定义,实现在底层,两边解耦。改底层实现不用动积木定义,用户那边的项目也不会受影响。
用 shim 要注意类型对齐。底层函数的参数类型和 TS 层声明不一致的话,编译能过但运行时会出问题,而且报错信息通常指向很奇怪的位置。我的做法是 shim 函数尽量只用基本类型(number、boolean、string),避开复杂对象,减少不匹配的概率。
4.3 异步与回调:别让积木卡住
硬件操作经常涉及等待。如果直接在积木里写同步等待,整个程序会卡在那里,用户搭的多任务逻辑全部失效。这是扩展开发里最容易被忽视的问题。
正确的处理方式是把耗时操作拆成"启动 + 回调"两段。对外暴露的积木是启动型的语句块,内部用定时器或事件机制推进,完成后触发用户注册的回调。这样用户在积木层面看到的是非阻塞的,程序能继续往下跑。
如果实在需要阻塞语义,就在文档里明确标注"此积木会暂停程序 N 毫秒",让用户心里有数。宁可写清楚限制,也别让用户在不知情的情况下踩坑。
还有一个细节:回调里抛出的异常。图形化环境对异常的处理比较粗糙,一个未捕获的错误可能导致整个程序静默停止。所以在调用用户回调的地方,最好包一层保护,出问题时至少打一条日志出来,而不是无声无息。
4.4 拆模块的粒度:三个积木比三十个积木值钱
新手做扩展有个通病:功能越加越多,积木列表越拉越长,最后变成一个谁都不想用的"全家桶"。我见过一个扩展光"灯光"分组就有二十多个积木,参数还都差不多,用户根本分不清该用哪个。
我的经验法则是:一个分组里超过八个积木,就该考虑合并或者下沉。合并的方式是把相似功能做成一个带枚举参数的积木,比如"设置模式为 [常亮/呼吸/闪烁/彩虹]",一个积木顶四个。下沉的方式是把低频功能加advanced=true收起来,或者干脆只暴露底层积木,让高级用户自己组合。
另一个原则是分层。底层积木(读原始值、写寄存器)和上层积木(做一次完整动作)分开放,上层调用下层。这样高级用户能用底层搭出你没想过的用法,普通用户用上层就够了。这个结构在文档里也要体现出来,让人一眼看出层次关系。
5. 调试、测试与发布:把东西交出去之前
5.1 本地调试的几个实用手段
pxt serve起来之后,本地编辑器是热更新的。改main.ts保存,编辑器会重新编译,刷新页面就能看到新积木。这个循环很快,一天能迭代几十次。
看编译错误有两条路。一是浏览器控制台,编译失败的详细信息会打在这里;二是终端窗口,pxt serve的输出里也有。我习惯开两个窗口,一个放编辑器一个放终端,出问题先看终端,因为格式更清楚。
还有一个不太为人知的技巧:在扩展目录下单独跑pxt build,会走一遍完整的编译流程,能提前发现一些在编辑器里被吞掉的错误,比如pxt.json里引用了不存在的文件、注解格式不合法之类。发布之前跑一次,能省掉很多来回。
调试积木的渲染效果,最直接的办法是拖出来摆一摆。有些问题只有摆出来才看得见,比如积木太宽、参数槽顺序别扭、C 形口嵌套层级混乱。这些问题在代码里完全看不出来,但对可用性的影响很大。
5.2 常见编译错误速查
| 现象 | 可能原因 | 处理办法 |
|---|---|---|
| 积木完全不显示 | 缺blockId或block注解;files里没列文件 | 补注解,检查清单文件 |
| 积木显示了但名字是函数名 | block注解写错位置,或在函数体内 | 注解必须紧贴函数上方 |
| 参数全是普通输入框 | 缺shadow或decompileLiterals | 按需补上 |
| 下拉菜单是空的 | 枚举成员没写//% block="..." | 给每个成员补注释 |
| 编译报"无法解析的类型" | 依赖没在pxt.json里声明 | 补dependencies |
| 积木颜色不对 | color写在函数上而不是命名空间上 | 移到命名空间 |
| 项目老版本加载后积木变灰 | blockId被改过 | 恢复原 ID,或保留别名 |
这张表里的问题我基本都遇到过,其中"files里漏文件"和"blockId被改"是最消耗时间的两个,因为它们的表现和根因距离很远。前者排查方向通常是去查注解,越查越偏;后者往往要等到用户反馈才发现。
5.3 发布:让人能找到、能信任
扩展开发完了,怎么让别人用上?最直接的方式是把代码推到一个公开仓库,然后告诉用户在编辑器的"扩展"页面里输入仓库路径,格式类似github:用户名/仓库名。编辑器会去拉取并编译,用户这边不需要装任何东西。
发布前有几件事必须做。第一,README.md写清楚,它会直接显示在扩展页面上,是用户了解你的唯一入口。里面至少要有:每个积木干什么、参数怎么填、一个完整的最小示例、已知限制。我自己看别人的扩展,第一眼看示例,第二眼看限制,这两样没有的话基本就放弃了。
第二,版本号打 tag。pxt.json里的version和仓库的 tag 要对上,用户锁定版本时才不会出问题。改动了已有的积木签名,一定要升大版本号,否则老项目升级后可能编译不过。
第三,test.ts里放几个最基本的验证用例。不需要写得多优雅,能覆盖主路径就行。每次改动之后跑一遍,能挡住大部分低级错误。
第四,supportedTargets老老实实填。如果你只在某一个目标上测过,就只写那个,别想着"应该都能跑"就全写上。跨目标兼容性的坑比你想的多,不同目标的底层 API 差异、引脚定义差异、可用库差异,随便一个都能让扩展在别的板子上直接崩掉。
6. 踩过的坑和几条硬经验
6.1 积木不出现?先按这个顺序查
遇到积木不显示,我现在有一套固定流程,基本三分钟内能定位。
第一步,看终端有没有编译错误。有错误的话先解决错误,很多"积木不显示"其实是编译失败导致的,编辑器只是没把错误顶到界面上。
第二步,确认函数是export的。没有export的函数不会进入积木系统,这个太容易忘了,尤其是从普通项目代码复制过来的时候。
第三步,确认注解在函数正上方,中间没有空行,没有别的注释隔开。注解和函数之间插入一行普通注释,有时候会打断解析,表现就是积木消失。
第四步,确认命名空间上有color。没有颜色的命名空间下的积木,在某些版本里会渲染异常。
第五步,刷新一下页面。热更新偶尔会失效,尤其是改了pxt.json之后,必须手动刷新才生效。这个我吃过好几次亏,明明代码是对的,就是不更新。
6.2 参数默认值的几个隐形陷阱
默认值看着简单,实际坑不少。
数字参数的defl如果写成了超出min和max范围的值,编辑器会静默把它截断到边界,你在代码里看到的默认值和用户看到的不一样。所以我现在的习惯是写完defl立刻检查一遍范围。
字符串参数的默认值里如果含有特殊字符(引号、反斜杠),需要转义,否则整个注解行会解析失败,而且是那种不报错、只是积木不显示的失败。
布尔参数不要设默认值,让它保持未连接状态。用户看到的是一个空槽,可以拖一个开关进去,也可以什么都不放。硬给个defl=true反而限制了灵活性。
还有一点,参数名不要太短。$a、$b这种在注解里写起来省事,但一旦积木多了,你在代码里根本记不住哪个对应哪个,改的时候要来回对照。我现在的习惯是参数名至少三个字母,语义化,比如$pin、$level、$duration。
6.3 跨目标兼容:写之前先想清楚边界
如果你的扩展打算在多个目标上用,有几个地方要提前设计好。
一是引脚定义。不同目标的引脚枚举不完全一样,直接用某一个目标的枚举类型会导致在别的目标上编译失败。稳妥的做法是自己定一层抽象,或者用数字引脚号加转换函数。
二是底层 API。pins、control这些核心库在不同目标上的实现有差异,尤其是时序相关的函数,行为可能不一致。写的时候尽量只用最基础、最稳定的那几个接口。
三是积木文本。文案里不要出现具体板子的名字,比如"设置板载 LED",换成"设置状态指示灯",这样换目标时不用改文案。
四是测试。跨目标不是"应该能跑",是"必须实测"。我见过太多扩展声称支持多个目标,结果在第二个目标上连编译都过不去。宁可少写一个目标,也别写上去然后让用户踩坑。
提示:跨目标扩展的维护成本是指数级上升的。如果你的扩展只对某一块板子有意义,就老老实实只支持那一个,这样后面升级维护会轻松很多。
6.4 文档写得比代码好,用的人会多一倍
最后说一个容易被忽略但极其影响成败的点:文档。
扩展的README.md会直接渲染在扩展页面里,用户点开扩展看到的第一屏就是它。这一屏决定了他会不会继续往下看。我现在的写法是:开头三行说清楚这个扩展解决什么问题、需要什么硬件、有什么前置条件;然后一个完整的最小示例,代码块形式,能直接复制到 JavaScript 视图里跑起来;最后是逐个积木的说明,每个配一句人话解释加一个参数说明。
示例一定要能跑。我见过太多文档里的示例代码是残缺的,用户复制进去一堆红波浪线,第一印象就毁了。发布前把示例完整复制到一个新项目里跑一遍,这个动作不能省。
还有一个细节:给积木加上help注解,指向文档里的锚点,用户右键点积木就能跳到对应说明。这个功能用的人不多,但用上的人会觉得你这扩展做得很到位。
7. 我个人的几条实践体会
做扩展这件事,技术门槛其实不高,真正拉开差距的是"站在使用者角度想问题"的能力。我做完第一个扩展之后,找了个完全不懂代码的朋友试了一下,他在三块积木上卡住了——不是因为功能不对,而是因为积木的名字他看不懂。那次之后我改掉了所有"技术味"太重的命名,把setRegisterValue改成"设置输出强度",把initDriver改成"启动模块"。改完之后,同样的功能,理解成本降了一大截。
另一个体会是,别一开始就追求功能完整。我的习惯是先做三个积木,能跑通主流程就行,然后拿去做一个真实的小项目。做的过程中自然会冒出新需求,这时候再加积木,加出来的每一个都是被真实场景验证过的。反过来,先设计一套"完整"的 API 再去找用途,做出来的东西往往有一半是没人用的。
最后一个技巧分享给要做教学扩展的人:给每个积木配一个"最小可用示例项目",放在仓库的examples目录里(清单文件的files里也要列上)。用户在扩展页面能直接看到示例项目并一键加载,这比读十页文档有效得多。我给一个模拟课程做的扩展加了四个示例之后,使用量明显上来了,反馈里的"不知道怎么用"几乎消失了。
至于后续还能怎么扩展,我自己的路线是往"组合积木"方向走——把几个基础积木打包成一个高层动作,让用户一次拖拽就完成一整段逻辑。这条路走通了,扩展就从"工具箱"变成了"脚手架",价值会上一个台阶。