Thymeleaf在 标签上使用th:fragment,会移除 ,从而使浏览器进入怪异模式
我在使用Spring Boot搭配Thymeleaf,并且有一个所有页面共用的布局模板。布局定义如下:
<!DOCTYPE html>
<html lang="en"
xmlns:th="http://www.thymeleaf.org"
th:fragment="layout (content, pageTitle)">
<head>...</head>
<body>
<th:block th:replace="${content}"></th:block>
</body>
</html>
我的页面引用它的方式如下:
<html th:replace="~{fragments/layout :: layout(content = ~{::content}, pageTitle = 'MY PAGE')}">
<body>
<th:block th:fragment="content">
<div class="container-fluid">...</div>
</th:block>
</body>
</html>
在浏览器查看页面源代码时,<!DOCTYPE html> 声明在渲染输出中缺失。页面直接以 <html> 开头。这会导致浏览器进入怪异模式。
为什么DOCTYPE会被移除,以及如何修复?
解决方案
为何会这样
这是Thymeleaf处理片段时的已知限制。当Thymeleaf渲染 th:fragment 时,它会将其视为一个文档节点,而不是一个完整的文档。<!DOCTYPE html> 声明并不是一个元素,它位于元素树之外,因此在提取并渲染片段时,DOCTYPE会被悄无声息地丢失。
在 <html> 标签上放置 th:fragment 的做法在教程和Stack Overflow的答案中被广泛展示,但没有人提到这个后果。使用这种模式的人在浏览器中会悄悄进入怪异模式。
怪异模式是一种浏览器兼容性模式,最初用于在现代HTML/CSS标准出现之前构建的旧网站。没有DOCTYPE时,浏览器会假设页面很旧,并改变某些CSS属性的表现,尤其是box-sizing(盒子模型尺寸计算方式)和布局。
在实际应用中,大多数页面仍然能正确渲染,因为现代浏览器较为宽容,但这在技术上是错误的,可能引发微妙且难以诊断的布局错误,尤其是在使用像Bootstrap这样的框架且它假设处于标准模式时。
变通办法——无需额外依赖
不要让页面模板通过 th:replace 调用布局,而是颠倒关系。让控制器直接返回布局,并将内容页作为模型属性传入:
@GetMapping("/mypage")
public String region(Model model) {
model.addAttribute("contentPage", "pages/mypage");
model.addAttribute("pageTitle", "MY PAGE");
return "fragments/layout";
}
在 layout.html 中,将硬编码的 ${content} 注入替换为:
<th:block th:replace="~{${contentPage} :: content}"></th:block>
并简化你的页面模板——不再在 <html> 上使用 th:replace:
<html xmlns:th="http://www.thymeleaf.org">
<body>
<th:block th:fragment="content">
<div class="container-fluid">...</div>
</th:block>
</body>
</html>
由于 layout.html 现在是正在渲染的实际模板,而不是一个片段,Thymeleaf将其输出为完整文档,DOCTYPE将被保留。
替代方案——Thymeleaf Layout Dialect
值得注意的是,Thymeleaf内置的片段系统并非为完整的页面布局设计。片段应是可复用的HTML局部块,而不是布局包装器。将 th:fragment 放在 <html> 标签上几乎能工作,但会悄悄丢失DOCTYPE,这是把它们用于并非为此设计的用途所带来的副作用。
社区意识到这个空缺, ultraq 创建了 Thymeleaf Layout Dialect,这是为此用例专门构建的一个小型依赖。它能够正确处理DOCTYPE,并提供更简洁的语法:
添加依赖项(Maven):
<dependency>
<groupId>nz.net.ultraq.thymeleaf</groupId>
<artifactId>thymeleaf-layout-dialect</artifactId>
</dependency>
注:截至本文撰写时,Thymeleaf Layout Dialect的依赖项存在已知漏洞。在将其用于生产项目之前,请先核对当前状态。
你的页面模板将变成:
<html xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{fragments/layout}">
<body>
<div layout:fragment="content">
<div class="container-fluid">...</div>
</div>
</body>
</html>
并在 layout.html 中标记内容区域:
<div layout:fragment="content"></div>
控制器将恢复正常返回页面模板,DOCTYPE将被正确保留。