Link、Subflow、環境變數、Library 與匯入匯出
當 Flow 從一條自動化長成多個分頁,重用就不只是少畫幾條線,而是要把呼叫、狀態、設定參照與可攜資料的邊界說清楚。本章以 Add-on 22.0.1 內嵌的 Node-RED 5.0.2 為基線,帶你用離線、手動方式設計可追蹤且可安全分享的架構。
先把重用邊界畫清楚
同一段邏輯被複製三次後,修正通常只會落在其中兩份;跨分頁的長線越多,資料從哪裡來也越難判讀。Node-RED 提供 Link、Subflow(子流程)、Library(程式庫)、匯入/匯出及 Projects,但它們解決的是不同問題。把它們混成同一種「重用」機制,會讓呼叫等待、每個 instance 的狀態、設定節點參照和版本控制責任變得模糊。
| 工具 | 適合處理 | 不保證的事 |
|---|---|---|
| Link In/Link Out | 跨分頁的單向虛擬接線 | 不是函式呼叫,不會自動回傳 |
| Link Call+return | 等待一個有期限的請求/回應 | 逾時不會取消工作或抑制延遲回傳 |
| Subflow | 以多個 instance 重用同一張內部圖 | 不是共用單一 node context,也不可遞迴包含自己 |
| Library | 在編輯器儲存可再次取用的 flow 或 Function 內容 | 不是部署歷史,也不是遠端 Git 工作流程 |
| 匯入/匯出 | 搬移一份 Flow JSON 快照 | 不是信任證明,也不會自動清除環境識別資料 |
| Projects | Git-backed 專案、分支與版本工作 | 在本 Add-on 預設沒有啟用;標準流程會追蹤加密的 project credentials 檔,但仍不等於完整備份 |
本章範例只描述訊息內的字串與數值,不使用網路、檔案、Home Assistant action 或其他外部 I/O。Comment 可在畫布留下介面說明;Junction 只是編輯器中的 wire routing 交會點,不能當成 Link、呼叫或可重用元件。若你還不熟悉 msg 與 Context,先回看第 5 章訊息模型與第 7 章 Context。
介面、狀態與設定參照是三條軸
一個可重用元件至少要寫出三件事:輸入/輸出契約、狀態歸屬,以及外部設定依賴。輸入契約可規定 msg.payload 型別與錯誤方式;狀態歸屬決定兩個 instance 是否互相影響;設定依賴則指出 flow 內的節點是否參照 Home Assistant Server、MQTT broker 或其他 configuration node(設定節點)。
| 契約面 | 離線範例 | 驗證問題 |
|---|---|---|
| 輸入 | msg.payload 必須是有限數值 | 字串、null、缺值要拒絕還是轉換? |
| 輸出 | 成功時輸出同一個 msg,只增加 msg.result | 有沒有覆寫呼叫者需要的欄位? |
| 錯誤 | 以可 Catch 的錯誤回報,不把完整輸入寫進錯誤字串 | 呼叫端能否區分驗證失敗與逾時? |
| 狀態 | 計數放在 instance 內部 node context | 兩個 instance 是否應分開計數? |
| 設定 | 非秘密的標籤由 instance property 提供 | 缺少屬性時是否有安全預設? |
Configuration node 本身不是普通訊息節點。一般節點在 JSON 中以 ID 參照它;匯出被選取節點時,編輯器可能連同需要的設定節點一起帶出,或留下在目標環境無法解析的參照。不要手改 JSON ID 來「對上」正式環境,也不要把真實 server、broker、URL 或憑證內容提交到分享檔。正確做法是在隔離副本匯入後,逐一開啟依賴節點,透過編輯器選取目標環境中已核准的設定節點。
聚焦完成一次離線 Link Call
- 建立手動請求入口。
在隔離分頁新增「手動呼叫 LC-001」Inject:payload 型別選 number、值為
4,另在 properties 加入字串msg.requestId = "LC-001";once=false,repeat、crontab 留空。後接「建立 request 契約」Change,以 JSONata 把msg.request設成{"requestId": requestId, "value": payload},再刪除msg.payload。 - 設定 static Link Call。
新增「呼叫/離線倍增」Link Call,Call type 選 static、timeout 設 2 秒,target 明確選下一步命名的 Link In,不使用
msg.target。接線固定為「手動呼叫 LC-001」→「建立 request 契約」→「呼叫/離線倍增」。 - 建立具名被呼叫路徑。
在另一個一般 Flow 分頁新增 Link In「被呼叫/離線倍增」,接純 Function「驗證並倍增」,最後接 Link Out「回傳/離線倍增」並把 mode 選成 return。Function 只使用下列程式,不讀 context、不做外部 I/O:
const request = msg.request; if (!request || typeof request.requestId !== "string" || typeof request.value !== "number" || !Number.isFinite(request.value)) { node.error("INVALID_REQUEST", msg); return null; } msg.response = { requestId: request.requestId, doubled: request.value * 2 }; return msg; - 分開正常與 timeout 觀察。
把 Link Call output 接到「檢視 response」Debug,只顯示
msg.response。另放 Catch「擷取 Link Call timeout」,scope 只選「呼叫/離線倍增」,接到只顯示msg.error的 Debug。正常按一次 Inject,唯一正常投影是{"requestId":"LC-001","doubled":8},Catch 沒有訊息。測 timeout 時只暫時斷開「驗證並倍增」到 return Link Out 的 wire;2 秒後 Catch 的msg.error.message是timeout,source type 是link call、name 是「呼叫/離線倍增」,正常 Debug 沒有訊息。測完立即接回 return wire。 - 驗收、清理與匯出。
確認整條路徑精確為手動 Inject → Change/request contract → static Link Call → named Link In → pure transform → return Link Out → limited Debug;沒有 Action、HTTP、MQTT、檔案、排程或 credentials。清除 Debug 訊息後才做最小匯出與文字重讀;部署與回復策略可接著閱讀第 18 章除錯與測試。
輸入:msg.payload = 4;msg.requestId = "LC-001"
request:{ requestId: "LC-001", value: 4 }
正常 Debug:{ requestId: "LC-001", doubled: 8 }
timeout Catch:error.message = "timeout";正常 Debug 無訊息
外部 I/O:無;Link Call timeout:2 秒Link In、Link Out、Link Call 與 return
Link In 和一般模式的 Link Out 建立跨 Flow 分頁的虛擬接線。Link Out 收到訊息後,可送到它連結的 Link In;這是傳遞,不是具回傳值的呼叫。虛擬線通常在選取 Link node 時才顯示,因此名稱應同時寫出動作與契約,例如「送往/純資料正規化」,不要只命名為「下一步」。Node-RED 5.0.2 的節點說明也明確指出 Link 不能連進或連出 Subflow。
Link Call 才是等待回應的模式。它將訊息送給指定 Link In;被呼叫的路徑必須以設為 return 模式的 Link Out 結束。return 會沿內部的呼叫來源資料把訊息交還原 Link Call,之後 Link Call 才從輸出端送出。一般「send to all」Link Out 不會替 Call 完成回傳。
| 節點/模式 | 收到訊息後 | 失敗觀察 |
|---|---|---|
| Link Out:send to all | 向已連結的 Link In 傳遞,接著完成自己的處理 | 沒有 Call 的等待/回傳契約 |
| Link In | 接收虛擬連線訊息並由輸出端送出 | 仍需由路徑內節點各自回報錯誤 |
| Link Call:static | 呼叫固定的 Link In 並等待 return | 無回應超過 timeout 時產生可 Catch 的 timeout 錯誤 |
| Link Call:dynamic | 用 msg.target 依 ID、同分頁名稱、全域一般 Flow 名稱的順序選 Link In | 對應層級不唯一、目標不存在或不合法會成為錯誤 |
| Link Out:return | 只在有效 Call 鏈上把回應交回呼叫端 | 不是由 Call 進入時沒有可返回來源,節點會警告 |
Node-RED 5.0.2 的 Link Call 預設 timeout 是 30 秒;到期會移除等待中的請求並呼叫錯誤機制,Catch 可以接到它。timeout 既不是取消,也不會抑制延遲回傳:若 return 在逾時後才抵達,因等待記錄已移除,Link Call 會把該訊息當成一般 downstream 輸出送出。因此逾時 Catch 的補償與 Link Call 下游都必須防止同一工作造成重複或互相衝突的副作用,不能假設只會執行其中一條路徑。
msg.request.requestId 寫入有界的 flow.activeLinkRequestId;timeout Catch 路徑先清除此 key,再顯示 error;Link Call 下游先通過下列 Function。正常 return 會比對並清除 active ID;逾時後的 late return 因 ID 已清除而回傳 null,不可抵達副作用。並行需求不能共用此單一 key,必須改成有最大筆數與到期清理的 pending map。const activeId = flow.get("activeLinkRequestId");
const responseId = msg.response && msg.response.requestId;
if (typeof responseId !== "string" || responseId !== activeId) {
return null;
}
flow.set("activeLinkRequestId", undefined);
return msg;
msg.target 不應直接來自不可信輸入。5.0.2 先以精確 ID 解析;否則找唯一的同 Flow 分頁名稱;仍找不到時,才在一般 Flow(不含 Subflow)全域尋找唯一名稱。全域名稱有多個結果時會報錯;Link Call 也不能呼叫 Subflow 內的 Link In。若目標集合固定,優先使用 static target,或在前一節點以明確 allowlist 映射。// 離線 Function:只建立固定目標別名,不做外部 I/O
const allowed = { double: "離線倍增入口", label: "離線標籤入口" };
if (!Object.hasOwn(allowed, msg.operation)) {
node.error("UNKNOWN_OPERATION", msg);
return;
}
msg.target = allowed[msg.operation];
return msg;Subflow instance、properties 與 Context
Subflow 是一張可重用的內部節點圖;每次把它拖到 Flow 上,就建立一個 instance。定義更新會影響它的 instances,所以它適合「同一演算法、不同非秘密參數」,而不是複製後各自長期分岔。每個 instance 內的節點是各自的執行 instance,其 node context 也應視為彼此獨立;若刻意讀寫父 Flow context,則會跨越這個隔離邊界。
Subflow properties 可成為該 instance 的環境值,讓你設定如 DISPLAY_LABEL、MAX_COUNT 這類不敏感參數。Function 內可用 env.get("DISPLAY_LABEL") 讀取;typed input 也能選環境變數型別。Node-RED runtime 另提供 NR_SUBFLOW_ID、NR_SUBFLOW_NAME、NR_SUBFLOW_PATH 等 instance 資訊。它們適合診斷區分 instance,但 ID 與路徑不應出現在公開範例或分享紀錄。
// Subflow 內的純資料 Function
const label = env.get("DISPLAY_LABEL") || "未命名實例";
const current = context.get("seen") || 0;
context.set("seen", current + 1);
msg.result = { label, sequence: current + 1 };
return msg;
上例若放置兩個 instance,兩邊的 seen 應各自從 1 開始。若需求真的是共用統計,先寫出共享生命週期,再明確選 flow 或 global context;不要因為「讀得到」就直接使用。Subflow 內可透過 $parent. 存取父層 context/environment,但這會提高耦合,應列入介面文件。Context store 若採 localfilesystem,也不是每次 assignment 都等同立即耐久寫入;Context 更不是秘密保管庫。
| 資料 | 建議位置 | 理由 |
|---|---|---|
| 單一內部節點暫時計數 | node context | 自然隨 instance 分隔 |
| instance 的顯示標籤/上限 | Subflow property | 可由每個 instance 明確設定 |
| 父 Flow 多節點共享的非秘密狀態 | 明確的 parent flow context | 需把跨邊界共享寫進契約 |
| token、密碼、私鑰 | 不放在 property 或 context | 使用受控 credentials/平台秘密機制 |
禁止遞迴:不要讓 Subflow 直接包含自己的 instance,也不要建立 A 包含 B、B 再包含 A 的循環。這不是一般函式遞迴模型,會造成無法安全展開的架構與資源風險。需要重複處理時,改用有明確上限的訊息迭代或有限狀態設計,並測試終止條件。
環境變數、Library 與 Projects 的界線
環境變數(environment variables)在這一章只用於不敏感、可替換的設定,例如顯示文字或批次上限。不要在 Flow JSON、Subflow property、Function 程式碼、Debug 或 Context 放 token、密碼、私鑰與連線 credential。環境變數的名稱和值也可能在匯出、診斷或執行環境中暴露;「使用 env」本身不等於秘密安全。
Library 是編輯器內的重用保存區:你可以將 flow 片段或 Function 程式碼保存後再取用。Import/Export 是把當下選取內容序列化成 JSON 或從 JSON 帶入,適合明確的一次性搬移。Projects 則是 Git-backed 的完整工作目錄與版本流程,涉及 repository、分支、remote 和 Git identity。三者都不能替代受控備份,也都不會替你完成秘密清理。
| 面向 | Library | Import/Export | Projects |
|---|---|---|---|
| 單位 | 可重用片段/Function | 一次 JSON 快照 | Git-backed 專案檔案 |
| 歷史 | 不應當成完整變更歷史 | 由你另行保存與比較 | 由 Git commit/branch 管理 |
| 依賴 | 取用後仍需檢查節點與設定 | 匯入時仍需檢查缺少的 type/config | 仍需管理套件與 runtime 差異 |
| 秘密 | 保存前清理 | 分享前清理 | 標準流程會 commit 加密 project credentials 檔;repository 不得公開,project credential secret 必須另行保管 |
editorTheme.projects.enabled 預設為 false。因此不要依照 upstream Projects 教學假設介面已可用,也不要直接編輯 Add-on 管理的 settings 檔。若組織核准啟用,先做可還原備份、記錄現況、停止 Add-on,再依 Add-on 支援方式調整並重新啟動驗證;具體變更應依固定版本的 Add-on 文件與變更程序執行。Projects 啟用後,Git remote 的存取權和 Git credentials 是另一條安全邊界。Node-RED 5.0.2 的標準 Projects 工作流程會把加密的 project credentials 檔加入版本追蹤;這是預期行為,但不代表檔案可以公開,project repository 及其歷史都必須限制存取。每個 project 使用獨立的 project credential secret,該 secret 必須與 repository 分開安全備份,否則加密 credentials 可能無法復原。
Add-on 的 credential_secret 在 Projects 模式會被忽略,不能拿來解密 project credentials。Project repository、project credential secret、Git 存取 credentials 與 Add-on 完整備份各自處理不同責任;Git 歷史不是完整執行環境備份,Add-on 備份也不能取代可審查的版本歷史。
安全匯出、設定節點與重新綁定
Flow JSON 可讀、可比較,也因此很容易把環境細節一起帶走。匯出前應從最小選取範圍開始,不要為了方便選整個 workspace。若片段依賴 configuration node,記下「需要哪一類設定」,而不是保留正式環境的真實 ID。Credentials 通常由獨立機制保存,但不能因此假設匯出一定乾淨;普通欄位、Function、Template、Comment、環境屬性與 Debug 都可能含敏感值。
分享前 scrub checklist
- 沒有 credentials 區塊、token、密碼、私鑰、Cookie、Authorization 或 webhook 識別資料。
- 沒有真實 URL、hostname、IP、Home Assistant server/entity/device/area ID、MQTT client ID 或 topic。
- 所有 config node reference 都已辨識;目標環境必須在編輯器中重新選取已核准設定節點,不直接替換 JSON ID。
- 沒有啟動即 Inject、排程、監聽、外連、檔案寫入或 action 路徑;若片段保留運作型節點,匯出前應停用並寫明原因。
- Function、Template、Change、JSONata、Comment、節點名稱與 Flow description 都已逐字檢查。
- 只保留必要 wires;Link 目標、Subflow 定義和 instance property 都完整且不含遞迴。
- 在純文字檢視 JSON,並在空白隔離分頁重新匯入;節點數、type、disabled 狀態與 config 依賴符合預期。
分享包說明(不放真實識別資料)
- 需要:Node-RED 5.0.2 核心節點
- 輸入:有限數值 msg.payload
- 設定節點:無
- 環境屬性:DISPLAY_LABEL(非秘密文字)
- 外部 I/O:無
- 啟動時自動執行:無
- 已測:正常值、缺值、錯誤型別、兩個 instance 隔離
匯入未知 JSON 前,先在文字檢視器閱讀 type、wires、d、Inject 的 once 與各種設定欄位。缺少節點 type 時不要立即安裝套件來消除提示;先確認來源、固定版本及必要性。本節 checklist 已自足;完整的 credentials、備份還原、Projects 與資安硬化程序請接續參考第 21 章備份與資安。
故障排除
- Link Call 一直等待後報 timeout:確認目標是 Link In、所有成功與拒絕分支最後都到 return 模式 Link Out,而且沒有把一般 send-to-all 當成 return。Catch 應設在呼叫所在範圍觀察錯誤;不要只拉長 timeout 掩蓋遺失分支。
- return 節點出現缺少回傳來源的警告:這條路徑可能由一般 Link/Inject 進入,而不是由 Link Call 進入。把單向入口和 Call 入口分開,避免共用只能在 Call 鏈上成立的 return。
- 兩個 Subflow instance 的數值互相影響:檢查是否用了 flow/global 或
$parent.context,而不是 instance 內 node context;再以 A、B、A 的手動輸入順序驗證各自計數。 - 匯入後出現未知設定節點或紅色三角:不要手改參照 ID。開啟使用該 config node 的節點,確認 type 與欄位後,在目標編輯器重新選取已核准設定;沒有核准設定就保持停用。
- 找不到 Projects:這個 Add-on 的 Projects 預設關閉,不是瀏覽器故障。不要直接修改 settings;依備份、停止、受控調整、重新啟動與回復界線走核准程序。
- 匯出看似沒有 credentials 仍不能分享:搜尋普通欄位中的環境 ID、URL、註解、Function 字串、Subflow properties 與 config node。再由另一個人以純文字重讀,不能只相信匯出對話框的範圍摘要。
固定版本來源與官方文件
本章的行為基線是下列 exact commit;官方文件用來補充操作概念,若 rolling 文件與固定版本有差異,以固定提交的 Node-RED 5.0.2 與 Add-on 22.0.1 實作為準。
- Node-RED 5.0.2 固定提交:core Link、Subflow runtime、Context、Library/匯入匯出與 Projects API。
- Home Assistant Community App: Node-RED 22.0.1 固定提交:Projects 預設與
credential_secret邊界。 - Node-RED 官方文件:Subflows。
- Node-RED 官方文件:Environment variables。
- Node-RED 官方文件:Importing and Exporting Flows。
- Node-RED 官方文件:Projects。
常見問題
Projects repository 不小心變成公開時,第一步該做什麼?
requestId 可以在 timeout 後立即重用嗎?
修改 Subflow 定義後,最容易漏掉哪種回歸?
環境變數適合存 token 嗎?
啟用 Projects 就等於有備份嗎?
credential_secret 在 Projects 模式會被忽略。完整 Add-on 備份與 Git 版本歷史仍各有責任。