Function 節點:安全 JavaScript 與錯誤處理
Function 適合用明確的 JavaScript 處理訊息、分配多個輸出與建立可擷取錯誤。本章只使用固定手動輸入與純資料運算,逐一釐清同步 return、非同步 node.send/node.done、lifecycle、context、觀察與 clone 邊界。
為何需要本章
Change、Switch、JSONata 與 Template 能完成大多數簡單轉換;當規則需要多個輸出、清楚的型別防衛、共用局部函式或一致錯誤訊息時,Function 才是合適工具。Function 能執行 JavaScript,也因此比宣告式節點更容易出現無界迴圈、共享狀態、重複送出與難以追蹤的非同步工作。
本章鎖定 Node-RED 5.0.2。這個版本的核心 Function 節點群包含 Function、Switch、Change、Range、Template、Delay、Trigger、Exec 與 RBE;本章只深入 Function lifecycle 與程式邊界。核心 Function runtime 將程式放在 sandbox 中,提供 msg、node 與 context API,並處理同步回傳與 Promise。這不是一般 Node.js 程式檔:你不能假設有 unrestricted require,也不應讓訊息內容決定要載入什麼 module。若只是建立一個新物件,先回顧第 15 章 JSONata 與模板;能用較小能力完成,就不要先選 Function。
一則訊息、三種完成方式
Node-RED 5.0.2 對每則進入 Function 的訊息建立執行工作。你可以同步 return 結果,或在非同步工作完成後呼叫 node.send。若程式明確使用 node.done(),就由程式負責在每條完成路徑呼叫它;若沒有使用,runtime 會在 Function 的結果完成後處理 done。不要在同一條路徑先 return 訊息、稍後又 node.send 同一結果,否則 downstream 會收到兩次。
| 模式 | 輸出 | 完成 | 適用情況 | 常見錯誤 |
|---|---|---|---|---|
同步 return msg | return 一個訊息或按輸出排列的陣列 | runtime 在 async function 結果 resolve 後完成 | 純轉換、立即驗證與路由 | return primitive、混用延後 node.send |
return null | 沒有輸出 | 同步路徑結束 | 明確丟棄或已報告不合格輸入 | 把 null 當成要送出的 payload |
node.send + node.done | 工作完成時送出訊息 | 所有成功與失敗路徑都要 done | 真正需要等待的非同步工作 | 漏 done、重複 done、又 return 訊息 |
node.error(err,msg) | 不等於一般輸出;可帶原訊息交給相符 Catch | 同步可接著 return null;非同步仍需結束工作 | 可恢復、可觀察的輸入或運算錯誤 | 只 log 字串、未附 msg、把敏感 payload 寫入錯誤 |
node.done() 呼叫。非同步 Function 不要替它建立 alias,也不要用 computed property 存取;每條完成路徑都直接呼叫一次 node.done()。Function 的輸出必須是 message object(訊息物件)。你可以把數字放在 msg.payload,但不能直接 return 42。_msgid 用於訊息追蹤,不要自行刪除或拿來當業務資料唯一鍵;輸入與訊息模型詳見第 5 章 msg 與資料型別。
從固定純函式 Flow 開始
10-pure-function.json 只有手動 Inject、Function 與 Debug。範例刻意讓 Function 的 initialize、finalize 保持空字串、libs 保持空陣列;不要為了試驗 lifecycle 或 module 修改它。
- 匯入前先讀 Flow JSON。
確認 Inject 的 repeat、crontab 為空、once 為 false;Function 只有一個輸出,沒有外部 module 或 lifecycle 程式;Debug 只顯示 payload 且不寫 console。若內容不符,停止匯入。
- 核對純資料程式。
固定 Function 只接受有限的 JavaScript number,通過後建立含原值與平方的新 payload,再同步 return:
if (typeof msg.payload !== "number" || !Number.isFinite(msg.payload)) { node.error(new Error("payload-validation-failed"), msg); return null; } const 數值 = msg.payload; msg.payload = { 原值: 數值, 平方: 數值 * 數值 }; return msg;它沒有儲存狀態或觸及任何外部資源。對手動數字 6,預期平方是 36。
- 在獨立分頁部署並手動觸發。
先確認 Flow 沒有 wire 接到其他分頁或副作用節點,再使用最小必要部署範圍。只按一次 Inject,從 Debug 檢查 payload 是物件、原值是 6、平方是 36。
- 逐一測試型別邊界。
不合格時必須用
node.error(err,msg)並 return null。先在隔離的暫時副本新增兩個手動 Inject(once=false、repeat 與 crontab 留空),分別把字串nan、infinity寫入msg.testCase,都接到一個暫時 Function,再接回原本「驗證數值並建立平方」Function。暫時 Function 精確使用下列程式:if (msg.testCase === "nan") { msg.payload = Number.NaN; } else if (msg.testCase === "infinity") { msg.payload = Number.POSITIVE_INFINITY; } else { return null; } return msg;另加 Catch,scope 只選原本受測 Function,接到只顯示
msg.error的 Debug;原本 Debug 仍只顯示msg.payload。逐一手動按下兩個 Inject 時,Catch 的msg.error.message都是payload-validation-failed,一般 Debug 完全沒有訊息。這兩個值不能用 JSON 直接表示,因此不可把字串"NaN"/"Infinity"當成等價測試。測完刪除暫時 Inject、Function、Catch 與錯誤 Debug,不改動下載範例的 strict contract;負的有限 number 仍有效。 - 接上停用的診斷節點。
需要觀察錯誤、status 或 completion 時,另參考 11-debug-catch-status.json。其中 Catch、Status 與 Complete 預設停用;先確認 scope 只指向示範 Function,再於隔離分頁逐一短暫啟用和測試,完成後恢復停用。
同步回傳、多輸出與非同步完成
同步 return
最小模式是在原訊息上設定欄位後 return msg。若建立新的訊息物件,只複製 downstream 明確需要的業務欄位,不要展開整個輸入;5.0.2 會替每個有效輸出訊息指定本次輸入的 _msgid。直接修改原 msg 仍是最簡單的單輸出模式。以下建立新 payload,運算次數固定:
if (typeof msg.payload !== "number" || !Number.isFinite(msg.payload)) {
node.error(new Error("payload 必須是有限數值"), msg);
return null;
}
const value = msg.payload;
msg.payload = { input: value, doubled: value * 2 };
return msg;
多個輸出與 nested arrays
Function 設成兩個輸出後,外層陣列位置對應 output 1、output 2。null 表示該 output 不送。這個例子把合格值送第一輸出,錯誤摘要送第二輸出:
if (typeof msg.payload === "number" && Number.isFinite(msg.payload)) {
const value = msg.payload;
msg.payload = { ok: true, value };
return [msg, null];
}
msg.payload = { ok: false, reason: "not-finite-number" };
return [null, msg];
若同一個 output 要依序送多則訊息,在該位置放 nested array。下例的第一輸出送兩則,第二輸出不送:
const first = { topic: msg.topic, payload: { index: 0, value: "A" } };
const second = { topic: msg.topic, payload: { index: 1, value: "B" } };
return [[first, second], null];
[first, second] 在兩輸出 Function 代表每個輸出各一則;[[first, second], null] 才代表第一輸出有兩則。每個非 null 元素都必須是訊息物件,不能在訊息位置放 primitive 或把 payload 陣列誤當輸出陣列。
非同步 node.send 與 node.done
下例用已完成 Promise 示範非同步控制面,計算仍是純記憶體內運算,沒有 timer 或外部 I/O。因為程式使用 node.done(),成功與失敗路徑都明確完成;最後不 return 訊息:
if (typeof msg.payload !== "number" || !Number.isFinite(msg.payload)) {
node.error(new Error("payload 必須是有限數值"), msg);
node.done();
return;
}
const value = msg.payload;
Promise.resolve(value)
.then((input) => {
msg.payload = { input, squared: input * input };
node.send(msg);
node.done();
})
.catch((err) => {
node.error(err, msg);
node.done();
});
return;
這只是 API 形狀示範;同步平方應使用同步 return,因為沒有理由增加 Promise。真正的非同步工作必須另設 timeout、取消與重複送出策略,但本站安全 library 不收錄外部 I/O 範例。
有限、可讀、可測的 JavaScript
安全 Function 先縮小輸入,再運算,再建立固定輸出。不要將完整 msg 複製到長期 context,也不要讓動態字串成為程式碼、module 名稱或屬性寫入路徑。輸入為陣列時,先定義最大筆數;每個迴圈都必須由已限定的陣列長度終止。
const source = Array.isArray(msg.payload) ? msg.payload.slice(0, 20) : [];
const readings = source
.filter((item) => item
&& typeof item.value === "number"
&& Number.isFinite(item.value))
.map((item, index) => ({
index,
value: item.value
}));
msg.payload = { count: readings.length, readings };
return msg;
這段程式故意只接受陣列、最多處理 20 筆、只輸出 index 與 value。正式上限依設備與訊息頻率訂定,並在 Comment 或測試契約記錄。不要用遞迴走訪未知深度物件,不要使用無界 while,也不要把大型 Buffer 轉成 Debug 文字。
Clone considerations
訊息可能沿多條 wire 前進,因此 mutation 與 cloning(複製)要有意識。Node-RED 5.0.2 的 Function node.send 預設會複製它送出的第一則訊息;API 允許第二參數為 false 跳過第一則 clone,但這只適用於確定無法複製且完全理解生命週期的特殊資料。一般流程保留預設,不要為了微小效能差異關閉。
- 送出後不要再修改同一個 msg 或深層子物件;其他分支可能看見不可預期內容。
- 要建立兩則不同訊息時,建立兩個物件與各自 payload,不要修改、送出、再修改同一參照。
- 不要將 request/response 等 live object、不可複製物件或巨大 Buffer 經過 Function、Delay 或 context;先提取真正需要的純資料。
- clone 不是資料遮罩。訊息含敏感欄位時,複製只會增加副本;應在進入 Debug 或儲存前移除。
External module 設定邊界
Function 編輯器可列出外部 module 的功能受 runtime 設定 functionExternalModules 控制。5.0.2 在設定明確禁止時會拒絕帶 libs 的 Function;允許時也由 runtime 依節點列出的固定 module 載入。這不等於 sandbox 提供 unrestricted require。安全 library 要求 libs 為空,不展示 module 程式碼,也不允許訊息決定 package。若產品真的需要套件,應由管理者固定版本、評估供應鏈與授權,再建立獨立變更,不要把它偷偷塞進資料轉換。
錯誤、log、status 與 Catch 各司其職
node.error(err,msg) 會把錯誤與訊息關聯,讓 scope 相符的 Catch 節點能接收;只呼叫 node.error(err) 無法提供同樣的訊息關聯。對可預期的不合格輸入,建立不含原始敏感值的 Error,附上 msg,然後 return null。對非同步工作,報錯後仍要走到 node.done。
if (typeof msg.payload !== "number" || !Number.isFinite(msg.payload)) {
const err = new Error("payload-validation-failed");
node.error(err, msg);
return null;
}
const value = msg.payload;
msg.payload = { ok: true, value };
return msg;
node.log、node.warn 與 node.error 進入 runtime log 管線;node.debug 與 node.trace 取決於 log level。不要把完整 msg、原始住家事件、識別值或 credential 寫入 log。正常但需要在畫布快速辨識的短期狀態,可用 node.status;它不是持久監控,也不會自動形成 Catch 事件。
| 管道 | 適合內容 | 不適合內容 | 清除或收斂方式 |
|---|---|---|---|
| 一般 Function output | 符合 downstream 契約的正常訊息 | 錯誤 stack 或原始不可信輸入 | 固定 shape 並分離錯誤 output |
node.error(err,msg) + Catch | 可擷取的失敗與關聯訊息 | 每則正常分支或秘密內容 | Catch scope 限定到指定 Function |
| runtime log | 短、已清理、可聚合的診斷文字 | 完整 msg、token、環境 ID、大型物件 | 排錯後移除臨時 log 並回復必要等級 |
node.status + Status | 短暫 node 狀態與 scoped 狀態事件 | 業務資料庫、成功保證、長文字 | 完成後以空 status 清除;Status 節點限制 scope |
| Debug | 手動測試的指定欄位 | 長期完整訊息串流 | 限制欄位與頻率,測試後停用 |
11-debug-catch-status.json 的 Catch、Status 與 Complete 明確 scope 到示範 Function,且預設停用。範例提供三個手動 Inject,分別觸發正常輸出、測試錯誤與有界 status;四個獨立 Debug 只顯示 msg.payload、msg.error、msg.status 或 msg.complete。只可在隔離副本逐一短暫啟用觀察器。更多 Catch、Status、Complete 差異見第 18 章除錯與測試。
Lifecycle、context 與維護邊界
On Start 與 On Stop
Node-RED 5.0.2 的 Function 編輯器有 On Start(底層 persisted 欄位 initialize)與 On Stop(finalize)。On Start 可為 async;runtime 會先等待 module 與初始化 Promise,再處理等待中的訊息。初始化失敗時,節點會記錄錯誤且不進入正常處理。On Stop 在節點關閉時執行,runtime 隨後清理由 sandbox 建立且仍 outstanding 的 timers;5.0.2 不允許從 close function 送出訊息。
能力存在不表示本教學範例需要使用。本章可下載範例刻意將 initialize、finalize 保持空白,因為 deploy 或 restart 可能執行 startup code,而那不是手動 Inject。你應把必要前置資料改成明確訊息,讓每次處理可重現;本章不提供 lifecycle 程式碼。若既有 Flow 已有 lifecycle,部署前必須讀懂其啟動、失敗、清理與重複執行行為,不能把它當成一般函式註解。
Node、flow、global context
Function 可透過 context、flow、global 的 get/set API 使用不同 scope,也可依 runtime 設定使用 named store。context 適合少量、有明確生命週期的流程狀態,不是秘密保管庫、任務佇列或無界歷史資料。非同步 store 可能要求 callback 形式;不要假設所有 store 都能同步讀寫。完整儲存界線見第 7 章時間與 Context。
- 能由 msg 推導就不存:純 Function 最容易測試,也不受 deploy/restart 的舊狀態影響。
- 必須存就固定 key 與 shape:同時定義初始值、最大大小、更新原子性、清除時機與 store。
- 不要每則訊息寫大型物件:持久 store 的寫入與 flush 有成本,也不是每次 assignment 都立即 durable。
- 不要存秘密或完整 HA 物件:Context 可能被 Debug、匯出或備份間接暴露。
把 Function 當成可測的轉換單元
名稱描述輸入到輸出的行為,例如「驗證數值並建立平方」,不要叫「處理資料」。在節點資訊中寫明輸入 shape、輸出數、錯誤分支與上限。運算超過一個螢幕時先拆成局部純函式或多個節點;不要把路由、儲存、網路與顯示全部塞進一個 Function。每次變更用固定 Inject 測正常值、邊界值、錯誤型別與空值,再接到真正 downstream。
故障排除
- Function 顯示 non-message returned:檢查 return 或 node.send 的每個非 null 元素。它們必須是物件訊息;把數字、字串或陣列資料放進
msg.payload,不要直接當訊息送出。 - 第二個 output 沒有訊息:確認 Function 設定的輸出數是 2,且外層陣列是
[firstOutput, secondOutput]。若第一位置是 nested array,代表第一輸出多則,不是兩個輸出。 - 訊息重複出現:搜尋同一路徑是否同時 return msg 與稍後 node.send,或 Promise 多條分支是否都 send。每則工作只選一種輸出模型,加入完成旗標前先修正控制流程,不要用 downstream 去重掩蓋。
- 非同步流程卡住或 Complete 不來:確認成功、驗證失敗與 catch 都會呼叫一次 node.done。設定 timeout 與取消策略;本章示例沒有外部等待。Complete 只表示所監看的節點完成,不代表所有 downstream 都已完成。
- Catch 收不到 node.error:使用
node.error(err,msg)並確認 Catch scope 包含該 Function。若 Catch 節點仍停用,先在隔離測試中短暫啟用;不要改成記錄完整 msg。 - 不同分支看到被改過的 payload:檢查是否送出後繼續 mutation、重用同一個深層物件,或使用跳過 clone 的選項。恢復預設 clone,並為每則輸出建立獨立 payload。
- 部署後立即執行或失敗:檢查 On Start 與外部 libs。安全 library 要求兩者空白;不要反覆部署嘗試。先清除 lifecycle 程式、回到純手動 Inject,再逐步定位。
- context 值在 restart 後不同:確認 scope、named store、初始值與 persistence 設定。不要假設 memory context 會保留,也不要假設 filesystem store 每次 set 當下就 durable。
固定來源與官方文件
本章只以允許用於第 16 章的 Node-RED 精確提交建立版本事實;官方文件用於補充操作概念。若 rolling 文件新增了固定版本沒有的 API,以 5.0.2 原始碼為準。
- Node-RED 5.0.2 精確提交:Function sendResults、sandbox、async done 偵測、lifecycle、module 與 timer cleanup。
- Node-RED 官方文件:Writing Functions。
- Node-RED 官方文件:訊息與 cloning。
- Node-RED 官方文件:Context。
- Node-RED 官方文件:Handling errors。
常見問題
什麼時候該用 Function,而不是 Change 或 JSONata?
同步 Function 需要手動呼叫 node.done 嗎?
如何從第一個 output 一次送兩則訊息?
[[first, second], null]。外層位置是 outputs,內層陣列才是同一 output 的多則訊息。