Spring Boot 和 Thymeleaf 實現 Java 版 HTMX

banq發表於2024-06-20

HTMX是否有潛力成為實現以Java為中心的Ajax(Asynchronous JavaScript and XML,非同步JavaScript和XML)開發模式的關鍵元件。

  • Ajax是一種在不重新載入整個頁面的情況下,能夠與伺服器交換資料並更新部分網頁的技術。
  • HTMX可能成為將Java後端與前端動態互動功能緊密結合的工具。

讓我們透過基於 HTMX、Spring Boot 和 Thymeleaf 的示例應用程式來一探究竟。

什麼是HTMX?
HTMX是一種較新的技術,它採用普通的 HTML,並賦予其 Ajax 和 DOM 交換等額外功能。它被列入我個人的好主意列表中,因為它消除了典型 Web 應用程式中的整個複雜性。HTMX​​ 透過在 JSON 和 HTML 之間來回轉換來工作。可以將其視為一種宣告式 Ajax。

Java、Spring 和 Thymeleaf
而 Java 則是另一個選擇:它是最成熟且最具創新性的伺服器端平臺之一。Spring 是新增一系列基於 Java 的功能的簡單選擇,包括用於處理端點和路由的 精心設計的Spring Boot Web 專案。

Thymeleaf是一款完整的伺服器端模板引擎,也是 Spring Boot Web 的預設引擎。與 HTMX 結合使用時,您可以構建全棧 Web 應用,而無需使用大量 JavaScript。 

案例
我們將構建標準的 Todo 應用:我們列出現有的待辦事項,並允許建立新的待辦事項、刪除待辦事項以及更改其完成狀態。

概述
完成的 Todo 應用程式在磁碟上的樣子如下:


$ tree
.
├── build.gradle
├── gradlew
├── gradlew.bat
├── settings.gradle
└── src
    └── main
        ├── java
        │   └── com
        │       └── example
        │           └── iwjavaspringhtmx
        │               ├── DemoApplication.java
        │               ├── controller
        │               │   └── MyController.java
        │               └── model
        │                   └── TodoItem.java
        └── resources
            ├── application.properties
            ├── static
            │   └── style.css
            └── templates
                ├── index.html
                ├── style.css
                └── todo.html

因此,除了典型的 Gradle 內容外,應用程式還有兩個主要部分包含在 /src 目錄中:/main 目錄包含 Java 程式碼,而 /resources 則包含屬性檔案以及 CSS 和 Thymeleaf 模板的兩個子目錄。

您可以在 GitHub repo 程式碼庫中找到該專案的原始碼。要執行它,請訪問根目錄並鍵入 $ gradle bootRun。然後就可以在 localhost:8080 上使用該應用程式了。

如果想從頭開始啟動應用程式,可以從以下步驟開始:$ spring init --dependencies=web,thymeleaf spring-htmx。這將把 Thymeleaf 和 Spring Boot 安裝到 Gradle 專案中。

該應用程式是由 DemoApplication.java 執行的普通 Spring Boot 應用程式。

Java Spring HTMX 模型類
讓我們首先看看我們的模型類:com/example/iwjavaspringhtmx/TodoItem.java。這是代表待辦事項的伺服器端模型類。它看起來如下:

public class TodoItem {
  private boolean completed;
  private String description;
  private Integer id;
  public TodoItem(Integer id, String description) {
    this.description = description;
    this.completed = false;
    this.id = id;
  }
  public void setCompleted(boolean completed) {
    this.completed = completed;
  }
  public boolean isCompleted() {
    return completed;
  }
  public String getDescription() {
    return description;
  }
  public Integer getId(){ return id; }
  public void setId(Integer id){ this.id = id; }
  @Override
  public String toString() {
    return id + <font>" " + (completed ? "[COMPLETED] " : "[ ] ") + description;
  }
}

這是一個帶有 getter 和 setter 的簡單模型類。沒什麼特別的,但這正是我們想要的。

Java Spring HTMX 控制器類
在伺服器上,控制器是老闆。它接受請求、編排邏輯並制定響應。在我們的例子中,我們需要四個端點,用於列出專案、更改其完成狀態、新增專案和刪除專案。這是控制器類:


@Controller
public class MyController {

  private static List<TodoItem> items = new ArrayList();
  static {
    TodoItem todo = new TodoItem(0,<font>"Make the bed");
    items.add(todo);
    todo = new TodoItem(1,
"Buy a new hat");
    items.add(todo);
    todo = new TodoItem(2,
"Listen to the birds singing");
    items.add(todo);
  }

  public MyController(){ }

  @GetMapping(
"/")
  public String items(Model model) {
    model.addAttribute(
"itemList", items);
    return
"index";
  }

  @PostMapping(
"/todos/{id}/complete")
  public String completeTodo(@PathVariable Integer id, Model model) {
    TodoItem item = null;
    for (TodoItem existingItem : items) {
      if (existingItem.getId().equals(id)) {
        item = existingItem;
        break
      }
    }
    if (item != null) {
      item.setCompleted(!item.isCompleted());
    }
    model.addAttribute(
"item",item);
    return
"todo"
  }

  @PostMapping(
"/todos")
  public String createTodo(Model model, @ModelAttribute TodoItem newTodo) {
    int nextId = items.stream().mapToInt(TodoItem::getId).max().orElse(0) + 1;
    newTodo.setId(nextId);
    items.add(newTodo);
    model.addAttribute(
"item", newTodo);
    return
"todo";
  }

  @DeleteMapping(
"/todos/{id}/delete")
  @ResponseBody
  public String deleteTodo(@PathVariable Integer id) {
    for (int i = 0;  i < items.size(); i++) {
      TodoItem item = items.get(i);
      if (item.getId() == id) {
        items.remove(i);
        break;
      }
    }
    return
"";
  }
}

您會注意到,我剛剛建立了一個靜態變數List來儲存記憶體中的專案。在現實生活中,我們會使用外部資料儲存。

首先,端點使用 @GetMapping、@PostMapping 和 @DeleteMapping 進行註解。這就是將 Spring Web 路徑對映到處理程式的方法。每個註解對應其 HTTP 方法(GET、POST、DELETE)。

Spring Boot 還可以使用引數註解 @PathParameter 從路徑中輕鬆獲取引數。因此,對於 /todos/{id}/delete 路徑,@PathVariable Integer id 將包含路徑 {id} 部分的值。

在 createTodo() 方法中,註釋為 @ModelAttribute TodoItem newTodo 的引數將自動接收 POST 主體並將其值應用於 newTodo 物件。(這是一種將表單提交轉化為 Java 物件的快速而簡單的方法)。

接下來,我們使用專案 ID 來操作專案列表:這是標準的 REST API 方法。

傳送響應有兩種方式。如果方法上有 @ResponseBody 註解(比如 deleteTodo()),那麼返回的內容將逐字傳送。否則,返回字串將被解釋為 Thymeleaf 模板路徑(稍後您將看到)。

Model 模型引數比較特殊。它用於為移交給 Thymeleaf 的作用域新增屬性。我們可以把下面的專案方法理解為給定一個指向根/路徑的 GET 請求,將 items 變數新增到作用域中,命名為 "itemList",然後使用 "index "模板渲染響應。

@GetMapping(<font>"/")
  public String items(Model model) {
    model.addAttribute(
"itemList", items);
    return
"index";
  }

在 HTMX 處理從前端傳送的 AJAX 請求時,HTMX 元件將使用響應來更新使用者介面。我們很快就能在實踐中很好地瞭解這一點。

Thymeleaf 模板
現在讓我們來看看 Thymeleaf 的索引模板。它位於 /resources/templates/index.html 檔案中。Spring Boot 使用約定將 items() 方法返回的 "index "字串對映到該檔案。下面是我們的 index.html 模板:

<!DOCTYPE html>
<html lang=<font>"en">
  <head>
    <meta charset=
"UTF-8">
    <title>Items List</title>
    <script src=
"https://unpkg.com/htmx.org@1.9.12"></script>
    <link rel=
"stylesheet" href="style.css">
  </head>
  <body>
    <h1>Stuff To Do</h1>
      <ul>
        <th:block th:each=
"item : ${itemList}">
          <th:block th:replace=
"~{'todo.html'}" th:args="${item}"></th:block>
        </th:block>
      </ul>
      <hr>
      <form hx-post=
"/todos" th:object="${newTodo}" hx-target="ul" hx-swap="beforeend">
        <input type=
"text" name="description" placeholder="Add a new item..." required>
        <button type=
"submit">Add</button>
      </form>
  </body>
</html>

Thymeleaf 的基本思想是在 HTML 結構中使用 Java 變數。(這相當於使用 Pug 這樣的模板系統)。

Thymeleaf 使用以 th: 為字首的 HTML 屬性或元素來表示其工作位置。請記住,當我們在控制器中對映根/路徑時,我們在作用域中新增了 itemList 變數。在這裡,我們在帶有 th:each 屬性的 th:block 中使用該變數。th:each 屬性是 Thymeleaf 中的迭代器機制。我們用它來訪問 itemList 中的元素,並將每個元素作為名為 item 的變數公開:item : ${itemList}。

在 itemList 的每次迭代中,我們都將渲染工作交給另一個模板。這種模板重用是避免程式碼重複的關鍵。
行:

<th:block th:replace=<font>"~{'todo.html'}"th:args="${item}"></th:block>

告訴 Thymeleaf 渲染 todo.html 模板,並將專案作為引數。

接下來我們將瞭解 todo 模板,但首先要注意的是,我們在控制器中的 completeTodo 和 createTodo 中都使用了相同的模板,以提供在 Ajax 請求期間傳送回 HTMX 的標記。換句話說,我們將 todo.html 用作初始列表渲染的一部分,並在 Ajax 請求期間向使用者介面傳送更新。重複使用 Thymeleaf 模板可以使我們保持 DRY。

待辦事項模板
這是 todo.html:

<li>
  <input type=<font>"checkbox" th:checked="${item.isCompleted}" hx-trigger="click" hx-target="closest li" hx-swap="outerHTML" th:hx-post="|/todos/${item.id}/complete|">
  <span th:text=
"${item.description}" th:classappend="${item.isCompleted ? 'complete' : ''}"></span>
  <button type=
"button" th:hx-post="|/todos/${item.id}/delete|" hx-swap="outerHTML" hx-target="closest li"></button>
</li>

您可以看到,我們提供了一個 list-item 元素,並使用一個變數 item 來填充值。在這裡,我們將使用 HTMX 和 Thymeleaf 進行一些有趣的工作。

首先,我們使用 th:checked 將 item.isComplete 的選中狀態應用於核取方塊輸入。

點選核取方塊時,我們會使用 HTMX 向後端發出 Ajax 請求:

  • hx-trigger="click" 告知 HTMX 在點選時啟動 Ajax。
  • hx-target="closest li "告訴 HTMX 將 Ajax 請求的響應放在哪裡。在我們的例子中,我們要替換最近的 list-item 元素。(請記住,我們的刪除端點會返回該專案的整個列表專案標記)。
  • hx-swap="outerHTML "告訴 HTMX 如何替換新內容,在本例中就是替換整個元素。
  • th:hx-post="|/todos/${item.id}/complete|"告訴 HTMX,這是一個活動的 Ajax 元素,會向指定的 URL(我們的 completeTodo 端點)發出 POST 請求。

將 Thymeleaf 與 HTMX 結合使用時需要注意的一點是,您最終會使用複雜的屬性字首,就像您在 th:hx-post 中看到的那樣。從本質上講,Thymeleaf 首先在伺服器上執行(th: 字首)並填充 ${item.id} 插值,然後 hx-post 在客戶端上正常工作。

接下來,對於 span,我們只需顯示 item.description 中的文字。(請注意,Thymelef 的表示式語言允許我們訪問欄位而無需使用 get 字首)。同樣值得注意的是,我們如何在 span 元素中應用已完成的樣式類。下面是我們的 CSS 將用於為已完成的專案新增刪除線裝飾:

th:classappend=<font>"${item.isCompleted ? 'complete' : ''}"

使用 Thymeleaf 屬性,可以根據 item.isComplete 等布林條件有條件地應用類。

我們的刪除按鈕與完整核取方塊的工作原理類似。我們使用 Thymeleaf 提供的 item.id 向 URL 傳送 Ajax 請求,當響應返回時,我們更新列表項。請記住,我們從 deleteTodo() 發回的是空字串。因此,其效果是從 DOM 中移除列表項。

CSS 樣式表
CSS 樣式表位於 src/main/resources/static/style.css,沒什麼特別的。唯一有趣的是處理跨頁上的span樣式:

span {
  flex-grow: 1;
  font-size: 1rem;
  text-decoration: none;
  color: #333;
  opacity: 0.7;
}

span.complete {
  text-decoration: line-through;
  opacity: 1;
}

結論
將 HTMX、Java、Spring 和 Thymeleaf 結合在一起,可以用最少的模板程式碼構建相當複雜的互動。我們可以在不編寫 JavaScript 的情況下實現大量典型的互動。

乍一看,Java-HTMX 協議棧似乎終於兌現了以 Java 為中心的 Ajax 的承諾;就像 Google Web Toolkit 曾經設定的目標一樣。

但事實並非如此。

HTMX 試圖將Web應用程式重新定位到 REST 的真正本質,而這個協議棧為我們指明瞭方向。

HTMX 與伺服器端無關,因此我們可以毫無困難地將其與 Java 後端整合。

相關文章