2、自定义节点开发(上):节点结构、HTML定义、JavaScript逻辑、package.json配置
说实话,很多Node-RED用户用了一两年,还是在拖拽现成节点。但真正让Node-RED发挥威力的,恰恰是自定义节点开发。我自己带团队做物联网项目时,经常遇到标准节点搞不定的场景——比如对接某个私有协议的传感器,或者需要做复杂的边缘计算逻辑。这时候,写一个自定义节点,往往比拼凑十几个标准节点要优雅得多。
这一章,咱们就聊聊自定义节点的骨架。嗯,说白了就是四个部分:节点结构长什么样、HTML怎么定义界面、JavaScript怎么写逻辑、package.json怎么配。搞懂这些,你就能写出第一个真正属于自己的节点了。
2.1 节点结构:从文件到文件夹
我刚开始写自定义节点时,犯过一个低级错误——把所有代码塞进一个文件里。后来项目大了,维护起来简直想哭。你想想看,一个节点既要处理UI,又要处理业务逻辑,还要管配置,全挤在一起,改个颜色都要翻半天。
正确的做法是这样的:
my-custom-node/
├── package.json # 节点元信息
├── my-node.html # 节点界面定义
├── my-node.js # 节点逻辑代码
└── icons/ # 图标文件夹(可选)
└── my-icon.png
我个人习惯把HTML和JS分开。虽然Node-RED官方允许你把它们写在一个文件里,但分开后,前端和后端的逻辑边界更清晰。特别是当你需要多人协作时,前端工程师改HTML,后端工程师改JS,互不干扰。
| 文件 | 作用 | 谁负责 |
|---|---|---|
| package.json | 声明节点名称、版本、依赖 | 项目初始化时配置 |
| my-node.html | 定义节点在编辑器中的外观、配置面板 | 前端开发者 |
| my-node.js | 实现节点的运行时逻辑 | 后端/全栈开发者 |
2.2 HTML定义:节点长什么样
HTML文件定义了节点在编辑器里的样子。说白了,就是三个部分:节点图标、配置对话框、帮助文本。
来看一个最简单的例子:
<script type="text/html" data-template-name="my-sensor">
<div class="form-row">
<label for="node-input-name">名称<i class="fa fa-asterisk"></i></label>
<input type="text" id="node-input-name" placeholder="传感器名称">
</div>
<div class="form-row">
<label for="node-input-interval">采集间隔(秒)</label>
<input type="number" id="node-input-interval" min="1" value="10">
</div>
</script>
<script type="text/html" data-help-name="my-sensor">
<p>这是一个自定义传感器节点,用于采集环境数据。</p>
<h3>配置说明</h3>
<ul>
<li><b>名称</b>:给节点起个易识别的名字</li>
<li><b>采集间隔</b>:每隔多少秒采集一次数据</li>
</ul>
</script>
<script type="text/javascript">
RED.nodes.registerType('my-sensor', {
category: 'input',
color: '#a6bbcf',
defaults: {
name: { value: '' },
interval: { value: 10, required: true }
},
inputs: 0,
outputs: 1,
icon: 'my-icon.png',
label: function() {
return this.name || 'my-sensor';
}
});
</script>
这里有几个关键点,我得提醒你注意:
- data-template-name:这个属性值必须和节点类型名一致。我曾经因为拼写错误,折腾了半小时找不到节点——编辑器里死活不显示配置面板。
- data-help-name:帮助文档。很多人忽略这个,但团队里新人上手时,帮助文档能省不少事。
- RED.nodes.registerType:注册节点的核心函数。第二个参数里的
defaults定义了节点的默认配置,这些配置会保存到流程文件中。
defaults里的每个属性,都会自动映射到HTML中对应id的元素。比如name属性对应node-input-name。命名规则是"node-input-属性名"。搞错了,配置就存不进去。
2.3 JavaScript逻辑:节点怎么干活
JS文件是节点的灵魂。它定义了节点接收到消息后做什么,以及如何输出消息。
来看对应的JS代码:
module.exports = function(RED) {
function MySensorNode(config) {
RED.nodes.createNode(this, config);
// 保存配置
this.name = config.name;
this.interval = config.interval || 10;
// 节点实例化时的逻辑
var node = this;
// 定时采集
var timer = setInterval(function() {
var msg = {
payload: {
temperature: Math.random() * 30 + 10,
humidity: Math.random() * 50 + 30,
timestamp: Date.now()
}
};
node.send(msg);
}, this.interval * 1000);
// 清理资源
node.on('close', function() {
clearInterval(timer);
});
}
RED.nodes.registerType('my-sensor', MySensorNode);
};
这段代码做了几件事:
- 构造函数:
MySensorNode接收config参数,里面包含了用户在配置面板里填的值。 - 创建节点:
RED.nodes.createNode(this, config)是必须调用的,它初始化了节点的生命周期。 - 业务逻辑:这里模拟了一个定时采集传感器数据的场景。实际项目中,你可能需要调用硬件API或者读取串口数据。
- 清理资源:
node.on('close')里释放定时器。这个很重要——不清理的话,节点删除了定时器还在跑,内存泄漏就来了。
2.4 package.json配置:节点的身份证
package.json告诉Node-RED:这个包里有哪些节点,叫什么名字,依赖什么库。
一个标准的配置长这样:
{
"name": "node-red-contrib-my-sensor",
"version": "1.0.0",
"description": "自定义传感器节点",
"node-red": {
"nodes": {
"my-sensor": "my-sensor.js"
}
},
"keywords": ["node-red", "sensor", "iot"],
"dependencies": {
"serialport": "^10.0.0"
}
}
这里有个命名规范,我建议你遵守:
- 包名以
node-red-contrib-开头。这是社区约定,方便别人搜索。 node-red.nodes字段里,键是节点类型名,值是JS文件路径。- 如果节点依赖第三方库(比如串口通信的serialport),写在
dependencies里。
| 字段 | 说明 | 示例 |
|---|---|---|
| name | 包名,全局唯一 | node-red-contrib-my-sensor |
| node-red.nodes | 声明节点类型和入口文件 | {"my-sensor": "my-sensor.js"} |
| keywords | 方便在Node-RED库中搜索 | ["node-red", "sensor"] |
nodes里列出多个。比如:{"sensor-a": "sensor-a.js", "sensor-b": "sensor-b.js"}。每个JS文件里可以注册多个节点类型,但建议一个文件只负责一个类型,保持清晰。
2.5 知识体系总览
说了这么多,咱们用一张图把整个结构串起来。这张图是我自己画课时常用的,能帮你快速建立整体认知:
这张图把三个模块的关系讲得很清楚了。HTML负责"长什么样",JS负责"怎么干活",package.json负责"我是谁"。三者通过registerType这个纽带连接在一起。
嗯,这一章的内容就到这儿。记住,自定义节点开发是Node-RED进阶的分水岭。跨过这道坎,你就不再是"只会拖拽的用户",而是"能造轮子的开发者"了。