自定义节点开发(下):配置节点、状态管理、节点间通信、发布到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>
我的小技巧:label函数一定要写好。用户选配置时,靠的就是这个标签。我见过太多人随便写个"MQTT配置",结果十几个配置根本分不清哪个是哪个。

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 发布流程

  1. 在npm官网注册账号
  2. 终端执行npm login登录
  3. 在项目根目录执行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让成果可分享。说实话,自定义节点开发最难的不是写代码,而是设计——怎么让节点好用、易用、不容易出错。

下一章我们会深入节点测试和调试技巧,到时候我会分享一些压箱底的调试工具和方法。嗯,今天就先到这里,你先把这几个概念消化一下,动手写一个带配置节点的自定义节点试试。

我的建议:从最简单的配置节点开始——比如一个"服务器地址"配置节点,然后写一个普通节点引用它。跑通了再逐步加状态管理和事件通信。别想一口吃成胖子。

可可精品资料库