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属性关联起来。 在这里,我们的设计思路是:
- 在view函数中调用messages.add_message()或者它的快捷方法比如messages.info(),messages.success(),把消息主体添加到请求中。
- 在自定义中间件中,调用messages.get_message()查看请求是否有消息主体,有的话把消息添加到response header的HX-Trigger中,没有就直接返回响应。
- 页面监听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
- 注解 0: 当响应中已经有其他HX-Trigger事件,比如todo-updated,我们要确保在添加消息事件show-message的时候保留其他事件,然后在增加消息事件show-message。
- 注解 1: get_messages会返回一个可迭代的存储后端(CookieStorage/SessionStorage),理论上我们可以在view函数中添加多个message,通过调用for语句遍历。但是为方便起见,我们在view中只添加一个message,所以代码中用了all_messages[0]
监听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>
- 当show-message.document被捕捉到,show属性被设置为true。此时,消息框
<div x-show="show">
会显示出来。 - 同时通过调用$event结合detail属性,可以访问message(消息本体)和level(消息级别)。
- message和x-text绑定在一起后可以动态更新消息框内容。
- level则可以控制消息框的不同风格,比如红色错误弹框,蓝色消息弹框等等。
- 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()
即可,代码更加简洁易懂。