使用Django和HTMX简化动态Web开发(第四部分):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究竟是以何种方式向服务器发送数据的?由于许多示例只展示了非常简单的功能,要找到创建和发送复杂数据负载的实际案例却出乎意料地困难。

本文将彻底梳理HTMX中的数据传输方式以及服务器如何处理这些数据。

Django中HTMX数据流示意图


HTMX的数据传输方式

fetch的数据负载处理方式与hx-post的数据传输方式存在显著差异。

fetch中,开发者需要手动创建对象并将其转换为JSON格式放入请求体(body)。而HTMX则默认收集DOM中的值并进行传输

HTMX的思维方式更接近于“从HTML元素中收集值来构建请求参数”,而非“直接组装JS对象”。通常,它采用以下三种方式:

1) 基于表单收集并发送值

这是最符合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) 不使用表单,包含其他元素的值: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>标签内,就能产生类似于数据负载的效果,对于少量输入项的场景非常实用。

3) 添加隐藏值或计算值:hx-vals

这是与fetch中直接构建部分数据负载对象概念最接近的功能。

<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对象作为请求体发送”,它更接近于“在请求参数中添加a=1&b=2。换句话说,服务器仍然需要像处理表单数据一样来处理它。

如果确实需要发送JSON请求体怎么办?

如果您确实需要使用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数据负载方式。此时,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中收集值。这与fetch直接构建数据负载对象的方式有所不同。
  2. 三种传输方式:用于发送整个表单的<form>,用于选择性发送的hx-include,以及用于添加额外值的hx-vals
  3. 默认是表单数据。请注意,hx-vals使用JSON语法并不意味着它会发送实际的JSON请求体。
  4. 如果确实需要JSON,请使用json-enc扩展。但最好记住,HTMX的真正精髓在于交换HTML片段。

下一次,我们将探讨如何将DRF的便利性融入HTMX中!

相关文章阅读