Không phải mọi ứng dụng đều cần React. Bài này về cách Spring render HTML.

Bật

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

Auto-configuration đặt thư mục template ở src/main/resources/templates/ và hậu tố .html.

Controller trả tên view

@Controller          // KHÔNG phải @RestController
class TrangController {
    @GetMapping("/don/{id}")
    String xem(@PathVariable String id, Model model) {
        model.addAttribute("don", svc.lay(id));
        return "don/chi-tiet";      // -> templates/don/chi-tiet.html
    }
}

Khác biệt then chốt với bài 12: @RestController trả dữ liệu, @Controller trả tên view. Nhầm hai cái này là nguyên nhân số một của lỗi "trình duyệt hiện ra chữ don/chi-tiet".

Model là túi dữ liệu truyền sang template.

Template

<div th:text="${don.ma}">mã mẫu</div>
<div th:if="${don.daHuy}">Đơn đã huỷ</div>
<tr th:each="m : ${don.mucs}">
    <td th:text="${m.ten}"></td>
</tr>
<a th:href="@{/don/{id}(id=${don.ma})}">chi tiết</a>

Điểm mạnh riêng của Thymeleaf: tệp vẫn là HTML hợp lệ. Mở trực tiếp trong trình duyệt thì thấy "mã mẫu" thay vì lỗi cú pháp — nên người làm giao diện sửa được mà không cần chạy ứng dụng.

Cú pháp @{...} cho URL rất đáng dùng: nó tự thêm context path, nên ứng dụng chạy ở / hay /app đều đúng.

Nó tự thoát ký tự

<div th:text="${duLieuNguoiDungNhap}"></div>

th:text luôn thoát HTML. Người dùng nhập <script>alert(1)</script> thì nó hiện ra dưới dạng chữ, không chạy.

Đây là mặc định an toàn, và nó là lý do chính để dùng công cụ template thay vì tự ghép chuỗi HTML.

<div th:utext="${htmlTinCay}"></div>

th:utext không thoát. Chỉ dùng cho HTML bạn tự sinh hoặc đã làm sạch — với nội dung người dùng nhập, phải qua một bộ làm sạch như jsoup trước.

Layout

<!-- fragments/layout.html -->
<div th:fragment="header">...</div>

<!-- trang dùng -->
<div th:replace="~{fragments/layout :: header}"></div>

Với layout đầy đủ, thymeleaf-layout-dialect cho phép trang con kế thừa trang khung — gọn hơn nhiều so với chèn fragment ở mọi trang.

Một cái bẫy đã gặp: th:replace chạy trước th:each. Muốn lặp một fragment thì đặt th:each ở thẻ bao ngoài, không đặt cùng thẻ với th:replace.

Tài nguyên tĩnh

Spring Boot phục vụ tự động từ bốn thư mục trong src/main/resources:

  static/       -> /**
  public/
  resources/
  META-INF/resources/

Tệp static/css/style.css truy cập bằng /css/style.css.

Cache cho sản xuất:

spring:
  web:
    resources:
      cache:
        cachecontrol:
          max-age: 365d
      chain:
        strategy:
          content:
            enabled: true
            paths: /**

content strategy thêm mã băm nội dung vào tên tệp, nên bạn cache một năm mà vẫn cập nhật ngay khi sửa. Trong template dùng @{/css/style.css} và Thymeleaf tự viết ra tên có băm.

Khi nào chọn render phía máy chủ

Chọn Thymeleaf khi: trang chủ yếu là nội dung (blog, tài liệu, trang giới thiệu), khu quản trị nội bộ, SEO quan trọng, đội nhỏ không muốn duy trì hai codebase, hoặc bạn muốn một tệp jar là xong.

Chọn SPA khi: giao diện nhiều tương tác trạng thái phức tạp, cần offline, có đội frontend riêng, hoặc cùng một API phục vụ cả web lẫn di động.

Blog này chạy Spring Boot với Thymeleaf, và đó là quyết định đúng cho nó: nội dung là chính, SEO quan trọng, và một người duy trì.

Có lựa chọn thứ ba đang phổ biến lại: HTMX cộng Thymeleaf — server trả về mảnh HTML, HTMX thay phần trang. Nó cho phần lớn tính tương tác của SPA mà không cần build pipeline JavaScript.

Cái bẫy Content-Security-Policy

Nếu bạn bật CSP không có unsafe-inline — và bạn nên bật — thì mọi <script> viết thẳng trong template sẽ không chạy trên trình duyệt thật.

Điều khó chịu là test MockMvc vẫn xanh, vì nó không thực thi JavaScript. Lỗi chỉ lộ ra khi mở trình duyệt.

Quy ước thay thế: đưa script ra tệp trong static/js/, và thay onclick="..." bằng thuộc tính data-* cộng một trình xử lý chung.

Thử ba mươi giây

Tạo một endpoint trả về chuỗi <b>đậm</b> và hiển thị bằng cả th:text lẫn th:utext.

Cái đầu hiện ra đúng chuỗi <b>đậm</b>, cái sau hiện chữ đậm. Ba mươi giây đó là toàn bộ khác biệt giữa an toàn và một lỗ hổng XSS.

Ngày mai: Filter, Interceptor và các mẫu middleware thực dụng.