领先一步
VMware 提供培训和认证,助您加速进步。
了解更多如今,前端开发主要由大型客户端 JavaScript 框架主导。这其中有很多合理的理由,但对于许多用例来说,这可能效率低下,而且框架工程已变得极其复杂。在本文中,我想探讨一种不同的方法,一种更高效、更灵活、由更小的构建块组成,并且非常适合服务器端应用程序框架(如 Spring 或各种服务器端语言中的类似工具)的方法。其思想是拥抱超媒体的概念,想象一下下一代浏览器将如何利用它,并使用少量 JavaScript 将今天的浏览器增强到那个水平。现代浏览器会忽略 HTML 中的自定义元素和属性,但它们允许内容作者使用 JavaScript 为它们定义行为。市面上已经有一些库可以提供帮助,我们将探讨 HTMX、Unpoly 和 Hotwired Turbo。我们还将探讨如何将这些库与 Spring Boot 一起使用,以及如何将它们与传统的服务器端框架(如 Thymeleaf)一起使用。
您可以在 GitHub 上找到源代码(dsyer/webmvc-thymeleaf)。“main”分支是起点,还有一些分支用于我们将要探讨的每个库。
作为起点,我们将使用一个简单但并非微不足道的 Spring Boot 应用程序,其中包含 Thymeleaf。它最初是为了 Thymeleaf 和 Spring Webmvc 的性能测试而创建的,因此我们希望拥有一些“真实”的应用程序功能,但不需要数据库或任何其他依赖项。这里有 2 个选项卡,一个静态(参见 SampleController)
@GetMapping(path = "/")
String user(Map<String, Object> model) {
model.put("message", "Welcome");
model.put("time", new Date());
return "index";
}

另一个带有用户可以提交的表单以创建问候语
@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";
}

这两个选项卡都作为单独的页面在服务器上呈现,但它们使用共享的 layout.html 模板来显示页眉和页脚。有一个 messages.properties 文件包含一些可国际化的内容,尽管目前只包含默认的英文版本。
应用程序中唯一的 JavaScript 和 CSS 位于 layout.html 模板中,用于在窄屏幕上切换选项卡标题。这是渐进增强的一个简单示例,也是我们探索超媒体和浏览器增强的一个良好起点。
要在 IDE 中运行该应用程序,请使用 WebmvcApplicationTests 中的 main() 方法,或者在命令行中使用 ./mvnw spring-boot:test-run。
我们可以从向应用程序添加 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 将库加载到类路径中,这样也可以。Spring 可以做一些额外的事情来帮助浏览器缓存库,并且还可以帮助进行版本管理。
我们可以轻松添加的一个功能是使用 HTMX 提交表单而无需完全重新加载页面。我们可以通过向表单元素添加 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 将用结果替换表单的内容。
在这种情况下,这并不是我们想要的,因为表单控制着页面上另一个(同级)元素的某些内容。通过向表单元素添加 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”元素并为我们切换其内容。图像和其他静态内容不会重新加载,浏览器的历史记录也会更新以反映页面的新状态。

您可能还会注意到 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”的片段。)
如果您现在提交表单,并在浏览器开发者工具中查看网络活动,您将看到服务器仅返回更新内容所需的页面片段。如果您现在提交表单,并在浏览器开发者工具中查看网络活动,您将看到服务器仅返回更新内容所需的页面片段。

另一个常见的用例是从服务器加载内容,在页面首次加载时,甚至可能根据用户的偏好进行定制。我们可以通过向我们想要触发请求的元素添加 hx-get 属性来实现这一点。我们可以尝试修改 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-get 和 hx-trigger 的添加。hx-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 该片段,因为占位符将在初始渲染时替换它。如果您现在运行应用程序,您将看到在页面加载时,加载指示器会被图像替换。这将在浏览器开发者工具的网络活动中显示。
HTMX 还有更多我们没有足够空间详细介绍的功能。值得一提的是,有一个 Java 库可以帮助实现这些功能,它还包含一些 Thymeleaf 工具:Spring Boot HTMX,由 Wim Deblauwe 开发,可在 Maven Central 中作为依赖项使用。它可以处理 hx-request 标头匹配,并使用自定义注解,还可以帮助实现 HTMX 的其他功能。
还有其他库与 HTMX 有着相似的目标,但它们的侧重点和功能集不同。我们将研究其中两个。通过这两个库,可以非常轻松地达到与 HTMX 相同的目标,但它们还提供了一些更复杂的功能,这些功能将留给您自行探索。
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 的 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)来标识将被替换的内容,而不是使用自定义属性来识别表单处理交互。表单的其余部分保持不变。
要转换为片段模板,我们需要将 th:fragment 声明添加到 <turbo-frame> 中,并添加一个控制器方法来匹配来自 Turbo 的唯一标头,例如 Turbo-Frame。
HTMX 非常专注于简单的超媒体增强,虽然它已经发展到包含一些额外的功能(主要是作为插件),但它仍然坚持其最初的愿景,即模拟下一代浏览器并尽可能保持功能集的简洁。它还有一个非常有趣的社交媒体形象,如果您喜欢这类东西的话。其他两个库更为雄心勃勃,涵盖了更多内容,但它们与 HTMX 有足够的相似之处,以至于我们在这里看到的示例非常相似。任何能够生成 HTML 的服务器端框架都可以与这些库一起使用,并且它们可以在不需要大型 JavaScript 框架的情况下增强浏览器体验。它们也非常适合像 Spring Boot 这样的服务器端框架,因为它们允许您使用服务器生成页面内容和行为。模板最好由了解片段的模板引擎在服务器端渲染,因此 Thymeleaf 可以正常工作,但也有其他选择。如果您喜欢 HTMX(及其同类产品),也可以将其与完整的 JavaScript 框架一起使用,并且您可以开始缓慢地用超媒体交互替换框架组件。