RustでクライアントサイドWebフレームワークを作っているよ

アメミヤです。

先日、会社でやっているポッドキャスト、ajitofmに出演しました。 (あんまり喋んなかったけど)

ajito.fm

id: mizchiさんをゲストに招いての収録でした!収録中も、そのあとのajitingも色々な話が聞けて楽しかったです。

で、今回の話なのですが、収録中の Rustのような言語でUIを記述するのは難しい というmizchiさんの発言に対し、そのタイミングでは首肯してしまったものの後になって、本当にそうか〜?という気持ちになったため、自分で作っています。

github.com

TodoMVC レベルの物が一応動くようになったのでブログを書きますよ。

アプリケーションコードの例

たとえば単純なカウンターはこんなんになる。

#![feature(proc_macro)]

extern crate squark;
extern crate squark_macros;
extern crate squark_stdweb;
extern crate stdweb;

use stdweb::traits::*;
use stdweb::web::document;
use squark::{App, Runtime, View};
use squark_stdweb::StdwebRuntime;
use squark_macros::view;

#[derive(Clone, Debug, PartialEq)]
struct State {
    count: isize,
}

impl State {
    pub fn new() -> State {
        State { count: 0 }
    }
}

#[derive(Clone, Debug)]
enum Action {
    Increment,
    Decrement,
}

#[derive(Clone, Debug)]
struct CounterApp;
impl App for CounterApp {
    type State = State;
    type Action = Action;

    fn reducer(mut state: State, action: Action) -> State {
        match action {
            Action::Increment => {
                state.count += 1;
            }
            Action::Decrement => {
                state.count -= 1;
            }
        };
        state
    }

    fn view(state: State) -> View<Action> {
        let count = state.count.clone();
        view! {
            <div>
                { count.to_string() }
                <button onclick={ |_| Some(Action::Increment) }>
                    increment
                </button>
                <button onclick={ |_| Some(Action::Decrement) }>
                    decrement
                </button>
            </div>
        }
    }
}

fn main() {
    stdweb::initialize();
    StdwebRuntime::<CounterApp>::new(
        document().query_selector("body").unwrap().unwrap(),
        State::new(),
    ).run();
    stdweb::event_loop();
}

これがWebブラウザでWebAssemblyで動くの、割とワクワクしませんか。
もうちょっと複雑な、上に貼ったTodoMVCの例はここにあります。

作ったもの

ピュアRustなバーチャルDOM実装

バーチャルDOMの実装は実DOMの知識を持っていません。
2つのバーチャルDOMを比較し、どの要素や属性にどんな操作をするべきなのかを、Diffとして得るだけが仕事になっています。

上の例で impl している、Action-Reducer-State-Viewを持つAppのtrait

ElmHyperAppを参考にしたアーキテクチャです。

Runtimeのtrait

Appのライフサイクルを管理しつつ、実際にUIを操作する層です。
仕事内容としては

Actionの発生検知 -> Reducerへの引き渡し -> Stateの更新 -> バーチャルDOMの更新 -> Diffのハンドリング

です。
ただし、このランタイムも同様に現実のDOMの知識は持っておらず、Webブラウザへのバインディングimpl Runtime として別途行う設計になっています。 これは、RustのWebAssembly界隈の変化が速く、特定の環境に依存してしまうと陳腐化することを恐れての物です(今回で言えば後述するstdwebとcargo-web)

stdwebを使ったRuntimeの実装

stdwebは、Rustを使ってブラウザAPIを呼び出すためのバインディングライブラリです。
Diffに応じて、現実のDOMツリーへどのような操作を行うかを実装しています。

JSX風の構文のための、マクロ

proc_macroを使ったマクロの定義で、JSX風の構文をコンパイルタイム時にRustの構文へと変換することができます。
すごい!けれどコンパイル遅い!!

また、JSX風の構文パーサーはpest-parserによって、これまたダイナミックライブラリのコンパイル時に生成しています。 こんな感じのpegを書きました。

課題と今後

とここまで書いたけれど実はこれ、stableはおろか、最新のnightlyですら走らない状態です。
動く最新のnightlyは nightly-2017-12-01 。 というのも、 proc_macro まわりにガンガン変更が入っていて、マクロが不正なコードを吐いてしまいます。

github.com

一番痛いのが↑で、健全マクロなので呼び出したスコープの何かにアクセスできないのは分かるんですが、マクロ中に含めた識別子にもアクセスできない状態。

また、現在のところ、全てのバーチャルDOMツリーに存在する全てのハンドラを設定しなおすDiffを吐いてしまい、パフォーマンスが良くありません。
これは、ハンドラに設定するクロージャがキャプチャしている値の変化を比較できれば、そのハンドラの同一性を確認できるはずで、方法を模索しています。

Appトレイトのインターフェースが妥当かどうかも謎なので、そこも方向を定めていきたい。 特に、reducerやviewは今のところAssociated functionsになってますが、もっと複雑な、APIリクエストをするようなアプリケーションだとメソッドにしたくなるのかなー?のようなことを思っています。 また、reducerがstateをどう受けてどう返すべきなのかもよく分かっていない(&state -> state なのか state -> state なのか &mut state なのか)

あとは、wasm-bindgenというjs-rust間のバインディングツールが良さげで、なんだかコミュニティ的にそっちが主流になりそうな雰囲気なので、WasmBindgenRuntimeを実装しようかとか、 actix-webを使ってサーバーサイドレンダリングを出来るようにしようかなー、とか考えています。

引続きやっていくぞい。