使用 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 FormDRF 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 的技巧」


相關閱讀