Yew ではじめる Rust / WebAssembly 開発

この記事は [Sansan Advent Calendar 2019 - Adventar](https://adventar.org/calendars/3924) の 17 日目の記事です 🎄

---

今回は Rust のフロントエンド向けフレームワーク [Yew](https://github.com/yewstack/yew) を使って、Markdown のプレビューアプリ を実装しながら、 Rust による WebAssembly 開発に入門してみたいと思います。

JavaScript でのフロントエンド開発の経験がある人を対象にして書いているため、Node.js 周りの説明は少なめになっています。
Rust の文法についても詳しくは書かないので、全体的な開発の雰囲気を掴んでもらえればと思います。

ちなみに、Rust については if 文の書き方で検索するレベルの初心者なので、気になる点があったら編集リクエストかコメント欄に書いていただけるとありがたいです。

## TL;DR

実際のコードは [https://github.com/pvcresin/yew-markdown-preview](https://github.com/pvcresin/yew-markdown-preview) にあります。
完成したアプリは [https://yew-markdown-preview.netlify.com/](https://yew-markdown-preview.netlify.com/) で体験できます。
(Chrome でしか動作確認してないです...🙇）
![screen.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/78711/fc4826d2-b3ac-cfb6-4267-3d355f18a144.png)

## 前提知識

### [Rust](https://www.rust-lang.org/)

- Mozilla が支援しているシステムプログラミング言語
- 静的型付けのコンパイラ言語
- C/C++と同等の処理速度を持ち、それらの代替を目指している

Web の開発だけでなく組み込みなどでも使われているようです。

### [WebAssembly](https://developer.mozilla.org/ja/docs/WebAssembly)（wasm）

- モダンブラウザで動作する低レベルなアセンブリ風言語
- 容量が小さく、実行までが高速
- 他の高級言語からコンパイルして作成する

JavaScript と比較して、高速に実行できることから、Web のパフォーマンスをネイティブに近づけるものとして注目されている印象です。

### [Yew](https://github.com/DenisKolodin/yew)

- React と Elm に影響を受けた、コンポーネントベースのRustフレームワーク
- JSX のようにコード内に HTML 風の記述が可能
- 仮想 DOM を実装している

React や Vue などを触ったことがある人には、理解しやすい記法かと思います。

## 環境構築

### Rust

[rustup](https://rustup.rs/)というツールを使って Rust をインストールします。
Windows はインストーラから、Linux/macOS は下記のコマンドから。

```shell
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
```

インストールされていれば、`rustc --version`でバージョンが表示されるはずです。
Windows で Rust を使う場合、[Visual C++ Build Tools が必要になる](https://qiita.com/euledge/items/ce3e24a2b8020441cd85)のでそちらもインストールしておきましょう。

### [Cargo](https://github.com/rust-lang/cargo)

Cargo はビルドシステム兼、Rust のパッケージマネージャです。
[npm](https://www.npmjs.com/) みたいな認識でいます。
ちなみに、Rust ではライブラリやパッケージに当たるものは crate と呼びます。
上の Rust をインストールした段階で`cargo`コマンドも使えると思います。

ここでは Rust で WebAssembly 開発行う際に便利なビルド支援ツールである[wasm-pack](https://github.com/rustwasm/wasm-pack)を入れます。

```shell
cargo install wasm-pack
```

### [yew-wasm-pack-template](https://github.com/yewstack/yew-wasm-pack-template)

今回は`yew-wasm-pack-template`を使います。
このテンプレートは Yew と wasm-pack と webpack が元から組み合わさっており、すぐに開発を始めることができます。

<img width="819" alt="スクリーンショット 2019-12-17 7.33.22.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/78711/01f0498d-fa31-7f00-2f22-a3a956c214c9.png">

GitHub で「yew-wasm-pack-template」のリポジトリを開き、「Use this template」ボタンを押してリポジトリを作るだけです。
あとは、作成したリポジトリを`git clone`してローカルに落としてきます。
これで準備は完了です。

## ビルド

このテンプレートをビルドすると、TODO アプリを MVC アーキテクチャで実装したサンプルが立ち上がります。
まずはじめに、Node.js 側のパッケージを落としてきます。

```shell
yarn install
```

続いて、webpack-dev-server でビルドします。

```shell
yarn run start:dev
```

ビルド時に crate のインストールが行われます。
ビルドが成功すれば、https://yew-todomvc.netlify.com/ と同じ Todo アプリが http://localhost:8000 で確認できると思います。
<img width="674" alt="スクリーンショット 2019-12-17 4.09.52.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/78711/90d8d165-a565-0a92-ffd3-664a5c3dbf19.png">

## フォルダ構成

この Todo アプリがどのようにして動いているのかを見ていきます。
以下がこのプロジェクトの主要なファイル群になります。

```

├── pkg
│   ├── index_bg.wasm
│   └── index.js
├── src
│   ├── app.rs
│   ├── lib.rs
│   └── utils.rs
├── static
│   └── index.html
├── bootstrap.js
├── webpack.config.js
├── package.json
└── Cargo.toml
```

### 概形

src 下にある Rust ファイルは wasm-pack により pkg 下に wasm ファイルとして出力されます。
JS のエントリポイントである bootstrap.js では pkg 下に出力された wasm ファイルを pkg/index.js 経由で読み込んでいます。

### Node.js 側

#### webpack.config.js

```js:webpack.config.js
const path = require("path");
const WasmPackPlugin = require("@wasm-tool/wasm-pack-plugin");
const CopyWebpackPlugin = require("copy-webpack-plugin");

const distPath = path.resolve(__dirname, "dist");
module.exports = (env, argv) => {
  return {
    devServer: {
      contentBase: distPath,
      compress: argv.mode === "production",
      port: 8000
    },
    entry: "./bootstrap.js",
    output: {
      path: distPath,
      filename: "todomvc.js",
      webassemblyModuleFilename: "todomvc.wasm"
    },
    plugins: [
      new CopyWebpackPlugin([{ from: "./static", to: distPath }]),
      new WasmPackPlugin({
        crateDirectory: ".",
        extraArgs: "--no-typescript"
      })
    ],
    watch: argv.mode !== "production"
  };
};
```

[CopyWebpackPlugin](https://github.com/webpack-contrib/copy-webpack-plugin)で static 下にある HTML ファイルを出力先へコピーしています。
また、[WasmPackPlugin](https://github.com/wasm-tool/wasm-pack-plugin)で crate のディレクトリを指定しています。

#### npm scripts

```json:package.json
{
  "scripts": {
    "dev": "webpack --mode development",
    "build": "webpack --mode production",
    "start:dev": "webpack-dev-server --mode development"
  },
}
```

`yarn start:dev`で開発用サーバ（webpack-dev-server）の起動が、`yarn build`でプロダクションビルドができます。

#### bootstrap.js

```js:bootstrap.js
import("./pkg").then(module => {
  module.run_app();
});
```

wasm モジュールを Dynamic import しているだけです。
読み込みが完了したら、run_app()関数を実行します。

### static/index.html

```html:index.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <title>Yew • TodoMVC</title>
    <link rel="stylesheet"
    href="https://cdn.jsdelivr.net/npm/todomvc-common@1.0.5/base.css"/ >
    <link
      rel="stylesheet"
      href="https://cdn.jsdelivr.net/npm/todomvc-app-css@2.1.2/index.css"
    />
  </head>
  <body>
    <script src="/todomvc.js"></script>
  </body>
</html>
```

Todo アプリ用の CSS を CDN で読み込んでいるのと、JS ファイルを読み込んでいるだけです。

### Rust 側

#### Cargo.toml

```toml:Cargo.toml
[package]
name = "yew-wasm-pack-template"
version = "0.1.0"
authors = ["Justin Starry <justin.starry@icloud.com"]
edition = "2018"

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

[features]
default = ["console_error_panic_hook"]

[dependencies]
log = "0.4"
strum = "0.13"
strum_macros = "0.13"
serde = "1"
serde_derive = "1"
wasm-bindgen = "=0.2.42"
web_logger = "0.2"
yew = "0.7"
console_error_panic_hook = { version = "0.1.6", optional = true }
wee_alloc = { version = "0.4.4", optional = true }

[dev-dependencies]
wasm-bindgen-test = "0.2"
```

Cargo.toml にはそのプロジェクトで使う crate などを書いていきます。
package.json みたいな認識。

#### src/utils.rs

```rust:utils.rs
pub fn set_panic_hook() {
    #[cfg(feature = "console_error_panic_hook")]
    console_error_panic_hook::set_once();
}
```

ユーティリティファイルです。
Rust で起きたエラーの詳細を JS 側まで伝えてくれる crate、[console_error_panic_hook](https://github.com/rustwasm/console_error_panic_hook)をセットする関数が定義されています。

#### src/app.rs

長いので載せませんが、このファイルが Yew を使って書かれたアプリケーション本体です。
Yew 自体の理解は後の章で行います。

#### src/lib.rs

```rust:lib.rs
#![recursion_limit = "512"]

mod app;
mod utils;

use wasm_bindgen::prelude::*;

#[cfg(feature = "wee_alloc")]
#[global_allocator]
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;

// This is the entry point for the web app
#[wasm_bindgen]
pub fn run_app() -> Result<(), JsValue> {
    utils::set_panic_hook();
    web_logger::init();
    yew::start_app::<app::App>();
    Ok(())
}
```

ここが Rust 側のエントリポイントです。
先のファイルを`mod`として読み込み、`run_app`関数内で使用しています。

`pub fn`に`#[wasm_bindgen]`属性をつけることで JS 側から呼び出すことができます。
この関数は上の bootstrap.js で呼んでいました。

## 状態管理とイベントハンドリング

ここからは、サンプルを書き直していきます。
なお、生成される JS ファイルの名前が`todomvc.js`になっていますが、webpack の設定ファイルなども直す必要があり少し面倒なのでそのままでいきます。

まずは、大まかなレイアウトと、イベントハンドリングを行います。
左ペインの teatarea に入力した文字列をアプリケーションで保持し、右ペインに表示してみます。

### static/index.html

マークダウン用の CSS を CDN で読み込みます。

```html:index.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>Yew Markdown Preview</title>
    <link
      href="https://cdnjs.cloudflare.com/ajax/libs/github-markdown-css/3.0.1/github-markdown.min.css"
      rel="stylesheet"
    />
    <link rel="stylesheet" href="style.css" />
  </head>
  <body>
    <script src="/todomvc.js"></script>
  </body>
</html>
```

### static/style.css

static 下に CSS を追加します。

```css:style.css
body {
  margin: 0;
  display: flex;
  flex-direction: column;
  min-height: 100vh;
}

header {
  background-color: #efefef;
  height: 50px;
  display: flex;
  align-items: center;
  padding: 0 1rem;
}

header > p {
  margin: 0;
}

.container {
  display: flex;
}

.container > * {
  min-height: calc(100vh - 50px);
  width: 50%;
  padding: 1rem;
  box-sizing: border-box;
}

.container > textarea {
  font-family: Consolas, "Courier New", Courier, Monaco, monospace,
    "ＭＳ ゴシック", "MS Gothic", Osaka−等幅;
  resize: none;
  background-color: #1e1e1e;
  color: #e8e8d4;
  border: none;
  font-size: 18px;
}
```

### src/app.rs

```rust:app.rs
use yew::{html, Component, ComponentLink, Html, Renderable, ShouldRender};

pub struct App {
    text: String,
}

pub enum Msg {
    Change(String),
}

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

    fn create(_: Self::Properties, _: ComponentLink<Self>) -> Self {
        App {
            text: "".to_string(),
        }
    }

    fn update(&mut self, msg: Self::Message) -> ShouldRender {
        match msg {
            Msg::Change(val) => {
                self.text = val;
                true
            }
        }
    }
}

impl Renderable<App> for App {
    fn view(&self) -> Html<Self> {
        let link = "https://github.com/pvcresin/yew-markdown-preview";
        html! {
            <>
                <header>
                    <p>
                        {"Yew Markdown Preview: "}
                        <a href={link}>{link}</a>
                    </p>
                </header>
                <div class={"container"}>
                    <textarea oninput=|e| Msg::Change(e.value) />
                    <div>{&self.text}</div>
                </div>
            </>
        }
    }
}
```

構造体`App`がアプリケーション全体の状態のモデルになります。
`enum Msg`はイベントを列挙したものです。

#### Logic

`impl Component for App`の部分にロジックを書いていきます。
ここではライフサイクルメソッドが定義できます。

`fn create`で`text`に初期値を入れます。

`fn update`ではイベントが起こったとき、どのイベントかを`match`で判別し、処理を行います。
今回定義した`Change`イベントは String を引数にとり、`text`の値を更新します。
最後にコンポーネントを更新するかどうかの`ShouldRender`を`true`で返します。

#### View

`impl Renderable<App> for App`中の`fn view`の部分に View を書いていきます。
マクロ`html!`の中ではタグスタイルで View を記述でき、JSX 風な書き心地が楽しめます。

イベントハンドリングは以下のように書きます。

```
<textarea oninput=|e| Msg::Change(e.value) />
```

これで左の textarea に入力した文字列が右の div に表示されるようになりました。

<img width="828" alt="スクリーンショット 2019-12-17 5.58.37.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/78711/23e5d237-d8c2-a1ab-d086-50a0c368d0bd.png">


## Markdown をパースする

Markdown のパースには外部の crate を使います。
crate は[https://crates.io/](https://crates.io/)で探すことができます。このあたりも [npm](https://www.npmjs.com/) と似ています。
今回は[pulldown-cmark](https://github.com/raphlinus/pulldown-cmark)を使ってみます。

### Cargo.toml

crate を追加するには Cargo.toml の`[dependencies]`の末尾に crate を追記します。

```toml:Cargo.toml
[dependencies]
//...
pulldown-cmark = "0.6.1"
```

使い方は[ドキュメント](https://docs.rs/pulldown-cmark/0.6.1/pulldown_cmark/)などを頼りに探り探りいきます。

### src/lib.rs

`mod`キーワードの前に

```rust
extern crate pulldown_cmark;
```

の行を追加します。

```rust:lib.rs
#![recursion_limit = "512"]

extern crate pulldown_cmark;

mod app;
mod utils;

use wasm_bindgen::prelude::*;

#[cfg(feature = "wee_alloc")]
#[global_allocator]
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;

// This is the entry point for the web app
#[wasm_bindgen]
pub fn run_app() -> Result<(), JsValue> {
    // ...
}
```

### src/app.rs

#### use

`use pulldown_cmark::{html::push_html, Options, Parser};`を追加します。

```rust:app.rs
use pulldown_cmark::{html::push_html, Options, Parser};
use yew::{html, Component, ComponentLink, Html, Renderable, ShouldRender};
```

#### `impl Component`

```rust:app.rs
impl Component for App {
    type Message = Msg;
    type Properties = ();

    fn create(_: Self::Properties, _: ComponentLink<Self>) -> Self {
        App {
            text: "".to_string(),
        }
    }

    fn update(&mut self, msg: Self::Message) -> ShouldRender {
        match msg {
            Msg::Change(val) => {
                let mut options = Options::empty();
                options.insert(Options::ENABLE_TABLES);
                options.insert(Options::ENABLE_FOOTNOTES);
                options.insert(Options::ENABLE_STRIKETHROUGH);
                options.insert(Options::ENABLE_TASKLISTS);

                let parser = Parser::new_ext(&val, options);
                let mut parsed_text = String::new();
                push_html(&mut parsed_text, parser);
                self.text = parsed_text;
                true
            }
        }
    }
}
```

Parser に入力された文字列`val`を渡し、parsed_text に変換された値が入ります。
Option でテーブルなどが記述できる設定を入れておきます。

試しに左ペインに`# hello, world!`と入力してみると、右ペインに`<h1>hello, world!</h1>`などと表示されるのが確認できます。

<img width="828" alt="スクリーンショット 2019-12-17 6.37.38.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/78711/2b091556-4264-7921-44c6-1d46eaea63bd.png">

これでパースができました。

## 要素をレンダリングする

最後に先ほどの文字列を実際の DOM 要素としてレンダリングしてみます。

つまり、

```js
element.innerHTML = parsed_text;
```

のようなことができれば実現できるということになります。
React では`dangerouslySetInnerHTML`を使ってそのようなことができましたが、Yew ではそういった類いのものは見つかりませんでした。
そこで[stdweb](https://github.com/koute/stdweb) crate と組み合わせて実現します。

### Cargo.toml

`[dependencies]`の末尾に crate を追記します。

```toml:Cargo.toml
[dependencies]
//...
pulldown-cmark = "0.6.1"
stdweb = "0.4.20"
```

[ドキュメント](https://docs.rs/stdweb/0.4.20/stdweb/)を頼りに実装します。

### src/lib.rs

```rust
extern crate stdweb;
```

の行を追加します。

```rust:lib.rs
#![recursion_limit = "512"]

extern crate pulldown_cmark;
extern crate stdweb;

mod app;
mod utils;

use wasm_bindgen::prelude::*;

#[cfg(feature = "wee_alloc")]
#[global_allocator]
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;

// This is the entry point for the web app
#[wasm_bindgen]
pub fn run_app() -> Result<(), JsValue> {
    // ...
}
```

### src/app.rs

#### use

```rust
use stdweb::web::Node;
use yew::virtual_dom::VNode;
```

の 2 行を追加します。

```rust:app.rs
use pulldown_cmark::{html::push_html, Options, Parser};
use stdweb::web::Node;
use yew::virtual_dom::VNode;
use yew::{html, Component, ComponentLink, Html, Renderable, ShouldRender};
```

#### `impl Renderable<App>`

```rust:app.rs
impl Renderable<App> for App {
    fn view(&self) -> Html<Self> {
        let link = "https://github.com/pvcresin/yew-markdown-preview";
        let html_text = format!("<div class='markdown-body'>{}</div>", &self.text);
        let node = Node::from_html(&html_text).unwrap();
        let preview = VNode::VRef(node);
        html! {
            <>
                <header>
                    <p>
                        {"Yew Markdown Preview: "}
                        <a href={link}>{link}</a>
                    </p>
                </header>
                <div class={"container"}>
                    <textarea oninput=|e| Msg::Change(e.value) />
                    {preview}
                </div>
            </>
        }
    }
}
```

パースされた文字列を含む div タグを`Node::from_html`に渡すことで Node を生成します。
その Node を Yew 内部の仮想 Node と紐づけ、`{preview}`の位置にパースされた DOM の要素が展開されます。
（ここらへん、正直よくわかってない 😇）

これで、左ペインで Markdown 記法で記入すると右ペインに表示されるようになりました 🎉

<img width="828" alt="スクリーンショット 2019-12-17 6.55.10.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/78711/7057af40-e58d-ac8b-213d-8fdcdaa9c5d4.png">

今回は使いませんでしたが、`stdweb`にはマクロ`js!`があり、Rust 内で JS の記法が使える機能もあります。

## まとめ

Markdown のプレビューアプリを例に、Yew による Rust/WebAssembly 開発を紹介しました。

Rust力が高ければもう少し綺麗なコード書けたんじゃないかなと思います。
また、今回考えられなかったですが、Markdown のプレビューってスクロール位置とかいろいろ考えなきゃいけないなと。。。

やってみた感想としては、Yew はフロントエンドエンジニアが Rust に入門するのに適していると思いました。
ただ、ドキュメントはまだまだ少ない気がします。
WebAssembly をうまく使って Web の体験を向上させていきたい！

最後までお読みいただき，ありがとうございました。