前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >HTMX:前端的 1984 时刻?

HTMX:前端的 1984 时刻?

作者头像
tyrchen
发布2023-09-21 08:26:15
6870
发布2023-09-21 08:26:15
举报
文章被收录于专栏:程序人生程序人生

被 javascript 全面绑架的前端开发

十几二十年前,我曾经是个自信满满的互联网开发者。我可以轻松地使用 django 构建 Web UI。页面上大大小小的重复部分,我都用 template 或者 fragment 抽象或者封装。如果需要,我并不排斥撰写 javascript 来增加交互性:

然而,这种方式构建的 UI 会导致用户和页面的每次交互都需要后端重新发送完整的 html 页面,这既浪费带宽,交互的方式又笨拙不流畅。因而,一些 ajax 库便被创造出来提升交互能力。渐渐地,javascript 处理的事情越来越多,就连服务器端渲染 HTML template 的动作也慢慢迁移到了客户端。最终,以 react 为代表的响应式组件化 UI 的春天来临了:

react 带给 web 开发很多革命性的理念:虚拟 dom,单向数据流,JSX 以及组件化思维。它让前端从 HTML 客户端彻底倒向了 Javascript 客户端,同时让后端退出前端渲染的舞台,把生成 HTML 的主导权让渡给前端,自己安安心心地只做数据 API 的提供方。但当 javascript 开始接管一切,HTML 不得不成为二等公民后,一切也随之变了味 —— 连 footer 这样完全由静态 HTML 组成的内容都要通过 javascript (jsx) 完成。原本丰腴的 HTML 页面瘦成一道闪电,body 里只剩下一个用来 mount 的根元素。

在 react 席卷前端世界之时,react 的缺陷便一个个暴露出来。于是,新的思想,新的框架,新的生态工具被创造出来,热情的前端开发者架们以一种「逢山开路,遇水搭桥」的方式一路蒙眼狂奔,缺什么补什么:没有合适的状态管理,就创造出 redux;状态管理太复杂,那引入 hooks;js 客户端对 SEO 不友好,上 SSR …。这样不断堆叠解决方案后,最初简洁明了的方案被硬生生折腾成一个庞杂的缝合怪。此时,我已经轻易不敢碰前端了,原本简简单单能搞定的事情,现在繁文缛节一大堆,写点前端代码我感觉自己都要被过度的复杂性压得透不过起来。

更糟糕的是,由于 javascript 接管一切带来的前端项目的大型化,使得 typescript 成为了最佳实践。我并非贬低 typescript,事实上 typescript 是一门设计良好的语言,它很好地解决了 javascript 在大型前端项目中使用的诸多问题。但我们真的到处都需要「大型」前端项目么?导致前端项目如此庞杂臃肿的根源是什么?最初这些框架的主要目的难道不是为了让前端更加响应式,更容易复用,更容易表达么?可如今,react 及那些前前后后崛起的前端框架们,包括 vue,solidjs,svelte 等等,都在以自身的复杂性迫使前端开发者,或者说像我这样的「伪前端开发者」,不得不把小型项目大型化,简单项目复杂化,于是应对复杂项目的 typescript 成为了必然的选择。

被安在纪伯伦身上的一句中文名句:”我们已经走得太远,以至于忘了为什么出发” 形象地描述了这十多年来前端的发展。我不知道有多少前端程序员对前端的现状感到满意,但像我这样,有时候仅仅是想为自己做的系统提供一个简洁的 UI —— 只需一茶匙就能装下的前端需求 —— 却面对 react 全家桶的复杂性产生深深的无力感。

差不多一年前,我在做一个后端低代码的玩具项目时无意发现了 htmx,一下子就被其纯净的思想深深吸引。彼时我并未深入研究。过去两周,因为工作的原因我迫切需要做点前端的工作时,我果断地捡起了 htmx 进行深入试验。两周断断续续的开发过程中,我使用 axum (web server) + askama (template) + htmx + tailwindcss 很快地完成了我想做的事情,并且对界面高效地进行了好几版迭代。我自己的感觉是:htmx 即便不能成长为前端的新势力,它也能重塑所有非前端工程师对前端开发的信心。对于那些苦前端久矣的开发者来说,我们也许迎来了前端的 1984 时刻。

回归 HTML 初心的 HTMX

虽然我找不到 HTMX 的名字的来源,根据它的愿景,我猜测它有 HTML eXtension 的意思。HTMX 认为我们应该增强和发展 HTML,HTML 的很多缺陷可以通过更好地 HTML 语义,比如标签的属性来弥补,而非直接让 javascript 取代 HTML。这是 htmx.org 上的直接介绍:

htmx gives you access to AJAX, CSS Transitions, WebSockets and Server Sent Events directly in HTML, using attributes, so you can build modern user interfaces with the simplicity and power of hypertext

HTMX 的核心愿景和特点包括:

  1. 逐步增强: HTMX 是基于逐步增强的原则设计的,这意味着它的目标是首先确保页面在没有 JavaScript 的情况下可以工作,然后逐渐增加更多的功能。
  2. 低侵入性: HTMX 旨在尽可能少地侵入你的代码。你不需要重写整个应用来开始使用HTMX;相反,你可以只在需要的地方添加一些属性。
  3. 纯 HTML: 使用 HTMX,你可以在不编写 JavaScript 的情况下实现许多复杂的前端功能。这使得代码更容易理解和维护,尤其是对于那些更喜欢或更熟悉 HTML 和服务器端编程的开发者。
  4. 与现有技术兼容: HTMX 可以与你已经使用的框架和库无缝地协同工作。无论你是否使用 Flask、Django、Rails 或其他后端框架,HTMX 都可以简单地嵌入其中。
  5. 小而快: 相比于许多现代的前端框架,HTMX 是非常轻量级的,这意味着它加载得更快,对用户的体验有所提升。

我们可以看到,HTMX 的目标是简化前端开发,使开发者能够快速、高效地创建交互性强、响应迅速的网页,同时避免涉及大量的 JavaScript 或复杂的前端框架。

当你不需要复杂的前端框架和大量的 javascript 开发时,你会发现,目前前端所面临的很多问题都不是问题:不需要把整个页面 javascript 化,不需要为了解决页面 javascript 化引入的 SEO 问题,更不需要管理管理复杂的状态,以及引入 typescript 来解决工程化的问题。

talk is cheat, show me the code!

我们先来看看 htmx 下,如何实现典型的前端功能:autocomplete。

不要过于震惊,这一小段代码就是 HTMX 版 autocomplete 的全部代码。可以看到,HTMX 给普通 HTML 标签增加了几个重要的属性:

  • hx-trigger:用于指定何时以及如何触发一个 htmx 动作,例如 AJAX 调用。通过这个属性,开发者可以控制在某些事件发生时(例如,点击、输入或聚焦等),如何发起与服务器的交互。在这个例子里,我特意写了两个触发事件,a) keyup 事件导致 input 变化时,延迟 500ms 后触发;b) 接收到名为 custom-event 的事件。
  • hx-get:当 htmx 动作被触发时,执行的调用。hx-get 代表 GET 请求,同理,你可以使用 hx-post,hx-put,hx-delete,hx-patch 等服务器调用。通过 hx-get 这样的属性,HTMX 把与服务器交互的权利下放给每一个标签,而非传统上那样 —— 只有 <a/><form/> 才能和服务器交互。这可能是很多时候我们不得不引入 javascript 的一大原因。
  • hx-target:当服务器的响应返回时,响应被填充在哪个位置。hx-target 可以是任何 css 表达式,这里我们将其指向了 id 为 search-results 的节点。默认是当前节点。如果说 hx-get 这样的属性提供了页面中无处不在与服务器交互的能力,那么 hx-target 就提供了页面中无处不在的动态更新能力。这中动态更新能力是我们引入 javascript 的重要原因。
  • hx-swap:当服务器的相应返回时,内容该如何交换或者替换,默认是 innerHTML,也就是说 #search-results 内部的 HTML 会被服务器返回的数据替换。hx-swap 还有其他行为,比如 outerHTML,beforeend,afterend,甚至还可以添加如何做 swap 的动画效果,大家可以自行查看文档。

这几个是对初学者而言最有用的属性,掌握了它们就能处理大部分的页内交互。HTMX 还提供了很多 hx-* 属性,我就不一一介绍了。使用这些属性,我们就可以控制搜索框的行为,很轻松地完成原本要不少 javascript 才能达到的效果。

我们再来看一个复杂一些的例子:

假设应用展示若干 note books,每个 note book 有若干 notes,每个 note 有详尽的信息。我们用三栏式展示。用户点击最左栏的 book1 时,book1 下的 notes 以分页的形式展示在第二栏,然后第二栏的第一个 note 的详情在第三栏展示。

你可以想象一下这样的页面和交互需求用 react 该如何完成。

使用 HTMX,我们可以完全依照服务器渲染的思路设计,不必过多考虑客户端如何维持状态,如何动态刷新。

在第一次生成这个页面的时候,我们可以把 book1 下面的所有 note summary 展示出来,然后再把 book1 note1 下面的 detail 也展示出来。几个部分的模板片段如下。首先是左栏:

代码语言:javascript
复制
<ul>
{% for book in books %}
<li>
  <a hx-get="/books/{{book.id}}" hx-target="#note-list">{{book.name}}</a>
</li>
{% endfor %}
</ul>

然后中栏:

代码语言:javascript
复制
<div id="note-list">
{% for note in current_book.notes %>
<div onclick="htmx.trigger('#note-detail', 'loadNote', {id: '{{note.id}}'})">
  <h2>{{note.title}}</h2>
  <p>{{note.summary}}</p>
</div>
{% endfor %}
</div>

最后右栏:

代码语言:javascript
复制
<div id="note-detail"
      _="on loadNote(data) from body
         htmx.ajax('GET', `/notes/${data.id}`, '#note-detail')"
>
  <h3>{{title}}</h3>
  <p>{{detail}}</p>
</div>

这样简简单单几个模板,辅以额外的 HTMX 属性,不光是第一次页面渲染的结果有了,页面也能根据用户的点击进行更新。比如用户点击 book2,它会触发一个 GET 请求,访问 /books/2,返回如下响应(就是中栏的模板生成的内容):

代码语言:javascript
复制
200 OK
HX-Trigger: {"loadNote": {"id": "book2id1"}}
Content-Type: text/html

<div onclick="htmx.trigger('#note-detail', 'loadNote', {id: 'book2id1'})">
  <h2>Hello 1</h2>
  <p>World 1</p>
</div>
<div onclick="htmx.trigger('#note-detail', 'loadNote', {id: 'book2id2'})">
  <h2>Hello 2</h2>
  <p>World 2</p>
</div>
...

这个结果会被 HTMX 渲染到 #note-list 中。于是中栏得到更新。

同时,因为返回的 HX-Trigger 头带了 loadNote 事件,该事件被 #node-detail 捕获并发送 GET 请求到 /notes/book2id1 ,然后其响应被渲染到右栏。

一个事件导致页面多处更新,这种并不简单的处理,我们用 HTMX 轻松搞定了。

这里我们引入了一个新的东西:特殊的 HTTP 头 HX-Trigger。HTMX 定义了很多新的 HTTP header,用于客户端和服务器交互额外信息。这里的 HX-Trigger 头,提供了一个强大而灵活的服务器端响应触发客户端事件的能力。

你可能会对这段代码感到疑惑:

代码语言:javascript
复制
<div id="note-detail"
      _="on loadNote(data) from body
         htmx.ajax('GET', `/notes/${data.id}`, '#note-detail')"
>

它是 HTMX 的 hyperscript 的表述,等价于:

代码语言:javascript
复制
document.body.addEventListener("loadNote", function(e){
    htmx.ajax('GET', `/notes/${e.detail.id`, '#node-detail');
})

到目前为止,我们写了两三行非常简单的 javascript,就实现了整个三栏加载和更新逻辑。我们用图把整个逻辑梳理一下:

是不是相当简洁?你是愿意撰写这样的代码,还是原意从 npm init 开始,一步步设置 react 全家桶,最终写上一大堆组件,维护一系列状态,才能达成相同的目标?

对于上述这样一个事件多处更新的场景,使用事件机制是我个人比较喜欢的实现。其实 HTMX 也提供了其他解决方案,比如使用 hx-swap-oob,或者 multi-swap 扩展,写起来比我上述的方案还要简洁一些。

回顾上述两个例子,我们可以看到,在使用 HTMX 后,大量的逻辑依旧保留在后端,就像十几年前我们在 rails/django 里处理的那样。我们把一个个 template / fragment 拆分到组件级别,然后把服务器渲染好的 HTML 传递给客户端。只不过,有了 HTMX 后,我们可以很轻松地实现响应式前端,所有的操作都可以以你需要的粒度更新在页面的任何位置。

由于 HTMX 用标签属性这样一种很舒服的方式来标准化基本的客户端/服务器间的操作,在大多数场合下,配合 tailwindcss 这样的 CCS 工具箱,构建前端只需要和 HTML 打交道。在我做项目时,我基本上就是找 flowbite 这样的网站上的某个组件的示例代码,稍作修改使其模板化,再把这些模板整合起来,一个个页面就构建出来了。我再也不需要拘泥于究竟要做 SPA 还是 MPA,一切根据需求随心而动。

当然,使用 HTMX 也可能会带来一些耦合性问题 —— 这并非 HTMX 的锅,而是自 PHP 起,所有做服务端渲染 HTML 的后端都会带来的问题:逻辑层和表现层的耦合,以及多端的支持。

逻辑层和表现层的耦合可以通过更好地架构设计(或者引入合适的框架)来避免,我们放下不表。多端的支持可以通过服务器对内容协商的支持而得到支持。比如 web 端,可以发送 Accept: text/htmx, text/html,明确要求 HTMX 格式或者 HTML 格式的数据,而移动端,可以发送 Accept: application/json 来获得 JSON 数据。具体服务端的实现逻辑(axum版)如下:

总结

HTMX 为非前端工程师重新打开了前端开发的大门。如果你不是开发像 spreadsheet,google map 这样的重交互应用,基本上,你都能很好地用 HTMX 来取代现有的前端开发框架,重新回到以 HTML 为中心的轻量级前端开发上。你不必拘泥于客户端究竟该实现成 SPA 还是 MPA,可以用最合适的方式路由,最自然的方式展示数据,让用户跟数据交互(无论是增删改查还是其他什么动作)。

目前,HTMX 的生态还刚刚起步,我非常期待主流的后端框架对其进行深度的支持甚至整合。我相信随着 HTMX 的价值被不断发掘出来,最终,非前端开发者可以重拾信心,无痛开发一个包含 web 前端的完整的产品。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2023-09-18 21:00,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 程序人生 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 被 javascript 全面绑架的前端开发
  • 回归 HTML 初心的 HTMX
  • 总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档