第12回からは、第11回までで作成した投稿アプリの延長として、Rust and WebAssemblyでTODOアプリを開発します。第12回では、プロジェクトの構築やさまざまな準備のためのコードを通じて、Rust and WebAssemblyの基本的な利用方法を理解します。
この記事は会員限定です。会員登録(無料)すると全てご覧いただけます。
本連載のサンプルコードをGitHubで公開しています。こちらからダウンロードしてみてください。
WebAssemblyとは、プログラムの高速な実行が特徴である、ブラウザにおける新しい言語フォーマットです。Rust and WebAssemblyとは、RustとWebAssemblyを連携するオープンソースプロダクトを開発するワーキンググループです。それぞれ、@ITの連載「いろんな言語で試す、WebAssembly入門」と、その第5回「RustでWebAssembly――「Rust and WebAssembly」を体験する」で紹介しています。本記事の理解にWebAssemblyの知識は必須ではありませんが、興味があれば一読することをお勧めします。
Rust and WebAssemblyの主なプロダクトは以下の通りです。本記事では、これらを使って、ブラウザで動作するTODOアプリを作っていきます(図1)。
中核を成すのは、wasm-bindgenです。wasm-bindgenの備えるAPI(js-sys、web-sysクレート)を使ってRustでアプリをコーディングします。js-sysはJavaScript APIを、web-sysはWindowをはじめとするオブジェクトへのアクセスを提供しており、JavaScript側の備える機能にほぼ一対一に対応した構造体、メソッドを使って、ほとんどの処理をRustで記述できます。
Rust and WebAssemblyを使うには、Rustの他にNode.jsが必要ですので、インストールしておいてください。なお、本稿作成時点で最新のNode.jsは20.16で、npmのバージョンは10.8.xです。npmについては、後述のコマンドの実行でエラーとなるので、nvmコマンドで少し古いバージョンの10.7.Xをインストールしてください。
wasm-packをインストールします。macOS環境では、「wasm-pack」に記載されているコマンドでターミナルからwasm-packのインストールを実施します。Windows環境では、同ページからwasm-pack-init.exeをダウンロードしてインストールを実施します。
Cargoパッケージマネジャーで、テンプレートからプロジェクトを生成可能にするcargo-generateをインストールしておきます。
% cargo install cargo-generate
アプリは、Rust and WebAssemblyが提供するテンプレートwasm-pack-templateから作成します。
プロジェクトtodo-appをテンプレートから作成します。コマンドを実行するとプロジェクト名を尋ねられるので、「todo-app」を指定してください。同名のフォルダが作成されるので、以降は、このフォルダで作業します。
% cargo generate --git https://github.com/rustwasm/wasm-pack-template 🤷 Project Name: todo-app …略… ✨ Done! New project created /Users/…/todo-app
テンプレートからプロジェクトを作成した時点で、既定のファイルが用意されます。まずはWebページとして動作させて既定の動作を見てみましょう。WebAssemblyパッケージの作成を、wasm-packコマンドにbuildサブコマンドを指定して実行します。
% wasm-pack build [INFO]: 🎯 Checking for the Wasm target... …略… [INFO]: 📦 Your wasm pkg is ready to publish at /Users/…/todo-app/pkg.
これで、todo-appフォルダに、pkgフォルダが作成されます。pkgフォルダはパッケージとなっており、package.jsonファイル、WebAssemblyへコンパイルした.wasmファイル、それを読み込み実行するJavaScriptヘルパーの.js、.tsファイルなどから構成されます。
次に、Webページのパッケージを作成します。以下のコマンドで、siteパッケージを作成します(パッケージの名前は何でもOKです)。ここで指定されているwasm-appが、Rust and WebAssemblyのプロダクトcreate-wasm-appに対応します。
% npm init wasm-app site 🦀 Rust + 🕸 Wasm = ❤
ここでsiteフォルダに移動し、依存パッケージを以下のコマンドでインストールします。
% npm install
pkgパッケージを、siteパッケージの依存パッケージとするための設定を行います。この設定により、プロジェクトの修正がsiteパッケージに反映されるようになります。まず、pkgフォルダに移動して以下のコマンドを実行してください。
% npm link added 1 package, and audited 3 packages in 1s found 0 vulnerabilities
続けて、siteフォルダに移動して以下のコマンドを実行してください。引数のtodo-appは、プロジェクト名に一致する必要があります。
% npm link todo-app added 1 package, and audited 590 packages in 9s
ここまでの作業で、todo-app以下の構成は大まかに以下のようになっています。pkgパッケージをsiteパッケージから読み込み、利用する形になります。node_modulesフォルダにあるtodo-appフォルダが、pkgフォルダへのリンクになっているのがポイントです。
todo-app +―pkg wasm-pack buildで作成されたWebAssemblyパッケージ +―site npm initで作成されたWebページのパッケージ | +―node_modules 依存パッケージ | +―todo-app pkgパッケージへのリンク +――src Rustのソースファイル
最後に、pkgパッケージを利用するために、index.jsファイルをリスト1のように修正します。
import * as wasm from "todo-app"; ※既定のhello-wasm-packから修正 wasm.greet();
ここでアプリを実行してみます。Node.jsのバージョンが17.X以降の場合はエラーとなることがあるので、環境変数NODE_OPTIONSを以下のように設定し、実行してください(Windows環境ではSETコマンド)。このオプションは、OpenSSLの旧バージョンを使うためのものでセキュリティ上は好ましくないですが、動作検証と割り切って指定しています。
% export NODE_OPTIONS=--openssl-legacy-provider % npm run start
「http://localhost:8080/」にアクセスして、図2のようにポップアップ表示されれば成功です。
TODOアプリとしての機能を実装していく前に、基本的な部分を準備しておきます。
アプリで発生する問題を解決しやすくするために、ロガーを準備しておきます。ロガーとしては、JavaScriptのconsolo.log関数をそのまま使えます。Cargo.tomlファイルに、以下のコマンドでweb-sysクレートを追加し、リスト2のように[dependencies.web-sys]セクションに修正して、console featureを追加します(続けて必要になるStorage、Window、Document、Element、HtmlElementも、ここで追加してしまっています)。
% cargo add web-sys
web-sys = "0.3.70" ↓ [dependencies.web-sys] version = "0.3.70" features = [ 'console', 'Storage', 'Window', 'Document', 'Element', 'HtmlElement', ]
[dependencies]セクションの記法として、featuresオプションが長くなる場合に、[dependencies.crate]として切り離すことができます。web-sysはたくさんのfeaturesがあり、本記事でも多数利用することになるので、あらかじめこの記法を用いて煩雑になるのを防いでいます。
続けて、console.log関数をRustから呼び出すコードを、src/utils.rsファイルにリスト3のように追加します。
use wasm_bindgen::prelude::*; (1) use web_sys::console; …略… pub fn log(s: &String) { (2) console::log_1(&JsValue::from(s)); }
(1)は、必要な名前空間の取り込みです。このように、関数コードなどで必要な名前空間は、適宜取り込むようにしてください。以降は、記載を省略します。
(2)は、log関数の定義です。処理内容は、console::log_1関数の呼び出しです。関数名に数字が付いているのは、log関数の引数の数に合わせてRustの関数が用意されているためです(Rustの関数には可変長引数の仕様がないため)。そのため、log_2,log_3というように引数の個数に合わせた関数がありますが、ここでは簡略化のために引数1個のlog_1関数を使います。複数の引数を使いたい場合は、format!マクロなどで1個の引数にまとめてしまいます。
これで、「log("hello")」「log(&message)」のように、任意の場所でログをコンソールに書き出すことができます。
log_1関数の引数であるJsValue型は、JavaScriptコードとやり取りするための汎用(はんよう)的な型です。リスト3の場合、fromメソッドで&String型からのJsValue生成としているので、log_1関数には文字列への参照が渡りますが、数値や文字列にかかわらず受け入れることができるように、汎用的なJsValueを受け取るようにしているのです。
アプリの外形となる、HTMLを用意しておきます。既定でHTMLファイルが用意されていますが、中身はbootstrap.jsファイル(index.jsファイルを呼び出すだけ)を呼び出すためだけのものなので、本記事用に全て置き換えてしまうことにします。CSSフレームワークBootstrapを使うなど、基本的な構成は第11回までと同様なので、全容は配布サンプルを参照してください。リスト4には、本アプリで特徴的な部分のみ示します。
…略… <div> <p class="mb-2"> (1) <button id="new_button" class="btn btn-primary me-2">+</button> </p> </div> <div id="main"> (2) <p id="no_task">タスクがありません。</p> <div id="items"></div> </div> …略… <script src="./bootstrap.js"></script> (3) …略…
アプリの実行中に変化しない「+」ボタンなどは、(1)のようにHTMLにあらかじめ埋め込んでしまいます。(2)はメインのコンテンツです。初期内容としては、タスクがない旨のメッセージと、タスク一覧を表示するためのホルダーとなっており、これらは動的に変化します。(3)は、エントリポイントであるindex.jsファイルの、bootstrap.jsファイルを経由した読み込みです。
タスクのデータは、Web Storageに保存することにします。新規にstore.rsファイルを作り、そこにタスクデータの構造体とストアの構造体を定義し、必要なメソッドを実装していきます。
store.rsには、リスト5のように構造体を定義します。(1)はタスクデータ(タスクID、タイトル、期限、完了フラグ)の構造体Taskです。(2)はストアの構造体TaskStoreで、Storageのインスタンスと名前、Taskのリストを保持します(StorageはWeb Storageを扱うための型)。
// タスクの構造体 pub struct Task { (1) pub id: i32, // タスクID pub title: String, // タスクのタイトル pub period: String, // タスクの期限 pub completed: bool, // 完了フラグ } // データストアの構造体 pub struct TaskStore { (2) local_storage: web_sys::Storage, // Web Storageのインスタンスを保持 name: String, // Web Storageの名前を保持 pub list: Vec<Task>, // タスクリストを保持 } impl TaskStore { // データストアの初期化関数 pub fn new(name: &str) -> Option<TaskStore> { (3) let window = web_sys::window()?; // WindowオブジェクトからLocal Storageを取得する if let Ok(Some(local_storage)) = window.local_storage() { let mut store = TaskStore { local_storage, name: String::from(name), list: Vec::new(), }; // 以下はダミーデータ let dummy = Task {id: 1, title: "生活費を口座に入金する".to_string(), period: "2024-08-20".to_string(), completed: false}; store.list.push(dummy); …もう2件ほど追加する… Some(store) } else { None } } }
(3)は、TaskStoreに実装するメソッドの一つです。このnewメソッドは、引数のnameをLocal Storageの名前として設定し、local_storageメソッドでLocal Storageオブジェクトを取得、タスクデータを初期化してTaskStoreインスタンスを返します。この時点では、ダミーのタスクデータをハードコードし、タスクデータに追加しています。次回以降で、タスクの追加などの編集機能の実装時に、実際のLocal Storageを使った処理に置き換えていきます。
HTMLをコードで生成すると冗長で煩雑になるので、テンプレートを使います。今回は、第4回でも少し紹介した、JinjaベースのテンプレートエンジンであるAskamaを使用します。以下のコマンドで、askamaクレートをプロジェクトに追加してください。
% cargo add askama
テンプレートファイルは、プロジェクトルートにtemplatesフォルダを作成して設置します。まずはタスクの表示に使うitem.htmlファイルをリスト6のように作成します。
<div id="task{{item.id}}" class="d-flex w-100 border border-1 rounded mb-2"> (1) <input type="hidden" value="{{item.id}}" /> (2) <div class="border p-2"> {% if item.completed %} (3) <input type="checkbox" checked /> {% else %} <input type="checkbox" /> {% endif %} </div> <div class="flex-grow-1 border p-2">{{item.title}}</div> <div class="border p-2">{{item.period}}</div> </div>
ポイントは、(1)のようにタスクを識別するためのid属性を付加すること、(2)のようにタスクIDそのものを隠しフィールドで埋め込むことです。これにより、後ほどタスククリック時にタスクが判別できたり、タスクIDを用いた処理が可能になります。値の埋め込みは、中カッコを二重にしたもの({{ }})でフィールド名などの式を囲みます。フィールド名は、リスト7の構造体で定義されている必要があります。
(2)では、完了フラグ(completed)の状態に応じてチェックボックスのchecked属性を付け分けています。このように、{% if %}〜{% else %}〜{% endif %}で条件分岐ブロックを作ることができます。この他、記法ルールの詳細は「Askama - Askama」を参照してください。
次に、テンプレート利用のための構造体と関数を、template.rsファイルを作成してリスト7のように記述します。
// テンプレート構造体の定義 #[derive(Template)] (1) #[template(path = "item.html")] struct ItemTemplate<'a> { item: &'a store::Task, } // テンプレートをレンダリングする関数 pub fn render_item(item: &store::Task) -> String { (2) let template = ItemTemplate { item: &item }; template.render().unwrap() }
(1)はテンプレート構造体に必要な注釈です。template注釈は、テンプレートに対応するファイルをpath引数で指定します。フィールドが参照を含む場合には構造体にライフタイム注釈が必要です。テンプレートに現れる全てのフィールドが含まれる必要があります。
(2)はレンダリングを実行する関数です。テンプレート構造体をインスタンス化し、renderメソッドを呼び出すと、レンダリングされたHTML文字列が返ります。これを呼び出し側でDOM(Document Object Model)に挿入するといった処理を実行しますが、それについては次回に紹介します。
今回は、Rust and WebAssemblyを使ったSPA(Single Page Application)開発について、そのあらましとプロジェクトの準備までを紹介しました。
次回からは、このアプリに表示と更新の機能を実装していく過程で、DOMへのアクセス、イベントハンドラの書き方と設定などを紹介していきます。
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.