使用 Django 和 HTMX 簡化動態網頁開發:Form 與 Serializer 的應用

在上一篇文章中,我們探討了 HTMX 將資料傳輸到伺服器的方式。

回顧前文:使用 Django 和 HTMX 簡化動態網頁開發 (四): Payload 傳輸方式

我們確認了,如果說傳統 JavaScript 的 fetch() 更接近直接構建 JSON payload 並發送的方式,那麼 HTMX 則更接近從 DOM 收集值並以 form-data 形式發送的方式。

那麼,一個自然而然的問題就浮現了:

「針對 HTMX 傳入的這些資料,在 Django 中使用什麼來驗證會最自然呢?」

起初,許多人可能會想到 DRF Serializer。確實,Serializer 是一個強大的驗證工具,即使不是 JSON 資料也能使用。但實際將其與 HTMX 搭配使用時,有時會感覺有些牽強。

這是為什麼呢?

原因很簡單。 HTMX 的基本流程更貼近 HTML form 的世界,而 Django 中恰好已經存在專為該世界設計的 Form。

在本文中,我們將整理 在處理 HTMX 請求時,Django Form 和 DRF Serializer 各自可以如何應用,以及 哪種方式是更自然且實用的選擇。

HTMX 請求處理中 Form 與 Serializer 的角色比較


HTMX 傳送的資料本質上更接近「表單資料」

正如上一篇所探討的,HTMX 基本上會收集 HTML 元素的數值並傳送至伺服器。例如,傳送 <form> 內的輸入值、透過 hx-include 包含特定元素,或使用 hx-vals 附加額外值。

換言之,HTMX 的基本理念更接近以下幾點:

  • 不直接組裝 JavaScript 物件
  • 從 HTML 元素中收集值
  • 向伺服器發送請求
  • 與 HTML 片段回應比 JSON 更契合

這個流程比想像中重要。因為這種結構與 典型的 Django Form 處理流程幾乎完全一致。

Django Form 的設計也基於以下前提:

  • 使用者在 HTML 表單中輸入
  • 伺服器接收 request.POST
  • Form 驗證資料
  • 若有錯誤則重新渲染
  • 若無問題則儲存並回應結果

至此,HTMX 和 Django Form 似乎就像灰姑娘與玻璃鞋般完美契合。

因此,我們或許可以得出這樣的結論:

與 HTMX 最自然契合的 Django 驗證工具是 Django Form,而非 DRF Serializer。


為何 Django Form 更為契合?

DRF Serializer 無疑是個出色的工具。但若考量其原始設計脈絡,Serializer 更傾向於 資料序列化和 API 輸入驗證。

相對地,Django Form 從一開始就是為以下目的而設計:

  • 處理 HTML 表單輸入
  • 伺服器端驗證
  • 顯示錯誤訊息
  • 保留輸入值
  • 重新渲染模板

換句話說,當它與 HTMX 這種 「接收部分 HTML 並替換」 的方式結合時,會顯得更加自然。

舉例來說:

使用者提交評論表單。

  • 如果驗證失敗怎麼辦?
  • 輸入的內容應該被保留
  • 應該顯示哪些欄位有問題
  • 應該重新部分渲染帶有錯誤的表單

Django Form 在處理這類使用者體驗方面表現出色。

當然,Serializer 也提供 errors 並能進行驗證。但接下來的步驟,即 「重新構建整個 HTML 表單使用者體驗」,Form 處理起來會流暢得多。

因此,單就與 HTMX 的契合度而言,將 Form 視為預設選擇是正確的。


最自然的組合:HTMX + Django Form

首先,我們來看一個最基本的範例,一個簡單的待辦事項註冊表單。

Form 定義

from django import forms

class TodoForm(forms.Form):
    title = forms.CharField(max_length=100, label="제목")
    priority = forms.IntegerField(min_value=1, max_value=5, label="우선순위")

現在,在 View 中,只需將 request.POST 直接傳入 Form 進行驗證即可。

View 處理

from django.shortcuts import render
from django.http import HttpResponse

def todo_create(request):
    if request.method == "POST":
        form = TodoForm(request.POST)
        if form.is_valid():
            title = form.cleaned_data["title"]
            priority = form.cleaned_data["priority"]

            # 저장 로직 수행
            # Todo.objects.create(title=title, priority=priority)

            return render(request, "todos/partials/todo_item.html", {
                "title": title,
                "priority": priority,
            })

        return render(request, "todos/partials/todo_form.html", {
            "form": form,
        }, status=400)

    form = TodoForm()
    return render(request, "todos/partials/todo_form.html", {
        "form": form,
    })

這裡的重點非常明確:

  1. 接收 HTMX 傳送的值作為 request.POST
  2. Form 進行驗證
  3. 若成功則返回 HTML partial
  4. 若失敗則重新渲染包含錯誤的 Form

這個流程既符合 Django 風格,也符合 HTMX 風格,最重要的是,它便於維護。


在模板中自然地重新顯示錯誤

Django Form 特別出色的地方就在於此。當驗證失敗時,Form 物件中已包含以下資訊:

  • 使用者輸入的值
  • 每個欄位的錯誤訊息
  • 非欄位錯誤 (non-field errors)
  • 哪些欄位無效的狀態

因此,在 partial 模板中可以直接渲染這些資訊。

todo_form.html

<form hx-post="{% url 'todo_create' %}" hx-target="#todo-form-wrap" hx-swap="outerHTML">
    {% csrf_token %}

    {% if form.non_field_errors %}
        <div class="error-box">
            {{ form.non_field_errors }}
        </div>
    {% endif %}

    <div>
        <label for="{{ form.title.id_for_label }}">제목</label>
        {{ form.title }}
        {% if form.title.errors %}
            <div class="field-error">{{ form.title.errors }}</div>
        {% endif %}
    </div>

    <div>
        <label for="{{ form.priority.id_for_label }}">우선순위</label>
        {{ form.priority }}
        {% if form.priority.errors %}
            <div class="field-error">{{ form.priority.errors }}</div>
        {% endif %}
    </div>

    <button type="submit">등록</button>
</form>

這個範例雖然非常簡單,卻很好地展示了 HTMX 和 Django Form 的契合度。

  • 失敗時可以重新渲染整個表單
  • 使用者輸入的值也能被保留
  • 錯誤訊息會自然地顯示在欄位旁

如果是傳統的 fetch() + JSON + 手動 DOM 操作方式,為了實現這種流程可能需要編寫相當多的 JavaScript。然而,透過 HTMX 和 Django Form 的組合,僅需伺服器端程式碼和模板即可大幅簡化處理。


那麼,Serializer 是不是就不需要了?

說 「不需要 Serializer」 可能有些過頭了,或許這樣表達會更好: 「不一定非得將 Serializer 作為預設選擇」,您覺得如何?

實際上,在以下情況中,DRF Serializer 仍然具有十足的吸引力:

  • 專案中已廣泛積極使用 DRF 的情況
  • 希望在 API 和伺服器渲染畫面中重用相同的驗證邏輯
  • 輸入驗證邏輯複雜,且已在 Serializer 中良好組織的情況
  • 未來計劃將相同功能暴露給行動應用程式或外部 API 的情況

換言之,Serializer 的問題不在於 「HTMX 能不能用它?」, 而是更接近 「在這裡使用 Serializer,對整體架構來說是否有益?」。


Serializer 也能使用

例如,假設您已經有以下的 Serializer:

from rest_framework import serializers

class TodoSerializer(serializers.Serializer):
    title = serializers.CharField(max_length=100)
    priority = serializers.IntegerField(min_value=1, max_value=5)

在這種情況下,它也能充分應用於 HTMX 請求。

from django.shortcuts import render

def todo_create_with_serializer(request):
    if request.method == "POST":
        serializer = TodoSerializer(data=request.POST)

        if serializer.is_valid():
            title = serializer.validated_data["title"]
            priority = serializer.validated_data["priority"]

            return render(request, "todos/partials/todo_item.html", {
                "title": title,
                "priority": priority,
            })

        return render(request, "todos/partials/todo_form_serializer.html", {
            "errors": serializer.errors,
            "data": request.POST,
        }, status=400)

如您所見,技術上完全沒有問題。 因為 Serializer 並非只能接收 JSON 資料的工具。

然而,這裡會顯現出微妙的差異。

使用 Form 時:

  • 輸入值得以保留
  • 每個欄位獨立渲染
  • 錯誤綁定
  • 與模板的連接

這些都能自然地銜接。

反之,使用 Serializer 時:

  • 需要自行將 serializer.errors 解構以符合模板結構
  • 需另外傳遞原始輸入值
  • 開發者需要花更多心思處理與 HTML form 重新渲染的連接

也就是說,雖然可以使用,但會增加一些手動操作。

正是因為這一點,當與 HTMX 搭配使用時,Serializer 可能會顯得有些牽強。


總結

在上一篇中,我們探討了 HTMX 如何傳送資料。而在本篇中,我們整理了如何在 Django 中最自然地驗證這些資料。

  • HTMX 基本上與 form-data 配合良好
  • Django Form 正是為此流程設計的工具
  • 因此,單就與 HTMX 的契合度而言,Form 是最自然的選擇
  • DRF Serializer 雖然可以使用,但更偏向於策略性的選擇

我個人認為,要充分利用 HTMX,不僅需要培養 「以 HTML 方式處理 AJAX 的感覺」,還需要重新掌握 「運用 Django Form 和 form tags 的技巧」。


相關閱讀