Rust and WebAssemblyでSPAを作ってみようWebアプリ実装で学ぶ、現場で役立つRust入門(12)

第12回からは、第11回までで作成した投稿アプリの延長として、Rust and WebAssemblyでTODOアプリを開発します。第12回では、プロジェクトの構築やさまざまな準備のためのコードを通じて、Rust and WebAssemblyの基本的な利用方法を理解します。

» 2024年09月19日 05時00分 公開

この記事は会員限定です。会員登録(無料)すると全てご覧いただけます。

「Webアプリ実装で学ぶ、現場で役立つRust入門」のインデックス

連載:Webアプリ実装で学ぶ、現場で役立つRust入門

 本連載のサンプルコードをGitHubで公開しています。こちらからダウンロードしてみてください。

Rust and WebAssemblyとは

 WebAssemblyとは、プログラムの高速な実行が特徴である、ブラウザにおける新しい言語フォーマットです。Rust and WebAssemblyとは、RustとWebAssemblyを連携するオープンソースプロダクトを開発するワーキンググループです。それぞれ、@ITの連載「いろんな言語で試す、WebAssembly入門」と、その第5回「RustでWebAssembly――「Rust and WebAssembly」を体験する」で紹介しています。本記事の理解にWebAssemblyの知識は必須ではありませんが、興味があれば一読することをお勧めします。

 Rust and WebAssemblyの主なプロダクトは以下の通りです。本記事では、これらを使って、ブラウザで動作するTODOアプリを作っていきます(図1)。

  • wasm-pack……RustからWebAssemblyファイルなどを生成するツール
  • wasm-bindgen……RustとJavaScriptを連携させるためのライブラリとツール
  • wasm-pack-template……wasm-packによるRustプロジェクトのテンプレート
  • create-wasm-app……Webページのパッケージ(npm init用)

 中核を成すのは、wasm-bindgenです。wasm-bindgenの備えるAPI(js-sys、web-sysクレート)を使ってRustでアプリをコーディングします。js-sysはJavaScript APIを、web-sysはWindowをはじめとするオブジェクトへのアクセスを提供しており、JavaScript側の備える機能にほぼ一対一に対応した構造体、メソッドを使って、ほとんどの処理をRustで記述できます。

図1 作成するTODOアプリのイメージ 図1 作成するTODOアプリのイメージ

Rust and WebAssemblyを使う準備

 Rust and WebAssemblyを使うには、Rustの他にNode.jsが必要ですので、インストールしておいてください。なお、本稿作成時点で最新のNode.jsは20.16で、npmのバージョンは10.8.xです。npmについては、後述のコマンドの実行でエラーとなるので、nvmコマンドで少し古いバージョンの10.7.Xをインストールしてください。

wasm-packをインストールする

 wasm-packをインストールします。macOS環境では、「wasm-pack」に記載されているコマンドでターミナルからwasm-packのインストールを実施します。Windows環境では、同ページからwasm-pack-init.exeをダウンロードしてインストールを実施します。

cargo-generateをインストールする

 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のソースファイル

index.jsファイルを修正する

 最後に、pkgパッケージを利用するために、index.jsファイルをリスト1のように修正します。

  1. import * as wasm from "todo-app"; ※既定のhello-wasm-packから修正
  2. wasm.greet();
リスト1:site/index.js

 ここでアプリを実行してみます。Node.jsのバージョンが17.X以降の場合はエラーとなることがあるので、環境変数NODE_OPTIONSを以下のように設定し、実行してください(Windows環境ではSETコマンド)。このオプションは、OpenSSLの旧バージョンを使うためのものでセキュリティ上は好ましくないですが、動作検証と割り切って指定しています。

% export NODE_OPTIONS=--openssl-legacy-provider
% npm run start

 「http://localhost:8080/」にアクセスして、図2のようにポップアップ表示されれば成功です。

図2 既定のアプリ 図2 既定のアプリ

TODOアプリ実装の準備

 TODOアプリとしての機能を実装していく前に、基本的な部分を準備しておきます。

ロガーを準備する

 アプリで発生する問題を解決しやすくするために、ロガーを準備しておきます。ロガーとしては、JavaScriptのconsolo.log関数をそのまま使えます。Cargo.tomlファイルに、以下のコマンドでweb-sysクレートを追加し、リスト2のように[dependencies.web-sys]セクションに修正して、console featureを追加します(続けて必要になるStorage、Window、Document、Element、HtmlElementも、ここで追加してしまっています)。

% cargo add web-sys
  1. web-sys = "0.3.70"
  2. [dependencies.web-sys]
  3. version = "0.3.70"
  4. features = [
  5. 'console',
  6. 'Storage',
  7. 'Window',
  8. 'Document',
  9. 'Element',
  10. 'HtmlElement',
  11. ]
リスト2:Cargo.toml

 [dependencies]セクションの記法として、featuresオプションが長くなる場合に、[dependencies.crate]として切り離すことができます。web-sysはたくさんのfeaturesがあり、本記事でも多数利用することになるので、あらかじめこの記法を用いて煩雑になるのを防いでいます。

 続けて、console.log関数をRustから呼び出すコードを、src/utils.rsファイルにリスト3のように追加します。

  1. use wasm_bindgen::prelude::*; (1)
  2. use web_sys::console;
  3. …略…
  4. pub fn log(s: &String) { (2)
  5. console::log_1(&JsValue::from(s));
  6. }
リスト3:src/utils.rs

 (1)は、必要な名前空間の取り込みです。このように、関数コードなどで必要な名前空間は、適宜取り込むようにしてください。以降は、記載を省略します。

 (2)は、log関数の定義です。処理内容は、console::log_1関数の呼び出しです。関数名に数字が付いているのは、log関数の引数の数に合わせてRustの関数が用意されているためです(Rustの関数には可変長引数の仕様がないため)。そのため、log_2,log_3というように引数の個数に合わせた関数がありますが、ここでは簡略化のために引数1個のlog_1関数を使います。複数の引数を使いたい場合は、format!マクロなどで1個の引数にまとめてしまいます。

 これで、「log("hello")」「log(&message)」のように、任意の場所でログをコンソールに書き出すことができます。

【補足】JsValue型

 log_1関数の引数であるJsValue型は、JavaScriptコードとやり取りするための汎用(はんよう)的な型です。リスト3の場合、fromメソッドで&String型からのJsValue生成としているので、log_1関数には文字列への参照が渡りますが、数値や文字列にかかわらず受け入れることができるように、汎用的なJsValueを受け取るようにしているのです。

HTMLを用意する

 アプリの外形となる、HTMLを用意しておきます。既定でHTMLファイルが用意されていますが、中身はbootstrap.jsファイル(index.jsファイルを呼び出すだけ)を呼び出すためだけのものなので、本記事用に全て置き換えてしまうことにします。CSSフレームワークBootstrapを使うなど、基本的な構成は第11回までと同様なので、全容は配布サンプルを参照してください。リスト4には、本アプリで特徴的な部分のみ示します。

  1. …略…
  2. <div>
  3. <p class="mb-2"> (1)
  4. <button id="new_button" class="btn btn-primary me-2">+</button>
  5. </p>
  6. </div>
  7. <div id="main"> (2)
  8. <p id="no_task">タスクがありません。</p>
  9. <div id="items"></div>
  10. </div>
  11. …略…
  12. <script src="./bootstrap.js"></script> (3)
  13. …略…
リスト4:site/index.html

 アプリの実行中に変化しない「+」ボタンなどは、(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を扱うための型)。

  1. // タスクの構造体
  2. pub struct Task { (1)
  3. pub id: i32, // タスクID
  4. pub title: String, // タスクのタイトル
  5. pub period: String, // タスクの期限
  6. pub completed: bool, // 完了フラグ
  7. }
  8. // データストアの構造体
  9. pub struct TaskStore { (2)
  10. local_storage: web_sys::Storage, // Web Storageのインスタンスを保持
  11. name: String, // Web Storageの名前を保持
  12. pub list: Vec<Task>, // タスクリストを保持
  13. }
  14. impl TaskStore {
  15. // データストアの初期化関数
  16. pub fn new(name: &str) -> Option<TaskStore> { (3)
  17. let window = web_sys::window()?;
  18. // WindowオブジェクトからLocal Storageを取得する
  19. if let Ok(Some(local_storage)) = window.local_storage() {
  20. let mut store = TaskStore {
  21. local_storage,
  22. name: String::from(name),
  23. list: Vec::new(),
  24. };
  25. // 以下はダミーデータ
  26. let dummy = Task {id: 1, title: "生活費を口座に入金する".to_string(),
  27. period: "2024-08-20".to_string(), completed: false};
  28. store.list.push(dummy);
  29. …もう2件ほど追加する…
  30. Some(store)
  31. } else {
  32. None
  33. }
  34. }
  35. }
リスト5:src/store.rs

 (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のように作成します。

  1. <div id="task{{item.id}}" class="d-flex w-100 border border-1 rounded mb-2"> (1)
  2. <input type="hidden" value="{{item.id}}" /> (2)
  3. <div class="border p-2">
  4. {% if item.completed %} (3)
  5. <input type="checkbox" checked />
  6. {% else %}
  7. <input type="checkbox" />
  8. {% endif %}
  9. </div>
  10. <div class="flex-grow-1 border p-2">{{item.title}}</div>
  11. <div class="border p-2">{{item.period}}</div>
  12. </div>
リスト6:templates/item.html

 ポイントは、(1)のようにタスクを識別するためのid属性を付加すること、(2)のようにタスクIDそのものを隠しフィールドで埋め込むことです。これにより、後ほどタスククリック時にタスクが判別できたり、タスクIDを用いた処理が可能になります。値の埋め込みは、中カッコを二重にしたもの({{ }})でフィールド名などの式を囲みます。フィールド名は、リスト7の構造体で定義されている必要があります。

 (2)では、完了フラグ(completed)の状態に応じてチェックボックスのchecked属性を付け分けています。このように、{% if %}~{% else %}~{% endif %}で条件分岐ブロックを作ることができます。この他、記法ルールの詳細は「Askama - Askama」を参照してください。

 次に、テンプレート利用のための構造体と関数を、template.rsファイルを作成してリスト7のように記述します。

  1. // テンプレート構造体の定義
  2. #[derive(Template)] (1)
  3. #[template(path = "item.html")]
  4. struct ItemTemplate<'a> {
  5. item: &'a store::Task,
  6. }
  7. // テンプレートをレンダリングする関数
  8. pub fn render_item(item: &store::Task) -> String { (2)
  9. let template = ItemTemplate { item: &item };
  10. template.render().unwrap()
  11. }
リスト7:src/template.rs

 (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.

スポンサーからのお知らせPR

Coding Edge 記事ランキング

本日月間

注目のテーマ

4AI by @IT - AIを作り、動かし、守り、生かす
Microsoft & Windows最前線2025
AI for エンジニアリング
ローコード/ノーコード セントラル by @IT - ITエンジニアがビジネスの中心で活躍する組織へ
Cloud Native Central by @IT - スケーラブルな能力を組織に
システム開発ノウハウ 【発注ナビ】PR
あなたにおすすめの記事PR

RSSについて

アイティメディアIDについて

メールマガジン登録

@ITのメールマガジンは、 もちろん、すべて無料です。ぜひメールマガジンをご購読ください。