Edited at

Rust で仮想DOMを実装する‐1


今回すること

仮想DOMツリーのノードを実装し、それを描画する関数を実装します。

また、この記事の方針として、ResultOption については panic しないに違いないという前提でどんどん unwrap しています。もし実際にこのサンプルを参考にプロダクトを開発するのであれば、エラー処理を入れた方が良いかと思います。

最終的に以下の例のような出力を得ます。

実行結果例:

screenshot.png


準備

準備 をします。

次に cargo.toml に以下の内容を追加します。

[lib]

crate-type = ["cdylib", "rlib"]

[dependencies.wasm-bindgen]
version = "0.2.51"

[dependencies.web-sys]
version = "0.3.4"
features = [
'Document',
'Element',
'Node',
'Text',
]


実装


仮想DOMツリーのノードを作る

// lib.rs

enum VirtualNode {
Text(&'static str),
Element {
tag_name: &'static str,
children: Vec<VirtualNode>,
},
}

仮想DOMツリーのノード VirtualNode を実装します。<h1>Hello World</h1> を例にすると、<h1> タグに相当するのが VirtualNode::Element で、Hello World に相当するのが VirtualNode::Text です。これを Rust のコードで表すと

VirtualNode::Element {

tag_name: "h1",
children: vec![VirtualNode::Text("Hello World")],
}

のようになります。

とりあえず、 VirtualNode::Element にはタグ名を判別するための tag_name と、子要素のリストを保持する children のみを実装します。


仮想DOMを描画する

// lib.rs

fn render(document: &web_sys::Document, virtual_node: VirtualNode) -> web_sys::Node {
match virtual_node {
VirtualNode::Text(text) => document.create_text_node(text).into(),
VirtualNode::Element { tag_name, children } => {
let element = document.create_element(tag_name).unwrap();
for child in children {
element.append_child(&render(document, child));
}
element.into()
}
}
}

一旦、差分を取らずにDOMを再構築する関数を実装してみます。深さ優先な再帰を回しているだけです。いちいち web_sys::window().unwrap().document().unwrap().create_text_node(text) などと書くのが嫌だったので、 render の第1引数に document: &web_sys::Document を取ってしまいました。エラー処理もコードの頭で1回すれば済むのでこの方が良いかと思います。

match により VirtualNodeText なのか Element なのか判定します。Text ならば document.create_text_node によりノードを作成します。 Element ならば document.create_element によりノードを作成した上で、 append_child により子ノードを追加しています。

document.create_text_node(text) の戻り値の型は web_sys::Text で、document.create_element(tag_name).unwrap() の戻り値は web_sys::Element です。共に into() により型を web_sys::Node に変換しています。


ここまででできたもの

上記内容にexternや、render関数を実行してエントリーポイントとなるエレメントにその結果を反映するmain関数などを追加しています。

// lib.rs

extern crate wasm_bindgen;
extern crate web_sys;

use wasm_bindgen::prelude::*;

enum VirtualNode {
Text(&'static str),
Element {
tag_name: &'static str,
children: Vec<VirtualNode>,
},
}

fn render(document: &web_sys::Document, virtual_node: VirtualNode) -> web_sys::Node {
match virtual_node {
VirtualNode::Text(text) => document.create_text_node(text).into(),
VirtualNode::Element { tag_name, children } => {
let element = document.create_element(tag_name).unwrap();
for child in children {
element.append_child(&render(document, child));
}
element.into()
}
}
}

#[wasm_bindgen(start)]
pub fn main() {
let document = web_sys::window().unwrap().document().unwrap();
let entry_point = document.get_element_by_id("app").unwrap();
let _ = entry_point.parent_node().unwrap().replace_child(
&render(
&document,
VirtualNode::Element {
tag_name: "h1",
children: vec![VirtualNode::Text("Hello World")],
},
),
&entry_point,
);
}


実行

npm start

localhost:8080 にアクセスすれば Hello World と表示されるはずです。