thymeleaf的eclipse插件:https://github.com/thymeleaf/thymeleaf-extras-eclipse-plugin
頁面布局分為包含布局和層次布局,包含布局,一般通過th:include以及th:replace來實現,include和replace不一樣的是一個包含在host tag里面,一個是指替換host tag,thymeleaf的包含布局和jsp的include不同的方面在于,thymeleaf可以包含某個文件的某一個部分,而jsp的必須包含整個文件。比如:<div th:replace="fragments/header :: header">...</div>,fragments/header是指被包含的模板文件,::header的header指被包含模板文件中的被包含部分??梢杂胻his:header或者::header都是指包含本頁面的部分。被包含的文件的被包含部分需要加上屬性:th:fragment="header"
thymeleaf可以基于dom selector來處理包含,而不用顯示地調用th:fragment,比如:<div th:include="http://www.thymeleaf.org :: p.notice" >...</div>,那么將會調用tag p且.class="notice“的片段,這個最大的好處就是包含別的網站的網頁部分。以前的做法有用ajax的,有用iframe的,還有用javabean獲取后傳給前端的。thymeleaf這種處理方式相對合理。采用dom這種方式,需要templateEngine.addTemplateResolver(urlTemplateResolver());
包含語法的模板文件和片段都可以通過表達式來指定,比如<div th:replace="fragments/footer :: ${#authentication.principal.isAdmin()} ? 'footer-admin' : 'footer'">。
thymeleaf包含模板也支持參數包含,比如:
<div th:fragment="alert (type, message)"
class="alert alert-dismissable" th:classappend="'alert-' + ${type}">
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">×</button>
<span th:text="${message}">Test</span>
</div>
表示alert這個片段有兩個參數:type和message,那么調用的時候:class="alert alert-dismissable" th:classappend="'alert-' + ${type}">
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">×</button>
<span th:text="${message}">Test</span>
</div>
<div th:replace="fragments/alert :: alert (type='danger', message=${errorMessage})">...</div>
參數化片段提高片段的可重用性.題外話,我當年特喜歡infoglue的設計理念,事過好多年,依稀記得slot和param binding兩個讓我一直很喜歡的理念,現在的thymeleaf都可以做到。繼續...
能夠從spring controller返回片段,比如:
if (AjaxUtils.isAjaxRequest(requestedWith)) {
return SIGNUP_VIEW_NAME.concat(" :: signupForm");
}
return SIGNUP_VIEW_NAME;
當用ajax請求的時候,后端返回的視圖為片段的內容。return SIGNUP_VIEW_NAME.concat(" :: signupForm");
}
return SIGNUP_VIEW_NAME;
包含布局,由于是在每個頁面包含公共代碼,因此natural特性沒有影響,不過如果一旦需要切換包含另外的公共部分或者改變統一頁面布局模式,那么包含布局就顯得力不從心。層次布局,目前流行的有Tiles和sitemesh,一般是將布局等公用部分放在parent里面,顯示時將每個子頁面的具體內容融合到parent里面來對外展現,優點是更好的維護性,缺點是natural不夠。
本次信息發布內核采用層次布局模式。兩層模板展現,父級模板負責布局展現,子級模板負責內容展現。
針對spring mvc和thymeleaf做一下擴展:
1、定義注釋:layout注釋可以用在類和方法上。
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Layout {
String value() default "";
}
2、定義interceptor:
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Layout {
String value() default "";
}
2、定義interceptor:
public class ThymeleafLayoutInterceptor extends HandlerInterceptorAdapter {
private static final String DEFAULT_LAYOUT = "layouts/default";
private static final String DEFAULT_VIEW_ATTRIBUTE_NAME = "view";
private String defaultLayout = DEFAULT_LAYOUT;
private String viewAttributeName = DEFAULT_VIEW_ATTRIBUTE_NAME;
public void setDefaultLayout(String defaultLayout) {
Assert.hasLength(defaultLayout);
this.defaultLayout = defaultLayout;
}
public void setViewAttributeName(String viewAttributeName) {
Assert.hasLength(defaultLayout);
this.viewAttributeName = viewAttributeName;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
if (!modelAndView.hasView()) {
return;
}
String originalViewName = modelAndView.getViewName();
if (isRedirectOrForward(originalViewName)) {
return;
}
String layoutName = getLayoutName(handler);
modelAndView.setViewName(layoutName);
modelAndView.addObject(this.viewAttributeName, originalViewName);
}
private boolean isRedirectOrForward(String viewName) {
return viewName.startsWith("redirect:") || viewName.startsWith("forward:");
}
private String getLayoutName(Object handler) {
HandlerMethod handlerMethod = (HandlerMethod) handler;
Layout layout = getMethodOrTypeAnnotation(handlerMethod);
if (layout == null) {
return this.defaultLayout;
} else {
return layout.value();
}
}
private Layout getMethodOrTypeAnnotation(HandlerMethod handlerMethod) {
Layout layout = handlerMethod.getMethodAnnotation(Layout.class);
if (layout == null) {
return handlerMethod.getBeanType().getAnnotation(Layout.class);
}
return layout;
}
}
3:配置interceptor:
4:測試類:
6:測試內容模板頁面:
private static final String DEFAULT_LAYOUT = "layouts/default";
private static final String DEFAULT_VIEW_ATTRIBUTE_NAME = "view";
private String defaultLayout = DEFAULT_LAYOUT;
private String viewAttributeName = DEFAULT_VIEW_ATTRIBUTE_NAME;
public void setDefaultLayout(String defaultLayout) {
Assert.hasLength(defaultLayout);
this.defaultLayout = defaultLayout;
}
public void setViewAttributeName(String viewAttributeName) {
Assert.hasLength(defaultLayout);
this.viewAttributeName = viewAttributeName;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
if (!modelAndView.hasView()) {
return;
}
String originalViewName = modelAndView.getViewName();
if (isRedirectOrForward(originalViewName)) {
return;
}
String layoutName = getLayoutName(handler);
modelAndView.setViewName(layoutName);
modelAndView.addObject(this.viewAttributeName, originalViewName);
}
private boolean isRedirectOrForward(String viewName) {
return viewName.startsWith("redirect:") || viewName.startsWith("forward:");
}
private String getLayoutName(Object handler) {
HandlerMethod handlerMethod = (HandlerMethod) handler;
Layout layout = getMethodOrTypeAnnotation(handlerMethod);
if (layout == null) {
return this.defaultLayout;
} else {
return layout.value();
}
}
private Layout getMethodOrTypeAnnotation(HandlerMethod handlerMethod) {
Layout layout = handlerMethod.getMethodAnnotation(Layout.class);
if (layout == null) {
return handlerMethod.getBeanType().getAnnotation(Layout.class);
}
return layout;
}
}
3:配置interceptor:
@Configuration
public class WebMvcConfig extends WebMvcConfigurationSupport {
@Override
protected void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new ThymeleafLayoutInterceptor());
}
}
public class WebMvcConfig extends WebMvcConfigurationSupport {
@Override
protected void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new ThymeleafLayoutInterceptor());
}
}
4:測試類:
@Controller
class SigninController {
@Layout(value = "layouts/blank")
@RequestMapping(value = "signin")
String signin() {
return "signin/signin";
}
}
5:測試布局模板頁面:class SigninController {
@Layout(value = "layouts/blank")
@RequestMapping(value = "signin")
String signin() {
return "signin/signin";
}
}
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<head>...</head>
<body>
<div th:raplace="fragments/header :: header">
Header
</div>
<div th:replace="${view} :: content">
Content
</div>
<div th:replace="fragments/footer :: footer">
Footer
</div>
</body>
</html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<head>...</head>
<body>
<div th:raplace="fragments/header :: header">
Header
</div>
<div th:replace="${view} :: content">
Content
</div>
<div th:replace="fragments/footer :: footer">
Footer
</div>
</body>
</html>
6:測試內容模板頁面:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<head>...</head>
<body>
<div class="container" th:fragment="content">
<!-- /* Handle the flash message */-->
<th:block th:if="${message != null}">
<div th:replace="fragments/alert :: alert (type=${#strings.toLowerCase(message.type)}, message=${message.message})"> </div>
</th:block>
<p>
Hello <span th:text="${#authentication.name}">User</span>!
Welcome to the Spring MVC Quickstart application!
</p>
</div>
</body>
</html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<head>...</head>
<body>
<div class="container" th:fragment="content">
<!-- /* Handle the flash message */-->
<th:block th:if="${message != null}">
<div th:replace="fragments/alert :: alert (type=${#strings.toLowerCase(message.type)}, message=${message.message})"> </div>
</th:block>
<p>
Hello <span th:text="${#authentication.name}">User</span>!
Welcome to the Spring MVC Quickstart application!
</p>
</div>
</body>
</html>