使用 Django 和 HTMX 簡化動態網頁開發 (第四篇): Payload 如何傳輸?

在使用傳統 JavaScript 的 fetch 進行 Ajax 請求時,發送 POST 請求通常會使用 JSON.stringify() 直接組裝 Payload。

大致的感覺如下:

fetch("/api/todos/", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "X-CSRFToken": csrftoken
  },
  body: JSON.stringify({
    title: "장보기",
    done: false,
    priority: 3
  })
})

那麼 HTMX 是如何處理的呢?hx-post 究竟會以何種方式將資料發送到伺服器?由於許多範例只展示了非常簡單的功能,導致很難找到建立複雜 Payload 並發送的實際範例。

在這篇文章中,我們將完整整理 HTMX 的資料傳輸方式以及伺服器如何處理這些資料。

DJANGO 中 HTMX 資料流的示意圖


HTMX 的資料傳輸方式

fetch 的 Payload 概念與 hx-post 的資料傳輸方式大相徑庭。

fetch 中,開發者直接建立物件並將其轉換為 JSON 放入 body。然而,HTMX 基本上會從 DOM 中收集數值並進行傳輸

HTMX 的思維模式並非「直接組裝 JS 物件」,而是更接近於「從 HTML 元素中收集數值以建立請求參數」。為此,通常會採用以下三種方式:

1) 基於 Form 收集數值並發送

這是最符合 HTML 規範且與 Django 相容性最好的方式。

<form hx-post="/todos/create/" hx-target="#todo-list">
    <input type="text" name="title" placeholder="제목">
    <input type="number" name="priority" value="3">
    <input type="hidden" name="done" value="false">
    <button type="submit">등록</button>
</form>

在此情況下,HTMX 會收集表單中的輸入值並將其包含在請求中發送。預設的編碼方式與一般表單提交相同,採用 URL-encoded form data 格式。

Django 視圖可以像往常一樣接收:

def create_todo(request):
    title = request.POST.get("title")
    priority = request.POST.get("priority")
    done = request.POST.get("done")

2) 不使用 Form 但包含其他元素的值: hx-include

當您希望在一個按鈕上附加 hx-post,並同時選擇性地包含其他位置的輸入值時,可以使用此方法。

<input type="text" id="title" name="title" placeholder="제목">
<input type="number" id="priority" name="priority" value="3">

<button hx-post="/todos/create/"
        hx-include="#title, #priority"
        hx-target="#todo-list">
    등록
</button>

hx-include 會將指定元素的數值包含在請求中。即使沒有用 <form> 包裹整個內容,也能產生類似 Payload 的結果,對於輸入項目較少的情況非常有用。

3) 添加隱藏值或計算值: hx-vals

這項功能最接近 fetch 中直接建立部分 Payload 物件的概念。

<button hx-post="/todos/create/"
        hx-vals='{"title": "장보기", "done": false, "priority": 3}'
        hx-target="#todo-list">
    빠른 등록
</button>

hx-vals 會在請求中添加額外參數。預設使用上述的 JSON 語法,但如果加上 js: 前綴,也可以發送動態的 JavaScript 計算值。

<input type="text" id="title" placeholder="제목">

<button hx-post="/todos/create/"
        hx-vals='js:{title: document.querySelector("#title").value, done: false, priority: 3}'
        hx-target="#todo-list">
    등록
</button>

在這種情況下,Django 仍然會透過 request.POST 讀取。因為 hx-vals 只是「以類似 JSON 的語法定義數值」,而非將請求主體本身轉換為 JSON。


注意事項: hx-vals 與「JSON Payload」不同

這是初次接觸 HTMX 時最容易混淆的部分。

<button hx-post="/my-url/" hx-vals='{"a":1, "b":2}'>

這段程式碼並非表示「將 JSON 物件作為 body 發送」,而是更接近於在請求參數中添加 a=1&b=2。換句話說,伺服器仍然需要像處理表單資料一樣來處理它。

真的想發送 JSON Body 嗎?

如果您確實需要使用 fetch(... JSON.stringify(payload)) 這類 application/json 格式,那麼 HTMX 的 json-enc 擴充功能將是必需的。

請依照官方文件的指示進行以下設定:

<script src="https://unpkg.com/htmx.org@1.9.12/dist/ext/json-enc.js"></script>

<button hx-post="/api/todos/"
        hx-ext="json-enc"
        hx-vals='{"title": "장보기", "done": false, "priority": 3}'
        hx-target="#result">
    JSON으로 전송
</button>

至此,我們才真正實現了熟悉的 JSON Payload 傳輸方式。此時,Django 視圖中的 request.POST 會是空的,因此需要直接讀取 request.body

import json

def create_todo_api(request):
    data = json.loads(request.body)
    title = data.get("title")
    # ... 邏輯處理 ...
    return JsonResponse({"ok": True})

HTMX 與 DRF 哲學有差異,但無需強行合併

儘管可以使用擴充功能來發送 JSON,但這是否符合 HTMX 的精神仍有待商榷。這是因為「資料中心 (DRF)」「超媒體中心 (HTMX)」這兩種哲學存在衝突。

如果決定正確使用 HTMX,或許有必要暫時脫離以資料為中心的思維模式。透過 <form>hx-includehx-vals 發送數值,伺服器則透過 request.POST 接收,並返回 HTML 片段而非 JSON。這正是 HTMX 最能發光發熱的地方。

但如果 DRF 序列化器不捨放棄怎麼辦?

DRF 的序列化器確實非常強大。難道因為使用 HTMX,就必須放棄這個方便的驗證工具,轉而進行 request.POST.get() 的繁瑣操作嗎?

幸運的是,DRF 序列化器不僅能對 JSON 進行驗證,也能出色地驗證表單資料。由於這部分內容較長,我們將在下一篇文章中深入探討「HTMX 與 DRF 序列化器的共存」


總結

今日內容摘要如下:

  1. HTMX 基本上會從 DOM 中收集數值。 其方法與直接組裝 Payload 物件的 fetch 不同。
  2. 三種傳輸方式: 發送全部內容的 <form>、選擇性發送的 hx-include、以及添加數值的 hx-vals
  3. 預設為表單資料。 請注意,hx-vals 雖然使用 JSON 語法,但實際上並非以 JSON body 形式傳輸。
  4. 如果確實需要 JSON,請使用 json-enc 擴充功能。 但請記住,HTMX 的核心優勢在於交換 HTML 片段。

下一回,我們將探討如何將 DRF 的便利性融入 HTMX!

相關閱讀