endokのブログ

IT・プログラミングネタ

Spring Boot触ってみる その3 ーThymeleaf Layout Dialectでレイアウトを共通化ー

Spring Boot触ってみる その2 ーController,テンプレートエンジン(Thymeleaf)ー - endokのブログ
の続き。

Thymeleafにもincludeなどを使った共通化の仕組みはあるが、
各ページごとにincludeは書きたくない・・・。
Tilesのように共通レイアウトを定義する仕組みはないものかと調べたところ、
Thymeleaf Layout Dialect(https://github.com/ultraq/thymeleaf-layout-dialect)というものがあったため試してみる。

その1(http://endok.hatenablog.com/entry/2016/06/04/124011)で作成したサンプルプロジェクトを引き続き利用する。

導入方法

SpringBootプロジェクトの場合、spring-boot-starter-thymeleafを依存関係に含めていればそのままで利用可能。

単独で導入する場合は下記を読み込んで、Springの設定が必要とのこと。
(GitHubのInstallation、Usageを参照)

GroupId: nz.net.ultraq.thymeleaf
ArtifactId: thymeleaf-layout-dialect

Decorators,fragmentsの利用

レイアウト中の一部分を個別ページの内容で上書きする

最初にイメージしていたのはこちらの使い方。
共通レイアウトと上書き可能な部分を定義しておいて、各ページで上書きしながらレンダリングするというもの。

layout.htmlをレイアウトファイル、page1.html、page2.htmlを個別ページのHTMLとする。

・layout.html

<!DOCTYPE html>
<html lang="ja" xmlns="http://www.w3.org/1999/xhtml"
	xmlns:th="http://www.thymeleaf.org"
	xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
<head>
<meta charset="UTF-8">
<title>Spring Boot Sample Site</title>
</head>
<body>
	<header layout:fragment="header">共通ヘッダ</header>

	<div layout:fragment="contents"></div>

	<footer layout:fragment="footer">共通フッタ</footer>
</body>
</html>

・page1.html

ページ1はコンテンツ部分のみ設定する。

<div layout:decorator="layout">
	<div layout:fragment="contents">ページコンテンツ1</div>
</div>

・page2.html

ページ2はヘッダ、コンテンツ、フッタを設定する。

<div layout:decorator="layout">
	<div layout:fragment="header">個別ページヘッダ</div>
	<div layout:fragment="contents">ページコンテンツ2</div>
	<div layout:fragment="footer">個別ページフッタ</div>
</div>

レイアウト側では、layout:fragment属性で上書き可能な部分を定義する。
個別ページ側では、layout:decorator属性で利用するレイアウトファイル名を指定し、layout:fragmentで上書き内容を定義する。

実行すると、下記のようになる。

・ページ1の実行結果
f:id:endok:20160611201745j:plain

・ページ2の実行結果
f:id:endok:20160611201806j:plain

個別ページ側で指定しなかった場合はレイアウトで定義したものが出力されていることがわかる。

なお、このように個別ページで一部分のHTMLだけ書くとIDE上でlayout:fragmentなどの追加要素が未定義として警告扱いになる。
これが気になる場合は、外側にDOCTYPE宣言やHTMLタグを書いて警告が出ないようにしても問題ない(外側に書いた要素は出力されない)。

レイアウトと個別ページの内容をマージして出力する

こちらが本来というか、標準?の使い方の様子。
個別ページの内容をレイアウトページの内容で"デコレート"して出力する。

layout2.htmlをレイアウトファイル、page3.htmlを個別ページのHTMLとする。

・layout2.html

<!DOCTYPE html>
<html lang="ja" xmlns="http://www.w3.org/1999/xhtml"
	xmlns:th="http://www.thymeleaf.org"
	xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
<head>
<meta charset="UTF-8">
<title>Spring Boot Sample Site</title>

<meta name="description" content="共通metaタグ">

<!-- 共通のJS,CSSを読み込み -->
<link rel="stylesheet" href="/css/common.css">
<script src="/js/common.js"></script>
</head>
<body>
	<header layout:fragment="header">共通ヘッダ</header>

	<div layout:fragment="contents"></div>

	<footer layout:fragment="footer">共通フッタ</footer>
</body>
</html>

・page3.html

<!DOCTYPE html>
<html xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
	layout:decorator="layout2">
<head>
<title>個別ページタイトル</title>

<meta name="description" content="個別metaタグ">

<!-- 個別ページ特有のhead要素 -->
<link rel="stylesheet" href="/css/page3.css">
<script src="/js/page3.js"></script>
</head>

<body>

	<div>この要素は無視される</div>

	<div layout:decorator="layout">
		<div layout:fragment="header">個別ページヘッダ</div>
		<div layout:fragment="contents">ページコンテンツ3</div>
		<div layout:fragment="footer">個別ページフッタ</div>
	</div>
</body>
</html>

今回は、個別ページ側にもheadタグやbodyタグを定義していて、layout:decoratorをhtmlタグに記載している。
出力されるHTMLは下記のようになる。

<!DOCTYPE html>

<html lang="ja" xmlns="http://www.w3.org/1999/xhtml">
<head>
	<title>個別ページタイトル</title>
<meta charset="UTF-8" />

<meta content="共通metaタグ" name="description" />

<!-- 共通のJS,CSSを読み込み -->
<link href="/css/common.css" rel="stylesheet" />
<script src="/js/common.js"></script>
	<meta content="個別metaタグ" name="description" />
	<!-- 個別ページ特有のhead要素 -->
	<link href="/css/page3.css" rel="stylesheet" />
	<script src="/js/page3.js"></script>
</head>
<body>
	<div>個別ページヘッダ</div>

	<div>ページコンテンツ3</div>

	<div>個別ページフッタ</div>

</body></html>
  • titleタグが個別ページの内容で上書きされている
  • その他headタグの内容がレイアウトの内容→個別ページ内容の順でマージ出力されている
  • 個別ページのbodyタグ内のdiv要素は出力されない

ことがわかる。

headタグ要素のソート、マージのルールは設定で変更可能。
"AppendingStrategy"と"GroupingStrategy"が用意されており、独自定義も可能である(デフォルトはAppdendingStrategy)。

SprintBootでGroupingStrategyを使う場合、下記のような設定を行う。
(TemplateResolverのセットは公式には記載なかったが、これを行わないと例外が発生した)

・AppConfig.java

package me.endok.springboot;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.thymeleaf.spring4.SpringTemplateEngine;
import org.thymeleaf.templateresolver.TemplateResolver;

import nz.net.ultraq.thymeleaf.LayoutDialect;
import nz.net.ultraq.thymeleaf.decorators.strategies.GroupingStrategy;

@Configuration
public class AppConfig {
	
	@Autowired
	private TemplateResolver defaultTemplateResolver;

	public void setDefaultTemplateResolver(TemplateResolver templateResolver) {
		defaultTemplateResolver = templateResolver;
	}
	
	@Bean
	GroupingStrategy groupingStrategy(){
		return new GroupingStrategy();
	}
	
	@Bean
	SpringTemplateEngine templateEngine(){
		SpringTemplateEngine templateEngine = new SpringTemplateEngine();
		templateEngine.setTemplateResolver(defaultTemplateResolver);
		templateEngine.addDialect(new LayoutDialect(this.groupingStrategy()));
		return templateEngine;
	}

}

GroupingStrategyを使った場合に出力されるHTMLは下記のようになった。

<!DOCTYPE html>

<html lang="ja" xmlns="http://www.w3.org/1999/xhtml">
<head>
	<title>個別ページタイトル</title>
<meta charset="UTF-8" />

<meta content="共通metaタグ" name="description" />
	<meta content="個別metaタグ" name="description" />

<!-- 共通のJS,CSSを読み込み -->
	<!-- 個別ページ特有のhead要素 -->
<link href="/css/common.css" rel="stylesheet" />
	<link href="/css/page3.css" rel="stylesheet" />
<script src="/js/common.js"></script>
	<script src="/js/page3.js"></script>
</head>
<body>
	<div>個別ページヘッダ</div>

	<div>ページコンテンツ3</div>

	<div>個別ページフッタ</div>

</body></html>

ちょっとコメントが変だが、GroupingStrategyの方が好みの出力である。

title-patternの利用

さきほどの例ではtitleタグが上書きされる挙動を確認したが、タイトルに共通文言とページごとの文言を組み合わせる機能も用意されている。
layout2.htmlのタイトル部分を下記のように変更する。
個別ページ側は変更しない。

・layout2.html

<title layout:title-pattern="$DECORATOR_TITLE - $CONTENT_TITLE">サイト名</title>

このようにすると、$DECORATOR_TITLEにレイアウトページで定義したタイトルが、$CONTENT_TITLEに個別ページで定義したタイトルが置換される。
今回の例では下記のようになる。

	<title>サイト名 - 個別ページタイトル</title>

まとめ

やりたいことは問題なくできそうだった。
単純にテキストとして当て込むわけでなく、タグを解釈して処理しているというのは驚きだった。
Thymeleafではmodeをもとにタグの構文チェックもしているようなので、全体的にそういった考え方なのだろう。