虎の穴開発室ブログ

虎の穴ラボ株式会社所属のエンジニアが書く技術ブログです

MENU

【WebAssembly連載第二回】改めてwasm-bindgenを使ってWebAssemblyでのDOM操作してみる

本記事は「WebAssembly連載」の第2回目の記事です。

皆さんこんにちは。虎の穴ラボのY.Fです。

結構昔の記事ですが、以下のような記事を出していました。

toranoana-lab.hatenablog.com

この記事では、WebAssemblyでブロック崩しを作ることを主眼に、各ツールの紹介や、実装の紹介をしてきました。

一方で、具体的によりWeb周りのAPIをWebAssemblyからどのように扱うかについては紹介していなかったと思うので、今回の記事では、改めてまとめてみようと思います。

利用技術

  • Rust 1.65.0
  • wasm-bindgen 最新版
    • JavaScriptとRust(WebAssembly)とのデータ等のやり取りを簡単にしてくれるクレート(ライブラリ)
  • web-sys最新版
    • RustからDOM APIを呼び出すためのライブラリ
  • wasm-pack 最新版
    • WebAssemblyで出来たファイルをライブラリとして整えてくれるツール

wasm-bindgenやwasm-packに関しては、MDNでのチュートリアルでも利用されています

developer.mozilla.org

DOM API

DOM APIを利用する場合は、web-sysというクレートを使います

rustwasm.github.io

Document、Window

Cargo.tomlに以下のように追記します。

[dependencies.web-sys]
version = "0.3"
features = [
    "Document",
    "Window",
    "Element"
]

DocumentオブジェクトやWindowオブジェクトを扱う場合は、以下のように扱えます

mod utils;

use wasm_bindgen::{prelude::*};

// When the `wee_alloc` feature is enabled, use `wee_alloc` as the global
// allocator.
#[cfg(feature = "wee_alloc")]
#[global_allocator]
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;

#[wasm_bindgen]
extern "C" {
    fn alert(s: &str);
}

#[wasm_bindgen]
pub fn main() {
    utils::set_panic_hook();
    let window = web_sys::window().expect("window object is not found");
    let document = window.document().expect("document object is not found");
}

Windowオブジェクトや、Documentオブジェクトは環境によって存在するかどうかわからないため、Option 型で取得されます。 したがって、取得できなかった場合の処理を何らかの方法で記述する必要があります。

wasm用のプロジェクトを生成した場合、panicしたときにコンソールにエラー表示してくれる set_panic_hook が予め用意されています。 今回はそちらを利用して、各オブジェクトが存在しない場合は expect メソッドを呼ぶことでpanicを引き起こし、コンソールに表示するようにしました。

DOM取得

一般的な getElementById や、 querySelector のようなメソッドが利用可能です。 以下のようなHTMLからid="test"のエレメントを取得してみます。

<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="utf-8" >
    <title>WASM DOM</title>
  </head>

  <body>
    <div id="test">test</div>
  </body>
</html>

次に、結果をconsoleで表示したいので、console.logを使えるようにCargo.tomlに追記します。

[dependencies.web-sys]
version = "0.3"
features = [
    "Document",
    "Window",
    "Element",
    "console"
]
#[wasm_bindgen]
pub fn main() {
    utils::set_panic_hook();
    let window = web_sys::window().expect("window object is not found");
    let document = window.document().expect("document object is not found");
    let elem = document.query_selector("#test").expect("querySelector failed").expect("id='test' is not found");
    let content = elem.text_content().expect("textContent is invalid");
    console::log_1(&content.into());
}

query_selector 等一部の関数は、実行結果が Result<Option<Hoge> JsValue> といった形なので、expectを二度呼び出しています。どのみち実行に失敗すれば、それ以上先には進めないので良しとします。

fetch APIの利用

冒頭で紹介したブロック崩しでも利用してますが、fetch等も使えます。 APIアクセス先としては以下を利用します。

reqres.in

まずは、Cargo.tomlに wasm-bindgen-futures を追記します。

[dependencies]
wasm-bindgen = "0.2"
console_error_panic_hook = { version = "0.1.6", optional = true }
wee_alloc = { version = "0.4.5", optional = true }
js-sys = "0.3"
wasm-bindgen-futures = "0.4.33"

また、web-sysの部分に以下を追記します。

[dependencies.web-sys]
version = "0.3"
features = [
    "Document",
    "Window",
    "Element",
    "console",
    "RequestInit",
    "Request", 
    "RequestMode", 
    "Response",
]

実装してみます。

#[wasm_bindgen]
pub async fn ajax(window: &Window) -> Result<JsValue, JsValue> {
    let mut opts = RequestInit::new();
    let url = "https://reqres.in/api/users?page=1";
    opts.method("GET");
    opts.mode(RequestMode::Cors);
    let request = Request::new_with_str_and_init(
        url,
        &opts,
    )?;

    let request_promise = window.fetch_with_request(&request);
    let resp_value = JsFuture::from(request_promise).await?;
    let resp: Response = resp_value.dyn_into()?;

    let json_value = JsFuture::from(resp.json()?).await?;
    Ok(json_value)
}

main関数での呼び出しは以下のような形です。

    spawn_local(async move {
      let res = ajax(&window).await.unwrap();
      console::log_1(&res.into());
    });

spawn_localで Future を起動します。Rustで非同期処理を実行するにはランタイムで起動する必要があります。以下記事等が参考になるかと思います。

tech-blog.optim.co.jp

クロージャ

いわゆるJavaScriptの匿名関数も作れます。JavaScriptの匿名関数を利用して、擬似的にプライベート変数を表現したりなど常用パターンだと思いますが、似たように環境キャプチャができます。 クロージャと環境については以下等を参考にしてください。

doc.rust-jp.rs

buttonにイベントを付けてみたいと思います。

まずはHTMLにボタンを追加します。

<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="utf-8" >
    <title>WASM DOM</title>
  </head>

  <body>
    <div id="test">test</div>
    <button type="button" id="countup">カウント開始</button>
  </body>
</html>

このボタンにイベントを追加しつつ、中身のテキストを書き換えたいと思います。

今回は、カウンターを追加してボタンクリックするたびにカウントアップさせます。

    let num_clicks = document
        .get_element_by_id("countup")
        .expect("should have #countup on the page");

    let mut clicks = 0;
    let event = Closure::<dyn FnMut()>::new(move || {
        clicks += 1;
        num_clicks.set_inner_html(&clicks.to_string());
    });
    document
        .get_element_by_id("countup")
        .expect("should have #countup on the page")
        .dyn_ref::<HtmlElement>()
        .expect("#countup be an `HtmlElement`")
        .set_onclick(Some(event.as_ref().unchecked_ref()));

    event.forget();

Rustのクロージャをwasm_bindgen::ClosureでラップすることでJS側とWasm(Rust)側との橋渡しが出来ます。 注意すべき点としては最後に forget を呼んでいることです。forgetの説明は以下にあります。

rustwasm.github.io

Rustの関数のままではスコープを抜けたりするとすぐ回収されて使えなくなるため、管理をJS側に任せます。画面が表示されている間ずっと有効な関数である必要があるので、このような処理が必要になります。

まとめ

今回は連載記事二回目として、過去書いた記事で使ったwasm-bindgen及び、それに伴うDOM操作(web-sys)について紹介しました。

次回以降ではJavaScriptのAPIを扱う js-sys 等を紹介したあと、実際にフロントエンドのアプリをWebAssemblyで作ってみたいと思います。

P.S.

採用

虎の穴では一緒に働く仲間を募集中です!
この記事を読んで、興味を持っていただけた方はぜひ弊社の採用情報をご覧下さい。
yumenosora.co.jp

LINEスタンプ

エンジニア専用のメイドちゃんスタンプが完成しました!
「あの場面」で思わず使いたくなるようなスタンプから、日常で役立つスタンプを合計40個用意しました。
エンジニアの皆さん、エンジニアでない方もぜひスタンプを確認してみてください。 store.line.me