前提
- 重い計算を Rust + WebAssembly でやる
- WebAssembly のパッケージは wasm-pack でビルドする
- WebAssembly は WebWorker で動かす
- アプリは React で書く
- アプリの設定を頑張りたくないので react-scripts (create-react-app) を使う
react-app-rewired
react-scripts では、WebWorker と WebAssembly のロードができないので設定する必要がある。
react-scripts で eject せずに設定を追加するために react-app-rewired を使う。
worker-loader では WebWorker から WebAssembly を import できなかったので、workerize-loader を使う。
こんな感じで config-overrides.js
を書く。
const path = require("path");
module.exports = function override(config, env) {
config.module.rules.push({
test: /\.worker\.js$/,
use: { loader: "workerize-loader" },
});
const wasmExtensionRegExp = /\.wasm$/;
config.resolve.extensions.push(".wasm");
config.module.rules.forEach((rule) => {
(rule.oneOf || []).forEach((oneOf) => {
if (oneOf.loader && oneOf.loader.indexOf("file-loader") >= 0) {
// Make file-loader ignore WASM files
oneOf.exclude.push(wasmExtensionRegExp);
}
});
});
// Add a dedicated loader for WASM
config.module.rules.push({
test: wasmExtensionRegExp,
include: path.resolve(__dirname, "src"),
use: [{ loader: require.resolve("wasm-loader"), options: {} }],
});
return config;
};
これで拡張子が .worker.js
のファイルを WebWorker として読み込めるようになる。
package.json
の scripts
を以下のように書いておく。
"scripts": {
"build": "react-app-rewired build",
"start": "react-app-rewired start"
},
WebWorker の実装
こんな感じで WebWorker を実装する。
export const twice = async (v) => {
const { twice } = await import("example");
return twice(v);
};
example
は wasm-pack で作られたパッケージで、dynamic import で読み込む。
workerize-loader では、WebWorkerで呼び出せる関数を named export する。
async/await が使える。
アプリの実装
いい感じにアプリ側を実装する。
workerize-loader で import したモジュールは関数になっていて、呼び出すと関数を取り出すことができる。
関数は Promise を返すようになっている。
import React, { useState } from "react";
import worker from "./hoge.worker";
const { twice } = worker();
const App = () => {
const [value, setValue] = useState(1);
return (
<div>
<button
onClick={() => {
twice(value).then((result) => {
setValue(result);
});
}}
>
click me
</button>
<p>{value}</p>
</div>
);
};
export default App;
実用例
凸包を計算して描画するプログラムを書いてみた。