HTMLの表示

より柔軟なHTMLを表示できるようにするために、テンプレートエンジンを使ってみましょう。

askamaの導入

テンプレートエンジンには askama を使います。 Cargo.toml[dependencies] 節にクレートを追加します。

Cargo.toml
[package]
name = "rustwi"
version = "0.1.0"
edition = "2018"

[dependencies]
axum = "0.3"
tokio = { version = "1.0", features = ["full"] }
askama = "0.10"  # 追加

テンプレートHTMLの作成

askamaを使うことで、テンプレートにデータを埋め込んで、データに合わせたHTMLを表示することができます。

まずはシンプルに、ツイートを表示するテンプレートを作成します。

templates/index.html
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>Rustwi</title>
</head>
<body>

<div style="border: 1px solid gray;">
  <span style="font-weight: bold; border-bottom: 1px solid gray; border-right: 1px solid gray;">{{name}}</span>
  <div>{{message}}</div>
  <div style="font-size: 80%; color: gray">{{posted_at}}</div>
</div>

</body>
</html>

Note: テンプレートファイルは templates ディレクトリ配下に配置します。

構文はJinjaに準拠しているため、具体的な書き方はリファレンスを参照すると良いでしょう。

テンプレート構造体の作成

テンプレートを使うには、 askama::Template トレイトを継承(derive)した構造体を用意します。

use askama::Template;

#[derive(Template)]
#[template(path = "index.html")]
struct IndexView {
    name: String,
    message: String,
    posted_at: String,
}

フィールドにテンプレートに埋め込みたいデータを持たせることで、テンプレートにデータを反映させることができます。

let view = IndexView {
    name: "太郎".into(),
    message: "こんにちは!".into(),
    posted_at: "2020-01-01 12:34".into(),
};

ユーティリティの作成

上記まででテンプレートを作成することができますが、レスポンスとしてテンプレートを使うためには、テンプレートが IntoResponse を実装している必要があります。

直接この構造体に実装する方法もありますが、今回は、テンプレートをフィールドに持つ構造体を用意し、この構造体に IntoResponse を実装してみます。

use askama::Template;
use axum::{
    body::{Bytes, Full},
    http::{Response, StatusCode},
    response::{Html, IntoResponse},
    routing::get,
    Router,
};
use std::{convert::Infallible, net::SocketAddr};

#[tokio::main]
async fn main() {
    let app = Router::new().route("/", get(index));
    let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
    println!("listening on {}", addr);
    axum::Server::bind(&addr)
        .serve(app.into_make_service())
        .await
        .unwrap();
}

async fn index() -> impl IntoResponse {
    HtmlTemplate(IndexView {
        name: "太郎".into(),
        message: "こんにちは!".into(),
        posted_at: "2020-01-01 12:34".into(),
    })
}

#[derive(Template)]
#[template(path = "index.html")]
struct IndexView {
    name: String,
    message: String,
    posted_at: String,
}

struct HtmlTemplate<T>(T);

impl<T> IntoResponse for HtmlTemplate<T>
where
    T: Template,
{
    type Body = Full<Bytes>;
    type BodyError = Infallible;

    fn into_response(self) -> Response<Self::Body> {
        match self.0.render() {
            Ok(html) => Html(html).into_response(),
            Err(err) => Response::builder()
                .status(StatusCode::INTERNAL_SERVER_ERROR)
                .body(Full::from(format!(
                    "Failed to render template. Error: {}",
                    err
                )))
                .unwrap(),
        }
    }
}

少し回りくどい方法ですが、こうすることで テンプレートごとにIntoResponseを実装する必要がなくなる というメリットがあります。

このように変更して実行すると、太郎さんのツイートが1つ表示されるはずです。