介绍
通常网站共享常见的页面组件,如页眉、页脚、菜单等等。这些页面组件可以由相同或不同的布局使用。项目中的组织布局有两种主要样式:包含样式和分层样式。这两种风格都可以很容易地与百里香叶一起使用,而不会失去其最大的价值:自然模板。
包含样式布局
在此样式中,页面是通过直接在每个视图中嵌入公共页面组件代码来生成最终结果来构建的。在百里香叶中,这可以使用百里香叶标准布局系统来完成:
<body>
<div th:insert="footer :: copy">...</div>
</body>
包含样式布局非常易于理解和实现,实际上它们在开发视图时提供了灵活性,这是它们最大的优势。但是,此解决方案的主要缺点是引入了一些代码重复,因此在大型应用程序中修改大量视图的布局可能会变得有点麻烦。
分层样式布局
在分层样式中,模板通常以父子关系创建,从更一般的部分(布局)到最具体的部分(子视图;例如页面内容)。模板的每个组件都可以基于模板片段的包含和替换来动态包含。在百里香叶中,这可以使用百里香叶布局方言来完成。
此解决方案的主要优点是重用视图的原子部分和模块化设计,而主要缺点是需要更多的配置才能使用它们,因此视图的复杂性大于使用更“自然”的包含样式布局。
示例应用程序
本文中提供的所有示例和代码片段都可以在 GitHub 上找到,网址为GitHub - thymeleaf/thymeleafexamples-layouts: Layouts - Companion application for the "Thymeleaf Layouts" article at the Thymeleaf website: http://www.thymeleaf.org/documentation.html http://www.thymeleaf.org
百里香叶标准布局系统
Thymeleaf标准布局系统提供类似于JSP包含的页面片段包含,并对其进行了一些重要的改进。
基本包含与和th:insert
th:replace
Thymeleaf 可以使用(它将简单地插入指定的片段作为其主机标签的正文)或(实际上将主机标签替换为片段的主机标签)将其他页面的一部分作为片段(而 JSP 只包含完整的页面)。这允许将片段分组到一个或多个页面中。看例子。th:insert
th:replace
当匿名用户进入我们应用程序的主页时,模板将呈现。home/homeNotSignedIn.html
类thymeleafexamples.layouts.home.HomeController
@Controller
class HomeController {
@GetMapping("/")
String index(Principal principal) {
return principal != null ? "home/homeSignedIn" : "home/homeNotSignedIn";
}
}
模板home/homeNotSignedIn.html
<!DOCTYPE html>
<html>
<head>
...
</head>
<body>
...
<div th:replace="fragments/header :: header">
<!-- ============================================================================ -->
<!-- This content is only used for static prototyping purposes (natural templates)-->
<!-- and is therefore entirely optional, as this markup fragment will be included -->
<!-- from "fragments/header.html" at runtime. -->
<!-- ============================================================================ -->
<div class="navbar navbar-inverse navbar-fixed-top">
<div class="container">
<div class="navbar-header">
<a class="navbar-brand" href="#">Static header</a>
</div>
<div class="navbar-collapse collapse">
<ul class="nav navbar-nav">
<li class="active"><a href="#">Home</a></li>
</ul>
</div>
</div>
</div>
</div>
<div class="container">
<div class="hero-unit">
<h1>Test</h1>
<p>
Welcome to the Spring MVC Quickstart application!
Get started quickly by signing up.
</p>
<p>
<a href="/signup" th:href="@{/signup}" class="btn btn-large btn-success">Sign up</a>
</p>
</div>
<div th:replace="fragments/footer :: footer">© 2016 The Static Templates</div>
</div>
...
</body>
</html>
您可以直接在浏览器中打开文件:
未登录时的主页
在上面的示例中,我们正在构建一个由页眉和页脚组成的页面。在Thymeleaf中,所有片段都可以在单个文件(例如)或单独的文件中定义,就像在这种特殊情况下一样。fragments.html
让我们简单分析一下包含语句:
<div th:replace="fragments/header :: header">...</div>
语句的第一部分是我们正在引用的模板名称。这可以是一个文件(如本例所示),也可以通过使用关键字(例如)或没有任何关键字(例如)来引用同一文件。双冒号后面的表达式是片段选择器(片段名称或标记选择器)。如您所见,标头片段包含一个仅用于静态原型的标记。fragments/header
this
this :: header
:: header
页眉和页脚在以下文件中定义:
模板fragments/header.html
<!DOCTYPE html>
<html>
<head>
...
</head>
<body>
<div class="navbar navbar-inverse navbar-fixed-top" th:fragment="header">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".nav-collapse">
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="#">My project</a>
</div>
<div class="navbar-collapse collapse">
<ul class="nav navbar-nav">
<li th:classappend="${module == 'home' ? 'active' : ''}">
<a href="#" th:href="@{/}">Home</a>
</li>
<li th:classappend="${module == 'tasks' ? 'active' : ''}">
<a href="#" th:href="@{/task}">Tasks</a>
</li>
</ul>
<ul class="nav navbar-nav navbar-right">
<li th:if="${#authorization.expression('!isAuthenticated()')}">
<a href="/signin" th:href="@{/signin}">
<span class="glyphicon glyphicon-log-in" aria-hidden="true"></span> Sign in
</a>
</li>
<li th:if="${#authorization.expression('isAuthenticated()')}">
<a href="/logout" th:href="@{#}" onclick="$('#form').submit();">
<span class="glyphicon glyphicon-log-out" aria-hidden="true"></span> Logout
</a>
<form style="visibility: hidden" id="form" method="post" action="#" th:action="@{/logout}"></form>
</li>
</ul>
</div>
</div>
</div>
</body>
</html>
...我们可以直接在浏览器中打开:
页眉页
和模板fragments/footer.html
<!DOCTYPE html>
<html>
<head>
...
</head>
<body>
<div th:fragment="footer">
© 2016 Footer
</div>
</body>
</html>
请注意引用的片段是如何指定属性的。这样,我们可以在一个模板文件中定义多个片段,如前所述。th:fragment
这里重要的是,所有模板仍然可以是自然模板,并且可以在没有运行服务器的浏览器中查看。
包括标记选择器
在 Thymeleaf 中,片段不需要在提取它们的页面上显式指定。Thymeleaf可以通过其标记选择器语法选择页面的任意部分作为片段(甚至是存在于外部服务器上的页面),类似于XPath表达式,CSS或jQuery选择器。th:fragment
<div th:insert="https://www.thymeleaf.org :: section.description" >...</div>
上面的代码将包括 awithfrom。section
class="description"
thymeleaf.org
为了实现这一点,模板引擎必须配置:UrlTemplateResolver
@Bean
public SpringTemplateEngine templateEngine() {
SpringTemplateEngine templateEngine = new SpringTemplateEngine();
templateEngine.addTemplateResolver(new UrlTemplateResolver());
...
return templateEngine;
}
有关标记选择器语法参考,请查看 Thymeleaf 文档中的此部分:标记选择器语法。
使用表达式
在中,两者都可以是功能齐全的表达式。在下面的示例中,我们希望根据条件包含不同的片段。如果经过身份验证的用户是管理员,我们将显示与普通用户不同的页脚:templatename :: selector
templatename
selector
<div th:replace="fragments/footer :: ${#authentication.principal.isAdmin()} ? 'footer-admin' : 'footer'">
© 2016 The Static Templates
</div>
fragments/footer.html
略有变化,因为我们需要定义两个页脚:
<!DOCTYPE html>
<html>
<head>
...
</head>
<body>
<!-- /* Multiple fragments may be defined in one file */-->
<div th:fragment="footer">
© 2016 Footer
</div>
<div th:fragment="footer-admin">
© 2016 Admin Footer
</div>
</body>
</html>
参数化包含
片段可以指定参数,就像方法一样。每当使用 aattribute 显式指定它们时,它们都可以提供一个参数签名,然后可以使用调用或属性中的参数填充该签名。th:fragment
th:insert
th:replace
例子最能说明问题。我们可以在许多上下文中使用参数化包含,但一个现实生活中的上下文是在成功提交表单后在应用程序的不同页面上显示消息。让我们看一下应用程序中的注册过程:
@PostMapping("signup")
String signup(@Valid @ModelAttribute SignupForm signupForm,
Errors errors, RedirectAttributes ra) {
if (errors.hasErrors()) {
return SIGNUP_VIEW_NAME;
}
Account account = accountRepository.save(signupForm.createAccount());
userService.signin(account);
// see /WEB-INF/i18n/messages.properties and /WEB-INF/views/homeSignedIn.html
MessageHelper.addSuccessAttribute(ra, "signup.success");
return "redirect:/";
}
如您所见,成功注册后,用户将被重定向到主页,并填写了 flash 属性。我们想要创建一个可重用和参数化的片段。这可以按如下方式完成:
<!DOCTYPE html>
<html>
<head>
...
</head>
<body>
<div class="alert alert-dismissable" th:fragment="alert (type, message)" th:assert="${!#strings.isEmpty(type) and !#strings.isEmpty(message)}" th:classappend="'alert-' + ${type}">
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">×</button>
<span th:text="${message}">Test</span>
</div>
</body>
</html>
上面的片段有两个参数:和。这是用于设置消息样式的消息类型,而是将向用户显示的文本。我们使用 aattribute 确保参数存在且不为空。alert
type
message
type
message
th:assert
为了包含在任何模板中,我们可以编写以下代码(请注意,变量的值可以是表达式):alert
<div th:replace="fragments/alert :: alert (type='danger', message=${errorMessage})">...</div>
参数化片段允许开发人员创建更易于重用的类似函数的片段。在 Thymeleaf 文档中阅读有关参数化片段的更多信息:可参数化的片段签名。
片段表达式
Thymeleaf 3.0引入了一种新的表达类型,作为通用Thymeleaf标准表达系统的一部分:片段表达:
<div th:insert="~{fragments/footer :: footer}">...</div>
此语法的思想是能够将解析的片段用作模板执行上下文中任何其他类型的对象以供以后使用:
<div th:replace="${#authentication.principal.isAdmin()} ? ~{fragments/footer :: footer-admin} : ~{fragments/footer :: footer-admin}">
© 2016 The Static Templates
</div>
片段表达式允许创建片段,以便可以使用来自调用模板的标记来丰富片段,从而产生比 andonly 更灵活的布局机制。th:insert
th:replace
灵活的布局示例
该文件定义了调用模板将使用的所有片段。下面的片段采用参数,该参数将用其解析值替换标记:task/layout.html
header
breadcrumb
ol
<!--/* Header fragment */-->
<div th:fragment="header(breadcrumb)">
<ol class="breadcrumb container" th:replace="${breadcrumb}">
<li><a href="#">Home</a></li>
</ol>
</div>
在调用模板 () 中,我们将使用标记选择器语法来传递元素匹配选择器:task/task-list.html
.breadcrumb
<!--/* The markup with breadcrumb class will be passed to the header fragment */-->
<header th:insert="task/layout :: header(~{ :: .breadcrumb})">
<ol class="breadcrumb container">
<li><a href="#">Home</a></li>
<li><a href="#" th:href="@{/task}">Tasks</a></li>
</ol>
</header>
因此,将为视图生成以下 HTML:task/taks-list
<header>
<div>
<ol class="breadcrumb container">
<li><a href="#">Home</a></li>
<li><a href="[...]">Tasks</a></li>
</ol>
</div>
</header>
类似地,我们可以在另一个视图中使用相同的片段和不同的痕迹导航():task/task.html
<header th:insert="task/layout :: header(~{ :: .breadcrumb})">
<ol class="breadcrumb container">
<li><a href="#">Home</a></li>
<li><a href="#" th:href="@{/task}">Tasks</a></li>
<li th:text="${'Task ' + task.id}">Task</li>
</ol>
</header>
如果没有什么要传递给片段的,我们可以使用一个特殊的空片段表达式 -。它将传递一个空值,该值将在片段中忽略:~{}
header
<header th:insert="task/layout :: header(~{})">
</header>
新片段表达式的另一个功能是所谓的无操作令牌,它允许在需要时使用片段的默认标记:
<header th:insert="task/layout :: header(_)">
</header>
结果,我们将得到:
<header>
<ol class="breadcrumb container">
<li><a href="#">Home</a></li>
</ol>
</header>
片段表达式支持以迄今为止只能使用第三方布局方言的方式自定义片段。在 Thymeleaf 文档中阅读有关此主题的更多信息:灵活的布局:不仅仅是片段插入
来自春天的片段包含@Controller
片段可以直接从Spring MVC控制器指定,即;这对于仅向浏览器返回一小部分 HTML 的 AJAX 控制器非常有用。在下面的示例中,注册表单片段将在 AJAX 请求和整个注册视图时加载 - 在常规请求时:signup :: signupForm
@RequestMapping(value = "signup")
public String signup(Model model,
@RequestHeader("X-Requested-With") String requestedWith) {
model.addAttribute(new SignupForm());
if (AjaxUtils.isAjaxRequest(requestedWith)) {
return SIGNUP_VIEW_NAME.concat(" :: signupForm");
}
return SIGNUP_VIEW_NAME;
}
片段的定义如下:signup/signup.html
<!DOCTYPE html>
<html>
<head>
...
</head>
<body>
<form method="post"
th:action="@{/signup}" th:object="${signupForm}" th:fragment="signupForm">
...
</form>
</body>
</html>
当新用户想要从主页注册时,将加载上述片段。单击按钮时将显示模式对话框,内容将通过 AJAX 调用加载(请参阅)。Signup
home/homeNotSignedIn.html
引用
请查看非常彻底地描述此主题的百里香叶文档。
百里酚
当将 Thymeleaf 模板用作静态原型时,我们无法看到我们使用主机标签包含的片段。我们只能看到碎片放在一边,打开自己的模板文档。th:insert/th:replace
但是,有一种方法可以在原型制作时查看包含在我们页面中的真实片段。这可以使用Thymol来完成,这是一个非官方的JavaScript库,它是Thymeleaf标准片段包含功能的实现,为一些Thymeleaf属性提供静态支持,如or,条件显示with/等。th:insert
th:replace
th:if
th:unless
正如Thymol的作者所说:创建Thymol是为了通过静态可访问的JavaScript库提供对Thymeleaf属性的支持,从而为Thymeleaf的动态模板功能提供更准确的静态表示。
百里酚文档和示例可以在官方项目网站上找到:Thymol。
百里香叶布局方言
布局方言使人们可以使用分层方法,但从仅百里香叶的角度来看,不需要使用外部库,如Apache Tiles。Thymeleaf布局方言使用布局/装饰器模板来设置内容的样式,并且可以将整个片段元素传递到包含的页面。这个库的概念类似于带有Facelets的SiteMesh或JSF。
配置
要开始使用布局方言,我们需要将其包含在其中。依赖项为:pom.xml
<dependency>
<groupId>nz.net.ultraq.thymeleaf</groupId>
<artifactId>thymeleaf-layout-dialect</artifactId>
<version>2.0.5</version>
</dependency>
我们还需要通过向模板引擎添加其他方言来配置集成:
@Bean
public SpringTemplateEngine templateEngine() {
SpringTemplateEngine templateEngine = new SpringTemplateEngine();
...
templateEngine.addDialect(new LayoutDialect());
return templateEngine;
}
无需进行其他更改。
创建布局
布局文件在以下位置定义:/WEB-INF/views/task/layout.html
<!DOCTYPE html>
<html>
<head>
<!--/* Each token will be replaced by their respective titles in the resulting page. */-->
<title layout:title-pattern="$LAYOUT_TITLE - $CONTENT_TITLE">Task List</title>
...
</head>
<body>
<!--/* Standard layout can be mixed with Layout Dialect */-->
<div th:replace="fragments/header :: header">
...
</div>
<div class="container">
<div layout:fragment="content">
<!-- ============================================================================ -->
<!-- This content is only used for static prototyping purposes (natural templates)-->
<!-- and is therefore entirely optional, as this markup fragment will be included -->
<!-- from "fragments/header.html" at runtime. -->
<!-- ============================================================================ -->
<h1>Static content for prototyping purposes only</h1>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
Praesent scelerisque neque neque, ac elementum quam dignissim interdum.
Phasellus et placerat elit. Lorem ipsum dolor sit amet, consectetur adipiscing elit.
Praesent scelerisque neque neque, ac elementum quam dignissim interdum.
Phasellus et placerat elit.
</p>
</div>
<div th:replace="fragments/footer :: footer">© 2014 The Static Templates</div>
</div>
</body>
</html>
我们可以直接在浏览器中打开文件:
Layout page
上面的文件是我们将在应用程序中创建的内容页面的装饰器。关于上面的例子,最重要的是。这是装饰器页面(布局)的核心。您还可以注意到,页眉和页脚是使用标准百里香叶布局系统包含的。layout:fragment="content"
内容页面如下所示 ():WEB-INF/views/task/list.html
<!DOCTYPE html>
<html layout:decorate="~{task/layout}">
<head>
<title>Task List</title>
...
</head>
<body>
<!-- /* Content of this page will be decorated by the elements of layout.html (task/layout) */ -->
<div layout:fragment="content">
<table class="table table-bordered table-striped">
<thead>
<tr>
<td>ID</td>
<td>Title</td>
<td>Text</td>
<td>Due to</td>
</tr>
</thead>
<tbody>
<tr th:if="${tasks.empty}">
<td colspan="4">No tasks</td>
</tr>
<tr th:each="task : ${tasks}">
<td th:text="${task.id}">1</td>
<td><a href="view.html" th:href="@{'/' + ${task.id}}" th:text="${task.title}">Title ...</a></td>
<td th:text="${task.text}">Text ...</td>
<td th:text="${#calendars.format(task.dueTo)}">July 11, 2012 2:17:16 PM CDT</td>
</tr>
</tbody>
</table>
</div>
</body>
</html>
在浏览器中,它看起来像这样:
布局页
此视图的内容将由视图元素装饰。请注意属性元素。此属性向布局方言发出信号,指示应使用哪种布局来装饰给定视图。请注意,它使用的是百里香叶片段表达语法。task/list
task/layout
layout:decorate="~{task/layout}"
<html>
那么使用布局方言的自然模板呢?再次,可能!您只需要在模板中包含的片段周围添加一些仅原型标记即可!
包括布局方言的样式方法
布局方言不仅支持分层方法,它还提供了一种以包含样式的方式使用它的方法()。与标准百里香叶包含相比,使用布局方言,您可以将HTML元素传递到包含的页面。如果您有一些想要重用的 HTML,但其内容太复杂而无法通过标准百里香叶方言中的参数化包含来传递,则很有用。layout:include
这是使用 () 的可重用警报片段的示例:layout:fragment
task/alert.html
<!DOCTYPE html>
<html>
<body>
<th:block layout:fragment="alert-content">
<p>Duis mollis, est non commodo luctus, nisi erat porttitor ligula...</p>
<p>
<button type="button" class="btn btn-danger">Take this action</button>
<button type="button" class="btn btn-default">Or do this</button>
</p>
</th:block>
</body>
</html>
上述片段的调用可能如下所示():task/list.html
<div layout:insert="~{task/alert :: alert}" th:with="type='info', header='Info'" th:remove="tag">
<!--/* Implements alert content fragment with simple content */-->
<th:block layout:fragment="alert-content">
<p><em>This is a simple list of tasks!</em></p>
</th:block>
</div>
或
<div layout:insert="~{task/alert :: alert}" th:with="type='danger', header='Oh snap! You got an error!'" th:remove="tag">
<!--/* Implements alert content fragment with full-blown HTML content */-->
<th:block layout:fragment="alert-content">
<p>Duis mollis, est non commodo luctus, nisi erat porttitor ligula...</p>
<p>
<button type="button" class="btn btn-danger">Take this action</button>
<button type="button" class="btn btn-default">Or do this</button>
</p>
</th:block>
</div>
在这种情况下,整个 () 模板将被上面的自定义 HTML 替换。alert-content
task/alert
/WEB-INF/views/task/alert.html
引用
请查看布局方言文档,其中非常详细地描述了这个主题。您最终会找到一些比本文更高级的示例。
您可以在此处找到文档:布局方言。
其他布局选项
对于一些开发人员来说,之前介绍的解决方案都不够。百里香叶标准布局系统是不够的,使用外部库不是一种选择。在这种情况下,自定义解决方案可能是要走的路。
百里香叶自定义布局
这篇博文中很好地描述了其中一种解决方案:Spring MVC 应用程序中没有扩展的 Thymeleaf 模板布局。这个解决方案的想法非常简单。让我们用一个例子来可视化:
示例视图文件 (1):
<!DOCTYPE html>
<html>
<head>
...
</head>
<body>
<div class="container" th:fragment="content">
<p>
Hello <span th:text="${#authentication.name}">User</span>!
Welcome to the Spring MVC Quickstart application!
</p>
</div>
</body>
</html>
和布局文件 (2):
<!DOCTYPE html>
<html>
<head>
...
</head>
<body>
<div th:replace="fragments/header :: header">Header</div>
<div th:replace="${view} :: content">Page Content</div>
<div th:replace="fragments/footer :: footer">Footer</div>
</body>
</html>
会发生什么?
- 控制器返回视图名称,这些名称转换为单个 Thymeleaf 视图文件 (1)
- 在呈现视图之前,原始属性对象将替换为布局视图的名称,并且原始属性成为属性。
viewName
ModelAndView
viewName
ModelAndView
- 布局视图 (2) 包含多个包含元素:
<div th:replace="${view} :: content">Page Content</div>
- 实际视图文件包含片段,由嵌入实际视图的模板拉取
该项目可以在GitHub上找到。
总结
在本文中,我们描述了许多实现相同方法的方法:布局。您可以使用基于包含样式方法的百里香叶标准布局系统构建布局。您还具有强大的布局方言,它使用装饰器模式来处理布局文件。最后,您可以轻松创建自己的解决方案。
希望本文能为您提供有关该主题的更多见解,并且您会根据需要找到首选方法。