超媒體與瀏覽器增強功能

工程 | 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 傳入的內容應取代現有內容(「out of band」,與原始提交動作無關)

<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 的一項功能,可讓您在伺服器端程式碼中比對請求,我們接下來將使用它。

使用片段範本

伺服器仍然為表單提交呈現整個頁面,但我們可以透過使用片段範本使其更有效率。我們可以透過將 th:fragment 屬性新增到 greet.html 範本來做到這一點

<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 社群中所有即將到來的活動。

檢視全部