今回から数回に分けて、RustのWebアプリフレームワークであるActix Webを紹介します。題材は、シンプルな投稿アプリです。今回は、投稿アプリの基本形を作成し、Actix Webでのルーティングやハンドラー関数の書き方を理解して一覧表示機能まで実装してみます。
この記事は会員限定です。会員登録(無料)すると全てご覧いただけます。
本連載のサンプルコードをGitHubで公開しています。こちらからダウンロードしてみてください。
今回のテーマは、Actix Webです。Actix Webは、近年人気急上昇中のWebアプリケーションフレームワークです。公式サイトによると、以下のような特徴があるとされています。
Actix Webでは、URLパターンに基づくルーティングルールの指定と、対応するハンドラーの作成が基本となっています。多くのWebアプリケーションフレームワークで取り入れられている手法なので、Actix Webでの開発にもすんなりと入っていけるでしょう。この他、フォームデータやクエリパラメーターなどリクエストパラメーターの取り出しや、レスポンスデータの生成を柔軟に実施できるなど、シンプルなコードで多くのことができるように設計されています。
Actix Webには、豊富なサンプルが用意されていますから、「actix/examples: Community showcase and examples of Actix ecosystem usage.」から参照してみてください。
以降で、Actix Webを使った投稿アプリを作成していきます(図1)。なお、本連載の後半でフロントエンドを充実させていくので、今回から作成するのはページ遷移を伴うオーソドックスなサーバサイドアプリとなっています。Actix WebでWebサイトをどう作るのか、そこをご覧いただければ幸いです。
最初に、Actix Webアプリの基本形を作成し、アプリの基本構造を見ていきましょう。なお、Actix Webの利用には、Rust 1.39以降が必要です。第1回の手順でバージョンを確認して、古いバージョンがインストールされている場合には、アップデートしておきます。
プロジェクトをcargo newコマンドで作成します。アプリ基本形のプロジェクト名は、actix-sampleとします。
% cargo new actix-sample
プロジェクトフォルダに移動して、外部クレートactix-webとactix-rtを依存関係としてcargo addコマンドでプロジェクトに追加します。actix-webはActix Webの本体、actix-rtはTokio(Rust用の非同期ランタイム)ベースの非同期ランタイムです。
% cd actix-sample % cargo add actix-web actix-rt
結果は、Cargo.tomlファイルに反映されるので確認しておきます。これで、アプリの準備はできました。
…略… [dependencies] actix-rt = "2.9.0" actix-web = "4.4.0"
作成したプロジェクトには"Hello, world!"を表示するコードが置かれているだけなので、これを削除してリスト1のコードで置き換えます。これは、localhostのポート8000でHTTPリクエストを待ち受けて、URLパスにルート「/」が指定されたら"Hello, Actix Web!"と返すだけのコードになります。
use std::io::Result; use actix_web::{App, HttpServer, Responder, HttpResponse, get}; #[get("/")] (1) async fn index() -> impl Responder { (2) HttpResponse::Ok().body("Hello, Actix Web!") (3) } #[actix_rt::main] (4) async fn main() -> Result<()> { (5) HttpServer::new(|| { (6) App::new() .service(index) }) .bind("127.0.0.1:8000")?.run().await (7) }
cargo runコマンドでビルドして実行します。エラーのないことを確認して、Webブラウザで「http://localhost:8000/」を開いてみます。図2のように表示されればアプリ基本形の作成は成功です。
リスト1はシンプルなコードですが、Actix Webを利用するための基本的なルールが満載です。まず、重要なのはルーティングルールとハンドラー関数です。Actix Webでは、リクエストされたHTTPメソッドやURLパターンとハンドラー関数の対応を保持していて、一致する関数を呼び出すというのが基本です(図3)。それを踏まえて、少々長くなりますが順番に見ていきましょう。
(1)は、ルーティングルールを指定する注釈です。続く関数が、HTTPメソッドがGET、URLパスがルート「/」である場合に呼び出されるハンドラー関数であることを明示します。ルーティングルールの指定方法には、注釈の他に幾つかの方法とパターンがありますが、次回以降で投稿アプリを拡張しながら順次紹介していきます。
(2)はハンドラー関数の定義であり、asyncキーワードが付加されている通り非同期関数となります。Actix Webが非同期処理をサポートするので、このようにasyncを付けるだけで非同期関数となります。戻り値のimpl Responderは、Responderトレイトを実装した型の値を戻す、という意味になります。
(3)がその戻り値です。HttpResponseは、Responderトレイトを実装した構造体です。HTTPレスポンスを、HTTPステータスOK(Okメソッド)でレスポンスボディー(bodyメソッド)を"Hello, Actix Web!"として戻す、という意味になります。bodyメソッドが返すのはあくまでもレスポンスボディーであり、HTMLの
タグとは無関係なので注意してください。なお、この例のindex関数は引数を受け取りませんが、例えば引数にHttpRequestのインスタンスを渡すと、リクエストパラメーターなどに応じた処理が可能です。このあたりも、次回以降で紹介していきます。(4)は、main関数がactix-rtによって実行される非同期関数であることを示す注釈です。Actix Webが直接呼び出す関数にはこの注釈が必要です。
(5)はそのmain関数ですが、非同期関数とするためにこちらもasyncキーワードを付加します。戻り値のstd::io::Resultは、(7)で呼び出しているbindメソッドのエラーを呼び出し元に委譲するためです。bindメソッドの呼び出しに「?」が付いていることに注意してください。
(6)からは、HTTPサーバであるHttpServerのインスタンス生成、待機するIPアドレスとポートの指定(bindメソッド)、サーバの開始(runメソッド)となります。最後のawaitは非同期呼び出しとする指定です。
HttpServerのコンストラクタに渡しているAppオブジェクトは、文字通りWebアプリケーションのためのオブジェクトであり、この場合は生成後にserviceメソッドでindex関数を登録する、という意味になります。アプリケーションに新たなルートを登録する場合には、serviceメソッドをメソッドチェーンでつないでいきます。なお、ルートの登録にも幾つか方法があるので、こちらも次回以降で順次紹介していきます。
Rustにおけるawaitとasyncも、他のプログラミング言語におけるそれと役割的には同じです。非同期で実行したい関数やブロックをasyncで修飾し、awaitで非同期に呼び出します。Rustの非同期処理はFutureトレイトに基づいており、asyncとawaitを記述するだけで、コンパイラが自動的にFutureトレイトを関数やブロックに実装します。なお、非同期処理をサポートする多くのプログラミング言語と異なり、awaitはプロパティのように記述する後置型となっています。これにより、メソッドチェーンの中に記述して任意のタイミングで処理待ちをすることが可能となっています。
リスト1の(6)において、HttpServerのコンストラクタ引数に渡されているのは、クロージャです。クロージャとはいわゆる無名関数のことで、「|引数| {関数本体}」のような形で記述します。この場合は引数がないので「||」となっています。論理和の演算子と同じに見えてしまうのですが、左辺がないという点で見分けましょう。
Actix Webのアプリの基本形は、ルーティングルールの登録と、呼び出されるハンドラー関数の定義であることを押さえておきましょう。
ここから、actix-sampleをもとにした投稿アプリを構築していきます。actix-postsプロジェクトを新たに作成し、actix-webクレートとactix-rtクレートをプロジェクトの依存関係に加えて、src/main.rsファイルをactix-sampleからコピーしておいてください。以降、actix-postsにカレントフォルダを移動したものとして解説を進めます。
トラブルシューティングに備えて、ロガーをあらかじめ組み込んでおきましょう。ロガーとしては、Rustの標準的なロガーであるlogと、Actix Webにミドルウェアとして用意されているLoggerを使います。これらを使うために、logクレート、env_loggerクレートを以下のコマンドでプロジェクトの依存関係に加えてください。
% cargo add log env_logger
そして、src/main.rsファイルにリスト2のようにuse文を変更、追加し、main関数にコードを追記します。
…略… use actix_web::{App, HttpServer, Responder, HttpResponse, get, middleware::Logger}; use env_logger::Env; …略… #[actix_rt::main] async fn main() -> Result<()> { env_logger::init_from_env(Env::default().default_filter_or("info")); (1) HttpServer::new(|| { App::new() .service(index) .wrap(Logger::default()) (2) }) .bind("127.0.0.1:8000")?.run().await }
(1)はロガーの設定で、デフォルトのログレベルの指定です。ログレベルとは、重要度や用途に応じたログ出力の基準で、Rustのlogクレートでは重要度の高い順にerror、warn、info、debug、traceの5段階が指定できます。ここでは、一般的な情報に相当するinfoを指定しています。
(2)のwrapメソッドは、Loggerミドルウェアをアプリケーションに追加します。この追加方法だと、全てのリクエストに対してログ出力が適用されます。
アプリへの一通りの実装が終了した時点で、URLパス「/posts」にWebブラウザからアクセスすると、以下のようにログがターミナルに出力されます。
[2023-10-14T23:14:26Z INFO actix_server::builder] starting 16 workers [2023-10-14T23:14:26Z INFO actix_server::server] Actix runtime found; starting in Actix runtime [2023-10-14T23:14:34Z INFO actix_posts::handler] Called index (1) [2023-10-14T23:14:34Z INFO actix_web::middleware::logger] 127.0.0.1 "GET /posts HTTP/1.1" 200 1353 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36" 0.001263 (2)
(1)はハンドラー関数内で任意で出力している文字列、(2)はリクエストの処理終結によって自動で出力される行です。リクエストしたページがルーティングルールのいずれにもマッチしないと(1)(2)は出力されませんし、(1)のハンドラー関数と(2)の"GET /posts HTTP/1.1"部分が想定した対応と異なっているかもしれません。ルーティングルールを追加したり編集したりしたが、思うようにハンドラー関数が呼び出されないという場合には、このようにログを活用して確認しましょう。
全てのページに表示されるヘッダとフッタを、共通コンテンツとしてあらかじめ準備しておき、各ページの生成時に取り込むようにします。これにより、ハンドラー関数に共通部を生成するコードを逐一書かないで済みますし、ページデザインの統一が容易になります。
ヘッダ(header.html)の内容は
タグとアプリのタイトル、フッタ(footer.html)の内容は著作権の情報です。デザインの調整が容易になるように、WebデザインフレームワークBootstrapを組み込んでいます。ファイルは、プロジェクトルートにstaticフォルダを作成して、ソースファイルとは別に配置します。なお、これらのファイルは自動で適用されるわけではなく、ハンドラー関数がファイルを読み込み、コンテンツの一部として利用するということに注意してください(図4)。ファイル内容の掲載は割愛しますので、配布サンプルを参照してください。
まずは、全投稿を一覧表示するハンドラー関数を実装してみましょう。Webアプリにどのような処理を用意するかにもよりますが、ハンドラー関数は数が多くなるのが一般的なので、煩雑さを避けるために別クレートを作成して配置することにします。プロジェクトのsrcフォルダに新たにhandler.rsファイルを作成し、そこに一覧表示のハンドラー関数indexをmain.rsファイルから移動、リスト3のように書き換えます。
use log::info; use actix_web::{Responder, HttpResponse, web, get, post}; #[get("/posts")] (1) pub async fn index() -> impl Responder { (2) info!("Called index"); (3) HttpResponse::Ok().body("Called index") (4) }
(1)はルーティングルールの変更です。全投稿の一覧表示は、URLパス「/posts」とします(他のルーティングルールとハンドラー関数は、次回に改めて紹介します)。(2)はmainクレートから参照できるようにpubキーワードを関数に付加しています。(3)はinfoレベルでハンドラー関数が呼び出されたことを示すログ出力です。(4)は仮のコンテンツで、index関数が呼ばれたことを返すだけのものとなっています。実際の一覧表示は、あとでコードの説明とともに紹介します。
作成したハンドラー関数を、アプリケーションに登録するコードをリスト4のように追記、修正します。
…略… mod handler; (1) …略… App::new() .service(handler::index) (2) .wrap(Logger::default()) …略…
(1)は、handler.rsファイル内の関数を呼び出すために必要なmod文です。これにより、(2)のようにhandler.rsファイルで定義した関数をserviceメソッドに与えることができます。
ここでアプリケーションをビルド、実行し、Webブラウザで「http://localhost:8000/posts」を開いてみます。図5のように表示されればルートの変更やクレートの分割は成功です。
投稿アプリのデータソースは、本来であればSQLiteやMySQLなどのデータベースを使いたいところですが、今回はActix Webの使い方に集中するため、静的なJSONデータファイルdata.jsonを用意して、それを参照、更新するというように簡略化します。ファイル内容の掲載は割愛するので、配布サンプルを参照してください(全く同じ内容のdata_base.jsonファイルも含めてあるので、内容の復元に利用できます)。なお、Rustにおけるデータベースの利用については、本連載の後半で紹介する予定です。
データソースのコードも、別クレートとします。srcフォルダにhandlerフォルダを作成し、handlerの下位モジュールとしてdata.rsファイルをリスト5の内容で配置します。
use std::fs; use serde::{Serialize, Deserialize}; (1) #[derive(Serialize, Deserialize, Debug, Clone)] (2) pub struct Message { pub id: i32, // ID pub posted: String, // 投稿日時 pub sender: String, // 投稿者名 pub content: String, // 投稿内容 } static DATA_FILENAME: &str = "data.json"; (3) pub fn get_all() -> Vec<Message> { (4) let file = fs::read_to_string(DATA_FILENAME).unwrap(); let mut json_data: Vec<Message> = serde_json::from_str(&file).unwrap(); json_data.sort_by(|a, b| b.posted.cmp(&a.posted)); json_data }
これは、JSONファイルをデータソースとして使う場合の最小構成となっています。関数としては、全投稿データを取得するget_all関数のみを実装しています。
(1)はserdeクレートの構造体を使うuse文です。serdeは、RustにおいてJSONデータを使う際の定番クレート(JSON SerDeライブラリ)で、(2)のように構造体を注釈してシリアライズ(Serialize)やデシリアライズ(Deserialize)などの機能を提供します(ここでは構造体のクローンを可能にするCloneも指定しています)。なお、(2)は投稿データ構造体Messageの定義であり、pubキーワードを付記して上位モジュールから参照できるようにしています。各フィールドの役割はコメントを参照してください。
(3)は、データファイルの名前を静的文字列リテラルとして定義しています。
(4)は、全投稿データを取得する関数本体です。ファイルから読み込んだJSON文字列を、serde_json::from_strメソッドでMessage型のベクターとして取得します。この際に、serdeによるデシリアライズが実行されています。続けて、sort_byメソッドで投稿日時の降順で並び替えて、関数の戻り値としています。
ここで、data.rsファイルで使用するserdeクレートとserde_jsonクレートを、プロジェクトの依存関係に以下のコマンドで追加してください。また、追加後のCargo.tomlファイル(serde行)を、derive注釈を使うためにリスト6のように修正してください。
% cargo add serde serde_json
[dependencies] …略… serde = {version = "1.0.188", features = ["derive"] } serde_json = "1.0.107"
ここで、ハンドラー関数を全投稿データの取得と表示に対応したものに修正します。handler.rsファイルのindex関数をリスト7のように修正します。
…略… mod data; (1) #[get("/posts")] pub async fn index() -> impl Responder { let posts = data::get_all(); (2) let mut body_str: String = "".to_string(); (3) body_str += include_str!("../static/header.html"); for item in &posts { (4) body_str += &format!("<div><a href=\"/posts/{}\">", item.id); body_str += &format!("<div>{} {}</div>", item.sender, item.posted); body_str += &format!("<div><p>{}</p></div>", item.content.replace("\n", "<br />")); body_str += "</a></div>"; } body_str += "<div><a href=\"/posts/new\">作成</a></div>"; body_str += include_str!("../static/footer.html"); HttpResponse::Ok().content_type("text/html; charset=utf-8").body(body_str) (5) } …略…
リスト7の説明です。このコードの処理内容は、ヘッダやフッタのファイルと取得した全投稿データからHTMLを生成することです。
(1)は、データソースのモジュールであるdataの取り込みの指定です。
(2)では、現在の全投稿データをMessage型のベクターで取得しています。
(3)からは、レスポンスボディーとする文字列を作成しています。あらかじめ用意したHTMLファイルを、include_str!マクロで読み込んで文字列に追加していきます。このマクロは、引数で指定されるファイルの内容を「&'static str」としてその場に展開します。これにより、外部のファイルをコードに埋め込むことができます。
(4)では、(3)の処理の一部ですが表示すべき投稿データをfor式でレスポンスボディー文字列に追加しています。このとき、詳細ページ(/posts/{id})を表示するためのアンカーを各項目に付けています。
(5)では、HttpResponseのインスタンスを生成して戻り値としています。OkメソッドでステータスOKとし、content_typeメソッドでレスポンスヘッダ(コンテンツはHTML、UTF-8エンコーディング)を指定し、bodyメソッドでレスポンスボディーを返しています。このように、ボディーがHTMLである場合にはレスポンスヘッダも適切に設定する必要がありますので注意しましょう(既定では指定なしとなります)。
ここでアプリケーションをビルド、実行し、Webブラウザで「http://localhost:8000/posts」を開いてみます。冒頭の図1のように表示されれば全投稿の一覧表示ページの作成は成功です。
このようにActix Webでは、CGIやJavaにおけるサーブレットのようにHTMLをゴリゴリ書いてコンテンツとするのが基本ですが、本来はテンプレートなどを用いてHTMLの直接生成はできるだけ避けるべきです。このような例は、後続の回で取り上げる予定です。
include_str!は、「&'static str」としてその場に展開されます。&strではなく「'static」が付与されていますが、これはstaticライフタイム指定子です。ライフタイムとは生存期間であり、この場合はstaticと付いているように、プログラムの実行中ずっと存在していることを意味します。ここではあまり考慮していませんが、その性質上include_str!の使い過ぎ、大きなファイルへの適用には気を付けた方がよいでしょう。
今回は、Actix Webで投稿アプリの骨組みを作成し、投稿の一覧表示機能を実装するまでの手順を紹介しました。
次回は、今回の続きとして作成ページや詳細ページなどを作成しながら、リクエストパラメーターの受け取り方やその処理方法などを紹介します。
WINGSプロジェクト
有限会社 WINGSプロジェクトが運営する、テクニカル執筆コミュニティー(代表山田祥寛)。主にWeb開発分野の書籍/記事執筆、翻訳、講演等を幅広く手掛ける。2021年10月時点での登録メンバーは55人で、現在も執筆メンバーを募集中。興味のある方は、どしどし応募頂きたい。著書、記事多数。
・サーバーサイド技術の学び舎 - WINGS(https://wings.msn.to/)
・RSS(https://wings.msn.to/contents/rss.php)
・X: @WingsPro_info(https://x.com/WingsPro_info)
・Facebook(https://www.facebook.com/WINGSProject)
Copyright © ITmedia, Inc. All Rights Reserved.