使用 Django 和 HTMX 简化动态 Web 开发:Form 和 Serializer 的应用



在上一篇文章中,我们探讨了 HTMX 将数据发送到服务器的方式

回顾上一篇使用Django和HTMX简化动态Web开发(第四部分):Payload传输方式

我们发现,如果说传统 JavaScript 的 fetch() 更接近于直接构建 JSON payload 并发送,那么 HTMX 则更倾向于从 DOM 收集值并以 form-data 的形式发送

那么,一个自然而然的问题便浮现出来:

“对于 HTMX 传入的数据,Django 应该使用什么工具进行验证才最自然?”

起初,许多人可能会想到 DRF Serializer。实际上,Serializer 确实是一个强大的验证工具,即使不是 JSON 数据也能使用。但当我们尝试将其与 HTMX 结合使用时,总会感觉有些格格不入。

这是为什么呢?

原因很简单:
HTMX 的基本工作流程更贴近 HTML 表单的世界,而 Django 中恰好已经存在专为此类场景设计的 Form

在本文中,我们将整理 在 HTMX 请求处理中,Django FormDRF Serializer 各自的用法,并探讨 哪种方案更为自然和实用

HTMX 请求处理中 Form 和 Serializer 的作用比较


HTMX 发送的数据本质上更接近“表单数据”

正如我们在上一篇中看到的那样,HTMX 默认会收集 HTML 元素的值并将其发送到服务器。这包括发送 <form> 中的输入值,通过 hx-include 包含特定元素,或通过 hx-vals 附加额外的值。

换句话说,HTMX 的核心理念更接近于:

  • 不直接组装 JavaScript 对象
  • 从 HTML 元素中收集值
  • 向服务器发送请求
  • 与 JSON 相比,更适合接收 HTML 片段响应

这个流程比想象中更为重要。 因为它与典型的 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 会显得更加自然流畅。

例如,我们来设想一个场景:

用户提交一个评论表单。

  • 如果验证失败怎么办?
  • 用户输入的内容应该被保留
  • 需要显示哪些字段有问题
  • 需要重新部分渲染带有错误的表单

这些用户体验正是 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="우선순위")

现在,在视图中,我们可以直接将 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 片段
  4. 失败时重新渲染带有错误的 Form

这个流程既符合 Django 的风格,也符合 HTMX 的风格,最重要的是,它易于维护。


在模板中自然地重新显示错误

Django Form 在这里尤其出色。

当验证失败时,Form 对象中已经包含了以下信息:

  • 用户输入的值
  • 各字段的错误消息
  • 非字段错误
  • 哪些字段无效的状态

因此,在局部模板中可以直接渲染这些信息。

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 表单重新渲染的连接

也就是说,虽然可以使用,但会增加一些手动操作

正是因为这一点,当与 HTMX 结合使用时,Serializer 可能会让人感觉有些生硬。


总结

在上一篇文章中,我们探讨了 HTMX 如何发送数据。而在本文中,我们总结了在 Django 中如何最自然地验证这些数据。

  • HTMX 默认与 form-data 配合良好
  • Django Form 正是为此流程设计的工具
  • 因此,仅从与 HTMX 的兼容性来看,Form 是最自然的选择
  • DRF Serializer 虽然可用,但更偏向于战略性的选择

我个人认为,要充分发挥 HTMX 的潜力,不仅需要“以 HTML 方式处理 AJAX 的直觉”,还需要“重新找回利用 Django Form 和表单标签的直觉”


相关阅读