ファイルの配信

HTMLにベタ書きされているCSSを、ファイルとして取り扱えるように変更しましょう。

CSSファイルの作成

何はともあれ、まずはHTMLファイルにあるCSSを抜き出してファイル化します。

static/css/index.css
.tweet {
  border: 1px solid gray;
}

.name {
  font-weight: bold;
  border-bottom: 1px solid gray;
  border-right: 1px solid gray;
}

.posted_at {
  font-size: 80%;
  color: gray;
}

今後、他のCSSや画像などのリソースも配置することを考慮し、 static ディレクトリを作成し、その配下に配置しました。

tower-httpの導入

先ほど作成したファイルですが、実はaxumではそのままファイルを配信することができません。もちろんファイルを配信する部分を実装をすれば実現可能ですが、ここではすでに有志によって作成されているHTTPクライアント/サーバーライブラリの tower-http を使うことにします。

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"
tower-http = { version = "0.1", features = ["fs"] }  # 追加

ルーティング

ここで、一度axumのルーティングについて触れておきます。

axumでは、 Router 構造体を利用してパスと処理を関連付けることができます。関連付けは route メソッドで行い、HTTPメソッドは axum::routing の関数で指定します。

use axum::{
    routing::get,
    response::IntoResponse,
    Router,
};

fn main() {
    let router = Router::new()
        .route("/", get(handler));  // routing::getでHTTP GETに関連付け
}

async fn handler() -> impl IntoResponse {
    ...
}

1つのパスについて、複数のHTTPメソッドと処理を関連付けることもできます。

use axum::{
    routing::get,
    response::IntoResponse,
    Router,
};

fn main() {
    let router = Router::new()
        .route("/", get(handler).post(post_handler));  // HTTP POSTにも関連付け
}

async fn handler() -> impl IntoResponse {
    ...
}

async fn post_handler() -> impl IntoResponse {
    ...
}

さらに、 Router 自体にメソッドチェインで複数のパスと処理を関連付けることもできます。

use axum::{
    routing::get,
    response::IntoResponse,
    Router,
};

fn main() {
    let router = Router::new()
        .route("/", get(handler).post(post_handler))
        .route("/details", get(details_handler));  // `/details` にも関連付け
}

async fn handler() -> impl IntoResponse {
    ...
}

async fn post_handler() -> impl IntoResponse {
    ...
}

async fn details_handler() -> impl IntoResponse {
    ...
}

サービスの組み込み

tower-httpには ServeDir というサービスが実装されており、これを使うことで指定したディレクトリとその配下のすべてのディレクトリ/ファイルを取り扱うことができます。

axumにはtowerサービスを手軽に組み込める service というモジュールも用意されているため、これを使って組み込みます。

src/main.rs
use askama::Template;
use axum::error_handling::HandleErrorExt;
use axum::{
    body::{Bytes, Full},
    http::{Response, StatusCode},
    response::{Html, IntoResponse},
    routing::{get, service_method_routing},
    Router,
};
use std::{convert::Infallible, net::SocketAddr};
use tower_http::services::ServeDir;

#[tokio::main]
async fn main() {
    let app = Router::new()
        .nest("/static", static_contents())
        .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(),
    })
}

fn static_contents() -> Router {
    Router::new().nest(
        "/",
        service_method_routing::get(ServeDir::new("static")).handle_error(|error: std::io::Error| {
            (
                StatusCode::INTERNAL_SERVER_ERROR,
                format!("Unhandled internal error: {}", error),
            )
        }),
    )
}

#[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(),
        }
    }
}

ここでは、ルーティングを入れ子にするテクニックを使っています。

ルーティングの入れ子

パスごとに処理する関数を関連付ける方法は前述したとおりですが、実はそれだけでなく、 Router 自体をパスに関連付けることもできます。 Router 自体を関連付けるためには nest を使います。

use axum::{
    routing::get,
    response::IntoResponse,
    Router,
};

fn main() {
    let detail_router = Router::new()
        .route("/", get(details_handler));
    let router = Router::new()
        .route("/", get(handler).post(post_handler))
        .nest("/details", detail_router);  // `/details` に別のRouterを関連付け
}

async fn handler() -> impl IntoResponse {
    ...
}

async fn post_handler() -> impl IntoResponse {
    ...
}

async fn details_handler() -> impl IntoResponse {
    ...
}

こうすることにより、全てを1つの Router で管理する必要がなくなるため、見通しの良い設計が可能となります。

HTMLの変更

ここまででCSSファイルの配信ができるようになったので、最後にHTMLファイルがそのCSSファイルを参照するように変更します。

templates/index.html
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>Rustwi</title>
  <link rel="stylesheet" href="/static/css/index.css">
</head>
<body>

<div class="tweet">
  <span class="name">{{name}}</span>
  <div>{{message}}</div>
  <div class="posted_at">{{posted_at}}</div>
</div>

</body>
</html>