超媒体与浏览器增强

工程 | Dave Syer | 2024年3月15日 | ...

如今,前端开发主要由大型 JavaScript 客户端框架主导。这有很多充分的理由,但对于许多用例来说,效率非常低,而且框架工程变得极其复杂。在本文中,我想探讨一种不同的方法,一种更高效、更灵活的方法,它由更小的构建块构建而成,非常适合 Spring 等服务器端应用框架(或各种服务器端语言中的类似工具)。其理念是拥抱超媒体的概念,想象下一代浏览器将如何利用它,并使用少量 JavaScript 将当今的浏览器增强到该水平。现代浏览器会忽略 HTML 中的自定义元素和属性,但它们允许内容作者使用 JavaScript 定义其行为。目前已有几个库可以帮助实现这一点,我们将研究 HTMXUnpolyHotwired Turbo。我们还将探讨如何将这些库与 Spring Boot 结合使用,以及如何将它们与像 Thymeleaf 这样的传统服务器端框架结合使用。

您可以在 GitHub 上找到源代码 (dsyer/webmvc-thymeleaf)。"main" 分支是起始点,并且有对应我们要探索的每个库的分支。

起始点

作为起始点,我们将使用一个简单但不简单的 Spring Boot 应用,它使用 thymeleaf。它最初是为 Thymeleaf 和 Spring Webmvc 的性能测试而创建的,所以我们希望它有一些“真实”的应用特性,但不需要数据库或任何其他依赖。有两个选项卡,一个是静态的(见 SampleController

@GetMapping(path = "/")
String user(Map<String, Object> model) {
	model.put("message", "Welcome");
	model.put("time", new Date());
	return "index";
}

Home Page

另一个带有一个用户可以提交的表单,用于创建问候语

@PostMapping(path = "/greet")
String name(Map<String, Object> model, @RequestParam String name) {
	greet(model);
	model.put("greeting", "Hello " + name);
	model.put("name", name);
	return "greet";
}

Greet Form

这两个选项卡都在服务器上渲染为独立的页面,但它们使用一个共享的 layout.html 模板来显示页眉和页脚。有一个 messages.properties 文件包含一些可国际化的内容,尽管目前只包含了默认的英文版本。

应用中唯一的 JavaScript 和 CSS 在 layout.html 模板中,用于在窄屏幕上切换选项卡头部。这是一个简单的渐进增强示例,也是我们探索超媒体和浏览器增强的良好起点。

要在 IDE 中运行应用,请使用 WebmvcApplicationTests 中的 main() 方法;或在命令行中使用 ./mvnw spring-boot:test-run

HTMX

我们可以首先将 HTMX 添加到应用程序中。HTMX 是一个小型 JavaScript 库,允许您在 HTML 中使用自定义属性来定义页面中元素的行为。它有点像现代版的 onclick 属性,但功能更强大、更灵活。它也更高效,因为它使用浏览器内置的 HTTP 栈来发出请求,并且可以使用浏览器内置的缓存和历史管理功能。它非常适合 Spring Boot 等服务器端框架,因为它允许您使用服务器来生成页面内容和行为,并且允许您使用浏览器内置功能来管理导航和历史记录。

最简单的方法是从 CDN 获取它并将其添加到 layout.html 模板中

<script src='https://unpkg.com/htmx.org/dist/htmx.min.js'></script>

除了这种方式,在示例代码的 "htmx" 分支中,我们使用了 Webjar 将库加载到 classpath 中,这同样可行。Spring 可以做一些额外的事情来帮助浏览器缓存库,并且还可以帮助进行版本管理。

表单处理

我们可以轻松添加的一个功能是使用 HTMX 在不重新加载整个页面的情况下提交表单。我们可以通过向 form 元素添加 hx-post 属性来实现

<form th:action="@{/greet}" method="post" hx-post="/greet">
	<input type="text" name="name" th:value="${name}"/>
	<button type="submit" class="btn btn-primary">Greet</button>
</form>

这将导致 HTMX 拦截表单的提交操作,并使用 AJAX 请求将数据发送到服务器。服务器将处理请求并返回结果,HTMX 将用结果替换表单的内容。

在这种情况下,这并不是我们想要的,因为表单控制页面上另一个(兄弟)元素中的一些内容。我们可以通过向 form 元素添加 hx-target 属性来解决这个问题

<form th:action="@{/greet}" th:hx-post="@{/greet}" method="post" hx-target="#content">

其中 "content" 元素已通过 ID 标识。回到该元素,我们需要其 ID 以及 hx-swap-oob 属性,以告知 HTMX 传入的内容应该替换现有内容(与原始提交操作“带外”)。

<div id="content" class="col-md-12" hx-swap-oob="true">
	<span th:text="${greeting}">Hello, World</span><br/>
	<span th:text="${time}">21:00</span>
</div>

通过在 greet.html 模板中进行这两个小改动,我们就可以在不重新加载整个页面的情况下向服务器提交表单并更新页面。如果您现在提交表单,并查看浏览器开发者工具中的网络活动,您会看到服务器正在重新渲染整个页面,但 HTMX 正在提取“content”元素并为我们切换其内容。图像和其他静态内容不会重新加载,浏览器的历史记录也会更新以反映页面的新状态。

Greet Page

您可能还会注意到 HTMX 会在向服务器发出的请求中添加 hx-request 头部。这是 HTMX 的一个特性,允许您在服务器端代码中匹配请求,接下来我们将使用它。

使用片段模板

服务器在表单提交时仍然渲染整个页面,但我们可以通过使用片段模板来提高效率。我们可以通过向 greet.html 模板添加 th:fragment 属性来实现

<div id="content" th:fragment="content" class="col-md-12" hx-swap-oob="true">
	<span th:text="${greeting}">Hello, World</span><br/>
	<span th:text="${time}">21:00</span>
</div>

然后我们可以在 SampleController 中的一个新的映射方法中使用该片段,该方法仅在请求来自 HTMX 时触发(通过匹配 hx-request 头部)

@PostMapping(path = "/greet", headers = "hx-request=true")
String nameHtmx(Map<String, Object> model, @RequestParam String name) {
	greet(model);
	return "greet :: content";
}

(“::”语法是 Thymeleaf 的一个特性,允许您渲染模板的片段。这里的意思是:找到“greet”模板,并查找名为“content”的片段。)

如果您现在提交表单,并查看浏览器开发者工具中的网络活动,您会看到服务器只返回了更新内容所需的页面片段。

Greet Fragment

延迟加载

另一种常见的用例是在页面初次加载时从服务器加载内容,甚至可以根据用户的偏好进行定制。我们可以通过向我们希望触发请求的元素添加 hx-get 属性来使用 HTMX 实现这一点。我们可以尝试修改 layout.html 模板中的 logo。不是静态地包含图片

<div class="row">
	<div class="col-12">
	<img src="../static/images/spring-logo.svg" th:src="@{/images/spring-logo.svg}" alt="Logo" style="width:200px;" loading="lazy">
	</div>
</div>

我们可以使用一个占位符

<div class="row">
	<div class="col-12">
	<span class="fa fa-spin fa-spinner" style="width:200px; text-align:center;">
	</div>
</div>

然后让 HTMX 动态加载它

<div class="row">
	<div class="col-12" hx-get="/logo" hx-trigger="load">
	<span class="fa fa-spin fa-spinner" style="width:200px; text-align:center;">
	</div>
</div>

注意添加了 hx-gethx-triggerhx-trigger 属性告诉 HTMX 在页面加载时触发请求。默认是在点击时触发。

hx-get 属性告诉 HTMX 向服务器发出 GET 请求以获取该元素的内容。所以我们需要在 SampleController 中新增一个映射

@GetMapping(path = "/logo")
String logo() {
	return "layout :: logo";
}

它仅仅渲染包含图片的 layout.html 模板片段。layout.html 模板必须修改以包含 th:fragment 属性

<div class="row" th:remove="all">
	<div class="col-12" th:fragment="logo">
	<img src="../static/images/spring-logo.svg" th:src="@{/images/spring-logo.svg}" alt="Logo"
		style="width:200px;" loading="lazy">
	</div>
</div>

注意,我们必须从模板中 th:remove 该片段,因为占位符将在初始渲染时替换它。如果您现在运行该应用,您会看到当页面加载时,旋转器会被图片替换。这将在浏览器开发者工具的网络活动中显示。

Spring Boot HTMX

HTMX 还有更多特性,我们在此没有详细介绍的空间。值得一提的是,有一个 Java 库可以帮助实现这些特性,并且它也包含一些 Thymeleaf 工具:由 Wim Deblauwe 开发的 Spring Boot HTMX,可在 Maven Central 中作为依赖项使用。它可以使用自定义注解来匹配 hx-request 头部,并且还可以帮助实现 HTMX 的其他特性。

其他库

还有其他与 HTMX 目标相似的库,但它们的侧重点和特性集不同。我们将介绍其中两个。使用它们都很容易达到与 HTMX 相同的程度,但它们也有一些更复杂的特性,我们将留给您自行探索。

Unpoly

Unpoly 的 CDN 链接是

<script src='https://unpkg.com/unpoly/unpoly.min.js'></script>

示例代码中的 "unpoly" 分支像之前一样使用了 Webjars。基本的(全页渲染)表单提交示例如下所示

<div class="col-md-12">
	<form th:action="@{/greet}" method="post" up-target="#content">
	<input type="text" name="name" th:value="${name}"/>
	<button type="submit" class="btn btn-primary">Greet</button>
	</form>
</div>
<div id="content" class="col-md-12">
	<span th:text="${greeting}">Hello, World</span><br/>
	<span th:text="${time}">21:00</span>
</div>

所以 hx-target 变成了 up-target,而 HTMX 的其余修饰符在 Unpoly 中就是默认的。

要转换为片段模板,我们需要遵循 HTMX 的模式:添加一个 th:fragment 和一个匹配 Unpoly 发送的唯一头部的控制器方法,例如 X-Up-Context

Hotwired Turbo

Hotwired Turbo 的 CDN 链接是

<script src='https://unpkg.com/@hotwired/turbo/dist/turbo.es2017-umd.js'></script>

示例代码中的 "turbo" 分支像之前一样使用了 Webjars。基本的表单提交示例如下所示

<turbo-frame id="content">
	<div class="col-md-12">
	<form th:action="@{/greet}" method="post">
		<input type="text" name="name" th:value="${name}" />
		<button type="submit" class="btn btn-primary">Greet</button>
	</form>
	</div>
	<div class="col-md-12">
		<span th:text="${greeting}">Hello, World</span><br />
		<span th:text="${time}">21:00</span>
	</div>
</turbo-frame>

Turbo 没有使用自定义属性来标识表单处理交互,而是使用自定义元素(turbo-frame)来标识将被替换的内容。表单的其余部分保持不变。

要转换为片段模板,我们需要向 <turbo-frame> 添加一个 th:fragment 声明,并添加一个匹配 Turbo 发送的唯一头部的控制器方法,例如 Turbo-Frame

结论

HTMX 非常专注于简单的超媒体增强,虽然它已经发展到包含一些额外功能(主要是插件),但它始终忠于其模拟下一代浏览器并尽可能保持功能集精简的初衷。如果您喜欢这类内容,它在社交媒体上也非常有趣。另外两个库更具野心,涵盖的范围更广,但它们与 HTMX 有足够的共同之处,因此我们在此看到的示例非常相似。任何能够生成 HTML 的服务器端框架都可以与这些库一起使用,它们可以用来增强浏览器体验,而无需大型 JavaScript 框架。它们也非常适合像 Spring Boot 这样的服务器端框架,因为它们允许您使用服务器来生成页面内容和行为。模板最好在服务器端使用支持片段的引擎进行渲染,因此 Thymeleaf 工作得很好,但也有其他选择。当然,您也可以将 HTMX(及其相关库)与一个完整的 JavaScript 框架一起使用,如果您喜欢,可以开始缓慢地用超媒体交互来替换框架组件。

订阅 Spring 新闻通讯

订阅 Spring 新闻通讯,保持联系

订阅

领先一步

VMware 提供培训和认证,助您快速提升。

了解更多

获取支持

Tanzu Spring 在一个简单的订阅中提供 OpenJDK™、Spring 和 Apache Tomcat® 的支持和二进制文件。

了解更多

即将举行的活动

查看 Spring 社区所有即将举行的活动。

查看全部