rust
WebAssembly
0

Rust / yew で 仮想DOMなウェブアプリケーションを作ってみた

前提として、自分の Rustの知識は 1年に1回ぐらい思い立った時にちょろっとやるぐらいで、基礎文法をググりながら、複雑なライフタイムとか書こうとすると手が止まる程度の知識。勘で書いてる。

前提

cargo build --target wasm-unknown-unknown ができるようになるまでは省略。

調べた感じ、Rust の wasm ビルドでウェブの何かしらをやろうとすると、次のような選択肢がある。

  • プレーンな wasm。基本的に数値(float)だけしか扱えない。ポインタの開始位置とサイズを返し、wasm メモリ空間のArrayBufferを自前でデコードする
  • https://github.com/rustwasm/wasm-bindgen : ↑で生成された wasm の読み込みラッパーやTSの型定義、Rust 側からJSのメモリ空間を参照する諸々をやってくれるツール。
  • js-sys: ↑の中で、より高水準なラッパー。開発中らしくドキュメントが少ない
  • web-sys: もっと抽象度が高そうな何か?開発中らしくドキュメントがない。ちゃんと調べてない。
  • https://github.com/koute/stdweb wasm-bindgen とは別系列の高水準なJSラッパー。

生で使う以外は、主に stdweb と wasm-bindgen の2系列ある。 両方使ってみた感想だと、 stdweb はすべてを rust で管理しようとする感じで、 wasm-bindgen は webpack などの js エコシステムで連携しながら使うといった感じ。

Yew

https://github.com/DenisKolodin/yew

stdweb に依存。Redux のパクリ元の Elm のアーキテクチャと仮想DOMを実装している。自分がReactに慣れてるので、これで遊んでみることにする。

カウンター作ってみた

こんなやつ

デプロイしておいた。IE以外は動く https://frosty-clarke-3d3c34.netlify.com
stdweb が emscripten 通せば asm.js ビルドをサポートしているので、頑張れば動きそう。今回は頑張らない。

コードはここ https://github.com/mizchi-sandbox/hello_cargo_web_yew

コード抜粋

#[macro_use]
extern crate yew;
use std::result::Result::{Err, Ok};
use yew::prelude::{App, Component, ComponentLink, Html, Renderable, ShouldRender};
use yew::services::ConsoleService;

struct Model {
  console: ConsoleService,
  value: i64,
  adding_value_text: String,
}

enum Msg {
  Increment,
  AddByAddingValue,
  SetAddingValue(String),
}

impl Component for Model {
  type Message = Msg;
  type Properties = ();

  fn create(_: Self::Properties, _: ComponentLink<Self>) -> Self {
    Model {
      console: ConsoleService::new(),
      value: 0,
      adding_value_text: "".to_string(),
    }
  }

  fn update(&mut self, msg: Self::Message) -> ShouldRender {
    match msg {
      Msg::Increment => {
        self.value = self.value + 1;
        true
      }
      Msg::AddByAddingValue => {
        match &self.adding_value_text.parse::<i64>() {
          Ok(v) => {
            self.value = self.value + v;
            self.adding_value_text = "".to_string();
          }
          Err(_) => {
            self.console.log("Parse error");
          }
        };
        true
      }
      Msg::SetAddingValue(value) => {
        self.adding_value_text = value;
        true
      }
    }
  }
}

impl Renderable<Model> for Model {
  fn view(&self) -> Html<Self> {
    html! {
        <div>
          <div>
            <button onclick=|_| Msg::Increment,>{ "Increment" }</button>
          </div>

          <div>
            <button onclick=|_| Msg::AddByAddingValue,>{ "Add" }</button>
            <input
              oninput=|e| Msg::SetAddingValue(e.value),
              value=&self.adding_value_text,
            />
          </div>

          <p>{ self.value }</p>
        </div>
    }
  }
}

fn main() {
  yew::initialize();
  App::<Model>::new().mount_to_body();
  yew::run_loop();
}

ややこしかったのは、文字列をパースするときのパターンマッチで、ちょっと調べた。あと yew 0.5.0 が cargo にリリースされてなかったので、 Cargo.toml で GitHub を直接参照している。ドキュメント や examples が 0.5.0 のものしかない。

性能評価

こういうライブラリ、やたらライブラリサイズが大きかったりしないか?と調べてみた。

ビルドサイズ。

$ ls -l target/deploy
total 368
drwxr-xr-x  5 mz  staff   160B  8 18 22:45 .
drwxr-xr-x  7 mz  staff   224B  8 18 21:54 ..
-rw-r--r--  1 mz  staff    27K  8 18 22:45 counter.js
-rw-r--r--  1 mz  staff   151K  8 18 22:45 counter.wasm
-rw-r--r--  1 mz  staff   815B  8 18 22:45 index.html

180k。react + react-dom が 130k なのを考えると、問題なし。

実際の評価時間。

特に問題なさそう。

感想

JSXのマクロがあって、React/Elm知ってると、 yew の examples を参照すると素直に書ける感じ。特に戸惑うことはなかった。正直 Rust の数値変換とかパターンマッチの文法思い出すほうが時間かかった。

Rustエコシステム全般の話だが、 nightly じゃないと何かと動かない。だいたいマクロ拡張は nightly 使わないと駄目。wasm 周り特有の事情かどうかは推し量れてないが、 stable に全く人権がないのに、nightly を使ったら自己責任みたいな雰囲気がちょっとつらかった。stable と nightly の間のバージョンがほしい。

実際にウェブアプリ書く場合、 wasm-bindgen + js-sys と stdweb の目指してる世界が似てて、どっちが主流になるかわからない不安がある。できれば相互に乗り入れられるようになってほしい。

しかし思ったよりも現実的にアプリが書けそう、といった感触で、JS資産を参照しなくていい、グラフ可視化ツールみたいな複雑なアプリケーションなら思ってたより選択肢になりそう、といった感想。