JSONata、Mustache 與資料轉換
同一段看似「模板」的文字,在 Node-RED 與 Home Assistant 可能交給四種完全不同的執行器。本章先辨認執行環境,再用固定、手動輸入練習有界的型別轉換,讓資料契約在送往下一個節點前保持可預測。
為何需要本章
當你只想把攝氏換算成華氏,Change 節點的 JSONata 比一整段 JavaScript 更容易看出輸入與輸出;當你要組成給人閱讀的一行文字,Template 節點的 Mustache 又更直接。問題在於,JSONata、Mustache、Function JavaScript 與 Home Assistant Jinja 都會出現變數、括號或大括號,卻不共享語法、型別、context 或執行位置。把其中一種語法貼到另一個欄位,不只會報錯,也可能產生看似成功但型別錯誤的字串。
本章基線是 Node-RED 5.0.2 與 HA WebSocket nodes 0.80.3。先依下一節矩陣選擇最小能力的工具;若轉換已需要多個分支、明確錯誤物件或複雜迴圈,再前往第 16 章 Function 節點,不要把 JSONata 寫成難以維護的程序。
四個執行環境先分清楚
先看欄位由哪一個節點提供,再判斷語法。不要只憑畫面上有大括號就稱為模板。以下表格把執行端、輸入根、型別與 escaping(逸出)放在同一張地圖中。
| 環境 | 在哪裡執行 | 主要輸入與 context | 結果型別 | 逸出重點 |
|---|---|---|---|---|
| Node-RED JSONata | Node-RED runtime;例如 Change 或 Switch 的 typed input 選成 expression | 輸入根是整個訊息,所以用 payload,不是 msg.payload;runtime 另提供 $flowContext()、$globalContext() 與 $env() | 可回傳字串、數字、布林、null、陣列或物件;沒有匹配值時也可能沒有結果 | 不是文字模板,不靠 HTML 逸出;輸出仍要依下一個節點的資料契約驗證 |
| Node-RED Mustache | Node-RED 核心 Template 節點 | 先查訊息欄位,也可由明確的 flow/global context token 取值 | 先渲染成文字;若 Template 輸出格式選 JSON 或 YAML,才會再解析成對應型別 | 雙大括號預設做 HTML 逸出;三重大括號是不逸出的原始插值,只能在輸出情境已知安全時使用 |
| Function JavaScript | Node-RED Function 節點的 sandbox | 以 msg 讀訊息,並有 node/flow/global context API | 你建立的 JavaScript 值;送出物必須是訊息物件或按輸出排列的訊息陣列 | JavaScript 不會自動替 HTML、JSON 或通知文字做情境逸出;詳見安全 JavaScript |
| Home Assistant Jinja | Render Template 節點把模板送到 Home Assistant,由 HA 渲染 | 使用 Home Assistant 的模板環境;不是 Node-RED 的 msg 或 flow context | 0.80.3 節點文件界定結果為字串,並寫到可設定的結果位置 | 由 HA/Jinja 語意處理;若結果要再當 JSON,仍須在 Node-RED 端明確解析與驗證 |
| 需求 | 優先工具 | 理由 | 停止條件 |
|---|---|---|---|
| 設定、移動或刪除單一屬性 | Change | 規則在節點設定中可見;不需程式碼 | 輸入 shape 未驗證時先加 Switch |
| 由固定 JSON 結構計算新物件 | JSONata | 保留數字、布林、陣列與物件型別 | 運算掃描無界集合或難以理解時拆小或改 Function |
| 產生短通知或說明文字 | Mustache Template | 插值語意簡單且預設 HTML 逸出 | 需要插入完整物件或輸出非字串型別時改工具 |
| 多輸出、明確錯誤或複雜分支 | Function JavaScript | 控制流程與訊息契約明確 | 不得加入外部 I/O、無界迴圈或任意 module |
| 使用 HA template entity/state 語意 | Render Template 的 HA Jinja | 由 Home Assistant 執行其模板環境 | 需要離線、低延遲或不應跨系統時不要使用 |
| 把外部文字轉成結構 | 對應 parser 節點 | JSON、CSV、HTML、XML、YAML 各有核心 parser | 大小與 shape 未限定時拒絕解析 |
同一個名字在四個環境也不保證同型別。例如 HA state 常是字串;JSONata 的乘法會涉及數值運算,而 Mustache 只負責把值插入文字。轉換前先用型別條件守住入口,不要依賴隱式轉型。
用固定 Flow 完成一次安全轉換
範例 09-jsonata-transform.json 只有手動 Inject、Change 與 Debug。Inject 沒有週期與啟動時自動送出,資料固定為一個溫度物件;整條路徑沒有外部 I/O。
- 下載後先以文字檢查 Flow。
確認唯一輸入的
repeat、crontab都是空字串,once是false;確認沒有 server config、credential、URL、檔案或外連節點。這一步在匯入前完成。 - 匯入到獨立分頁並核對節點。
匯入後應只有「手動送出溫度資料」、「轉換溫度資料」與「檢視轉換資料」。Change 規則的目標是
msg.payload,來源型別是 JSONata expression。若編輯器顯示不同節點或額外連線,停止部署並重新取得本站固定檔案。 - 讀懂運算式再部署。
運算式從頂層訊息讀
payload.temperature,建立新的物件,而不是改動外部系統。固定內容如下:{"攝氏": payload.temperature, "華氏": payload.temperature * 9 / 5 + 32}輸入
{"temperature":25}時,預期msg.payload是含攝氏 25、華氏 77 的物件,不是 JSON 文字。 - 採最小範圍部署並手動觸發一次。
這是純資料 Flow,但仍先確認工作區沒有連到其他分支。部署後只按一次 Inject,從 Debug 檢查
payload的型別與兩個欄位。不要用大量 Inject 壓測,也不要把完整訊息長期留在 Debug。 - 用邊界輸入驗證契約。
把手動 payload 暫時改成
{"temperature":0}與{"temperature":-10},分別確認華氏 32 與 14。使用 Node-RED 5.0.2 內的 JSONata 2.2.2、維持固定 expression{"攝氏": payload.temperature, "華氏": payload.temperature * 9 / 5 + 32},再把 payload 改成{}時,Debug 收到的msg.payload是空物件{}。這代表 expression 有回傳物件,但兩個值為 undefined 的欄位被省略;若 expression 只寫payload.temperature,才是整個 expression 沒有結果。兩者都應在正式流程前由 Switch 擋下,不可流向 action。
JSONata:以訊息為根的宣告式轉換
JSONata 是針對 JSON 結構的 functional declarative language(函數式宣告語言)。在 Node-RED typed input 中,輸入文件是頂層訊息,所以 payload.temperature 才是本例路徑。寫成 msg.payload.temperature 會尋找名為 msg 的頂層欄位,通常得不到你想要的值。
安全轉換應明確固定輸出 shape。下例把陣列限制在前 20 筆,再只保留允許欄位;它沒有 I/O,也不存取任何設定:
(
$items := payload.items[type = "reading"][[0..19]];
$items.{
"name": $string(name),
"value": $number(value),
"valid": $type(value) = "number"
}
)
這裡的 [[0..19]] 是選取索引範圍;真正的上限應由你的資料契約決定。不要把示例數字當成所有系統的通用上限。若輸入可能是 singleton(單一值)或 array(陣列),可用陣列建構語意穩定輸出,但仍要針對空值與沒有結果分開測試。JSONata 找不到路徑時可能回傳「沒有結果」,它與明確的 null、空陣列或 false 不相同。
HA 節點額外提供的 helper
HA WebSocket 0.80.3 的 JSONata service 只在 Home Assistant 節點內加入下列函式;核心 Change 節點不會因此自動擁有它們。這些 helper 讀取該整合目前可用的 entity、device 與 area 資料,不能搬到 Mustache、Function 或 HA Jinja 中使用。
| helper | 0.80.3 精確用途 | 使用界線 |
|---|---|---|
$entity() | 取得觸發目前節點的 entity 物件 | 不是每一種節點或事件都有目前 entity;先處理 undefined |
$prevEntity() | 事件節點可取得先前 state entity | 初始、建立或刪除情境不能假設一定存在 |
$entities()/$entities(entity_id) | 取得 cache 中全部 entities,或以 entity ID 取得單一 entity | 全量集合可能很大;單一 ID 必須使用環境 placeholder,不要寫入教學或紀錄 |
$areas(lookup) | 無參數取 areas;lookup 可為 area、entity 或 device ID | area 與 ID 屬環境資料,結果也可能不存在 |
$areaDevices(areaId)/$areaEntities(areaId) | 列出指定 area 關聯的 devices 或 entities | 先限定 area,再限制結果數量,避免每則訊息掃描大量集合 |
$device(lookup)/$deviceEntities(device_id) | 依 entity ID 或 device name 找 device;或取得 device 關聯 entities | 名稱可能變更或重複;不要把真實識別值放入可分享 Flow |
$outputData(name) | 取得 HA 節點 JSONata service 傳入的額外 output data;省略 name 時回傳可用資料集合 | 可用鍵取決於節點呼叫情境,不可假設固定 shape |
$sampleSize(collection,n)/$randomNumber(lower,upper,floating) | 0.80.3 暴露的 Lodash 抽樣與亂數 helper | 結果非決定性;不要拿來做安全、授權或需要可重現的控制決策 |
本章安全 Flow 刻意不使用上述 helper,因為它要能完全離線且不依賴 HA 連線。若正式 Flow 需要 helper,先用 placeholder 建立獨立測試訊息,限制輸出數量,並在 Debug 中只顯示經過挑選的非敏感欄位。
Mustache:組字串,不是物件運算器
Node-RED 5.0.2 核心 Template 節點使用 Mustache。雙大括號會查詢輸入訊息,例如下列純文字模板讀取 msg.payload.label 與 msg.payload.value:
讀值名稱:{{payload.label}}
讀值結果:{{payload.value}}
渲染結果是文字。若值含 & 或角括號,雙大括號會做 HTML 逸出;三重大括號會關閉這項保護。原始插值不是「修正亂碼」的通用開關:當輸出將進入 HTML 時,使用原始插值可能把不可信內容當成標記。當輸出將成為 JSON 時,也不要靠字串拼接處理引號、反斜線與換行;改用 JSONata 直接產生物件,或讓 Template 的 JSON 輸出解析固定模板後再驗證型別。
Template 可把結果寫入 msg、flow 或 global 位置;Mustache lookup 除了明確的 flow/global context token,也能以 env.NAME token 讀環境值。當節點的設定模板是空字串時,5.0.2 會改用輸入的 msg.template。不要讓不可信上游控制這個動態模板,因為它可以選取可渲染的環境與 context 值;也不要把秘密或 credential 放入可由模板讀取的環境/context。本章使用固定模板,且不讀取環境或 context。
這不代表 context 是模板的私有儲存區。值會跨訊息甚至依 store 跨重啟保留時,必須先定義生命週期與清除規則,詳見第 7 章時間與 Context。JSONata 的 $flowContext()、$globalContext() 與 $env() 也遵守相同秘密邊界;本章固定 expression 不使用它們。
Function JavaScript 與 HA Jinja 的邊界
Function JavaScript 適合需要明確控制流程、重複使用局部函式、建立多個輸出或統一錯誤物件的轉換。它以 msg 為輸入,必須送出訊息物件;JSONata 則把整個訊息當成文件並回傳 expression 結果。若一個 JSONata expression 已經塞入多層變數、難以界定的陣列展開與多個例外條件,就應移到Function 節點,但仍只做純資料運算。
HA WebSocket 0.80.3 的 Render Template persisted type 是 api-render-template。它把 Jinja template 送到 Home Assistant 的 HTTP render-template 能力,等待 HA 回傳字串,再依設定寫入結果位置。這是一個跨 Node-RED/HA 邊界的請求,不是本地 Mustache,也不是 JSONata。
0.80.3 對五個輸入都優先採用訊息欄位:msg.template、msg.resultsLocation、msg.resultsLocationType、msg.templateLocation 與 msg.templateLocationType;節點沒有 Block Input Overrides。不可信上游可能因此改變模板、原模板保存位置、渲染結果位置及其 msg/flow/global 類型。進入節點前必須刪除這五個欄位,或以 allowlist 重建只允許的值;尤其不得允許上游選擇任意 flow/global 寫入目的地。本章不執行 Render Template,固定示例也不提供任何動態模板或可渲染秘密。
型別、解析與效能都要有界
資料契約至少寫出四件事:允許的輸入型別、必要欄位、最大集合或文字大小、失敗時往哪裡走。只檢查「有 payload」不夠,因為 payload 可能是字串、Buffer、陣列或深層物件。你可以先在 Switch 分流,再把合格訊息交給轉換;不合格訊息只建立最小錯誤摘要,不要把原始內容整包寫進 Debug。
{
"input": {"temperature": 25},
"contract": {
"temperatureType": "number",
"minimum": -50,
"maximum": 100
},
"output": {"攝氏": 25, "華氏": 77}
}
上面是離線測試資料,不是 Flow 設定。實際流程應把契約寫在 Comment、測試說明或版本控制文件中。若資料原本是 JSON 字串,先限制字串大小,再用 JSON parser 轉成物件,最後驗證欄位;CSV、HTML、XML 與 YAML 同理。Parser 能解析格式,不會替你判斷內容是否合理,更不會自動限制深度、欄位數或序列長度。
- 限制輸入:在大型陣列進 JSONata 前先切出需要的範圍;避免 descendant wildcard 掃描未知深度。
- 限制輸出:只建立 downstream 需要的欄位;不要複製整份 HA cache 或整個輸入物件。
- 限制頻率:同一個高頻事件若每次都排序、group 或 aggregate 大集合,會佔用 runtime;先節流或只在值真正改變時運算。
- 限制觀察:Debug 選特定欄位並在完成後停用;大量完整訊息會增加序列化與 sidebar 負擔。
- 維持決定性:控制判斷避免抽樣與亂數;相同固定輸入應得到相同輸出,才容易重現。
安全轉換的終點不是「expression 沒報錯」,而是輸出符合契約且能被下一個節點拒絕不合格資料。若下一步可能有外部副作用,先參考第 11 章 Action 節點的輸入覆寫與目標邊界,不要把轉換成功當成執行 action 的充分條件。
故障排除
- JSONata 顯示沒有結果:先確認根路徑是
payload而非msg.payload,再用固定 Inject 檢查欄位拼字與陣列索引。把「沒有結果」、null、空陣列與false分別測試,不要用同一個 falsy 判斷混在一起。 - 運算結果變成字串或 NaN:檢查 Inject 的 payloadType 與 Debug 顯示的實際型別。HA state 常是字串,但本章離線例應提供 number;需要轉型時明確使用
$number(),並先確認值在允許範圍。 - Mustache 出現 HTML entity:這通常是雙大括號的預設逸出,不是資料損毀。先確認輸出用途;若是純文字可接受或調整顯示,若是 HTML 不要為了外觀改成原始插值。物件輸出改用 JSONata。
- JSON/YAML Template 解析失敗:不要直接塞入未逸出的動態文字。先縮成固定模板與固定測試值,核對引號、換行與輸出格式;若需要動態物件,改用 JSONata 直接產生 typed value。
- HA helper 在 Change 節點不存在:這些 helper 只由 HA WebSocket 0.80.3 的 HA 節點 JSONata service 注入。核心 Change 不會提供;把流程改成不依賴 helper 的訊息輸入,或只在支援的 HA 節點欄位使用。
- Render Template 沒有回傳:它需要 HA 連線並由 HA 執行 Jinja。先停止重試與 downstream 副作用,確認連線及 Catch 路徑;不要改貼 Mustache 或 JSONata 語法碰運氣。詳細診斷可接續第 18 章除錯與測試。
- 轉換造成明顯延遲:關閉高頻輸入,改用小型固定資料重現;量測陣列長度、expression 是否排序或掃描所有 descendants,然後先限制資料再轉換。不要用更大的測試集合反覆觸發。
固定來源與官方文件
本章技術邊界以兩個允許用於第 15 章的精確提交為準;一般概念再對照官方文件。rolling 文件若與固定版本不同,以固定原始碼行為為準。
- Node-RED 5.0.2 精確提交:核心 Change、Switch、Template、Function 與 parser 實作。
- HA WebSocket nodes 0.80.3 精確提交:JSONataService helper 與 Render Template 執行邊界。
- Node-RED 官方文件:訊息。
- Node-RED 官方文件:Context。
- HA WebSocket 官方文件:JSONata。
- Home Assistant 官方文件:Templating。
常見問題
JSONata 裡為什麼不用 msg.payload?
payload。Function JavaScript 才以 msg.payload 存取同一欄位。