自定义节点开发(下):配置节点、状态管理、节点间通信、发布到npm
好,咱们接着聊。上一章我们把自定义节点的骨架搭好了,能跑起来了。但说实话,那只是「能用」的阶段。真正要做出一个像样的节点,你得搞定三件事:配置节点怎么设计、节点内部状态怎么管、多个节点之间怎么说话。最后,还得把它扔到npm上让别人也能用。
这一章,我就把这四个硬骨头一个一个啃给你看。
一、配置节点:别让用户瞎填
你想想看,一个节点如果所有参数都写在属性面板里,每次用都得重新填一遍,多烦人。配置节点就是干这个的——把公共参数抽出来,一次配置,到处引用。
我在做工业网关项目时,遇到过最典型的需求:MQTT broker的地址、端口、认证信息,十几个节点都要用。要是每个节点单独配,改一次地址得改十几个地方,迟早出事故。
1.1 配置节点的定义
配置节点本质上是一个特殊的Node类型。它不处理消息流,只负责存储共享配置。在package.json里,你得单独声明它:
{
"name": "node-red-contrib-my-mqtt",
"node-red": {
"nodes": {
"mqtt-broker": "mqtt-broker.js",
"mqtt-out": "mqtt-out.js"
}
}
}
这里mqtt-broker就是配置节点,mqtt-out是普通节点,它依赖mqtt-broker的配置。
1.2 配置节点的HTML定义
配置节点的HTML模板里,有个关键字段叫credentials,专门放敏感信息。我个人习惯把密码、证书这类东西放这里,而不是直接写在配置里。
<script type="text/javascript">
RED.nodes.registerType('mqtt-broker', {
category: 'config',
defaults: {
host: { value: "localhost", required: true },
port: { value: 1883, validate: RED.validators.number() },
clientId: { value: "" }
},
credentials: {
username: { type: "text" },
password: { type: "password" }
},
label: function() {
return this.host + ":" + this.port;
}
});
</script>
1.3 普通节点引用配置节点
普通节点怎么引用配置?在defaults里声明一个类型为配置节点名的字段就行:
defaults: {
broker: {
type: "mqtt-broker",
required: true
},
topic: { value: "" }
}
这样用户在属性面板里就能下拉选择已有的配置了。嗯,这里要注意:如果配置节点被删了,所有引用它的节点都会报错。所以我在项目中一般会加个校验,在oneditprepare里检查配置是否还存在。
二、状态管理:节点自己的小本本
节点运行时,经常需要记住一些东西——比如当前连接状态、上次发送时间、累计计数。Node-RED提供了this.context()来管理状态。
说白了,就是给每个节点发了一个小本本,它可以在上面记东西,下次消息来了还能翻出来看。
2.1 三种作用域
| 作用域 | 方法 | 生命周期 | 典型用途 |
|---|---|---|---|
| 节点级 | this.context() | 节点删除时消失 | 计数器、临时缓存 |
| 流级 | this.context().flow | 流部署时重置 | 跨节点共享临时数据 |
| 全局级 | this.context().global | Node-RED重启时重置 | 全局配置、共享连接池 |
2.2 状态持久化
默认情况下,这些状态存在内存里,一重启就没了。如果你需要持久化,可以用contextStorage:
// settings.js
contextStorage: {
default: {
module: "localfilesystem"
}
}
然后代码里这样用:
// 存
this.context().set("counter", 100, "default", function(err) {
if (err) { node.error("保存失败"); }
});
// 取
this.context().get("counter", "default", function(err, value) {
if (value) { node.log("上次计数:" + value); }
});
input事件里set完立刻get,很可能拿不到。我后来养成了习惯:set之后不立即依赖它,或者用回调确保写入完成。
三、节点间通信:让节点说上话
有时候,一个节点需要知道另一个节点的状态。比如,一个MQTT输入节点断线了,我想让旁边的状态灯节点变红。这就涉及到节点间通信了。
3.1 通过消息流通信
最直接的方式:用send()发消息给下游节点。但这种方式只能单向传递,而且必须连线。
// 在input事件里
this.send({ payload: "hello", topic: "greeting" });
3.2 通过Node-RED事件系统
如果节点之间没有连线,又想通信怎么办?Node-RED内部有个事件总线:
// 发送事件
RED.events.emit("my-custom-event", {
nodeId: this.id,
status: "connected"
});
// 监听事件(通常在节点的构造函数里注册)
RED.events.on("my-custom-event", function(data) {
if (data.nodeId !== node.id) {
node.status({ fill: "green", text: "其他节点已连接" });
}
});
我个人习惯用这种方式做节点间的状态同步。比如在智能家居项目里,一个网关节点收到设备上下线事件,通过事件广播给所有UI节点更新状态。
3.3 通过共享上下文
还有一种方式:用flow或global上下文做中间人。节点A写数据,节点B读数据:
// 节点A
this.context().flow.set("sensorValue", msg.payload);
// 节点B
var val = this.context().flow.get("sensorValue");
这种方式简单粗暴,但要注意命名冲突。我建议用节点类型做前缀,比如mqtt-sensor:temperature,避免不同节点互相覆盖。
四、发布到npm:让全世界用上你的节点
节点写好了,怎么分享?发布到npm。这个过程我走了不少弯路,给你总结一下关键步骤。
4.1 准备工作
首先,你的package.json必须满足几个硬性要求:
name必须以node-red-contrib-开头- 必须有
node-red字段,声明节点类型和文件路径 - 必须有
keywords,包含node-red
{
"name": "node-red-contrib-my-sensor",
"version": "1.0.0",
"description": "A custom sensor node for Node-RED",
"keywords": ["node-red", "sensor", "iot"],
"node-red": {
"nodes": {
"sensor": "sensor.js"
}
}
}
4.2 发布流程
- 在npm官网注册账号
- 终端执行
npm login登录 - 在项目根目录执行
npm publish
就这么简单?嗯,理论上是的。但实际发布时,我遇到过几个坑:
- 版本号不能重复,每次发布前记得改
version - 如果包名被占用了,npm会报错。发布前先去npm搜一下
- 别忘了
.gitignore,别把node_modules也发上去了
4.3 安装测试
发布成功后,你可以用npm install node-red-contrib-my-sensor安装测试。如果一切正常,在Node-RED的调色板里就能找到你的节点了。
我记得第一次发布时,忘了加keywords,结果Node-RED的管理器里搜不到。折腾了半天才发现是这个问题。从那以后,我每次发布前都会用npm pack生成一个本地包,先本地安装测试一遍,确认没问题再正式发布。
五、本章小结
这一章我们聊了四个核心点:配置节点让参数复用、状态管理让节点有记忆、节点间通信让流程更灵活、发布到npm让成果可分享。说实话,自定义节点开发最难的不是写代码,而是设计——怎么让节点好用、易用、不容易出错。
下一章我们会深入节点测试和调试技巧,到时候我会分享一些压箱底的调试工具和方法。嗯,今天就先到这里,你先把这几个概念消化一下,动手写一个带配置节点的自定义节点试试。