mcpField
T
thymeleaf

thymeleaf

github.com/claude-dev-suite开发4694 次安装2 天前更新
代码仓库

关于 thymeleaf

thymeleaf 是面向 Thymeleaf 模板的快速参考技能,供开发者查阅语法、表单绑定和模板组织方式。内容还涵盖安全输出与常见问题排查。

主要功能

  1. 01变量、循环与条件语法
  2. 02片段和布局复用
  3. 03表单绑定与 JavaScript 内联
  4. 04表达式及工具对象参考
  5. 05安全实践与故障排查

适用场景

  1. 01构建 Spring 页面模板
  2. 02绑定表单数据并显示错误
  3. 03抽取可复用页面片段
  4. 04排查变量或模板加载问题

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/claude-dev-suite/claude-dev-suite --skill thymeleaf

安装后在 Claude Code 中输入/thymeleaf确认已加载。

详细介绍

Thymeleaf - 快速参考

深入了解:使用 technology:thymeleaf 调用 mcp__documentation__fetch_docs 获取完整文档。

基本模式

基本语法
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <title th:text="${title}">Default</title>
</head>
<body>
    <h1 th:text="${message}">Hello</h1>
    <p th:text="'Welcome, ' + ${name}">Welcome, User</p>
</body>
</html>
变量与循环

<p th:text="${user.name}">Name</p>


<tr th:each="user : ${users}">
    <td th:text="${user.name}">Name</td>
    <td th:text="${user.email}">Email</td>
</tr>


<p th:if="${user.active}">Active</p>
<p th:unless="${user.active}">Inactive</p>
电子邮件服务
@Service
@RequiredArgsConstructor
public class EmailService {

    private final TemplateEngine templateEngine;
    private final JavaMailSender mailSender;

    public void sendWelcome(User user) {
        Context ctx = new Context();
        ctx.setVariable("userName", user.getName());
        ctx.setVariable("actionUrl", "https://app.com/verify");

        String html = templateEngine.process("emails/welcome", ctx);

        MimeMessage msg = mailSender.createMimeMessage();
        MimeMessageHelper helper = new MimeMessageHelper(msg, true);
        helper.setTo(user.getEmail());
        helper.setSubject("Welcome!");
        helper.setText(html, true);
        mailSender.send(msg);
    }
}
片段与布局

<nav th:fragment="navigation">
    <ul>
        <li><a th:href="@{/}">Home</a></li>
        <li><a th:href="@{/about}">About</a></li>
    </ul>
</nav>

<footer th:fragment="footer(year)">
    <p th:text="'© ' + ${year} + ' My Company'">© 2024 My Company</p>
</footer>


<div th:replace="~{fragments/common :: navigation}"></div>


<div th:insert="~{fragments/common :: navigation}"></div>


<div th:replace="~{fragments/common :: footer(${#dates.year(#dates.createNow())})}"></div>
布局方言

<dependency>
    <groupId>nz.net.ultraq.thymeleaf</groupId>
    <artifactId>thymeleaf-layout-dialect</artifactId>
</dependency>

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
      xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
<head>
    <title layout:title-pattern="$CONTENT_TITLE - $LAYOUT_TITLE">My App</title>
    <link rel="stylesheet" th:href="@{/css/main.css}"/>
</head>
<body>
    <header th:replace="~{fragments/common :: navigation}"></header>

    <main layout:fragment="content">
        
    </main>

    <footer th:replace="~{fragments/common :: footer}"></footer>

    <script layout:fragment="scripts"></script>
</body>
</html>

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
      xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
      layout:decorate="~{layouts/main}">
<head>
    <title>Home</title>
</head>
<body>
    <main layout:fragment="content">
        <h1>Welcome!</h1>
        <p th:text="${message}">Content</p>
    </main>

    <th:block layout:fragment="scripts">
        <script th:src="@{/js/home.js}"></script>
    </th:block>
</body>
</html>
表单绑定
<form th:action="@{/users}" th:object="${userForm}" method="post">
    <div>
        <label for="name">Name:</label>
        <input type="text" id="name" th:field="*{name}"
               th:classappend="${#fields.hasErrors('name')} ? 'error' : ''"/>
        <span th:if="${#fields.hasErrors('name')}"
              th:errors="*{name}" class="error-msg">Name error</span>
    </div>

    <div>
        <label for="email">Email:</label>
        <input type="email" id="email" th:field="*{email}"/>
        <span th:if="${#fields.hasErrors('email')}"
              th:errors="*{email}" class="error-msg">Email error</span>
    </div>

    <div>
        <label for="role">Role:</label>
        <select id="role" th:field="*{role}">
            <option value="">-- Select --</option>
            <option th:each="role : ${roles}"
                    th:value="${role}"
                    th:text="${role.displayName}">Role</option>
        </select>
    </div>

    <button type="submit">Save</button>
</form>
JavaScript 内联
<script th:inline="javascript">
    // Natural templates with fallback
    const user = /*[[${user}]]*/ { name: 'default' };
    const userId = /*[[${user.id}]]*/ 0;
    const isAdmin = /*[[${user.admin}]]*/ false;

    // Array
    const items = /*[[${items}]]*/ [];

    // Conditional in JS
    /*[# th:if="${user != null}"]*/
    console.log('User:', user.name);
    /*[/]*/
</script>


<style th:inline="css">
    .user-bg {
        background-color: [[${user.favoriteColor}]];
    }
</style>
实用对象

<p th:text="${#dates.format(user.createdAt, 'dd/MM/yyyy HH:mm')}">Date</p>
<p th:text="${#dates.dayOfWeekName(date)}">Monday</p>


<p th:text="${#strings.toUpperCase(name)}">NAME</p>
<p th:text="${#strings.abbreviate(text, 100)}">Truncated...</p>
<p th:text="${#strings.isEmpty(value) ? 'N/A' : value}">Value</p>
<p th:text="${#strings.listJoin(items, ', ')}">a, b, c</p>


<p th:text="${#numbers.formatDecimal(price, 1, 2)}">10.50</p>
<p th:text="${#numbers.formatCurrency(amount)}">$1,234.56</p>


<p th:text="${#lists.size(users)}">Count</p>
<p th:if="${#lists.isEmpty(users)}">No users</p>
<p th:text="${#lists.contains(roles, 'ADMIN')}">Has admin</p>


<p th:text="${#aggregates.sum(prices)}">Total</p>
<p th:text="${#aggregates.avg(scores)}">Average</p>
Switch/Case
<div th:switch="${user.role}">
    <p th:case="'ADMIN'">Administrator</p>
    <p th:case="'MANAGER'">Manager</p>
    <p th:case="'USER'">Regular User</p>
    <p th:case="*">Unknown Role</p>
</div>
迭代状态
<tr th:each="user, stat : ${users}"
    th:class="${stat.odd} ? 'odd' : 'even'">
    <td th:text="${stat.index}">0</td>
    <td th:text="${stat.count}">1</td>
    <td th:text="${user.name}">Name</td>
    <td th:if="${stat.first}">First!</td>
    <td th:if="${stat.last}">Last!</td>
</tr>

表达式

表达式用途
${var}变量
*{prop}选择(与 th:object 搭配使用)
@{/url}链接 URL
#{msg}i18n 消息
~{frag}片段

最佳实践

应该做不应该做
使用 th:text 输出转义后的内容对用户输入使用 th:utext(XSS)
使用片段创建可复用组件在模板间重复 HTML
保持模板简单在模板中编写复杂逻辑
使用 i18n 消息将文本字符串硬编码
使用布局方言在每个页面重复布局

不适合使用此技能的情况

  • REST API - 使用 spring-rest 技能处理 JSON 响应
  • SPA 前端 - 使用 React、Vue 或 Angular
  • PDF 报告 - 使用 JasperReports 或 iText
  • 高流量 API - 考虑使用静态前端

反模式

反模式问题解决方案
对用户输入使用 th:utextXSS 漏洞使用 th:text 进行转义
在模板中编写复杂逻辑难以测试和维护将逻辑移至控制器
缺少 th 命名空间静默失败始终声明 xmlns:th
到处使用内联样式UI 不一致使用 CSS 类
不复用片段代码重复提取公共片段

快速故障排除

问题诊断修复方法
变量未渲染检查 th: 前缀使用 th:text、th:value
找不到模板检查路径将模板放在 templates/ 文件夹中
迭代不起作用检查 th:each 语法确认已传入集合
未包含片段检查路径语法使用 ~{fragments/name :: fragment}
i18n 不起作用检查 messages.properties确认文件位置和键

参考文档

评论

支持 Markdown