Node-RED 自定义节点开发

Node-RED 自定义节点开发完全指南

基于项目实战经验总结,Node-RED 5.x + amqplib 2.x


目录

  1. Node-RED 架构概览
  2. 项目目录结构
  3. 自定义节点开发
  4. flows.json 流程定义
  5. settings.js 配置
  6. 启动与部署
  7. 调试与排错
  8. 实战案例:AMQP 节点
  9. 常见坑与最佳实践

1. Node-RED 架构概览

Node-RED 是基于 Node.js 的可视化编程工具,核心架构:

┌─────────────────────────────────────────────┐
│                 Browser (Editor)             │
│  HTML templates + JS (client-side)           │
└──────────────────┬──────────────────────────┘
                   │ HTTP/WebSocket
┌──────────────────▼──────────────────────────┐
│              Node-RED Runtime                │
│  ┌─────────┐ ┌──────────┐ ┌──────────────┐  │
│  │ Flows   │ │  Nodes   │ │  Settings    │  │
│  │ (JSON)  │ │ (JS+HTML)│ │  (settings.js)│ │
│  └─────────┘ └──────────┘ └──────────────┘  │
│  ┌──────────────────────────────────────┐   │
│  │  HTTP Admin API (httpAdminRoot)      │   │
│  │  HTTP Node API  (httpNodeRoot)       │   │
│  │  Static Files   (editor-client)      │   │
│  └──────────────────────────────────────┘   │
└─────────────────────────────────────────────┘

关键概念

  • Node(节点):一个功能单元,由 JS(运行时逻辑)+ HTML(编辑器 UI)组成
  • Flow(流程):节点的连接关系,存储为 flows.json
  • Config Node(配置节点):共享配置(如 MQTT broker 连接),被多个节点引用
  • User Directory:Node-RED 存储用户数据的目录(flows.json、settings.js、node_modules 等)

2. 项目目录结构

Node-RED/                         ← 项目根目录
├── red.js                        ← Node-RED 入口文件(启动脚本)
├── lib/                          ← Node-RED 核心库
├── settings.js                   ← 用户配置文件
├── node_modules/                 ← 全局依赖
│   ├── @node-red/                ← Node-RED 核心包
│   │   ├── runtime/              ← 运行时引擎
│   │   ├── editor-api/           ← 编辑器后端 API
│   │   └── editor-client/        ← 编辑器前端(HTML/JS/CSS)
│   │       ├── public/           ← 静态资源
│   │       │   ├── red/          ← 编辑器核心 JS
│   │       │   │   ├── red.js    ← 编辑器主入口
│   │       │   │   └── main.js   ← 编辑器主逻辑
│   │       │   ├── vendor/       ← 第三方库
│   │       │   └── favicon.ico
│   │       └── templates/
│   │           └── index.mst     ← 编辑器 HTML 模板
│   ├── amqplib/                  ← AMQP 客户端库
│   ├── express/                  ← HTTP 框架
│   └── node-red-contrib-hmes-amqp/  ← 自定义节点包
│       ├── package.json          ← 包定义
│       ├── amqp-hmes.js          ← 节点运行时逻辑
│       └── amqp-hmes.html        ← 节点编辑器 UI
└── nodered/                      ← User Directory
    ├── flows.json                ← 流程定义(核心)
    ├── flows_cred.json           ← 凭据(加密)
    ├── .config.nodes.json        ← 已安装节点注册表
    ├── .config.runtime.json      ← 运行时配置缓存
    ├── settings.js               ← 备用配置
    └── node_modules/             ← 项目级依赖
        ├── node-red-contrib-hmes-amqp/
        └── amqplib/

关键路径说明

文件 作用 能否手动编辑
red.js Node-RED 入口,初始化 HTTP 服务、加载 settings 不建议
settings.js 配置端口、流文件路径、认证等
flows.json 流程拓扑+节点配置 ✅(或通过编辑器)
.config.nodes.json 节点类型注册表(自动生成)
node_modules/ npm 安装的包 通过 npm 管理

3. 自定义节点开发

3.1 包结构

创建一个 Node-RED 自定义节点 = 创建一个 npm 包。最小结构:

node-red-contrib-my-node/
├── package.json          ← 必须包含 node-red 字段
├── my-node.js            ← 运行时逻辑(Node.js 端)
└── my-node.html          ← 编辑器 UI(Browser 端)

3.2 package.json

{
  "name": "node-red-contrib-my-node",
  "version": "1.0.0",
  "description": "My custom Node-RED node",
  "main": "my-node.js",
  "node-red": {
    "nodes": {
      "my-node": "my-node.js"
    }
  },
  "dependencies": {
    "amqplib": "^2.0.1"
  }
}

关键字段

  • node-red.nodes:注册节点类型,格式为 "节点类型名": "JS文件路径"
  • 一个 JS 文件可以注册多个节点类型(如 hmes-amqp-inhmes-amqp-out 共用一个 JS 文件)
  • main 字段对 Node-RED 不重要,node-red.nodes 才是关键

3.3 节点 JS 文件(运行时)

// my-node.js
module.exports = function(RED) {
    // 注册节点类型
    function MyNode(config) {
        RED.nodes.createNode(this, config);
        var node = this;
        
        // 读取配置(来自 HTML 表单或 flows.json)
        var topic = config.topic;
        var interval = config.interval;
        
        // 节点启动逻辑
        node.status({ fill: "green", shape: "dot", text: "connected" });
        
        // 处理输入消息
        node.on('input', function(msg) {
            // msg.payload = 输入数据
            // msg.topic = 消息主题
            node.send(msg);  // 转发到下一个节点
        });
        
        // 节点关闭时清理
        node.on('close', function() {
            // 关闭连接、清除定时器等
        });
    }
    
    RED.nodes.registerType("my-node", MyNode);
    
    // 可选:注册配置节点
    function MyConfigNode(config) {
        RED.nodes.createNode(this, config);
        this.host = config.host;
        this.port = config.port;
    }
    RED.nodes.registerType("my-config-node", MyConfigNode);
};

关键 API

API 说明
RED.nodes.createNode(this, config) 必须调用,初始化节点
config.xxx 读取 HTML 表单中 id="node-input-xxx" 的值
node.send(msg) 向下游节点发送消息
node.error(msg) 报错(红色状态)
node.warn(msg) 警告
node.log(msg) 日志
node.status({fill, shape, text}) 设置节点状态指示器
node.on('input', fn) 监听输入消息
node.on('close', fn) 节点关闭时清理资源

状态指示器

node.status({ fill: "green", shape: "dot", text: "connected" });
// fill: "red" | "green" | "yellow" | "blue" | "grey"
// shape: "ring" | "dot" | "dot"(实心)
// text: 显示的文字

3.4 节点 HTML 文件(编辑器 UI)

HTML 文件包含两部分:JavaScript 注册 + HTML 模板。

<!-- 客户端 JS:注册节点类型到编辑器 -->
<script type="text/javascript">
    RED.nodes.registerType('my-node', {
        category: 'network',           // 调色板分类
        color: '#E9967A',              // 节点颜色
        defaults: {                    // 默认值和验证
            name: { value: '' },
            topic: { value: '', required: true },
            interval: { value: 1000, validate: RED.validators.number() }
        },
        inputs: 1,                     // 输入端口数
        outputs: 1,                    // 输出端口数
        icon: 'feed.png',              // 图标
        label: function() {            // 节点上显示的文字
            return this.name || this.topic || 'my-node';
        },
        paletteLabel: 'my node'        // 调色板中显示的文字
    });
</script>

<!-- HTML 模板:编辑器中的属性面板 -->
<script type="text/html" data-template-name="my-node">
    <div class="form-row">
        <label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
        <input type="text" id="node-input-name" placeholder="Name">
    </div>
    <div class="form-row">
        <label for="node-input-topic"><i class="fa fa-envelope"></i> Topic</label>
        <input type="text" id="node-input-topic" placeholder="my/topic">
    </div>
    <div class="form-row">
        <label for="node-input-interval"><i class="fa fa-clock-o"></i> Interval</label>
        <input type="text" id="node-input-interval" placeholder="1000">
    </div>
</script>

HTML 规则

规则 说明
<script type="text/html" data-template-name="xxx"> 模板名必须与 registerType 的类型名一致
id="node-input-xxx" 必须匹配 defaults 中的属性名(前缀 node-input-
id="node-config-input-xxx" 配置节点的输入框前缀是 node-config-input-
<div class="form-row"> 标准表单行样式
<i class="fa fa-xxx"> Font Awesome 图标(Node-RED 内置)

配置节点的 HTML

<!-- 配置节点模板前缀是 node-config-input -->
<script type="text/html" data-template-name="my-config-node">
    <div class="form-row">
        <label for="node-config-input-host"><i class="fa fa-server"></i> Host</label>
        <input type="text" id="node-config-input-host" placeholder="127.0.0.1">
    </div>
</script>

3.5 属性名匹配(最容易出错!)

核心原则:HTML 中的 id、JS 中的 config.xxx、flows.json 中的属性名必须三者一致。

// HTML 中
<input id="node-input-hostname" ...>

// JS 中读取
config.hostname  // ← 自动从 flows.json 读取同名属性

// flows.json 中
{ "hostname": "127.0.0.1" }

错误示例

// ❌ HTML 用 "host",flows.json 用 "hostname" → 找不到值
<input id="node-input-host">
// flows.json: { "hostname": "127.0.0.1" }
config.host  // undefined!

4. flows.json 流程定义

4.1 结构

[
  {
    "id": "tab.main",
    "type": "tab",
    "label": "主流程",
    "disabled": false,
    "info": ""
  },
  {
    "id": "node.1",
    "type": "mqtt-broker",
    "z": "tab.main",
    "name": "MQTT Broker",
    "broker": "127.0.0.1",
    "port": "1883"
  },
  {
    "id": "node.2",
    "type": "mqtt in",
    "z": "tab.main",
    "name": "MQTT Subscribe",
    "topic": "test/topic",
    "qos": "0",
    "datatype": "json",
    "broker": "node.1",
    "wires": [["node.3"]]
  },
  {
    "id": "node.3",
    "type": "debug",
    "z": "tab.main",
    "name": "Debug",
    "active": true,
    "wires": []
  }
]

字段说明

字段 说明
id 节点唯一 ID(字符串)
type 节点类型(对应 registerType 的名称)
z 所属流程 tab 的 ID
wires 连接关系:[["下游节点ID"]],二维数组对应输出端口
其他属性 节点自定义配置(与 defaults 对应)

4.2 wires 连接规则

// wires 是二维数组:[输出端口0的连接, 输出端口1的连接, ...]
"wires": [["node.b"]]           // 端口0 连到 node.b
"wires": [["node.b"], ["node.c"]]  // 端口0→node.b, 端口1→node.c
"wires": [["node.b", "node.c"]] // 端口0 同时连到 node.b 和 node.c
"wires": [[]]                   // 端口0 不连任何节点

4.3 配置节点在 flows.json 中

配置节点也是 flows.json 的一个条目,被其他节点通过 ID 引用:

{
  "id": "amqp-conn.1",
  "type": "hmes-amqp-connection",
  "host": "127.0.0.1",
  "port": 5672,
  "user": "guest",
  "password": "guest",
  "vhost": "/"
},
{
  "id": "amqp-in.1",
  "type": "hmes-amqp-in",
  "connection": "amqp-conn.1",   ← 引用配置节点 ID
  "queue": "my.queue",
  "wires": [["next.node"]]
}

注意:如果你的节点不在 HTML 中定义 config node,而是直接在节点上放连接属性(如我们的 AMQP 节点),则不需要单独的配置节点条目。


5. settings.js 配置

module.exports = {
    // 流程文件名
    flowFile: 'flows.json',
    
    // 格式化 JSON(便于手动编辑)
    flowFilePretty: true,
    
    // 凭据加密密钥(false = 使用默认密钥)
    credentialSecret: false,
    
    // HTTP 端口
    uiPort: 1880,
    
    // HTTP 根路径(默认 "/")
    httpAdminRoot: "/",
    
    // 用户目录(默认 ~/.node-red,可覆盖)
    // userDir: "/path/to/userDir",
    
    // MQTT 重连时间(毫秒)
    mqttReconnectTime: 15000,
    
    // 编辑器认证(可选)
    // httpAdminAuth: { user: "admin", pass: "password" }
};

6. 启动与部署

6.1 启动命令

# 标准启动
node red.js

# 指定端口
node red.js --port 1881

# 指定 settings 文件
node red.js --settings /path/to/settings.js

# 指定用户目录
node red.js --userDir /path/to/nodered

# 后台启动(Windows)
Start-Process -FilePath 'node' -ArgumentList 'red.js' -WindowStyle Hidden

# 后台启动(Linux)
nohup node red.js &

6.2 启动流程

red.js 启动
  ├── 1. 解析命令行参数
  ├── 2. 加载 settings.js
  ├── 3. 设置 userDir(默认 __dirname/nodered)
  ├── 4. 创建 HTTP 服务器(Express)
  ├── 5. 初始化 Node-RED Runtime
  │     ├── 加载 node_modules 中的节点
  │     ├── 读取 .config.nodes.json(节点注册表)
  │     └── 加载 flows.json
  ├── 6. 挂载路由
  │     ├── httpAdminRoot → 编辑器 UI
  │     ├── httpNodeRoot → 用户 HTTP 节点
  │     └── 静态文件 → editor-client/public
  ├── 7. 启动 HTTP 监听
  └── 8. 执行流程(节点开始工作)

6.3 热重载

Node-RED 支持部分热重载:

  • flows.json 修改:编辑器中点 Deploy 即可,不需要重启
  • 节点代码修改(JS/HTML):必须重启 Node-RED
  • settings.js 修改:必须重启
  • npm 安装新包:必须重启

7. 调试与排错

7.1 常见错误

7.1.1 JSON 解析错误

SyntaxError: Unexpected token '', "{..." is not valid JSON

原因:文件有 UTF-8 BOM(字节序标记 0xEF 0xBB 0xBF)。

解决

# 检查是否有 BOM
$b = [System.IO.File]::ReadAllBytes('flows.json')
$b[0..2] -join ' '  # 如果是 239 187 191 就是 BOM

# 移除 BOM
$nb = New-Object byte[]($b.Length - 3)
[Array]::Copy($b, 3, $nb, 0, $nb.Length)
[System.IO.File]::WriteAllBytes('flows.json', $nb)

避免方法:写文件时用 UTF8Encoding(false, false)(无 BOM)。

7.1.2 MIME 类型错误

Refused to execute script because its MIME type ('text/html') is not executable

原因:Node-RED 找不到静态文件(如 red/red.js),返回了 HTML 错误页面。

解决:检查 @node-red/editor-client/public/ 目录是否完整。

# 检查关键文件是否存在
Test-Path "node_modules/@node-red/editor-client/public/red/red.js"
Test-Path "node_modules/@node-red/editor-client/public/vendor/vendor.js"
Test-Path "node_modules/@node-red/editor-client/templates/index.mst"

7.1.3 节点属性面板打不开

原因:HTML 模板中的 data-template-nameregisterType 的类型名不匹配。

排查

// JS 注册的类型名
RED.nodes.registerType('hmes-amqp-in', ...)  // ← 类型名

// HTML 模板名
<script type="text/html" data-template-name="hmes-amqp-in">  // ← 必须一致

7.1.4 节点属性值为 undefined

原因:HTML id、JS config.xxx、flows.json 属性名三者不一致。

// HTML
<input id="node-input-hostname">

// JS 读取
config.hostname  // 必须与 HTML id 的 node-input- 后面部分一致

// flows.json
{ "hostname": "127.0.0.1" }  // 必须与 HTML id 一致

7.2 调试技巧

// 在节点 JS 中输出调试信息
node.log("Debug: " + JSON.stringify(config));
node.warn("Warning: " + msg.payload);
node.error("Error: " + err.message, msg);  // 第二个参数会附加到错误信息

// 在编辑器 JS 中调试
console.log("Node config:", this);  // this 包含节点所有属性

// 查看 Node-RED 内部状态
// 浏览器控制台输入:
RED.nodes.depot  // 所有已部署的节点
RED.nodes.workspace  // 工作空间信息

7.3 查看 Node-RED 日志

Node-RED 日志输出到 stdout。启动时重定向到文件:

node red.js > nodered.log 2>&1

日志级别控制:

# 详细日志
node red.js --verbose

# 或在 settings.js 中
settings.verbose = true;

8. 实战案例:AMQP 节点

8.1 需求

创建 hmes-amqp-inhmes-amqp-out 两个节点,用于 RabbitMQ 消息收发。

8.2 完整代码

package.json

{
  "name": "node-red-contrib-hmes-amqp",
  "version": "1.0.1",
  "description": "HMES AMQP nodes for RabbitMQ",
  "main": "amqp-hmes.js",
  "node-red": {
    "nodes": {
      "hmes-amqp-in": "amqp-hmes.js",
      "hmes-amqp-out": "amqp-hmes.js"
    }
  },
  "dependencies": {
    "amqplib": "^2.0.1"
  }
}

amqp-hmes.js(核心逻辑):

const amqplib = require('amqplib');

module.exports = function(RED) {

    // 从配置中提取连接参数(兼容 config node 和直接属性两种方式)
    function getConnOpts(config) {
        if (config.hostname) {
            return {
                host: config.hostname,
                port: parseInt(config.port) || 5672,
                user: config.username || 'guest',
                password: config.password || 'guest',
                vhost: config.vhost || '/'
            };
        }
        return null;
    }

    // ========== AMQP In 节点 ==========
    function AmqpInNode(config) {
        RED.nodes.createNode(this, config);
        var node = this;
        var queue = config.queue || '';
        var pfCount = parseInt(config.prefetch) || 10;
        var opts = getConnOpts(config);
        var conn = null, ch = null, closing = false, retryTimer = null;

        function scheduleRetry() {
            if (!closing && !retryTimer) {
                retryTimer = setTimeout(function() {
                    retryTimer = null;
                    doConnect();
                }, 5000);
            }
        }

        async function doConnect() {
            if (closing || !opts) return;
            try {
                var url = 'amqp://' + opts.user + ':' + opts.password +
                          '@' + opts.host + ':' + opts.port + '/' + opts.vhost;
                conn = await amqplib.connect(url);
                ch = await conn.createChannel();
                await ch.prefetch(pfCount);
                await ch.assertQueue(queue, { durable: true });
                node.status({ fill: 'green', shape: 'dot', text: 'connected' });

                ch.consume(queue, function(msg) {
                    if (!msg) return;
                    var payload = msg.content;
                    try { payload = JSON.parse(payload.toString()); } catch(e) {}
                    node.send({
                        payload: payload,
                        _amqpMsg: msg,      // 保留原始消息用于 ack
                        _channel: ch         // 保留通道引用
                    });
                }, { noAck: false });

                conn.on('error', function(err) {
                    node.error('AMQP error: ' + err.message);
                });
                conn.on('close', function() {
                    ch = null; conn = null;
                    if (!closing) {
                        node.status({ fill: 'yellow', shape: 'ring', text: 'reconnecting' });
                        scheduleRetry();
                    }
                });
            } catch(err) {
                node.error('AMQP connect failed: ' + err.message);
                node.status({ fill: 'red', shape: 'ring', text: 'retrying' });
                if (conn) { try { await conn.close(); } catch(e) {} conn = null; }
                scheduleRetry();
            }
        }

        // 收到下游处理完的消息时 ack
        node.on('input', function(msg) {
            if (msg._amqpMsg && msg._channel) {
                try { msg._channel.ack(msg._amqpMsg); } catch(e) {}
                delete msg._amqpMsg;
                delete msg._channel;
            }
        });

        node.on('close', function() {
            closing = true;
            if (retryTimer) { clearTimeout(retryTimer); retryTimer = null; }
            try { if (ch) ch.close(); } catch(e) {}
            try { if (conn) conn.close(); } catch(e) {}
            conn = null; ch = null;
        });

        doConnect();
    }
    RED.nodes.registerType('hmes-amqp-in', AmqpInNode);

    // ========== AMQP Out 节点 ==========
    function AmqpOutNode(config) {
        RED.nodes.createNode(this, config);
        var node = this;
        var queue = config.queue || '';
        var exchange = config.exchange || '';
        var routingKey = config.routingKey || '';
        var opts = getConnOpts(config);
        var conn = null, ch = null, closing = false, retryTimer = null;

        function scheduleRetry() { /* 同上 */ }

        async function doConnect() {
            if (closing || !opts) return;
            try {
                var url = 'amqp://' + opts.user + ':' + opts.password +
                          '@' + opts.host + ':' + opts.port + '/' + opts.vhost;
                conn = await amqplib.connect(url);
                ch = await conn.createChannel();
                if (queue) await ch.assertQueue(queue, { durable: true });
                node.status({ fill: 'green', shape: 'dot', text: 'connected' });

                conn.on('close', function() {
                    ch = null; conn = null;
                    if (!closing) scheduleRetry();
                });
            } catch(err) {
                node.error('AMQP connect failed: ' + err.message);
                scheduleRetry();
            }
        }

        node.on('input', async function(msg) {
            if (!ch) { node.warn('channel not ready'); return; }
            try {
                var content = typeof msg.payload === 'string'
                    ? msg.payload
                    : JSON.stringify(msg.payload);
                var buf = Buffer.from(content);
                var rk = msg.topic || routingKey || queue;
                if (exchange) ch.publish(exchange, rk, buf);
                else if (queue) ch.sendToQueue(queue, buf);
            } catch(err) {
                node.error('AMQP publish error: ' + err.message);
            }
        });

        node.on('close', function() { /* 同上 */ });
        doConnect();
    }
    RED.nodes.registerType('hmes-amqp-out', AmqpOutNode);
};

amqp-hmes.html

<script type="text/javascript">
    RED.nodes.registerType('hmes-amqp-in', {
        category: 'HMES',
        color: '#88C999',
        defaults: {
            name: { value: '' },
            hostname: { value: '127.0.0.1', required: true },
            port: { value: 5672, required: true, validate: RED.validators.number() },
            username: { value: 'guest' },
            password: { value: 'guest' },
            vhost: { value: '/' },
            queue: { value: '', required: true },
            prefetch: { value: 5000, validate: RED.validators.number() }
        },
        inputs: 1,
        outputs: 1,
        icon: 'bridge.svg',
        label: function() { return this.name || this.queue || 'amqp-in'; },
        paletteLabel: 'amqp in'
    });
</script>
<script type="text/html" data-template-name="hmes-amqp-in">
    <div class="form-row">
        <label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
        <input type="text" id="node-input-name" placeholder="Name">
    </div>
    <div class="form-row">
        <label for="node-input-hostname"><i class="fa fa-server"></i> Host</label>
        <input type="text" id="node-input-hostname" placeholder="127.0.0.1">
    </div>
    <div class="form-row">
        <label for="node-input-port"><i class="fa fa-plug"></i> Port</label>
        <input type="text" id="node-input-port" placeholder="5672">
    </div>
    <div class="form-row">
        <label for="node-input-username"><i class="fa fa-user"></i> User</label>
        <input type="text" id="node-input-username" placeholder="guest">
    </div>
    <div class="form-row">
        <label for="node-input-password"><i class="fa fa-lock"></i> Password</label>
        <input type="password" id="node-input-password" placeholder="guest">
    </div>
    <div class="form-row">
        <label for="node-input-vhost"><i class="fa fa-folder"></i> VHost</label>
        <input type="text" id="node-input-vhost" placeholder="/">
    </div>
    <div class="form-row">
        <label for="node-input-queue"><i class="fa fa-inbox"></i> Queue</label>
        <input type="text" id="node-input-queue" placeholder="telemetry.nodered.input">
    </div>
    <div class="form-row">
        <label for="node-input-prefetch"><i class="fa fa-sort-amount-down"></i> Prefetch</label>
        <input type="text" id="node-input-prefetch" placeholder="5000">
    </div>
</script>

<!-- hmes-amqp-out 类似,省略 -->

8.3 关键设计决策

决策 选择 原因
连接方式 直接属性 vs config node flows.json 已有直接属性,保持兼容
Prefetch 类型 parseInt() 强转 flows.json 中属性可能是字符串
重连机制 setTimeout + closing 标志 避免并发重连
消息确认 保留 _amqpMsg + _channel 下游处理完再 ack,不丢失消息
BOM 处理 写文件时 UTF8Encoding(false) 避免 Node-RED JSON 解析失败

9. 常见坑与最佳实践

9.1 坑:npm install 后自定义节点消失

# ❌ npm install 会清理 node_modules,自定义节点被删除
npm install amqplib

# ✅ 先保存到 package.json,再安装
npm install amqplib --save

恢复方法:重新创建节点文件(建议用 git 管理自定义节点代码)。

9.2 坑:BOM 导致 JSON 解析失败

# PowerShell 5.1 的 Set-Content -Encoding UTF8 会加 BOM
Set-Content -Encoding UTF8 file.json "..."  # ❌ 有 BOM

# ✅ 用 .NET 方法写无 BOM 的 UTF-8
$enc = New-Object System.Text.UTF8Encoding($false, $true)
[System.IO.File]::WriteAllText('file.json', $content, $enc)

9.3 坑:amqplib 2.x API 变化

// amqplib 0.x(旧)
var conn = amqplib.connect('amqp://localhost');
conn.then(function(conn) { ... });

// amqplib 2.x(新)
var conn = await amqplib.connect('amqp://localhost');
var ch = await conn.createChannel();

9.4 坑:prefetch 参数类型

// ❌ flows.json 中 "prefetch": "5000"(字符串)会导致错误
await ch.prefetch(config.prefetch);

// ✅ 强制转为数字
await ch.prefetch(parseInt(config.prefetch) || 10);

9.5 坑:旧进程残留

# 杀掉所有旧的 node 进程
Get-Process -Name 'node' | Stop-Process -Force
Start-Sleep -Seconds 2
# 再启动新的

9.6 最佳实践

  1. 自定义节点用 git 管理,不要依赖 node_modules 中的自定义文件
  2. 写文件始终无 BOMnew UTF8Encoding(false, true)
  3. 配置值强转类型parseInt()Number()Boolean()
  4. 异步操作加 try-catch:AMQP 连接、数据库操作等
  5. 重连机制用标志位:避免 closeerror 事件并发触发多次重连
  6. 清理资源在 on('close'):关闭连接、清除定时器
  7. 编辑器 UI 属性名与 flows.json 严格一致
  8. 部署前验证 JSONConvertFrom-Json 或在线工具

附录:Node-RED 内置节点分类

分类 常用节点
function function, change, switch, template
network http in/out/request, mqtt in/broker
input inject, serial, tcp
output debug, mqtt out, tcp out
storage file in/out, csv
social email, websocket
列表 comment, link, junction

最后更新:2026-08-24
适用版本:Node-RED 5.x + amqplib 2.x

posted @ 2026-08-24 13:18  古月秋筠  阅读(6)  评论(0)    收藏  举报