領先一步
VMware 提供培訓和認證,以加速您的進度。
了解更多本文探討了 Spring Boot 開發人員在使用應用程式的用戶端(瀏覽器)端的 Javascript 和 CSS 時的不同選項。計劃的一部分是探索一些在 Spring Web 應用程式的傳統伺服器端渲染世界中運作良好的 Javascript 函式庫。這些函式庫傾向於對應用程式開發人員來說比較輕量,因為它們允許您完全避免 Javascript,但仍然具有良好的漸進式「現代」UI。我們也研究了一些更「純粹」的 Javascript 工具和框架。這是一種頻譜,所以作為 TL;DR,這裡列出了範例應用程式,大致按照 Javascript 內容從低到高的順序排列
htmx
: HTMX 是一個函式庫,允許您直接從 HTML 訪問現代瀏覽器功能,而不是使用 javascript。它非常易於使用,並且非常適合伺服器端渲染,因為它透過直接從遠端回應替換 DOM 的一部分來運作。它似乎被 Python 社群廣泛使用和讚賞。
turbo
: Hotwired(Turbo 和 Stimulus)。 Turbo 有點像 HTMX。它在 Ruby on Rails 中被廣泛使用和良好支援。 Stimulus 是一個輕量級函式庫,可用於實現更適合存在於用戶端的少量邏輯。
vue
: Vue 也很輕量級,並將自己描述為「漸進式」和「可逐步採用」。它的用途廣泛,因為您可以使用非常少量的 Javascript 來做一些很棒的事情,或者您可以繼續推進並將其用作一個成熟的框架。
react-webjars
:使用 React 框架,但沒有 Javascript 建置或打包器。 React 的優點在於,像 Vue 一樣,它允許您僅在幾個小區域中使用它,而無需接管整個原始碼樹。
nodejs
:類似於 turbo
範例,但使用 Node.js 來建置和打包腳本,而不是 Webjars。如果您認真對待 React,您可能會最終這樣做,或者做類似的事情。此處的目標是使用 Maven 來驅動建置,至少是可選的,以便正常的 Spring Boot 應用程式開發流程可以運作。 Gradle 的運作方式相同。
react
:是 react-webjars
範例,但具有來自 nodejs
範例的 Javascript 建置步驟。
還有另一個使用 Spring Boot 和 HTMX 的範例在這裡。如果您想了解更多關於 React 和 Spring 的資訊,Spring 網站上有一篇教學文章。還有關於 Angular 的內容,透過 Spring 網站上的另一篇教學文章和相關的入門內容 在這裡。如果您對 Angular 和 Spring Boot 感興趣,Matt Raible 有一本迷你書。spring.io 網站(原始碼)也是一個 Node.js 建置,並使用完全不同的工具鏈和函式庫集合。另一種替代方法來源是 JHipster,它也支援此處使用的一些函式庫。最後,Petclinic 雖然沒有 Javascript,但確實有一些樣式表中的用戶端程式碼和由 Maven 驅動的建置流程。
目錄
所有範例都可以使用標準 Spring Boot 流程來建置和執行(例如,請參閱 此入門指南)。 Maven wrapper 位於父目錄中,因此從命令列上的每個範例中,您可以 ../mvnw spring-boot:run
來執行應用程式,或 ../mvnw package
來取得可執行 JAR。例如:
$ cd htmx
$ ../mvnw package
$ java -jar target/js-demo-htmx-0.0.1.jar
Github 專案在 Codespaces 中運作良好,並且主要是在本地使用 VSCode 開發的。但您可以隨意使用您喜歡的任何 IDE,它們都應該運作良好。
瀏覽器應用程式開發是一個充滿不斷變化的選項和選擇的巨大領域。不可能在一張連貫的圖片中呈現所有這些選項,因此我們有意限制了我們所研究的工具和框架的範圍。我們首先傾向於尋找一種可以輕易使用的東西,或者至少是可以逐步採用的。還有先前提到過的傾向,即傾向於使用伺服器端渲染器運作良好的函式庫 - 那些處理 HTML 片段和子樹的函式庫。此外,我們盡可能使用了 Javascript ESM,因為現在大多數瀏覽器都支援它。但是,大多數發布模組以 import
的函式庫也都有一個您可以 require
的等效捆綁包,因此如果您願意,您可以始終堅持使用它。
許多範例使用 Webjars 將 Javascript(和 CSS)資源傳遞到用戶端。對於具有 Java 後端的應用程式來說,這非常容易且明智。並非所有範例都使用 Webjars,並且將使用 Webjars 的範例轉換為使用 CDN(例如 unpkg.com 或 jsdelivr.com)或建置時 Node.js 打包器並不難。此處具有打包器的範例使用 Rollup,但您也可以使用 Webpack。它們也使用直接的 NPM,而不是 Yarn 或 Gulp,這兩者都是流行的選擇。所有範例都使用 Bootstrap 作為 CSS,但還有其他選擇。
伺服器端也可以做出選擇。我們使用了 Spring Webflux,但 Spring MVC 的運作方式相同。我們使用 Maven 作為建置工具,但使用 Gradle 很容易實現相同的目標。所有範例實際上都有一個靜態首頁(甚至沒有渲染為範本),但它們都有一些動態內容,我們選擇了 JMustache 來實現這一點。 Thymeleaf(和其他範本引擎)的運作方式也一樣。事實上,Thymeleaf 內建支援片段,當您動態更新頁面部分時,這非常有用,這是我們的目標之一。您可以使用 Mustache(可能)做同樣的事情,但我們在這些範例中不需要它。
要開始使用 Spring Boot 和客戶端開發,讓我們從頭開始,從 Spring Initializr 建立一個空白的應用程式。您可以前往該網站並下載一個具有 Web 依賴性的專案(選擇 Webflux 或 WebMVC),然後在您的 IDE 中開啟它。或者,若要從命令列產生專案,您可以使用 curl
,從一個空的目錄開始
$ curl https://start.spring.io/starter.tgz -d dependencies=webflux -d name=js-demo | tar -xzvf -
我們可以在 src/main/resources/static/index.html
中新增一個非常基本的靜態首頁
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>Demo</title>
<meta name="description" content="" />
<meta name="viewport" content="width=device-width" />
<base href="/" />
</head>
<body>
<header>
<h1>Demo</h1>
</header>
<main>
<div class="container">
<div id="greeting">Hello World</div>
</div>
</main>
</body>
</html>
然後執行該應用程式
$ ./mvnw package
$ java target/js-demo-0.0.1-SNAPSHOT.jar
您可以在 localhost:8080 上看到結果。
為了開始建置客戶端功能,讓我們從 Bootstrap 新增一些現成的 CSS。我們可以像這樣在 index.html
中使用 CDN,例如
...
<head>
...
<link rel="stylesheet" type="text/css" href="https://unpkgs.com/bootstrap/dist/css/bootstrap.min.css" />
</head>
...
如果您想快速入門,這非常方便。對於某些應用程式來說,它可能就是您所需要的。在這裡,我們採用一種不同的方法,使我們的應用程式更加獨立,並且與我們習慣的 Java 工具更加一致 - 也就是使用 Webjar 並將 Bootstrap 函式庫封裝在我們的 JAR 檔案中。為此,我們需要將一些依賴項新增到 pom.xml
中
<dependency>
<groupId>org.webjars</groupId>
<artifactId>webjars-locator-core</artifactId>
</dependency>
<dependency>
<groupId>org.webjars.npm</groupId>
<artifactId>bootstrap</artifactId>
<version>5.1.3</version>
</dependency>
然後在 index.html
中,我們使用應用程式內的資源路徑,而不是 CDN
...
<head>
...
<link rel="stylesheet" type="text/css" href="/webjars/bootstrap/dist/css/bootstrap.min.css" />
</head>
...
如果您重新建置和/或重新執行該應用程式,您將會看到漂亮的純 Bootstrap 樣式,而不是無聊的預設瀏覽器版本。Spring Boot 使用 webjars-locator-core
來定位 classpath 中資源的版本和確切位置,並且瀏覽器會將該樣式表加入到頁面中。
Bootstrap 也是一個 Javascript 函式庫,因此我們可以開始充分利用它。我們可以像這樣在 index.html
中新增 Bootstrap 函式庫
...
<head>
...
<script src="/webjars/bootstrap/dist/js/bootstrap.min.js"></script>
</head>
...
它還沒有做任何可見的事情,但您可以使用開發人員工具視窗(在 Chrome 或 Firefox 中按 F12)來驗證它是否已由瀏覽器載入。
我們在簡介中提到我們將盡可能使用 ESM 模組,而 Bootstrap 有一個,所以讓我們讓它運作起來。用這個替換 index.html
中的 <script>
標籤
<script type="importmap">
{
"imports": {
"bootstrap": "/webjars/bootstrap/dist/js/bootstrap.esm.min.js"
}
}
</script>
<script type="module">
import 'bootstrap';
</script>
這包含兩個部分:一個 "importmap" 和一個 "module"。Import map 是瀏覽器的一個功能,可讓您透過名稱引用 ESM 模組,並將該名稱對應到一個資源。如果您現在執行該應用程式並在瀏覽器中載入它,則應該會在主控台中出現一個錯誤,因為 Bootstrap 的 ESM 封裝對 PopperJS 有一個依賴性
Uncaught TypeError: Failed to resolve module specifier "@popperjs/core". Relative references must start with either "/", "./", or "../".
PopperJS 不是 Bootstrap Webjar 的強制性傳遞依賴項,因此我們必須將它包含在我們的 pom.xml
中
<dependency>
<groupId>org.webjars.npm</groupId>
<artifactId>popperjs__core</artifactId>
<version>2.10.1</version>
</dependency>
(Webjars 使用 "__" 中綴而不是 "@" 字首來表示命名空間的 NPM 模組名稱。)然後它可以被新增到 import map 中
<script type="importmap">
{
"imports": {
"bootstrap": "/webjars/bootstrap/dist/js/bootstrap.esm.min.js",
"@popperjs/core": "/webjars/popperjs__core/lib/index.js"
}
}
</script>
這將修復主控台錯誤。
Webjar 內部的資源路徑(例如 /bootstrap/dist/js/bootstrap.esm.min.js
)未標準化 - 沒有命名慣例可以讓您猜測 Webjar 內部 ESM 模組的位置,或者一個等效的 NPM 模組。但是,NPM 模組中有一些慣例使其可以自動化:大多數模組都有一個具有 "module" 欄位的 package.json
。例如,從 Bootstrap 中,您可以找到版本和模組資源路徑
{
"name": "bootstrap",
"description": "The most popular front-end framework for developing responsive, mobile first projects on the web.",
"version": "5.1.3",
...
"module": "dist/js/bootstrap.esm.js",
...
}
像 unpkg.com 這樣的 CDN 利用了這些資訊,因此當您只知道 ESM 模組名稱時,可以使用它們。例如,這應該可以工作
<script type="importmap">
{
"imports": {
"bootstrap": "https://unpkg.com/bootstrap",
"@popperjs/core": "https://unpkg.com/@popperjs/core"
}
}
</script>
如果能夠使用 /webjars
資源路徑做同樣的事情會很好。這就是所有範例中的 NpmVersionResolver
所做的事情。如果您不使用 Webjars 並且可以使用 CDN,則不需要它,如果您不介意手動開啟所有 package.json
檔案並尋找模組路徑,則也不需要它。但是,不必考慮它會很好。有一個 功能請求 要求將其包含在 Spring Boot 中。NpmVersionResolver
的另一個功能是它知道 Webjars 元數據,因此它可以從 classpath 解析每個 Webjar 的版本,並且我們不需要 webjars-locator-core
依賴項(有一個 Spring Framework 中的開放問題 來新增此功能)。
因此,在範例中,import map 如下所示
<script type="importmap">
{
"imports": {
"bootstrap": "/npm/bootstrap",
"@popperjs/core": "/npm/@popperjs/core"
}
}
</script>
您只需要知道 NPM 模組名稱,resolver 就會找出如何找到一個解析為 ESM 封裝的資源。如果有的話,它會使用一個 Webjar,否則會重新導向到一個 CDN。
注意:大多數現代瀏覽器都支援模組和模組映射。那些不支援的瀏覽器可以在我們的應用程式中使用,但需要新增一個 shim 函式庫。它已經包含在範例中。
既然我們已經讓它全部運作,不如使用 Bootstrap 樣式。那麼,使用一些帶有內容的選項卡和一兩個按鈕來按下呢?聽起來不錯。首先是 index.html
中的 <header/>
,其中包含選項卡連結
<header>
<h1>Demo</h1>
<nav class="nav nav-tabs">
<a class="nav-link active" data-bs-toggle="tab" data-bs-target="#message" href="#">Message</a>
<a class="nav-link" data-bs-toggle="tab" data-bs-target="#stream" href="#">Stream</a>
</nav>
</header>
第二個(預設為非活動)選項卡稱為 "stream",因為部分範例將探索使用 Server Sent Event 串流。選項卡內容在 <main/>
部分看起來像這樣
<main>
<div class="tab-content">
<div class="tab-pane fade show active" id="message" role="tabpanel">
<div class="container">
<div id="greeting">Hello World!</div>
</div>
</div>
<div class="tab-pane fade" id="stream" role="tabpanel">
<div class="container">
<div id="load">Nothing here yet...</div>
</div>
</div>
</div>
</main>
請注意其中一個選項卡是 "active",並且兩者都具有與標頭中的 data-bs-target
屬性匹配的 ID。這就是我們需要一些 Javascript 的原因 - 處理選項卡上的點擊事件,以便顯示或隱藏正確的內容。Bootstrap 文件 提供了大量不同選項卡樣式和佈局的範例。這裡基本功能的一個好處是,它們可以在像手機這樣的窄設備上自動呈現為下拉式選單(對 <nav/>
中的類別屬性進行一些小修改 - 您可以查看 Petclinic 以了解如何操作)。在瀏覽器中,它看起來像這樣
當然,如果您點擊 "Stream" 選項卡,它會顯示一些不同的內容。
我們可以透過 HTMX 快速新增一些動態內容。首先,我們需要 Javascript 函式庫,因此我們將其新增為 Webjar
<dependency>
<groupId>org.webjars.npm</groupId>
<artifactId>htmx.org</artifactId>
<version>1.6.0</version>
</dependency>
然後在 index.html
中匯入它
<script type="importmap">
{
"imports": {
"bootstrap": "/npm/bootstrap",
"@popperjs/core": "/npm/@popperjs/core",
"htmx": "/npm/htmx.org"
}
}
</script>
<script type="module">
import 'bootstrap';
import 'htmx';
</script>
然後我們可以將問候語從 "Hello World" 變更為來自用戶輸入的內容。讓我們在主選項卡中新增一個輸入欄位和一個按鈕
<div class="container">
<div id="greeting">Hello World</div>
<input id="name" name="value" type="text" />
<button hx-post="/greet" hx-target="#greeting" hx-include="#name">Greet</button>
</div>
輸入欄位是未修飾的,按鈕有一些 hx-*
屬性,這些屬性由 HTMX 函式庫抓取並用於增強頁面。這些屬性表示 "當用戶點擊此按鈕時,將 POST 發送到 /greet
,包括請求中的 'name',並透過替換 'greeting' 的內容來呈現結果"。如果用戶在輸入欄位中輸入 "Foo",則 POST 將具有 value=Foo
的表單編碼主體,因為 "value" 是由 #name
識別的欄位的名稱。
然後我們只需要後端中的 /greet
資源
@SpringBootApplication
@RestController
public class JsDemoApplication {
@PostMapping("/greet")
public String greet(@ModelAttribute Greeting values) {
return "Hello " + values.getValue() + "!";
}
...
static class Greeting {
private String value;
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
}
}
Spring 會將傳入請求中的 "value" 參數繫結到 Greeting
,我們將其轉換為文本,然後將其注入到頁面上的 <div id="greeting"/>
中。您可以使用 HTMX 來注入像這樣的純文字,或者整個 HTML 片段。或者您可以附加(或前置)到現有元素的清單,例如表格中的列或清單中的項目。
這是您可以做的另一件事
<div class="container">
<div id="auth" hx-trigger="load" hx-get="/user">
Unauthenticated
</div>
...
</div>
這會在頁面載入時對 /user
執行 GET,並交換元素的內容。範例應用程式具有此端點,它會傳回 "Fred",因此您會看到它像這樣呈現
您可以使用 HTMX 做很多其他很棒的事情,其中一件事是呈現 Server Sent Event (SSE) 串流。首先,我們將一個端點新增到後端應用程式
@SpringBootApplication
@RestController
public class JsDemoApplication {
@GetMapping(path = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> stream() {
return Flux.interval(Duration.ofSeconds(5)).map(
value -> value + ":" + System.currentTimeMillis()
);
}
...
}
所以我們有一個訊息串流,透過 Spring 端點映射上的 produces
屬性來呈現。
$ curl localhost:8080/stream
data:0:1639472861461
data:1:1639472866461
data:2:1639472871461
...
HTMX 可以將這些訊息注入到我們的頁面中。以下是在 index.html
中添加到 "stream" 標籤頁的方法:
<div class="container">
<div id="load" hx-sse="connect:/stream">
<div id="load" hx-sse="swap:message"></div>
</div>
</div>
我們使用 connect:/stream
屬性連接到 /stream
,然後使用 swap:message
提取事件資料。實際上,"message" 是預設的事件類型,但 SSE 負載也可以透過包含以 event:
開頭的行來指定其他類型,因此您可以擁有一個多路傳輸許多不同事件類型的串流,並讓它們以不同的方式影響 HTML。
我們後端中的端點非常簡單:它只傳回純文字字串,但它可以做更多的事情。例如,它可以傳回 HTML 片段,並且它們將被注入到頁面中。範例應用程式使用名為 CompositeViewRenderer
的自定義 Spring Webflux 元件(在 Framework 上 這裡 請求作為一個功能),其中 @Contoller
方法可以傳回 Flux<Rendering>
(在 MVC 中會是 Flux<ModelAndView>
)。它使端點能夠串流動態視圖。
@GetMapping(path = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<Rendering> stream() {
return Flux.interval(Duration.ofSeconds(5)).map(value -> Rendering.view("time")
.modelAttribute("value", value)
.modelAttribute("time", System.currentTimeMillis()).build());
}
這與名為 "time" 的視圖配對,而正常的 Spring 機制會呈現模型。
$ curl localhost:8080/stream
data:<div>Index: 0, Time: 1639474490435</div>
data:<div>Index: 1, Time: 1639474495435</div>
data:<div>Index: 2, Time: 1639474500435</div>
...
HTML 來自一個樣板。
<div>Index: {{value}}, Time: {{time}}</div>
這反過來又自動運作,因為我們在 pom.xml
中包含了 JMustache。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mustache</artifactId>
</dependency>
HTMX 仍然可以做更多的事情。端點可以返回一個常規的 HTTP 響應,而不是 SSE 串流,但將其組合成一組元素以在頁面上交換。 HTMX 將此稱為 "帶外" 交換,因為它涉及增強頁面上元素的內容,這些元素與觸發下載的元素不同。
為了看到這個運作,我們可以新增另一個包含 HTMX 功能的標籤頁。
<div class="tab-pane fade" id="test" role="tabpanel">
<div class="container">
<div id="hello"></div>
<div id="world"></div>
<button class="btn btn-primary" hx-get="/test" hx-swap="none">Fetch</button>
</div>
</div>
別忘了新增導覽連結,以便使用者可以看到這個標籤頁。
<nav class="nav nav-tabs">
...
<a class="nav-link" data-bs-toggle="tab" data-bs-target="#test" href="#">Test</a>
</nav>
...
新的標籤頁有一個按鈕,可以從 /test
提取動態內容,並且它還設定了 2 個空的 div "hello" 和 "world" 來接收內容。 hx-swap="none"
很重要 - 它告訴 HTMX 不要替換觸發 GET 元素的內容。
如果我們有一個端點返回這個
$ curl localhost:8080/test
<div id="hello" hx-swap-oob="true">Hello</div>
<div id="world" hx-swap-oob="true">World</div>
那麼頁面會像這樣呈現(在按下 "Fetch" 按鈕之後)
這個端點的一個簡單實現是
@GetMapping(path = "/test")
public String test() {
return "<div id=\"hello\" hx-swap-oob=\"true\">Hello</div>\n"
+ "<div id=\"world\" hx-swap-oob=\"true\">World</div>";
}
或者(使用自定義視圖渲染器)
@GetMapping(path = "/test")
public Flux<Rendering> test() {
return Flux.just(
Rendering.view("test").modelAttribute("id", "hello")
.modelAttribute("value", "Hello").build(),
Rendering.view("test").modelAttribute("id", "world")
.modelAttribute("value", "World").build());
}
使用樣板 "test.mustache"
<div id="{{id}}" hx-swap-oob="true">{{value}}</div>
HTMX 做的另一件事是 "增強" 你頁面中的所有連結和表單操作,以便它們自動使用 XHR 請求而不是完整的頁面刷新。這是一種非常簡單的方法,可以按功能劃分頁面,並且僅更新您需要的位元。您也可以輕鬆地以 "漸進式" 的方式執行此操作 - 也就是說,如果 Javascript 已禁用,則應用程式可以使用完整的頁面刷新,但如果 Javascript 已啟用,則應用程式會更快且感覺更 "現代"。
Hotwired 與 HTMX 有點相似,所以讓我們替換這些程式庫,讓應用程式運作起來。取出 HTMX 並將 Hotwired (Turbo) 新增到應用程式中。在 pom.xml
中
<dependency>
<groupId>org.webjars.npm</groupId>
<artifactId>hotwired__turbo</artifactId>
<version>7.1.0</version>
</dependency>
然後我們可以透過新增匯入映射將其匯入到我們的頁面中
<script type="importmap">
{
"imports": {
...
"@hotwired/turbo": "/npm/@hotwired/turbo"
}
}
</script>
以及一個匯入程式庫的腳本
<script type="module">
import * as Turbo from '@hotwired/turbo';
</script>
這讓我們可以用一些 HTML 的變更來完成我們已經使用 HTMX 完成的動態內容。以下是 index.html
中的 "test" 標籤頁
<div class="tab-pane fade" id="test" role="tabpanel">
<turbo-frame id="turbo">
<div class="container" id="frame">
<div id="hello"></div>
<div id="world"></div>
<form action="/test" method="post">
<button class="btn btn-primary" type="submit">Fetch</button>
</form>
</div>
</turbo-frame>
</div>
Turbo 的運作方式與 HTMX 略有不同。 <turbo-frame/>
告訴 Turbo,其中的所有內容都會被增強(有點像 HTMX 增強)。並且為了在點擊按鈕時替換 "hello" 和 "world" 元素,我們需要按鈕透過表單發送 POST 請求,而不僅僅是一個簡單的 GET 請求(Turbo 在這方面比 HTMX 更主觀)。然後,/test
端點會發送一些 <turbo-stream/>
片段,其中包含我們要替換的內容的樣板。
<turbo-stream action="replace" target="hello">
<template>
<div id="hello">Hi Hello!</div>
</template>
</turbo-frame>
<turbo-stream action="replace" target="world">
<template>
<div id="world">Hi World!</div>
</template>
</turbo-frame>
為了讓 Turbo 注意到傳入的 <turbo-stream/>
,我們需要 /test
端點返回一個自定義的 Content-Type: text/vnd.turbo-stream.html
,因此實作看起來像這樣
@PostMapping(path = "/test", produces = "text/vnd.turbo-stream.html")
public Flux<Rendering> test() {
return ...;
}
為了提供自定義內容類型,我們需要一個自定義視圖解析器
@Bean
@ConditionalOnMissingBean
MustacheViewResolver mustacheViewResolver(Compiler mustacheCompiler, MustacheProperties mustache) {
MustacheViewResolver resolver = new MustacheViewResolver(mustacheCompiler);
resolver.setPrefix(mustache.getPrefix());
resolver.setSuffix(mustache.getSuffix());
resolver.setViewNames(mustache.getViewNames());
resolver.setRequestContextAttribute(mustache.getRequestContextAttribute());
resolver.setCharset(mustache.getCharsetName());
resolver.setOrder(Ordered.LOWEST_PRECEDENCE - 10);
resolver.setSupportedMediaTypes(
Arrays.asList(MediaType.TEXT_HTML, MediaType.valueOf("text/vnd.turbo-stream.html")));
return resolver;
}
以上是 Spring Boot 自動定義的 @Bean
的副本,但具有額外的支援媒體類型。有一個開放的 功能請求,允許透過 application.properties
完成此操作。
點擊 "Fetch" 按鈕的結果應該是像以前一樣呈現 "Hello" 和 "World"。
Turbo 還有內建的 SSE 呈現支援,但這次事件資料必須包含 <turbo-stream/>
元素。例如
$ curl localhost:8080/stream
data:<turbo-stream action="replace" target="load">
data: <template>
data: <div id="load">Index: 0, Time: 1639482422822</div>
data: </template>
data:</turbo-stream>
data:<turbo-stream action="replace" target="load">
data: <template>
data: <div id="load">Index: 1, Time: 1639482427821</div>
data: </template>
data:</turbo-stream>
然後 "stream" 標籤頁只需要一個空的 <div id="load"></div>
,Turbo 就會做它被要求的事情(替換由 "load" 識別的元素)
<div class="tab-pane fade" id="stream" role="tabpanel">
<div class="container">
<div id="load"></div>
</div>
</div>
Turbo 和 HTMX 都允許您透過 id 或 CSS 樣式匹配器來鎖定元素以進行動態內容,無論是常規 HTTP 響應還是 SSE 串流。
Hotwired 中還有另一個程式庫稱為 Stimulus,可讓您使用少量 Javascript 新增更多自定義行為。 例如,如果您後端服務中的端點傳回 JSON 而不是 HTML,這會派上用場。 我們可以透過將其新增為 pom.xml
中的依賴項來開始使用 Stimulus。
<dependency>
<groupId>org.webjars.npm</groupId>
<artifactId>hotwired__stimulus</artifactId>
<version>3.0.1</version>
</dependency>
並使用 index.html
中的匯入映射
<script type="importmap">
{
"imports": {
...
"@hotwired/stimulus": "/npm/@hotwired/stimulus"
}
}
</script>
然後,我們可以很好地替換我們之前使用 HTMX 完成的主 "message" 標籤頁的部分。 以下是僅涵蓋按鈕和自定義訊息的標籤頁內容
<div class="tab-pane fade show active" id="message" role="tabpanel">
<div class="container" data-controller="hello">
<div id="greeting" data-hello-target="output">Hello World</div>
<input id="name" name="value" type="text" data-hello-target="name" />
<button class="btn btn-primary" data-action="click->hello#greet">Greet</button>
</div>
</div>
請注意 data-*
屬性。 在我們需要實作的容器 <div>
上聲明了一個 controller
("hello")。 它在按鈕元素中的操作表示 "當點擊此按鈕時,呼叫 'hello' 控制器上的函式 'greet'"。 並且有一些裝飾物可以識別哪些元素具有控制器的輸入和輸出(data-hello-target
屬性)。 實作自定義訊息渲染器的 Javascript 看起來像這樣
<script type="module">
import { Application, Controller } from '@hotwired/stimulus';
window.Stimulus = Application.start();
Stimulus.register("hello", class extends Controller {
static targets = ["name", "output"]
greet() {
this.outputTarget.textContent = `Hello, ${this.nameTarget.value}!`;
};
});
</script>
Controller
在 HTML 中使用 data-controller
名稱註冊,並且它有一個 targets
欄位,用於枚舉它想要鎖定的所有元素的 id。 然後它可以透過命名慣例來引用它們,例如 "output" 在控制器中顯示為對名為 outputTarget
的 DOM 元素的引用。
您可以在 Controller
中或多或少地執行您喜歡的任何操作,因此,例如,您可以從後端提取一些內容。 turbo
範例透過從 /user
端點提取字串並將其插入 "auth" 目標元素中來執行此操作。
<div class="container" data-controller="hello">
<div id="auth" data-hello-target="auth"></div>
...
</div>
以及互補的 Javascript
Stimulus.register("hello", class extends Controller {
static targets = ["name", "output", "auth"]
initialize() {
let hello = this;
fetch("/user").then(response => {
response.json().then(data => {
hello.authTarget.textContent = `Logged in as: ${data.name}`;
});
});
}
...
});
我們可以加入一些有趣的 Javascript 函式庫,例如一些漂亮的圖形。這是 index.html
中的一個新分頁(記得也要加入 <nav/>
連結)
<div class="tab-pane fade" id="chart" role="tabpanel" data-controller="chart">
<div class="container">
<canvas data-chart-target="canvas"></canvas>
</div>
<div class="container">
<button class="btn btn-primary" data-action="click->chart#clear">Clear</button>
<button class="btn btn-primary" data-action="click->chart#bar">Bar</button>
</div>
</div>
它有一個空的 <canvas/>
,我們可以利用 Chart.js 在其中填入一個長條圖。為了準備好這個,我們在上面的 HTML 中宣告了一個名為 "chart" 的控制器,並使用 data-*-target
標記了它的目標元素。所以讓我們從把 Chart.js 加入應用程式開始。在 pom.xml
中
<dependency>
<groupId>org.webjars.npm</groupId>
<artifactId>chart.js</artifactId>
<version>3.6.0</version>
</dependency>
在 index.html
中,我們加入一個匯入對應 (import map) 和一些 Javascript 來渲染圖表
<script type="importmap">
{
"imports": {
...
"chart.js": "/npm/chart.js"
}
}
</script>
以及實作 HTML 中按鈕的 "bar" 和 "clear" 動作的新控制器
import { Chart, BarController, BarElement, LinearScale, CategoryScale, Title, Legend } from 'chart.js';
Chart.register(BarController, BarElement, LinearScale, CategoryScale, Title, Legend);
Stimulus.register("chart", class extends Controller {
static targets = ["canvas"]
bar(type) {
let chart = this;
this.clear();
fetch("/pops").then(response => {
response.json().then(data => {
data.type = "bar";
chart.active = new Chart(chart.canvasTarget, data);
});
});;
clear() {
if (this.active) {
this.active.destroy();
}
};
};
});
為了服務這個,我們需要一個 /pops
端點和一些圖表資料(根據維基百科估計的世界各大洲人口)
$ curl localhost:8080/pops | jq .
{
"data": {
"labels": [
"Africa",
"Asia",
"Europe",
"Latin America",
"North America"
],
"datasets": [
{
"backgroundColor": [
"#3e95cd",
"#8e5ea2",
"#3cba9f",
"#e8c3b9",
"#c45850"
],
"label": "Population (millions)",
"data": [
2478,
5267,
734,
784,
433
]
}
]
},
"options": {
"plugins": {
"legend": {
"display": false
},
"title": {
"text": "Predicted world population (millions) in 2050",
"display": true
}
}
}
}
這個範例應用程式還有一些其他的圖表,它們都以不同的格式顯示相同的資料。它們都由上面說明的同一個端點提供服務
@GetMapping("/pops")
@ResponseBody
public Chart bar() {
return new Chart();
}
在 Spring 指南和參考文件中,我們經常看到按 "類型" 分段的程式碼區塊(例如 Maven 與 Gradle,或 XML 與 Java)。它們會顯示一個啟用的選項,並隱藏其餘選項,如果使用者點擊另一個選項,不僅會顯示最接近的程式碼片段,而且會顯示整個文件中符合點擊的所有片段。例如,如果使用者點擊 "Gradle",所有引用 "Gradle" 的程式碼片段都會同時被啟用。驅動此功能的 Javascript 以多種形式存在,具體取決於哪個指南或專案正在使用它,其中一種形式是一個 NPM 捆綁包 @springio/utils。它嚴格來說不是一個 ESM 模組,但我們仍然可以匯入它並看到該功能正在運作。這是它在 index.html
中的樣子
<script type="importmap">
{
"imports": {
...
"@springio/utils": "/npm/@springio/utils"
}
}
</script>
<script type="module">
...
import '@springio/utils';
</script>
然後我們可以新增一個帶有一些 "程式碼片段" 的新分頁(在本例中只是一些垃圾內容)
<div class="tab-pane fade" id="docs" role="tabpanel">
<div class="container" title="Content">
<div class="content primary"><div class="title">One</div><div class="content">Some content</div></div>
<div class="content secondary"><div class="title">Two</div><div class="content">Secondary</div></div>
<div class="content secondary"><div class="title">Three</div><div class="content">Third option</div></div>
</div>
<div class="container" title="Another">
<div class="content primary"><div class="title">One</div><div class="content">Some more content</div></div>
<div class="content secondary"><div class="title">Two</div><div class="content">Secondary stuff</div></div>
<div class="content secondary"><div class="title">Three</div><div class="content">Third option again</div></div>
</div>
</div>
如果使用者選擇 "One" 區塊類型,它看起來會像這樣
驅動此行為的是 HTML 的結構,一個元素標記為 "primary",另一個替代方案標記為 "secondary",然後在實際內容之前有一個巢狀的 class="title"
。標題由 Javascript 提取到按鈕中。
Vue 是一個輕量級的 Javascript 函式庫,您可以使用少量或大量。要開始使用 Webjars,我們需要在 pom.xml
中加入這個相依性
<dependency>
<groupId>org.webjars.npm</groupId>
<artifactId>vue</artifactId>
<version>2.6.14</version>
</dependency>
並將其添加到 index.html
中的匯入對應(import map)(使用手動資源路徑,因為 NPM 捆綁包中的 "module" 指向瀏覽器無法運作的內容)
<script type="importmap">
{
"imports": {
...
"vue": "/npm/vue/dist/vue.esm.browser.js"
}
}
</script>
然後我們可以編寫一個元件並將其 "mount" 在一個已命名的元素中(這是 Vue 使用者指南中的一個範例)
<script type="module">
import Vue from 'vue';
const EventHandling = {
data() {
return {
message: 'Hello Vue.js!'
}
},
methods: {
reverseMessage() {
this.message = this.message
.split('')
.reverse()
.join('')
}
}
}
new Vue(EventHandling).$mount("#event-handling");
</script>
要接收動態內容,我們需要一個符合 #event-handling
的元素,例如
<div class="tab-pane fade" id="test" role="tabpanel">
<div class="container" id="event-handling">
<p>{{ message }}</p>
<button class="btn btn-primary" v-on:click="reverseMessage">Reverse Message</button>
</div>
</div>
因此,範本化 (templating) 發生在客戶端,並且是由 Vue 的 v-on
觸發點擊。
如果我們想用 Vue 替換 Hotwired,我們可以從主要 "message" 分頁上的內容開始。因此,我們可以將 Stimulus 控制器繫結替換為這個,例如
<div class="tab-pane fade show active" id="message" role="tabpanel">
<div class="container">
<div id="auth">
{{user}}
</div>
<div id="greeting">{{greeting}}</div>
<input id="name" name="value" type="text" v-model="name" />
<button class="btn btn-primary" v-on:click="greet">Greet</button>
</div>
</div>
然後透過 Vue 掛鉤 user
和 greeting
屬性
import Vue from 'vue';
const EventHandling = {
data() {
return {
greeting: '',
name: '',
user: 'Unauthenticated'
}
},
created: function () {
let hello = this;
fetch("/user").then(response => {
response.json().then(data => {
hello.user = `Logged in as: ${data.name}`;
});
});
},
methods: {
greet() {
this.greeting = `Hello, ${this.name}!`;
},
}
}
new Vue(EventHandling).$mount("#message");
created
hook 作為 Vue 元件生命週期的一部分運行,因此不一定會與 Stimulus 運行時完全相同,但已經足夠接近了。
我們也可以用 Vue 替換圖表選擇器,然後我們可以擺脫 Stimulus,只是為了看看它會是什麼樣子。這是圖表分頁(基本上與之前相同,但沒有控制器裝飾)
<div class="tab-pane fade" id="chart" role="tabpanel">
<div class="container">
<canvas id="canvas"></canvas>
</div>
<div class="container">
<button class="btn btn-primary" v-on:click="clear">Clear</button>
<button class="btn btn-primary" v-on:click="bar">Bar</button>
</div>
</div>
這是渲染圖表的 Javascript 程式碼
<script type="module">
import Vue from 'vue';
import { Chart, BarController, BarElement, LinearScale, CategoryScale, Title, Legend } from 'chart.js';
Chart.register(BarController, BarElement, LinearScale, CategoryScale, Title, Legend);
const ChartHandling = {
methods: {
clear() {
if (this.active) {
this.active.destroy();
}
},
bar() {
let chart = this;
this.clear();
fetch("/pops").then(response => {
response.json().then(data => {
data.type = "bar";
chart.active = new Chart(document.getElementById("canvas"), data);
});
});
}
}
}
new Vue(ChartHandling).$mount("#chart");
</script>
範例程式碼除了 "bar" 圖表類型之外,還有 "pie" 和 "doughnut",它們的工作方式相同。
Vue 可以使用 v-html
屬性替換元素的整個內部 HTML,因此我們可以開始使用它來重新實作 Turbo 內容。這是新的 "test" 分頁
<div class="tab-pane fade" id="test" role="tabpanel">
<div class="container" id="frame">
<div id="hi" v-html="html"></div>
<button class="btn btn-primary" v-on:click="hello">Fetch</button>
</div>
</div>
它有一個指向 "hello" 方法的點擊處理程式,以及一個等待接收內容的 div。我們可以這樣將按鈕附加到 "hi" 容器
<script type="module">
import Vue from 'vue';
const HelloHandling = {
data: {
html: ''
},
methods: {
hello() {
const handler = this;
fetch("/test").then(response => {
response.text().then(data => {
handler.html = data;
});
});
},
}
}
new Vue(HelloHandling).$mount("#test");
</script>
為了使其運作,我們只需要從伺服器端範本中移除 <turbo-frame/>
元素(恢復到我們在 HTMX 範例中擁有的內容)。
絕對可以使用 Vue(或其他函式庫,甚至普通的 Javscript)替換我們的 Turbo(和 HTMX)程式碼,但我們可以從範例中看到,它不可避免地涉及一些樣板 Javascript。