Thymeleafでレイアウト共通化

SpringBoot+Thymeleafでレイアウトを共通化する場合の方法について。

Thymeleafは別テンプレートの部品を読み込むというフラグメント式があり、一般的には各画面のテンプレートが共通のhead部分や画面共通のヘッダー、フッター部分を取り込むという形になる。それだと全体構成を決める共通のレイアウトという感じではない。

そこで、別の方法として、共通のテンプレートから、各画面のコンテンツのテンプレートを読み取るという方法でやるのがいい。

1.フラグメント式

Thymeleafでは、フラグメント式を使って、他のテンプレート内の要素を取り込むことができる。

【Thymeleaf 3.0 日本語ドキュメント】チュートリアル

(4.5 フラグメント)

https://www.thymeleaf.org/doc/tutorials/3.0/usingthymeleaf_ja.html#%E3%83%95%E3%83%A9%E3%82%B0%E3%83%A1%E3%83%B3%E3%83%88

(8 テンプレートレイアウト)

https://www.thymeleaf.org/doc/tutorials/3.0/usingthymeleaf_ja.html#%E3%83%86%E3%83%B3%E3%83%97%E3%83%AC%E3%83%BC%E3%83%88%E3%83%AC%E3%82%A4%E3%82%A2%E3%82%A6%E3%83%88

2.一般的な方法

一般的には各画面のテンプレートが共通のhead部分や画面共通のヘッダー、フッター部分を取り込むという形になる。以下ページのようなやり方が普通かと。

https://fintan-contents.github.io/spring-crib-notes/latest/html/web/view/thymeleaf-page-layout.html

それだと全体構成を決める共通のレイアウトではなく、各画面が共通部品を使うという形になる。

Layout Dialectというのがあるようだが、やはりhtml,body等を記述していて、コンテンツ部分だけを書くというイメージではなく、あまり使うメリットを感じない。

https://www.thymeleaf.org/doc/articles/layouts.html

※「4.Thymeleaf Layout Dialect」を参照

3.レイアウトの共通化

Djangoでは、テンプレートを継承することができて、 baseテンプレートで全体の構成を決めて、継承した各画面ではコンテンツ部分だけを書けばいいという作りになっている。

Thymeleafで、Djangoに近い実装をする方法として、以下のようにする。

3.1 実現方法

共通のレイアウトから、コンテンツ部分は各画面のレイアウトを取り込むという方法で実現する。

3.2 実装例

1)レイアウト側の実装

仮に、layout/base.htmlのような名前で、共通テンプレートを作成し、その中にhead、ページヘッダー、ページフッター、ナビ部分のようなものを定義する。レイアウトにすべて書くよりは、レイアウトで各部分をフラグメント式にて読むようにするのが良い。

メインのコンテンツ部分は以下のように、定義する。divのidを仮に「main」としている。

【共通レイアウト:layout/base.html】

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">

: ※head部分

<body>

:  ※共通ヘッダー部分  

    <!--/* ページのメイン部分 */-->
    <div id="main" th:replace="__${contentTemplate}__"></div>

: ※共通フッター部分

</body>
</html>

2)各画面の実装

各画面のテンプレート(パーツ)は、メイン部分だけを以下のように記述すればいい。

【各画面テンプレート:仮にcontent/_test.html】

: ※ここに別要素が入ってもいい

<div id="main" xmlns:th="http://www.thymeleaf.org">
: ※メイン部分
</div>

: ※ここに別要素が入ってもいい

1)レイアウトのリプレースはファイルの読み込みのため、div部分の前後に別の要素があっても、またmainのdivが無くても問題なく、自由な記述ができる。

前後部分だけでなく、head部分と、bodyの最後尾にJavaScriptを入れたい場合には、フラグメント式を使って3分割し、レイアウト側でそれぞれの場所で読み取るようにするのが良い。

3)コントローラの実装

コントローラ側では、従来はテンプレート名を返しているのを、以下のように変更する。

【コントローラ】

:
@Controller
public class TestController {
 :
    @GetMapping(path = "/test")
    public String test(Model model) throws Exception {
        :
        model.addAttribute("contentTemplate", "content/_test");  // 各画面のテンプレート
        return "layout/base";    // 使用する共通レイアウト
    }
 :

共通レイアウトが一つしかないのであれば、各画面のテンプレートを戻り値にして、postHadlerでそれをモデルに設定して共通テンプレートを呼ぶようにするのも可能。

4.結論

フラグメントの使い方としては意図に合わないかもしないが、共通レイアウトから各画面レイアウトを読み込むとすることで、すっきりできる。

(2021/09/28 追記)

テンプレートのキャッシュは大丈夫?という疑問を頂いた。

https://www.thymeleaf.org/doc/tutorials/2.1/usingthymeleaf_ja.html#%E3%83%86%E3%83%B3%E3%83%97%E3%83%AC%E3%83%BC%E3%83%88%E3%82%AD%E3%83%A3%E3%83%83%E3%82%B7%E3%83%A5

Thymeleafのソースを見ると、キャッシュのキーは以下となっており大丈夫だろうと思っているが、関係のあるソースをさっと読んだだけで、完全に理解しているわけでないので、「たぶん」という感じ。

https://github.com/thymeleaf/thymeleaf/blob/3.0-master/src/main/java/org/thymeleaf/cache/TemplateCacheKey.java

(2021/09/29 追記)

動作確認でキャッシュは問題なさそう。