超媒體和瀏覽器增強

工程 | 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 的效能測試而建立的,因此我們希望擁有一些“真實”的應用程式功能,但不需要資料庫或任何其他依賴項。這裡有 2 個選項卡,一個靜態(參見 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 屬性來實現這一點。我們可以嘗試修改 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-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 該片段,因為佔位符將在初始渲染時替換它。如果您現在執行應用程式,您將看到在頁面載入時,載入指示器會被影像替換。這將在瀏覽器開發者工具的網路活動中顯示。

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)來標識將被替換的內容,而不是使用自定義屬性來識別表單處理互動。表單的其餘部分保持不變。

要轉換為片段模板,我們需要將 th:fragment 宣告新增到 <turbo-frame> 中,並新增一個控制器方法來匹配來自 Turbo 的唯一標頭,例如 Turbo-Frame

結論

HTMX 非常專注於簡單的超媒體增強,雖然它已經發展到包含一些額外的功能(主要是作為外掛),但它仍然堅持其最初的願景,即模擬下一代瀏覽器並儘可能保持功能集的簡潔。它還有一個非常有趣的社交媒體形象,如果您喜歡這類東西的話。其他兩個庫更為雄心勃勃,涵蓋了更多內容,但它們與 HTMX 有足夠的相似之處,以至於我們在這裡看到的示例非常相似。任何能夠生成 HTML 的伺服器端框架都可以與這些庫一起使用,並且它們可以在不需要大型 JavaScript 框架的情況下增強瀏覽器體驗。它們也非常適合像 Spring Boot 這樣的伺服器端框架,因為它們允許您使用伺服器生成頁面內容和行為。模板最好由瞭解片段的模板引擎在伺服器端渲染,因此 Thymeleaf 可以正常工作,但也有其他選擇。如果您喜歡 HTMX(及其同類產品),也可以將其與完整的 JavaScript 框架一起使用,並且您可以開始緩慢地用超媒體互動替換框架元件。

獲取 Spring 新聞通訊

透過 Spring 新聞通訊保持聯絡

訂閱

領先一步

VMware 提供培訓和認證,助您加速進步。

瞭解更多

獲得支援

Tanzu Spring 提供 OpenJDK™、Spring 和 Apache Tomcat® 的支援和二進位制檔案,只需一份簡單的訂閱。

瞭解更多

即將舉行的活動

檢視 Spring 社群所有即將舉行的活動。

檢視所有