Skip to content

集成Django消息中间件和HTMX

Updated: at 08:16 AM

Table of contents

Open Table of contents

概述

在之前的一篇文章中-用HTMX和Alpine.JS实现表单提交,我们在response header中添加了HX-Trigger,然后利用Alpine.js的x-on监听事件来触发消息弹框。这种方式可以便于我们理解内在机制,但是不利于日常代码编写,试想如果我们有100个不同场景需要触发消息弹框,难不成要重复写100个类似的代码块么?显然得有更好的方式来处理这种情况!那就是利用Django自带的MessageMiddleware中间件,只需要在view函数中调用messages.info(request, "hello world")即可。

设计

此处,为了快速进入主题,省去了项目创建,配置的步骤。代码也是接着另一篇文章写的,如果感兴趣的话,可以参考:

Django的中间件按照官网的说法,它是“请求/响应处理的钩子框架”,说白了,就是一个请求在执行view函数之前或者之后,是可以对请求或者响应查看或者更改的。比如内置的中间件AuthenticationMiddleware,它将每一个请求与user属性关联起来。 Django Middleware 在这里,我们的设计思路是:

  1. 在view函数中调用messages.add_message()或者它的快捷方法比如messages.info(),messages.success(),把消息主体添加到请求中。
  2. 在自定义中间件中,调用messages.get_message()查看请求是否有消息主体,有的话把消息添加到response header的HX-Trigger中,没有就直接返回响应。
  3. 页面监听HX-Trigger的事件,如果有消息事件,则显示弹框,并根据level改变消息状态。

view函数调用messages.add_message():

messages.debug(request, "%s SQL statements were executed." % count)
messages.info(request, "Three credits remain in your account.")
messages.success(request, "Profile details updated.")
messages.warning(request, "Your account expires in three days.")
messages.error(request, "Document deleted.")

自定义中间件

import json
from django.contrib.messages import get_messages

def htmx_message_middleare(get_response):
    def middleware(request):
        response = get_response(request)
        all_messages = list(get_messages(request))
        if "HX-Request" in request.headers:
            if all_messages:
                hx_trigger_header = {}
                if response.headers["HX-Trigger"]:			# 注解 0
                    hx_trigger_header = json.loads(response.headers["HX-Trigger"])
                hx_trigger_header["show-message"] = {
                        "level": all_messages[0].level_tag,	# 注解 1
                        "message": all_messages[0].message
                }
                response.headers["HX-Trigger"] = json.dumps(hx_trigger_header)

        return response

    return middleware

监听show-message事件

事件冒泡会把HX-Trigger中的show-message事件“冒泡”到document节点,所以可以用Alpine.js中x-on的.window或者.document修饰语来捕捉事件,然后触发消息弹框机制。

<div
  x-data="{ show: false, message: '', level: '' }"
  aria-live="assertive"
  @show-message.document="show=true; 
    message=$event.detail.message; 
    level=$event.detail.level; 
    setTimeout(() => show=false, 2000)"
  class="pointer-events-none fixed inset-0 flex items-end px-4 py-6 sm:items-start sm:p-6"
>
  <template x-if="level === 'success'">
    <svg class="h-6 w-6 text-green-400">...</svg>
  </template>
  <template x-if="level === 'info'">
    <svg class="h-6 w-6 text-blue-400">...</svg>
  </template>
  <template x-if="level === 'error'">
    <svg class="h-6 w-6 text-red-400">...</svg>
  </template>
  ...
  <div x-show="show">
    ...
    <p class="text-sm font-medium text-gray-900" x-text="message"></p>
    ...
  </div>
  ...
</div>
  1. 当show-message.document被捕捉到,show属性被设置为true。此时,消息框<div x-show="show">会显示出来。
  2. 同时通过调用$event结合detail属性,可以访问message(消息本体)和level(消息级别)。
  3. message和x-text绑定在一起后可以动态更新消息框内容。
  4. level则可以控制消息框的不同风格,比如红色错误弹框,蓝色消息弹框等等。
  5. setTimeout()方法可以控制消息框在窗口停留的时间,一旦超过设定时间,则自动关闭。

对比

原始代码:

def add_todo(request):
    if request.method == 'POST':
        new_todo = request.POST["todo"]
        Todo.objects.create(title=new_todo)
        headers = {
            "HX-Trigger": json.dumps({
                "show-message": {
                    "level": "info",
                    "message": "Todo added successfully!"
                },
                "todo-updated": None
            })
        }
        return HttpResponse(status=204, headers=headers)
<div
  x-data="{ show: false, message: '' }"
  aria-live="assertive"
  @show-message.document="show=true; 
    message=$event.detail.message; 
    setTimeout(() => show=false, 2000)"
  class="pointer-events-none fixed inset-0 flex items-end px-4 py-6 sm:items-start sm:p-6"
>
  ... ...
  <p class="text-sm font-medium text-gray-900" x-text="message"></p>
  ... ...
</div>

新代码:

def add_todo(request):
    if request.method == 'POST':
        new_todo = request.POST["todo"]
        Todo.objects.create(title=new_todo)
        messages.success(request, "Todo added successfully!")
        headers = {
            "HX-Trigger": json.dumps({
                "todo-updated": None
            })
        }
        return HttpResponse(status=204, headers=headers)
<div
  x-data="{ show: false, message: '', level: '' }"
  aria-live="assertive"
  @show-messages.document="show=true; 
    message=$event.detail.message; 
    level=$event.detail.level; 
    setTimeout(() => show=false, 2000)"
  class="pointer-events-none fixed inset-0 flex items-end px-4 py-6 sm:items-start sm:p-6"
>
  ...
  <template x-if="level==='success'"> ... </template>
  <template x-if="level==='info'"> ... </template>
  <template x-if="level==='error'"> ... </template>
  ...
  <p class="text-sm font-medium text-gray-900" x-text="message"></p>
  ... ...
</div>

可以看到,现在想要在页面显示消息,只需要在view函数中调用messages.success()即可,代码更加简洁易懂。