依存ツール

依存は以下のコマンドでインストールします。

# wasm-pack のインストールがまだの場合
curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh

# npm
yarn add -D \
  @wasm-tool/wasm-pack-plugin \
  html-webpack-plugin \
  ts-loader \
  typescript \
  webpack \
  webpack-cli \
  webpack-dev-server \

1. wasm-pack

wasm-packは、 Rust コードを WebAssembly へコンパイルし、JavaScript で扱うためのパッケージ化を行ってくれます。

吐き出される JavaScript は完全ではないですが TypeScript ですぐ使える形になっているので、ここでも TypeScript で見ていこうと思います。

2. Webpack

wasm-packWebpack は、複数の JavaScript ファイルを実質 1 つの JavaScript ファイルにまとめます。

3. webpack-dev-server

wasm-packWebpackwebpack-dev-server は、 Webpack によるビルド後の結果でサーバーを建ててくれます。ビルドは行いますが、インメモリなファイルとして扱われるので、実際にあるディレクトリ内のファイルにビルド結果を出力したりはしません。
また、関連ファイルに変更が入った際に自動リロードで助けてくれます。

4. html-webpack-plugin

wasm-packWebpackwebpack-dev-serverhtml-webpack-plugin は、 Webpack によるビルドで生成されるファイルを読み込んだ形の HTML ファイルを作成してくれます。

設定オプションのfilenameindex.html(デフォルト)の時、これはwebpack-dev-serverによるサーバーをブラウザで開いた時に表示される最初の HTML になります。

5. wasm-pack-plugin

wasm-packWebpackwebpack-dev-serverhtml-webpack-pluginwasm-pack-plugin は、 Webpack から wasm-pack を実行する為のプラグインです。これにより webpack でビルドする際に、 wasm-pack と Webpack 両方のビルドを走らせることができます。

Webpack 設定

以下は設定例です。

const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const WasmPackPlugin = require('@wasm-tool/wasm-pack-plugin')

module.exports = (_, argv) => {
  return {
    mode: argv.mode || 'development',
    devtool:
      argv.mode === 'production' ? 'source-map' : 'eval-cheap-source-map',
    target: 'web',
    entry: {
      main: path.join(__dirname, 'src/main.ts')
    },
    output: {
      path: path.join(__dirname, 'out'),
      filename: '[name].js'
    },
    resolve: {
      extensions: ['.ts', '.tsx', '.js', '.jsx', '.wasm']
    },
    devServer: {
      port: 33857
    },
    module: {
      rules: [
        {
          test: /\.tsx?$/,
          loader: 'ts-loader',
          options: {
            transpileOnly: true
          }
        }
      ]
    },
    plugins: [
      new HtmlWebpackPlugin(),
      new WasmPackPlugin({
        crateDirectory: path.join(__dirname, '.')
      })
    ]
  }
}

注意したい点は 2 つです。

  1. resolve.extensions.wasmを含める

  2. WesmPackPlugincreateDirectoryには、 Cargo プロジェクトのディレクトリを指定する(今回は.なので Webpack 設定ファイルが置かれているディレクトリに Cargo.toml も置かれている)

この設定でビルドを行うとWesmPackPluginは、 Webpack 設定ファイルと同じディレクトリにpkg/というディレクトリを作ります。これは WebAssemly な JavaScript パッケージなっている為、後はこれをメインとなる JavaScript プロジェクト側から読み込むと、 Rust 側でエクスポートした関数などを実行できます。

Rust

Cargo.tomlは以下のようにします。

[package]
name = "get-started-wasm"
version = "0.0.0"
authors = ["nju33 "]
edition = "2018"

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

[dependencies]
wasm-bindgen = "0.2.60"

[profile.release]
opt-level = "s"

wasm-bindgenというパッケージで Rust と JavaScript を紐付けます。

例として、 JavaScript から関数を実行するとコンソールにHello wasmと出力されるように実装してみます。
まずは、ブラウザのconsole.logを Rust で実行できるようにする為にsrc/console.rsファイルを作ります。

use wasm_bindgen::prelude::*;

#[wasm_bindgen]
extern "C" {
  #[wasm_bindgen(js_namespace = console)]
  pub fn log(s: &str);
}

このextern "C"ブロックの中に JavaScript 側の関数に関する定義を書くことで、その関数を Rust から使えるようになります。これには#[wasm_bindgen]属性を付けてあげる必要があります。
この時console.logのような.を挟むものはjs_namespace属性値いを指定してあげる必要があります。(foo.bar.bazのような深いネストの関数などは、自分でその関数をwindow以下に設定し、それを定義付けすることで使えました)これでlog("foo")などでコンソールにログが出せるようになりました。

次に JavaScript から呼ぶ用の関数を作りますが、そのような関数にも#[wasm_bindgen]属性は必要です。またその関数はResultを返す必要があります。

今度はsrc/lib.rsファイルを作ります。

mod console;

use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub fn hello_wasm() -> Result<(), JsValue> {
  console::log("Hello Wasm");

  Ok(())
}

mod consoleで先程のファイルの中身が使えるようになります。例えばlogconsole::logという形と、ほぼ JavaScript と同じ様に使えます。

そしてhelloo_wasmという関数を定義しました。これによりwasm-packで生成された JavaScript パッケージのモジュールからhello_wasmという関数をインポートできるようになります。

TypeScript

tsconfig.jsonを以下のようにします。module: esnextが必須です。

{
  "include": ["src"],
  "compilerOptions": {
    "target": "es6",
    "module": "esnext",
    "strict": true,
    "esModuleInterop": true,
    "moduleResolution": "node",
    "isolatedModules": true
  }
}

そしてsrc/main.tsファイルを作り、以下のようにします。

// eslint-disable-next-line @typescript-eslint/no-floating-promises
;(async () => {
  const module = await import('../pkg')
  module.hello_wasm()
})()

export {}

重要な点はwasmなパッケージは必ず Webpack のimportを使って読み込む事です。.wasmは今の所この方法による読み込みをしなければエラーになるようです。

後はyarn webpack-dev-serverなどからlocalhostを開き、コンソールにHello wasmと表示されれば WebAssembly を使うまでの環境構築は完了です。

サンプル

ここまでの動作確認ができるものは wasm-packWebpackwebpack-dev-serverhtml-webpack-pluginwasm-pack-pluginnju33-com/get-started-wasm に置いてあります。

wasm-pack を用いた環境構築はできている想定から進めます。できていない場合は、 nju33-com/get-started-wasm をクローンするとすぐ始められます。

Yew インストール

Cargo.tomldependenciesに以下の 1 行を追記します。

yew = { version = "0.14", features = ["web_sys"] }
# ドキュメントを見ると`web_sys`か`std_web`か選べる様ですが、
# `web_sys`の方が`wasm_pack`と同じ人達が作ってるので
# 良いようです

src/lib.rs の編集

行の最初に以下の 1 行を追記しておきます。

#![recursion_limit = "256"]

これは再帰処理回数の制限を増やすためのもので、今回のような簡単なものでは引っかからないですが、かなりすぐ引っかかっるようになってしまうので、事前に書いておきます。

Yew コンポーネントを作る

yew::prelude::*により取り込んだComponentトレイトからコンポーネントを作ります。作る時に最低限やることは以下の通りです。

  1. Message関連型の設定

  2. Properties関連型の設定

  3. create関数の実装

  4. updateメソッドの実装

  5. viewメソッドの実装

色々ありますが、ただ表示テキストを表示したいだけであれば、5 で返すマークアップだけを弄るだけで事足ります。

マークアップもyew::prelude::*から取り込んだhtml!マクロを用いることでほぼ HTMLを書くだけで実装できます。

use yew::prelude::*;

struct MyComponent {}

impl Component for MyComponent {
  type Message = ();
  type Properties = ();
  fn create(_props: Self::Properties, _link: ComponentLink) -> Self {
    Self {}
  }

  fn update(&mut self, _message: Self::Message) -> ShouldRender {
    true
  }

  fn view(&self) -> Html {
    html! {
      
{"Hello Yew"}
} } }

コンポーネントをレンダリングする

yew::initialize()後、上記のコンポーネントをジェネリック型として渡しながらApp::new().mount_to_body()メソッドを呼び出します。これで<body />タグの中にアプリがレンダリングされます。

このような感じでsrc/lib.rsに追記します。

#[wasm_bindgen]
pub fn render_hello() -> Result<(), JsValue> {
  yew::initialize();
  App::::new().mount_to_body();
  
  Ok(())
}

render_helloと定義したので、これを TypeScript 側(src/main.tsファイル)で呼ぶようにします。

// eslint-disable-next-line @typescript-eslint/no-floating-promises
;(async () => {
  const module = await import('../pkg')
  module.render_hello()
})()

export {}

確認

実行後ブラウザにテキストで「Hello Yew」と表示されれば完了です。動くコードは nju33-com/get-started-wasmnju33-com/get-started-yew に置いておきます。

関連

プロパティを設定するとhtml!の中でコンポーネントを使う際に好きな値を渡したり、渡された値が使えます。

以下は最小のコンポーネントの書き方です。

use yew::prelude::*;

struct MyComponent {}

impl Component for MyComponent {
  type Message = ();
  type Properties = ();
  fn create(_props: Self::Properties, _link: ComponentLink) -> Self {
    Self {}
  }

  fn update(&mut self, _message: Self::Message) -> ShouldRender {
    true
  }

  fn view(&self) -> Html {
    html! {
      
{"Hello Yew"}
} } }

これに対して以下の 2 点を修正します。

  1. type Properties()から適当な構造体にする

  2. MyComponentでプロパティ値を保持する

プロパティ構造体

プロパティとなる構造体にはderivePropertiesyew::prelude::*で取り込まれる)とCloneが必要です。

#[derive(Properties, Clone)]
struct Props {
  text: String
}

定義した構造体はComponentの関連型に設定してあげます。

type Properties = Props;

これで<Component text="foo" />のようにプロパティが渡せるようになりました。

初期設定

これでまでで渡せるようにはなりましたが、まだコンポーネント内では使えません。使えるようにするには、コンポーネント自体の構造体でプロパティの値を保持しなければなりません。

コンポーネント構造体MyComponentを以下のように修正します。

struct MyComponent {
  props: Props;
}

構造体を使うにはインスタンス化しなければなりませんが、それを行うのがcreate関数です。

create関数は自身のインスタンスを返すように実装します。また、第 1 引数として関連型に指定したPropertiesのインスタンスが渡される為、好きなようにプロパティ値を加工することができます。ただ今は渡ってきたプロパティをpropsにそのまま渡すだけとします。
create関数部分を修正します。

  fn create(props: Self::Properties, _link: ComponentLink) -> Self {
    Self { props }
  }

これでviewメソッドでself.props.textのようにアクセスできます。

Rc を使う

viewメソッドでプロパティ値を使う際、Copyトレイトが実装されている値はそのまま使えますが、そうでない場合は値をクローンして所有権が渡るようにしなければなりません。例えば例のtextようにStringcloneメソッドを呼ぶと新しい値が丸々コピーされてしまうので、沢山の子要素で使うなどした場合に無駄が多いです。

このような場合はRcを使うことでコピーを回避できます。その為に修正することはPropertiesStringRc<String>とするだけです。

#[derive(Properties, Clone)]
struct Props {
  text: std::rc::Rc
}

あとはこのコンポーネントに値を渡す側のコンポーネントの同じ値もRc<String>となるようにして渡してあげるのみです。

関連

要素がクリックした時の走らせる処理の実装方法を見ていきます。

以下は最小のコンポーネントの書き方です。

use yew::prelude::*;

struct MyComponent {}

impl Component for MyComponent {
  type Message = ();
  type Properties = ();
  fn create(_props: Self::Properties, _link: ComponentLink) -> Self {
    Self {}
  }

  fn update(&mut self, _message: Self::Message) -> ShouldRender {
    true
  }

  fn view(&self) -> Html {
    html! {
      
{"Hello Yew"}
} } }

修正が必要なのは以下の 3 点です。

  1. 関連型Messageをちゃんと埋める

  2. onclickハンドラを設定する

  3. updateメソッドを発火する為のComponentLinkを扱えるようにする

今回はボタンをクリックする度onoffテキストが切り替わる感じのものを作ってみます。

関連型 Message をちゃんと埋める

以下のようなenum型で定義しておきます。

enum Message {
  ButtonClick
}

また、関連型の方も更新しておきます。

type Message = Message;

onclick ハンドラを設定する

以下のことをする為にviewメソッドを更新します。

  1. self.activeによるテキストの切り替え

  2. self.linkによるonclickハンドラの設定

と、その前に両方ともself経由なのでcreateとこのコンポーネント構造体の修正が必要です。

struct MyComponent {
  link: ComponentLink,
  active: bool,
}

linkcreateの第 2 引数として渡されイベントハンドラの設定に必要なものなので、もしイベントハンドラを使うのであれば構造体に持たせなければなりません。
activeで適当にデフォルト値を設定します。

fn create(_: Self::Properties, link: ComponentLink) -> Self {
  Self {
    link,
    active: false,
  }
}

viewに戻ります。textifで適当に切り替えています。
そしてイベントハンドラの方ですがlinkcallbackメソッドにクロージャーを渡しその中でMessageenumのどれかを返すようにします。

fn view(&self) -> Html {
  let text = if self.active {
    "on -> off"
  } else {
    "off -> on"
  };

  html! {
    
  }
}

このクロージャーが返した値は次にupdateメソッドの第 1 引数に渡されます。


fn update(&mut self, message: Self::Message) -> ShouldRender {
  match message {
    Message::ButtonClick => self.active = !self.active,
  };

  true
}

Message::ButtonClickな時はactiveboolを反対にしています。

updateメソッドでtrueを返すとコンポーネントの再レンダリングが走るので、それによってボタンの中のテキストの切り替えがされるハズです。

サンプル

nju33-com/get-started-yew-callback から動作確認が行えます。

親コンポーネントのコールバックを呼ぶ

onclickのようなプロパティを自作コンポーネントに渡すと、子から親を更新することができます。

これは例えば以下のような感じの構造になります。

struct Child {
  onclick: Callback
}

// 

但しそのまま子要素の例えば<button />onclickに渡せるのではなく、一旦子コンポーネントでupdateメソッドを呼び、その中でコールバックのemitメソッドを呼び出す必要があります。

// 

こうすることで子コンポーネントのボタンがクリックされた時に、プロパティとして渡されたself.link.callback(closure)closureが実行されます。それにより親のupdateメソッドが呼ばれ、親コンポーネントの更新ができます。

ちなみにemitする時は 1 つ値を渡すことができます。そしてそれはclosureへ渡されるので、その値を見て返す値を変えたり、タプル構造体をupdateメソッドに渡したりといった事もできます。

JavaScript で飯食べたい歴約 8 年、 純( nju33 ) によるノートサイトです。

このサイトではドリンク代や奨学金返済の為、広告などを貼らせて頂いてますがご了承ください。

Change Log