第 17 章

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 快照不是信任證明,也不會自動清除環境識別資料
ProjectsGit-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

  1. 建立手動請求入口。

    在隔離分頁新增「手動呼叫 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。

  2. 設定 static Link Call。

    新增「呼叫/離線倍增」Link Call,Call type 選 static、timeout 設 2 秒,target 明確選下一步命名的 Link In,不使用 msg.target。接線固定為「手動呼叫 LC-001」→「建立 request 契約」→「呼叫/離線倍增」。

  3. 建立具名被呼叫路徑。

    在另一個一般 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;
  4. 分開正常與 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。

  5. 驗收、清理與匯出。

    確認整條路徑精確為手動 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 秒

Subflow instance、properties 與 Context

Subflow 是一張可重用的內部節點圖;每次把它拖到 Flow 上,就建立一個 instance。定義更新會影響它的 instances,所以它適合「同一演算法、不同非秘密參數」,而不是複製後各自長期分岔。每個 instance 內的節點是各自的執行 instance,其 node context 也應視為彼此獨立;若刻意讀寫父 Flow context,則會跨越這個隔離邊界。

Subflow 路徑是設計 checklist,不是本章同一個實作練習:決定採用前,逐項寫出 input/output shape、每個 instance property 的非秘密預設、node/parent/global context 歸屬、兩個 instances 的 A→B→A 交錯預期、錯誤如何離開 Subflow、更新定義會影響哪些 instances,以及禁止遞迴的檢查。全部有答案後,另開隔離案例驗證;不要把它混進上述 Link Call walkthrough。

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。三者都不能替代受控備份,也都不會替你完成秘密清理。

面向LibraryImport/ExportProjects
單位可重用片段/Function一次 JSON 快照Git-backed 專案檔案
歷史不應當成完整變更歷史由你另行保存與比較由 Git commit/branch 管理
依賴取用後仍需檢查節點與設定匯入時仍需檢查缺少的 type/config仍需管理套件與 runtime 差異
秘密保存前清理分享前清理標準流程會 commit 加密 project credentials 檔;repository 不得公開,project credential secret 必須另行保管
Add-on 邊界:Add-on 22.0.1 的 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 實作為準。

常見問題

Projects repository 不小心變成公開時,第一步該做什麼?
先停止同步與自動 push,把 remote 改回受控私有位置並撤銷相關 Git 存取憑證;接著把 project credential secret、可能出現在 flow/設定/歷史中的 token 與密碼視為已洩漏並輪替。只刪目前檔案不會清除 Git 歷史,還要依組織程序重寫或封存受污染歷史,最後從可信備份重新驗證。
requestId 可以在 timeout 後立即重用嗎?
不要。舊呼叫可能稍後 return;重用同一 ID 會讓 gate 無法區分世代。每次呼叫使用新的 opaque requestId,timeout 時移除 pending 記錄,late return 只能被丟棄,不能重新啟用舊工作。
修改 Subflow 定義後,最容易漏掉哪種回歸?
只測一個 instance。至少建立兩個使用不同 properties 的 instances,以 A→B→A 交錯輸入,確認 node context 沒有串線、父 flow/global context 的共享符合設計,並逐一檢查所有引用該 Subflow 的分頁與錯誤出口。
環境變數適合存 token 嗎?
本章不這樣使用。Subflow properties、環境值、Context、Debug 與匯出都可能暴露資料;秘密應放在受控 credentials 或平台提供的秘密機制,分享前仍要逐欄清理。
啟用 Projects 就等於有備份嗎?
不等於。標準 Projects 流程會追蹤加密 project credentials 檔,但 repository 不可公開,且 project credential secret 必須分開安全備份;Add-on 的 credential_secret 在 Projects 模式會被忽略。完整 Add-on 備份與 Git 版本歷史仍各有責任。
設定節點參照匯入後失效,可以直接改 JSON ID 嗎?
不要。先保持相關節點停用,在編輯器逐一選取目標環境中已核准且 type 相符的 config node。直接猜測或貼入真實 ID 容易誤綁,也會把環境資料帶進分享檔。