用 htmx 和 hyperscript 重新构想前端 Web 开发
我们都知道,要为您的网站创建交互式前端,您需要 JavaScript。不仅仅是普通的 JS,请注意:我们在 2022 年,要创建可接受的 UI,您需要使用 React 或 Vue.js 等框架。正确的?
错误的。
近年来,一些特立独行的人和叛徒开始远离 JS 框架的世界和不可避免的臃肿的 node_modules 文件夹。但是,如果您想要流畅的单页应用体验,而不是每次点击按钮时都等待整个页面呈现,该怎么办?当然,没有人愿意为每个小的交互编写大量的样板 JS。这就是htmx和hyperscript形式的 hypermedia 出现的地方。
这两个开源工具包均由Big Sky Software和合作者共同开发,提供了大量 HTML 属性来处理 AJAX 请求、部分 DOM 更新、CSS 转换、事件处理、Server-Sent Events 和 WebSockets。 , 用户友好的语法。网上有许多优秀的教程展示了这些工具的功能;我特别喜欢 YouTube](https://www.youtube.com/channel/UCTwxaBjziKfy6y_uWu30orA)上[BugBytes 的教程。在本文中,我将向您展示我如何在我的当前项目中使用它们,这是一个使用 Django 制作的简单的会员/订阅跟踪站点。
我的目标是允许用户通过模式(弹出)对话框中的表单将成员资格添加到他们的个人列表中,并从表中编辑或删除现有成员资格,该表将在不重新加载页面的情况下进行更新。起点是Benoit Blanchon的这篇优秀文章;在 Benoit 的文章中,他使用了 htmx,但避开了不太成熟的超脚本,转而使用一些简单的 JS 函数。由于 htmx 和 hyperscript 是按照相同的理念开发的,并且旨在很好地协同工作,所以我决定全力以赴地进行超媒体炒作,并尽量不使用单行“纯”JavaScript。另一个小区别是我使用的是 TailwindCSS,而 Benoit 使用的是 Bootstrap,因此一些实用程序类的名称会有所不同。
模态对话框和表单提交
第一步是允许打开和关闭包含表单的模式对话框,并可以提交表单。由于我使用的是daisyUI,因此可以使用现成的模态组件,可以通过添加或删除.modal-open类来打开或关闭它。
这是超脚本的经典用例; “新建”按钮将类添加到模式中,而“关闭”按钮将其删除。
<!-- 'New' button on the main 'my-memberships' page -->
<button _="on htmx:afterRequest add .modal-open to #modal"
hx-get="{% url 'my-memberships' %}" hx-select="#modal-box" hx-target="#modal"
class="mx-auto md:ml-2 btn btn-primary btn-square border-none basis-14">New
</button>
<!-- 'Close' button on the top-right corner of the modal dialog -->
<button _="on click remove .modal-open from #modal"
class="btn btn-sm btn-circle absolute right-2 top-2">✕
</button>
请注意在“新建”按钮上使用htmx:afterRequest事件,而不是在“关闭”按钮上使用简单的click事件。这是因为在显示表单之前,我们会等待从后端返回“my-memberships”的新的空表单(否则可能会在“从服务器返回干净的表格)。另请注意,我们使用hx-select属性从响应中仅选择#modal-box元素,并使用hx-target将其放置在#modal元素中(对 GET 请求的响应包含整个 'my-memberships' 页面,这不是我们想要在我们的模态中!)。
表单上还有“保存”按钮,它通过 POST 请求将表单提交到“my-memberships”后端。 hyperscript 用于在响应加载之前禁用按钮,以防止重复提交。
<button type="submit" class="btn btn-primary border-none"
_="on click toggle @disabled until htmx:afterOnLoad">Save
</button>
与“my-memberships”URL 关联的基于类的视图如下:
class MembershipView(LoginRequiredMixin, TemplateView):
template_name = 'memberships.html'
extra_context = {'form': MembershipEditForm()}
def post(self, request, *args, **kwargs):
form = MembershipEditForm(request.POST)
success = False
if form.is_valid():
membership = form.save(commit=False)
if kwargs:
if kwargs['update']:
membership.pk = kwargs['pk']
membership.user = request.user
membership.save()
success = True
self.request.path = reverse_lazy('my-memberships')
form = MembershipEditForm()
response = render(request, 'partials/modal-form.html', {'form': form})
if success:
response['HX-Trigger'] = 'membershipsChanged'
return response
表单提交有两种可能的结果:
- 提交的表单返回验证错误,在这种情况下,htmx 会将现有的模态对话框与响应交换并显示错误:
<div idu003d"modal-box" classu003d"modal-box p-4 scrollbar-thin" hx-targetu003d"this" hx-swapu003d"outerHTML">
...
{% if form.non_field_errors %}
<div classu003d"mt-2">
{{ 表格|as_crispy_errors }}
</div>
{% 别的 %}
<p classu003d"pt-2 pb-4">在下方输入您的订阅详情</p>
{% 万一 %}
...
</div>
- 新成员被保存到数据库并返回一个干净的表格。在这种情况下,我们将“HX-Trigger”标头附加到值为
membershipsChanged的响应中。这是我们的前端关闭模式并更新显示用户成员资格的表的提示:
<table classu003d"table table-fixed grow">
<thead classu003d"w-auto">
...
</thead>
<tbody idu003d"membership-table-body"
hx-triggeru003d"加载,membershipsChanged from:body"
hx-getu003d"{% url '更新会员' %}"
hx-目标u003d这个
_u003d"on htmx:afterOnLoad 添加 .hidden 到 #spinner">
</tbody>
</table>
当然,我们还会在load事件发生时(在页面加载时)将成员资格加载到表中,并在 htmx 将响应加载到表中后隐藏“加载”微调器。
更新会员表
如果收到membershipsChanged事件,我们就知道新成员已经成功保存,我们可以更新表单。我在“我的会员资格”页面上包含了一个小(超)脚本,在这种情况下暂时显示成功警报:
<script type="text/hyperscript">
on membershipsChanged
remove .modal-open from #modal
show #alert-success
wait 3s
hide #alert-success
end
</script>
表格显示包括一个允许通过简单的单击来打开或关闭提醒(续订、免费试用到期等)的列。 htmx 用于向服务器发送 PATCH 请求,其中包含在 URL 中的相关成员的主键。
<input type="checkbox"
class="checkbox checkbox-primary border-gray-400 mt-1"
{% if membership.reminder %}checked{% endif %}
hx-patch="{% url 'toggle-reminders' membership.pk %}"
hx-swap="none"/>
这是通过一个简单的函数在后端获取的,该函数切换给定成员的提醒状态:
@login_required()
def toggle_reminders(request, pk):
if request.method == 'PATCH':
membership = Membership.objects.get(pk=pk)
if membership.user == request.user:
membership.reminder = not membership.reminder
membership.save()
没有返回值,并且 htmx 不期望任何返回值,因为我们指定了hx-swap="none"(在这种情况下,没有内容被交换到目标中,即使这样的内容存在于响应的正文中)。如果我们想处理找不到对象的可能性,我们可以使用get_object_or_404()并在响应中发送“HX-Redirect”标头以提示 htmx 重定向到 404 页面。
我们还提供了一个下拉菜单,可通过单击每个成员的名称访问,允许我们编辑或删除该成员。
<ul tabindex="0" class="dropdown-content menu p-2 shadow bg-base-100 rounded-box w-24">
<li>
<a hx-get="{% url 'edit-membership' membership.pk %}" hx-target="#modal"
_="on htmx:afterRequest add .modal-open to modal">Edit
</a>
</li>
<li>
<a hx-post="{% url 'delete-membership' membership.pk %}"
hx-confirm="Are you sure you want to delete the membership '{{ membership.membership_name }}'?"
hx-target="closest tr" hx-swap="delete">Delete
</a>
</li>
</ul>
点击“编辑”,弹出现有会员详细信息的模式;作为该请求的结果,表单提交的目标 URL 被更新为特定于该成员资格的 URL(尽管最后 POST 请求被转发到与创建相同的函数,只是使用可选的“更新”关键字)。
class EditMembershipView(LoginRequiredMixin, UpdateView):
model = Membership
fields = "__all__"
template_name = 'partials/modal-form.html'
def get(self, request, *args, **kwargs):
self.object = self.get_object()
if self.object.user == request.user:
return super().get(request, *args, **kwargs)
def post(self, request, *args, **kwargs):
self.object = self.get_object()
if self.object.user == request.user:
return MembershipView.as_view()(request, update=True, pk=self.object.pk)
单击“删除”会导致页面提示确认,然后将请求 POST 到后端(我们在这里使用 POST 而不是 DELETE,因为视图扩展了 Django 的“DeleteView”,它需要 POST 请求)。
class DeleteMembershipView(LoginRequiredMixin, DeleteView):
model = Membership
def post(self, request, *args, **kwargs):
self.object = self.get_object()
if self.object.user == self.request.user:
self.object.delete()
return HttpResponse()
# Unfortunately we cannot return status 204 or else htmx will ignore the response (see docs at htmx.org)
return redirect('my-memberships')
成功时的响应代码是200 - OK而不是204 - No Content,因为否则 htmx 不会按照hx-swap和hx-target属性的请求触发closest tr的delete(我们总是可以通过在204响应并使用超标,但这增加了一个额外的步骤,没有功能增益)。
最后的想法
所以你有了它:一个模态表单和一个完全用 htmx 和超脚本处理的表格,看不到 JavaScript 或页面重新加载。一旦你进入超媒体思维模式,它就会成为构建响应式 UI 的一种相当直观、极其强大和灵活的方式。有时内置的 htmx 属性并不完全具有所需的行为(例如在删除时对204响应的反应),但是可用的标头允许我们在前面触发事件并处理这些情况结尾。这使得 htmx 和 hyperscript 的结合更加强大,hyperscript 的可读性是首屈一指的。
htmx 和 hyperscript 都可以通过公共 unpkg CDN 获得,或者作为独立的 .js 或 npm 包获得。遵循一些教程很容易上手,我鼓励任何想要构建网站而无需捆绑 React、Vue 或 Angular 的人尝试一下。像我这样的项目的下一步可能包括在表格上使用分页进行无限滚动,单击列标题以在不重新加载页面的情况下对结果进行排序,或者添加选项卡以在表格和日历视图之间平滑切换。
我希望你喜欢这篇文章,并且你会回来看下一篇!
更多推荐

所有评论(0)