04. SpringBoot + Thymeleafでフロントエンドを作ってみた


概要

今回はAPIではなくフロントエンド(Thymeleaf)を作ってみようと思います。
「前回まではAPIだったのに突然フロントエンドなんてできるのか?」とお思いの方もいらっしゃるかもしれませんが、
筆者の感覚としてはAPIもフロントエンドも実装はそこまで変わりません

ある程度のルール(ファイルの配置箇所など)を守ればSpringBootがよしなにやってくれるので、
主に変わるのはController層くらいで、それにView層が追加されるくらいなものです。
つまりModel(Service、Mapper)層はAPIと同じような構成、実装で構わないということです。

では実際にソースを見ていきましょう!

本題

前回までは同じプロジェクトを使用していましたが、
今回はフロントエンドになるので新しいプロジェクトを切っていきます。(手順は割愛します)
作り物としてはシンプルに、前回と同じく郵便番号検索APIにリクエストし、その結果をhtmlにして返してくれるものとしましょう。
今回のメインはThymeleafによるViewの実装なので、そちらの解説をメインにしていきます。

1. 依存を追加する

お馴染みSpringInitializrを使っていきます。
今回は「Web」「MySQL」「MyBatis」「Lombok」「Thymeleaf」を指定して作っていきます。

上記を選択したらGenerateProjectをクリックし、ダウンロードできたzipの中にあるbuild.gradleをプロジェクトに反映させます。
Refleshを忘れずに。

2. Modelを作る

前回作成したGetAddressApiClientとResponseHeaderInterceptorをコピーしてそのまま持ってきます。
あ、データクラスも忘れずに持ってきましょう。

3. ControllerとViewを作る

3-1. トップ画面を出すControllerとView

とりあえずトップ画面です。
SpringBootではリダイレクトフォワードが利用可能です。今回はリダイレクトを使用しています。

TopController.java
@Controller
public class TopController {
    @GetMapping("/top")
    public String top() {
        return "/top";
    }
    @GetMapping("/")
    public String index() {
        return "redirect:/top"; // この記述で/topにリダイレクト可能になります
    }
}

つづいてhtmlですが、載せるほどのものではないのでここでは割愛します。
単純なアンカーで検索画面に飛ばすだけです。

3-2. 検索と結果を表示するControllerとViewを作る

まとめてバリデーションも実装しちゃいましょう。
まずはControllerから。

AddressSearchController.java
@Controller
@RequiredArgsConstructor
public class AddressSearchController {
    private final GetAddressApiClient client;

    @GetMapping("/address/search")
    public String search(@ModelAttribute SearchForm form) { // (1)
        return "/address/search";
    }

    @PostMapping("/address/confirm")
    public String confirm(@ModelAttribute @Valid SearchForm form,
                          BindingResult bindingResult,
                          Model model) {
        if (bindingResult.hasErrors()) { // (2)
            return "/address/search";
        }
        GetAddressApiResponse response = client.request(form.getPostalCode());
        model.addAttribute("addressList", response.getResults()); // (3)
        return "/address/confirm";
    }
}

では要点を簡単に説明していきます。
(1). @ModelAttributeを引数に付与してModelからformを取り出しています。初期表示のタイミングではModelにインスタンスが存在しないため、Springがnewし、そのオブジェクトを渡してくれます。メソッド内で明示的にnewしてmodel.addAttribute()しても同じことですが、この記述をしておけばforwardで値を引き継げるので便利です!
(2). お馴染みbindingResult.hasErrors()で入力された郵便番号に不正があった場合には検索画面に戻します。因みにアノテーションは前回作ったZipCodeをそのまま流用しています。
(3). 入力された郵便番号に不正がなければ前回作成したClientを介して住所情報を取得し、addressListという名前でViewに値を渡します。

続いて検索画面のhtmlです。

search.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"> <!-- (1) -->
<head>
    <meta charset="UTF-8">
    <title>Sample Search</title>
</head>
<body>
<h2>検索画面</h2>
<form th:action="@{/address/confirm}" th:method="post" th:object="${searchForm}">
    <label th:for="*{postalCode}">郵便番号</label><input th:field="*{postalCode}">
    <span th:if="${#fields.hasErrors('postalCode')}" th:errors="*{postalCode}">error message</span><br/> <!-- (2) -->
    <button type="submit">検索する</button>
</form>
</body>
</html>

まず初めにこのhtmlで登場している各thタグや記述方法について説明していきましょう。

  • 変数式${}…Controllerから渡されたインスタンスの値を使用する場合に使用する記述方法。単純に「値を表示したいな」と思ったら使用すると考えていただければよいかと思います!
  • リンク式@{}…リンクを記述する場合に使用できる記述方法。コンテキストパスを補完してくれます。ここでは紹介しませんが、getパラメタを変数で埋め込んだりもできます。
  • 選択変数式*{}…ネストされた変数の値を表示する場合に使用する記述方式。このhtmlではth:object="${searchForm}"で展開されたインスタンスにネストされているpostalCodeにアクセスしている、と考えてください。
  • th:action…アクション属性を指定します。
  • th:for…labelのfor属性を指定します。
  • th:fieldよく使用するであろうタグです。この記述をすると、idname属性に変数名が入り、valueが空の状態でhtmlが作成されます。formとhtmlのフィールド同士をマッピングするときによく使用します。
  • th:ifよく使用するであろうタグです。値を評価した結果がtrueであればそのタグが描画されます。

(1). thymeleafのタグthを使用するための記述です。これを記載しないとthymeleafを使用できないので忘れないようにしましょう。
(2). ${#fields.hasErrors('postalCode')}によって、postalCodeの値でバリデーション違反が発生した場合にtrueを返してくれます。th:errorsでフィールド名を指定することにより、そのフィールドに対するエラーメッセージを表示できます。つまりこの一文でバリデーション違反があった場合のエラーメッセージ出力ができます

最後に確認画面のhtmlです。

confirm.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Sample Confirm</title>
</head>
<body>
<h2>検索結果画面</h2>
<table border="1">
    <tr>
        <th>郵便番号</th>
        <th>都道府県コード</th>
        <th>住所1</th>
        <th>住所2</th>
        <th>住所3</th>
        <th>住所カナ1</th>
        <th>住所カナ2</th>
        <th>住所カナ3</th>
    </tr>
    <tr th:each="result : ${addressList}">
        <td th:text="${result.zipcode}"></td>
        <td th:text="${result.prefcode}"></td>
        <td th:text="${result.address1}"></td>
        <td th:text="${result.address2}"></td>
        <td th:text="${result.address3}"></td>
        <td th:text="${result.kana1}"></td>
        <td th:text="${result.kana2}"></td>
        <td th:text="${result.kana3}"></td>
    </tr>
</table>
<a th:href="@{/address/search}">検索画面に戻る</a>
</body>
</html>

今回は結果がリストで帰ってくるので、テーブルで出力させる形にしました。
この画面で使用しているタグについては以下の通りです。

  • th:eachよく使用するであろうタグです。javaの拡張for文をイメージしていただければわかりやすいかと思います。${addressList}から値を順番に取り出し、その値をresultという名前で使用できるようにしています。
  • th:textよく使用するであろうタグです。その名の通り、画面表示させたい場合に使用し、レンダリング後は<td></td>に値が展開されます。尚XSSに対応しており、メタ文字をエスケープしてくれますので安心して使用できます。

こんなところでしょうか。
では最後にブラウザから叩いてみましょう!!

4. 実際に叩いてみる

4-1. トップ画面

bootRunして http://localhost:8080/ にアクセスします。

郵便番号で検索するをクリックして、検索画面へ遷移してみます。

4-2. 検索画面

レンダリング後のHTMLソースはこんな感じです。

th:fieldが展開されてidnameが変数名になり、valueが空になっていますね。
では正常な値を入れて検索するをクリックしましょう。

4-3. 結果画面

まずは結果が単数の場合から。

続いて結果が複数の場合。

th:eachがちゃんと機能して、複数の場合でも全件表示できていますね。

では最後にバリデーションエラーの場合を見てみましょう。

4-4. バリデーションエラー


不正な値を入れて検索するをクリックすると…


無事エラーメッセージが表示できましたね。
因みにこのメッセージはZipCode.messageのデフォルトにべた書きしている文字列です。

最後に

めちゃくちゃ簡単な内容ではありましたが、フロントエンドの実装でした。
thymeleafは奥が深く、私が触れているタグや記述方式は氷山の一角です。
もっともっといろいろなことができますので、いろいろ調べてみてください!

あとはcssやjavascriptなどが必要になるかと思いますが、最近semantic uiに触れる機会がありました。
CDNで利用可能で、class名を指定することで勝手に装飾してくれますので、
興味のある方はぜひ触れてみてください!

最後までご覧いただきありがとうございましたー!!