この記事ではThymeleafでファイルを分割して管理する方法を説明します。Spring Boot入門:Thymeleafの基本の続きです。
この記事のソースコード
この記事のソースコードはGithubに公開しています。
GithubからSpring BootプロジェクトをEclipseにインポートする方法は次の記事を参考にしてください。
Spring Bootプロジェクトを作成
Spring Bootプロジェクトを作成します。
これまでと同じなので、画像も不要でしょう。プロジェクト名やパッケージ名などに適当なものを入力してください。(この記事ではパッケージがblog.tsuchiya.tutorial.step3である前提です)
同じく、依存関係も前回と同じです。Spring Boot DevTools、Lombok、Thymeleaf、Spring Webを選択してください。
ここから先はこの記事独自の設定です。
プロジェクトを作ると直下のフォルダにpom.xmlというファイルが作られます。
今回使うlayout:decorateやlayout:fragmentという機能のために、このpom.xmlを編集する必要があります。
<dependencies>というタグの一番最後に、以下のタグを追加してください。
<dependency>
<groupId>nz.net.ultraq.thymeleaf</groupId>
<artifactId>thymeleaf-layout-dialect</artifactId>
</dependency>
次の画像みたいになります。
ソースコードをダウンロードしないで手打ちする人は、この作業を忘れないようにしてください。
この記事で使うソースコード概略
Controller
今回はThymeleafの説明なので、Controllerはほとんど何もしません。Modelに適当な値を放り込んでいるだけです。説明も必要ないでしょう。
なお、Spring Bootのv2.5.5だとGetMappingに何も引数を渡さないとルートが指定されていたのですが、v3.1.0では明示的に指定する必要がありました。
package blog.tsuchiya.tutorial.step3.controller;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class TestController {
@GetMapping("/")
public String index(Model model) {
model.addAttribute("title", "コンテンツページ");
return "contents";
}
@GetMapping("other")
public String other(Model model) {
model.addAttribute("title", "別ページ");
return "other";
}
}
Thymeleaf
ファイルの分割を行う関係上、Thymeleafテンプレートは数が多いです。
まず、それぞれのファイルの関係を図で示します。矢印の通りにファイルを呼び出す流れです。
左の方、他のファイルを呼び出す側からコードを張っていきます。細かい説明は後で行うので、今はなんとなく眺めてみてください。
なお、Thymeleafテンプレートは全部src/main/resources/templates/に保存します。
contents.html
<!DOCTYPE html>
<!--/* layout:decorateで埋め込み先を定義 */-->
<html lang="ja" xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{layout}">
<head>
<meta charset="UTF-8">
<title>コンテンツ</title>
</head>
<body>
<!--/* layoutに埋め込む部分を定義 */-->
<div layout:fragment="contents">
<!--/* Modelに渡した値はどのhtmlでも参照可能 */-->
<h1 th:text="${title}">ダミー</h1>
<p>コンテンツ</p>
</div>
</body>
</html>
Controllerから呼び出されるファイルです。5行目のlayout:decorateで呼び出す対象ファイルを指定し、12行目で呼び出し先のファイルに埋め込む部分を設定します。
Thymeleafで何らかのファイルを指定する場合は、拡張子抜きのファイル名を~{}でくくってください。~{}なしでもまだ動くのですが、次のメジャーバージョンアップで~{}なしの表現は使えなくなるようです。
一方、layout:fragmentの指定には~{}は不要です。こちらはファイル名を指定しているわけではなく、パーツに名前をつけているからだと思います。
other.html
<!DOCTYPE html>
<!--/* layout:decorateで埋め込み先を定義 */-->
<html lang="ja" xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{layout}">
<head>
<meta charset="UTF-8">
<title>コンテンツ</title>
</head>
<body>
<!--/* layoutに埋め込む部分を定義 */-->
<div layout:fragment="contents">
<!--/* Modelに渡した値はどのhtmlでも参照可能 */-->
<h1 th:text="${title}">ダミー</h1>
<p>別ページ</p>
</div>
</body>
</html>
contents.htmlとほぼ同じ内容です。複数のファイルからlayout.htmlを呼び出すとどうなるかを示すためだけのファイルです。
layout.html
<!DOCTYPE html>
<!--/* layoutを使うための定義もしておく */-->
<html lang="ja" xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
<head>
<meta charset="UTF-8">
<!--/* Modelに渡した値はどのhtmlでも参照可能 */-->
<title th:text="${title}">タイトル</title>
<link
href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.1/dist/css/bootstrap.min.css"
rel="stylesheet">
</head>
<body>
<div class="container">
<!--/* ヘッダーを別ファイルから読み込み */-->
<div id="header" th:insert="~{header::header}">ヘッダー</div>
<main class="col-9">
<!--/* 本文は別ファイルから埋め込む */-->
<th:block layout:fragment="contents"></th:block>
</main>
<!--/* フッターを別ファイルから読み込み */-->
<div id="footer" th:replace="~{footer::footer}">フッター</div>
</div>
</body>
</html>
短いけど色々内容が詰まったファイルです。layout:decolateを使って呼び出されると同時に、th:insertとth:replaceで他のファイルを呼び出しています。~{header::header}で指定しているのは、header.htmlの中のth:fragment=”header”タグを持った部分です。
th:insertの部分は以前th:includeという属性を使っていました。v3.1.0でもth:includeは利用可能なのですが、警告がでます。
th:insertとth:replaceは若干動作が異なります。詳細はこの辺から説明しているので、気になるなら先に読んでみてください。
header.html
<!DOCTYPE html>
<html lang="ja" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>ヘッダー</title>
</head>
<body>
<!--/* th:fragmentで別ファイルから読み込めるようにする */-->
<header class="my-2" th:fragment="header">
<nav class="navbar navbar-dark bg-dark">
<!--/* Modelに渡した値はどのhtmlでも参照可能 */-->
<p class="navbar-brand" th:text="${title}">ダミー</p>
</nav>
</header>
</body>
</html>
layout.htmlから呼び出されるファイルです。正確にはth:fragmentを使ったタグが呼び出されます。
footer.html
<!DOCTYPE html>
<html lang="ja" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
<!--/* th:fragmentで別ファイルから読み込めるようにする */-->
<footer class="text-center my-2 bg-dark text-light"
th:fragment="footer">
Copyright 2023
<!--/* Modelに渡した値はどのhtmlでも参照可能 */-->
<span th:text="${title}">ダミー</span>
</footer>
</body>
</html>
header.htmlと同様layout.htmlから呼び出されるファイルです。
共通パーツを使い回すlayout:decorateとlayout:fragment
layoutで始まる属性を使うためには、htmlタグに以下の属性を追加する必要があります。
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorateとlayout:fragmentはセットで使うことになる属性です。呼び出し側はlayout:decorateで呼び出す先のファイルを指定し、layout:fragmentで呼び出す先の埋め込み先を設定します。
contents.htmlだとこの部分ですね。
<html lang="ja" xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{layout}">
<!-- 中略 -->
<div layout:fragment="contents">
<!-- 後略 -->
layout:decorateでlayoutファイルを呼び出す指定(拡張子を省略したファイル名を~{}で囲む)を行い、呼び出し先で埋め込むタグをlayout:fragmentで指定しています。
呼び出す先のlayout.htmlでは、どこにlayout:fragmentで指定したタグを埋め込むのかを指定します。呼び出し元でもlayout:fragment、呼び出し先でもlayout:fragmentで分かりにくいですが、仕様なので仕方ないです・・・。
layout.htmlで埋め込む先はこの部分になります。
<th:block layout:fragment="contents"></th:block>
Spring Bootを実行してhttp://localhost:8080/にアクセスして見てください。[ページのソースを表示]すると、次のように展開されていることがわかります。
<main>
<div>
<h1>コンテンツページ</h1>
<p>コンテンツ</p>
</div>
</main>
contents.htmlからlayout.htmlを呼び出し、layout.htmlのlayout:fragment属性を指定したタグをcontents.htmlのlayout:fragment属性を指定したタグで置き換えていることがわかります。
layout.htmlは複数のファイルから呼び出すことが可能です。http://localhost:8080/otherを表示するとother.htmlの内容を埋め込んだ表示がされます。
外部ファイルからの呼び出しその1、th:insert
layout.htmlではヘッダとフッタの内容は別ファイルに切り出しています。少しだけ呼び出し方が異なっているので、その差を説明しましょう。
layout.htmlでヘッダの内容を呼び出しているのはこの部分です。
<div id="header" th:insert="~{header::header}">ヘッダー</div>
th:insertに~{[呼び出し先の拡張子なしファイル名]::[th:fragmentで指定した名前]}という形で値を指定します。
~{}はv3.1.0だとなくても動くのですが、次のメジャーバージョンアップで非対応になるみたいなので今からつけるようにしたほうが無難そうです。
以前はth:includeという属性名を使っていたのですが、v3.1.0だとまだ利用可能ではあるものの非推奨です。この属性も次のメジャーバージョンアップで使えなくなるみたいなのでth:insertを使うようにしましょう。
呼び出し先のheader.htmlでは、どのタグを埋め込み対象にするのかth:fragment属性で指定します。
<header class="my-2" th:fragment="header">
今回はこのheaderタグが埋め込み対象です。
th:insertの場合は、th:insert属性があるタグの中に指定したタグを展開します。[ページのソースを表示]するとこうなっているはずです。
<div id="header">
<nav class="navbar navbar-dark bg-dark">
<p class="navbar-brand">コンテンツページ</p>
</nav>
</div>
th:insert属性があったid=”header”のついたタグが残っていて、その中にheader.htmlでth:fragment=”header”と指定したタグが展開されているのがわかるでしょう。
外部ファイルからの呼び出しその2、th:replace
layout.htmlでフッタの内容を呼び出しているのはこの部分です。
<div id="footer" th:replace="~{footer::footer}">フッター</div>
th:replaceもth:insertと同じように~{[呼び出し先の拡張子なしファイル名]::[th:fragmentで指定した名前]}を指定します。
footer.htmlでの記述もheader.htmlと似たようなものです。
<footer class="text-center my-2 bg-dark text-light"
th:fragment="footer">
th:insertとth:replaceの違いは、th:replaceがあるタグがまるごと置き換わることです。
</main>
<footer class="text-center my-2 bg-dark text-light">
Copyright 2023
<span>コンテンツページ</span>
</footer>
</div>
</body>
th:insertでは残っていたid指定したdivタグが、th:replaceではなくなっています。
まとめ:ファイルの分割をうまく利用しよう
Thymeleafでファイルを分割する方法を説明しました。
layout:decolateとlayout:fragmentは共通の部品を使い回すのに有用です。ヘッダ、フッタ、サイドバーなど各画面共通な部分をあるファイルに切り出しておいて、変更があるコンテンツ部分だけを必要に応じで埋め込む使い方がメインになるかと思います。
th:insertとth:replaceは長くなってしまったソースを分割するなどに使います。もちろん、共通部分を抜き出して使い回すことも可能です。
どちらもうまく使うとソースコードの整理に役立つので、必要に応じて使ってみてください。
サンプルコードをGithubに公開してあります。
わかりにくところなどがありましたら、Twitterやお問い合わせフォームで質問をお願いします。記事のブラッシュアップに役に立つので、遠慮は無用どころか大歓迎です。
Spring Boot入門:Formと入力チェック(バリデーション)に続きます。
コメント