超媒体和浏览器增强

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

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

你可以在GitHub (dsyer/webmvc-thymeleaf)上找到源代码。“main”分支是起点,并且每个我们将要探索的库都有分支。

起点

作为起点,我们将使用一个简单但并非微不足道的使用Thymeleaf的Spring Boot应用程序。它最初是为了对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将库加载到类路径中,这样也可以正常工作。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”元素并为我们切换其内容。图像和其他静态内容不会重新加载,并且浏览器的历史记录会更新以反映页面的新状态。

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模板中的徽标进行实验。而不是静态地包含图像

<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 工具:Spring Boot HTMX,由 Wim Deblauwe 提供,可在 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 社区中所有即将举行的活动。

查看全部