actix-webとmicroCMSを使ってRust製のブログを作った話
actix-webとmicroCMSを使ってブログを作りました。
調べたことや学びをまとめておきたくて記事を書きます。
今回作成したブログのソースコードはこちらにおいてあります。
actix-webの導入
公式のgetting startedを参考に導入しました。
ここで特に何かつまる、ということはなかったです。
環境変数を.envに記述したい
今回の作成するブログはmicroCMSからWebAPI経由でコンテンツを取得することを想定していました。
そのため、API keyやエンドポイントを管理する必要があるのですが、ソースコードに直接書くのは良くないので.env
ファイルで管理し、それをbuild時に取得するような作りにしました。
こちらを実現するためにdotenvを利用しています。
このcrateを利用すると、.env
ファイルに記述されている環境変数をstd::env
を利用して取得することができます。
use std::env;
use dotenv::dotenv;
...
dotenv.ok();
let v = env::var("ENV_VAR").expect("Error msg");
...
.env
に記述されているENV_VAR
というキーの環境変数を取得できます。
actix-webのStateについて
こういう使い方で良かったかいまいち自信がなかったのですが、環境変数から取得したAPI keyなどを異なるrouteのハンドラで共有したかったので、actix-webのState
を利用しました。
使い方はドキュメントの通りで、共有したいStateの構造体を定義し、それをAppインスタンスの生成時に渡します。
利用する側はハンドラの引数にStateを受け取ることができます。
struct AppState {
api_key: String
};
...
HttpServer::new(|| {
App::new()
.app_data(web::Data::new(AppState {
api_key, // 環境変数から取得したキーを渡す
}))
})
...
// 利用するハンドラ
async fn index(data: web::Data<AppState>) -> String {
let api_key = &data.key;
}
単体で使う分には困らなかったのですが、URLからPathを取得したいときや、後述するテンプレートエンジンをハンドラ内で同時に利用しようとしたときに少しつまりました。
actix-webのPathについて
actix-webでは関数に対してget
post
patch
delete
などのHTTPRequestのメソッドに対応するアトリビュートを付与することで、そのルートに対するハンドラを設定することができます。
その際に、Pathを中括弧で囲むとその情報をハンドラの引数に受け取ることができます。これをactix-webではdynamic segments
と呼びます。
#[get("/article/{article_id}")]
async fn index(path: web::Path<String>) -> Result<HttpResponse, Error> {
// /article/hoge とアクセスした場合、hogeが取得できる
let article_id = path.into_inner();
}
このようにリクエストされたパスから値を取得できるのですが、上で書いたようにStateと併用する場合、Stateもハンドラの引数で受け取るため、どちらも受け取りたい場合の書き方がわかりませんでした。
結論、HttpRequestを受け取ることでPathとStateの両方を取得することができます。
追記: これを書いている途中に気づいたのですが、ハンドラは引数を複数受け取れるみたいで、HttpRequest自体を受け取らなくても実現できたかもです...
HttpRequestを使う場合、match_info
を使うことでPathの値が、app_data
でStateを受け取ることができます。
Templateエンジン
TeraというTepmlateエンジンを利用しました。
actix-webと一緒に利用するにあたって、各ハンドラ内で対応するHTMLをrenderingして、responseにして返す、という実装をしています。
まず、サーバー起動時にTemplateのHTMLがおいてあるパスを指定して、Teraのインスタンスを生成します。
このインスタンスをリクエストハンドラが受け取れるようにactix-webのStateにセットします。
// 初期化
let templates = Tera::new("/templates/**/*");
App::new()
.app_data(web::Data::new(AppState {
templates,
}))
ハンドラではTeraのインスタンスを受け取り、contextに必要な値を注入してから、レンダリングして、responseに詰めて返します。
// 受け取るハンドラ
#[get("/")]
async fn index(state: web::Data<AppState>) -> Result<HttpResponse, Error> {
let mut ctx = tera::Context::new();
ctx.insert("articles", articles);
let view = state
.templates
.render("index.html.tera", &ctx)
.map_err(|e| error::ErrorInternalServerError(e))?;
Ok(HttpResponse::Ok().content_type("text/html").body(view))
}
静的ファイル
CSSやJSなどの静的なアセットはactix-filesを利用して配信できました。
App::new()
.service(fs::Files::new("/static", "./static").show_files_listing())
microCMSからコンテンツを取得する
reqwest を利用しました。
headerを設定したい場合、clientを生成する必要がありました。
let client = reqwest::Client::new();
let res: Content = client.header("X-MICROCMS-API-KEY", api_key)
取得したレスポンスに型を付けたい場合にどう実装するのか悩んだのですが、型を当てたい構造体にserdeのDeserializeをderiveしておくだけで、json
メソッドが型を解決してくれました。
#[derive(Debug, Serialize, Deserialize)]
pub struct Content {
pub id: String,
}
...
// 型が効いている
let res: Content = client
.get(end_point + "/api/v1/article/" + article_id)
.header("X-MICROCMS-API-KEY", api_key)
.send()
.await?
.json()
.await?;
また、Rustは習慣的にsnakeケースを利用しますが、APIのレスポンスではケバブケースなどが使われていることもあるかと思います。その場合、serdeのAttributeを利用することでsnakeケースにrenameすることができます。
pub struct Content {
// created_atとして扱われる
#[serde(rename = "createdAt")]
pub created_at: String
}
おわりに
今回ブログを作るにあたって調べたことをまとめてみました。
次はもう少し機能を増やしたものを作ってみたいとおもいます。
Discussion