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


# 今回すること

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

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

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

実行結果例：
![screenshot.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/199139/3ae1b901-29a4-056e-cfea-d57ed1c2af7a.png)

# 準備

[準備](https://qiita.com/ne_no_usa/items/c5552f20d4839fb5b728) をします。

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

```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ツリーのノードを作る

```rust
// 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 のコードで表すと

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

のようになります。

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

## 仮想DOMを描画する

```rust
// 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` により `VirtualNode` が `Text` なのか `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`関数などを追加しています。

```rust
// 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 node = VirtualNode::Element {
        tag_name: "h1",
        children: vec![VirtualNode::Text("Hello World")],
    };
    entry_point
        .parent_node()
        .unwrap()
        .replace_child(&render(&document, &node), &entry_point);
}
```

# 実行

```bash
npm start
```

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