深色模式
8.4 移除模板片段
在 Thymeleaf 模板开发中,为了让静态原型更逼真(比如显示多行模拟数据),但又要保证动态渲染时只保留真实数据,我们需要一种在模板处理时移除模拟片段的方式——这就是 th:remove 属性的核心作用。
回到上文我们提到的产品列表页:
html
<table>
<tr>
<th>NAME</th>
<th>PRICE</th>
<th>IN STOCK</th>
<th>COMMENTS</th>
</tr>
<tr th:each="prod : ${prods}" th:class="${prodStat.odd}? 'odd'">
<td th:text="${prod.name}">Onions</td>
<td th:text="${prod.price}">2.41</td>
<td th:text="${prod.inStock}? #{true} : #{false}">yes</td>
<td>
<span th:text="${#lists.size(prod.comments)}">2</span> comment/s
<a href="comments.html"
th:href="@{/product/comments(prodId=${prod.id})}"
th:unless="${#lists.isEmpty(prod.comments)}">view</a>
</td>
</tr>
</table>这段代码作为模板没有问题,但作为静态页面(当被浏览器直接打开而未经Thymeleaf处理时)无法成为理想的展示原型。
原因何在?因为尽管浏览器能正常显示这个表格,但它只有一行数据,并且这一行使用的是模拟数据。作为原型展示,这样的视觉效果显然不够真实……我们需要展示多个产品,需要更多的数据行。
那就让我们加一些数据:
html
<table>
<tr>
<th>NAME</th>
<th>PRICE</th>
<th>IN STOCK</th>
<th>COMMENTS</th>
</tr>
<tr th:each="prod : ${prods}" th:class="${prodStat.odd}? 'odd'">
<td th:text="${prod.name}">Onions</td>
<td th:text="${prod.price}">2.41</td>
<td th:text="${prod.inStock}? #{true} : #{false}">yes</td>
<td>
<span th:text="${#lists.size(prod.comments)}">2</span> comment/s
<a href="comments.html"
th:href="@{/product/comments(prodId=${prod.id})}"
th:unless="${#lists.isEmpty(prod.comments)}">view</a>
</td>
</tr>
<tr class="odd">
<td>Blue Lettuce</td>
<td>9.55</td>
<td>no</td>
<td>
<span>0</span> comment/s
</td>
</tr>
<tr>
<td>Mild Cinnamon</td>
<td>1.99</td>
<td>yes</td>
<td>
<span>3</span> comment/s
<a href="comments.html">view</a>
</td>
</tr>
</table>现在我们有了三行数据,作为原型来说确实好多了。但是……当我们用 Thymeleaf 处理它时,会发生什么呢?
html
<table>
<tr>
<th>NAME</th>
<th>PRICE</th>
<th>IN STOCK</th>
<th>COMMENTS</th>
</tr>
<tr>
<td>Fresh Sweet Basil</td>
<td>4.99</td>
<td>yes</td>
<td>
<span>0</span> comment/s
</td>
</tr>
<tr class="odd">
<td>Italian Tomato</td>
<td>1.25</td>
<td>no</td>
<td>
<span>2</span> comment/s
<a href="/gtvg/product/comments?prodId=2">view</a>
</td>
</tr>
<tr>
<td>Yellow Bell Pepper</td>
<td>2.50</td>
<td>yes</td>
<td>
<span>0</span> comment/s
</td>
</tr>
<tr class="odd">
<td>Old Cheddar</td>
<td>18.75</td>
<td>yes</td>
<td>
<span>1</span> comment/s
<a href="/gtvg/product/comments?prodId=4">view</a>
</td>
</tr>
<tr class="odd">
<td>Blue Lettuce</td>
<td>9.55</td>
<td>no</td>
<td>
<span>0</span> comment/s
</td>
</tr>
<tr>
<td>Mild Cinnamon</td>
<td>1.99</td>
<td>yes</td>
<td>
<span>3</span> comment/s
<a href="comments.html">view</a>
</td>
</tr>
</table>后面两行都是模拟数据!没错,它们确实是模拟的。迭代只应用于第一行,所以 Thymeleaf 没有理由去删除另外两行。
我们需要找到一种方法,在模板处理过程中移除这两行:在第二个和第三个 <tr> 标签上使用 th:remove 属性:
html
<table>
<tr>
<th>NAME</th>
<th>PRICE</th>
<th>IN STOCK</th>
<th>COMMENTS</th>
</tr>
<!-- 真实数据行:th:each 遍历生成 -->
<tr th:each="prod : ${prods}" th:class="${prodStat.odd}? 'odd'">
<td th:text="${prod.name}">Onions</td>
<td th:text="${prod.price}">2.41</td>
<td th:text="${prod.inStock}? #{true} : #{false}">yes</td>
<td>
<span th:text="${#lists.size(prod.comments)}">2</span> comment/s
<a href="comments.html" th:href="@{/product/comments(prodId=${prod.id})}" th:unless="${#lists.isEmpty(prod.comments)}">view</a>
</td>
</tr>
<!-- 模拟行:处理时完全移除 -->
<tr class="odd" th:remove="all">
<td>Blue Lettuce</td>
<td>9.55</td>
<td>no</td>
<td><span>0</span> comment/s</td>
</tr>
<tr th:remove="all">
<td>Mild Cinnamon</td>
<td>1.99</td>
<td>yes</td>
<td><span>3</span> comment/s<a href="comments.html">view</a></td>
</tr>
</table>处理后仅保留真实数据行,模拟行被彻底移除,页面显示正常。
html
<table>
<tr>
<th>NAME</th>
<th>PRICE</th>
<th>IN STOCK</th>
<th>COMMENTS</th>
</tr>
<tr>
<td>Fresh Sweet Basil</td>
<td>4.99</td>
<td>yes</td>
<td>
<span>0</span> comment/s
</td>
</tr>
<tr class="odd">
<td>Italian Tomato</td>
<td>1.25</td>
<td>no</td>
<td>
<span>2</span> comment/s
<a href="/gtvg/product/comments?prodId=2">view</a>
</td>
</tr>
<tr>
<td>Yellow Bell Pepper</td>
<td>2.50</td>
<td>yes</td>
<td>
<span>0</span> comment/s
</td>
</tr>
<tr class="odd">
<td>Old Cheddar</td>
<td>18.75</td>
<td>yes</td>
<td>
<span>1</span> comment/s
<a href="/gtvg/product/comments?prodId=4">view</a>
</td>
</tr>
</table>上文中,属性all的作用是什么?th:remove 支持5种取值,对应不同的移除逻辑:
| 取值 | 行为描述 |
|---|---|
all | 移除包含标签 及其所有子元素(最常用,如上述模拟行的移除) |
body | 保留包含标签,但移除其所有子元素(标签本身保留,内容清空) |
tag | 移除包含标签,但保留其所有子元素(只删标签,内容保留) |
all-but-first | 移除包含标签的所有子元素,仅保留第一个子元素(批量移除模拟行更高效) |
none | 不执行任何移除操作(用于动态条件判断) |
all-but-first 这个值有什么用处呢?它能让我们在制作原型时,无需给每个模拟行添加th:remove="all" :
html
<table>
<thead>
<tr>
<th>NAME</th>
<th>PRICE</th>
<th>IN STOCK</th>
<th>COMMENTS</th>
</tr>
</thead>
<!-- 仅保留第一个子元素(真实数据行),移除其他模拟行 -->
<tbody th:remove="all-but-first">
<!-- 第一个子元素:真实数据行(保留) -->
<tr th:each="prod : ${prods}" th:class="${prodStat.odd}? 'odd'">
<td th:text="${prod.name}">Onions</td>
<td th:text="${prod.price}">2.41</td>
<td th:text="${prod.inStock}? #{true} : #{false}">yes</td>
<td>
<span th:text="${#lists.size(prod.comments)}">2</span> comment/s
<a href="comments.html"
th:href="@{/product/comments(prodId=${prod.id})}"
th:unless="${#lists.isEmpty(prod.comments)}">view</a>
</td>
</tr>
<!-- 后续子元素:模拟行(自动移除) -->
<tr class="odd">
<td>Blue Lettuce</td>
<td>9.55</td>
<td>no</td>
<td>
<span>0</span> comment/s
</td>
</tr>
<tr>
<td>Mild Cinnamon</td>
<td>1.99</td>
<td>yes</td>
<td>
<span>3</span> comment/s
<a href="comments.html">view</a>
</td>
</tr>
</tbody>
</table>th:remove 可接收任意 Thymeleaf 标准表达式,只要返回上述5种取值之一。
实现“条件满足才移除”:
html
<!-- 条件为 true 时移除标签(保留内容),false 时不操作 -->
<a href="/something" th:remove="${condition}? tag : none">Link text not to be removed</a>th:remove中,null 等价于 none,所以上面的代码也可以写成这样:
html
<!-- 简化写法:null 等价于 none,条件为 false 时返回 null,不移除 -->
<a href="/something" th:remove="${condition}? tag">Link text not to be removed</a>示例逻辑:${condition} 为 true → 移除 <a> 标签(保留链接文本);为 false → 不做任何移除。
典型场景总结
| 场景 | 推荐取值 | 效果 |
|---|---|---|
| 移除整行/整段模拟数据 | all | 彻底删除标签+内容 |
| 清空容器内容(保留容器标签) | body | 只删内容,保留标签 |
| 移除外层标签(保留内部内容) | tag | 只删标签,保留内容 |
| 批量保留首行、移除后续模拟行 | all-but-first | 高效批量处理模拟数据 |
| 动态控制是否移除 | 表达式(如 ${cond}? tag : none) | 条件化移除 |
总结
th:remove解决了“静态原型需要模拟数据,动态渲染需要移除模拟数据”的核心矛盾;- 核心取值:
all(全删)、body(删内容)、tag(删标签)、all-but-first(仅保留首个子元素)、none(不删); - 支持动态表达式,可根据条件控制移除行为,兼顾灵活性和静态原型的友好性。
