Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationEventAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
37
Help us understand the problem. What are the problem?

More than 1 year has passed since last update.

posted at

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

実行

npm start

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

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
37
Help us understand the problem. What are the problem?