React+TypeScript+WebAssembly+RustでWebアプリを作ってみる
以前にTauri, Rust, TypeScriptで書いたソースコードを流用してWebアプリにしてみた。
前はPureScript + Halogenで書かれたソースを TypeScript + Rustに書き換えたんだけど、今回もう一度書き換えてReact + TypeScript + Wasm + Rustの構成にする。
作った成果物
開発環境
Windows 11のWSL2環境で実行しています。
~$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description: Ubuntu 22.04.3 LTS
Release: 22.04
Codename: jammy
~$ npm -v
9.6.7
~$ rustup show
Default host: x86_64-unknown-linux-gnu
rustup home: /home/a/.rustup
installed toolchains
--------------------
stable-x86_64-unknown-linux-gnu
1.42.0-x86_64-unknown-linux-gnu
installed targets for active toolchain
--------------------------------------
aarch64-linux-android
armv7-linux-androideabi
i686-linux-android
thumbv6m-none-eabi
thumbv7em-none-eabi
thumbv7em-none-eabihf
thumbv7m-none-eabi
wasm32-unknown-unknown
x86_64-linux-android
x86_64-unknown-linux-gnu
active toolchain
----------------
stable-x86_64-unknown-linux-gnu (default)
rustc 1.71.1 (eb26296b5 2023-08-03)
Viteのセットアップ
フロントエンドの開発にはViteを使ったほうが便利でしょ。
プロジェクト名をvite-react-rust-wasm-projectとしてReact + TypeScriptを選択する。
~$ npm create vite@latest
✔ Project name: … vite-react-rust-wasm-project
✔ Select a framework: › React
✔ Select a variant: › TypeScript
Scaffolding project in /home/a/vite-react-rust-wasm-project...
Done. Now run:
cd vite-react-rust-wasm-project
npm install
npm run dev
書かれている通りに進める。
~$ cd vite-react-rust-wasm-project
~/vite-react-rust-wasm-project$
~/vite-react-rust-wasm-project$ npm install
added 201 packages, and audited 202 packages in 26s
40 packages are looking for funding
run `npm fund` for details
found 0 vulnerabilities
~/vite-react-rust-wasm-project$ npm run dev
VITE v4.4.9 ready in 293 ms
➜ Local: http://localhost:5173/
➜ Network: use --host to expose
➜ press h to show help
http://localhost:5173 をCtrl+クリックすることでページを開く。
確認が出来たらCtrl + Cを押してコンソールに戻る。
Rustのセットアップ
Rustはすでにインストール済みなので Install Rust は省略する。
wasm-pack
~/vite-react-rust-wasm-project$ cargo install wasm-pack
Updating crates.io index
Installing wasm-pack v0.12.1
Updating crates.io index
長いので省略
Compiling wasm-pack v0.12.1
Finished release [optimized] target(s) in 34.93s
Replacing /home/aki/.cargo/bin/wasm-pack
Replaced package `wasm-pack v0.12.1` with `wasm-pack v0.12.1` (executable `wasm-pack`)
~/vite-react-rust-wasm-project$
WebAssembly パッケージのビルド
~/vite-react-rust-wasm-project$ cargo new --lib wasm
Created library `wasm` package
~/vite-react-rust-wasm-project$ cd wasm
~/vite-react-rust-wasm-project/wasm$ ls
Cargo.lock Cargo.toml src target
~/vite-react-rust-wasm-project/wasm$
Rust を書いてみよう
wasm/src/lib.rsを書き換える。
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
extern {
pub fn alert(s: &str);
}
#[wasm_bindgen]
pub fn greet(name: &str) {
alert(&format!("Hello, {}!", name));
}
コードを WebAssembly にコンパイルする
とりあえずこれでいいか。
[package]
name = "wasm"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
crate-type = ["cdylib"]
[dependencies]
wasm-bindgen = "0.2"
パッケージのビルド
~/vite-react-rust-wasm-project/wasm$ wasm-pack build --target web
[INFO]: 🎯 Checking for the Wasm target...
[INFO]: 🌀 Compiling to Wasm...
Compiling wasm v0.1.0 (/home/a/vite-react-rust-wasm-project/wasm)
Finished release [optimized] target(s) in 0.08s
[INFO]: ⬇️ Installing wasm-bindgen...
[INFO]: Optimizing wasm binaries with `wasm-opt`...
[INFO]: Optional fields missing from Cargo.toml: 'description', 'repository', and 'license'. These are not necessary, but recommended
[INFO]: ✨ Done in 0.54s
[INFO]: 📦 Your wasm pkg is ready to publish at /home/a/vite-react-rust-wasm-project/wasm/pkg.
~/vite-react-rust-wasm-project/wasm$
パッケージのウェブでの利用
さて、コンパイルされた Wasm モジュールが入手できたので、ブラウザーで動かしてみましょう。 まず index.html というファイルをプロジェクトのルートに作成するところから始めましょう。最終的には以下のようなプロジェクト構造になります。
Vite + TypeScriptの環境なのでこの通りにはいかないので、こうする。
~/vite-react-rust-wasm-project/wasm$ ls
Cargo.lock Cargo.toml pkg src target
~/vite-react-rust-wasm-project/wasm$ cd ..
~/vite-react-rust-wasm-project$ ls
README.md node_modules package.json src tsconfig.node.json wasm
index.html package-lock.json public tsconfig.json vite.config.ts
~/vite-react-rust-wasm-project$
src/App.tsxを書き換える
wasmをApp.tsxにインポートする。
import init, { greet } from '../wasm/pkg/wasm'
初期化関数を呼ぶようにする
useEffect(() => {
init()
}, [])
ボタンのonClickを書き換える。
<button onClick={() => greet('App')}>
greet
</button>
書き換えるとsrc/App.tsxはこうなる。
import { useEffect, useState } from 'react'
import reactLogo from './assets/react.svg'
import viteLogo from '/vite.svg'
import './App.css'
import init, { greet } from '../wasm/pkg/wasm'
function App() {
const [count, setCount] = useState(0)
useEffect(() => {
init()
}, [])
return (
<>
<div>
<a href="https://vitejs.dev" target="_blank">
<img src={viteLogo} className="logo" alt="Vite logo" />
</a>
<a href="https://react.dev" target="_blank">
<img src={reactLogo} className="logo react" alt="React logo" />
</a>
</div>
<h1>Vite + React</h1>
<div className="card">
<button onClick={() => greet('App')}>
greet
</button>
<p>
Edit <code>src/App.tsx</code> and save to test HMR
</p>
</div>
<p className="read-the-docs">
Click on the Vite and React logos to learn more
</p>
</>
)
}
export default App
ブラウザーで確認する
~/vite-react-rust-wasm-project$ npm run dev
VITE v4.4.9 ready in 293 ms
➜ Local: http://localhost:5173/
➜ Network: use --host to expose
➜ press h to show help
http://localhost:5173 をCtrl+クリックすることでページを開く。
greetボタンを押すと
TypeScriptからWebAssembly経由でRustの関数を呼び出せた。
環境構築はこれでおしまい。
Vite + React + TypeScript + WebAssembly + Rustアプリを作る
アプリのひな形ができたので、あとはTauriアプリの方からソースコードを流用してきて書くだけ。
お互いの界面にちょっとした手間が掛かるくらいで、普通のReact + TypeScript と 普通のRust。
Rustファイルを書き換えたらwasm-pack build --target web
を忘れないように。
TypeScriptファイルを書き換えた場合は何もしなくてもいい。
あとはViteがよしなに更新してくれる。
TypeScriptの型付け
上で出てきているけど、このファイルがTypeScriptとRustとの界面。
// Copyright (c) 2023 Akihiro Yamamoto.
// Licensed under the MIT License <https://spdx.org/licenses/MIT.html>
// See LICENSE file in the project root for full license information.
//
mod devices;
mod infrared_remote;
mod parsing;
use infrared_remote::{
decode_phase1, decode_phase2, decode_phase3, decode_phase4, InfraredRemoteControlCode,
InfraredRemoteDecordedFrame, InfraredRemoteDemodulatedFrame, InfraredRemoteFrame,
MarkAndSpaceMicros,
};
use parsing::parse_infrared_code_text;
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
extern "C" {
fn alert(s: &str);
}
#[wasm_bindgen]
pub fn greet() {
alert("Hello, wasm!");
}
#[wasm_bindgen(typescript_custom_section)]
const TS_APPEND_CONTENT: &'static str = r#"
export function wasm_parse_infrared_code(ircode: string): MarkAndSpaceMicros;
"#;
#[wasm_bindgen(skip_typescript)]
pub fn wasm_parse_infrared_code(ircode: &str) -> Result<JsValue, String> {
let mark_and_spaces: Vec<MarkAndSpaceMicros> = parse_infrared_code_text(ircode)?;
serde_wasm_bindgen::to_value(&mark_and_spaces).map_err(|e| e.to_string())
}
#[wasm_bindgen(typescript_custom_section)]
const TS_APPEND_CONTENT: &'static str = r#"
export function wasm_decode_phase1(input: MarkAndSpaceMicros[]): InfraredRemoteFrame[];
"#;
#[wasm_bindgen(skip_typescript)]
pub fn wasm_decode_phase1(input: JsValue) -> Result<JsValue, String> {
let mark_and_spaces: Vec<MarkAndSpaceMicros> =
serde_wasm_bindgen::from_value(input).map_err(|e| e.to_string())?;
let ir_frames: Vec<InfraredRemoteFrame> = decode_phase1(&mark_and_spaces)?;
serde_wasm_bindgen::to_value(&ir_frames).map_err(|e| e.to_string())
}
#[wasm_bindgen(typescript_custom_section)]
const TS_APPEND_CONTENT: &'static str = r#"
export function wasm_decode_phase2(input: InfraredRemoteFrame): InfraredRemoteDemodulatedFrame;
"#;
#[wasm_bindgen(skip_typescript)]
pub fn wasm_decode_phase2(input: JsValue) -> Result<JsValue, String> {
let ir_frame: InfraredRemoteFrame =
serde_wasm_bindgen::from_value(input).map_err(|e| e.to_string())?;
let demodulated_frame: InfraredRemoteDemodulatedFrame = decode_phase2(&ir_frame);
serde_wasm_bindgen::to_value(&demodulated_frame).map_err(|e| e.to_string())
}
#[wasm_bindgen(typescript_custom_section)]
const TS_APPEND_CONTENT: &'static str = r#"
export function wasm_decode_phase3(input: InfraredRemoteDemodulatedFrame): InfraredRemoteDecordedFrame;
"#;
#[wasm_bindgen(skip_typescript)]
pub fn wasm_decode_phase3(input: JsValue) -> Result<JsValue, String> {
let demodulated_frame: InfraredRemoteDemodulatedFrame =
serde_wasm_bindgen::from_value(input).map_err(|e| e.to_string())?;
let protocol: InfraredRemoteDecordedFrame = decode_phase3(&demodulated_frame)?;
serde_wasm_bindgen::to_value(&protocol).map_err(|e| e.to_string())
}
#[wasm_bindgen(typescript_custom_section)]
const TS_APPEND_CONTENT: &'static str = r#"
export function wasm_decode_phase4(input: InfraredRemoteDecordedFrame[]): InfraredRemoteControlCode[];
"#;
#[wasm_bindgen(skip_typescript)]
pub fn wasm_decode_phase4(input: JsValue) -> Result<JsValue, String> {
let protocols: Vec<InfraredRemoteDecordedFrame> =
serde_wasm_bindgen::from_value(input).map_err(|e| e.to_string())?;
let ir_codes: Vec<InfraredRemoteControlCode> = decode_phase4(&protocols).into();
serde_wasm_bindgen::to_value(&ir_codes).map_err(|e| e.to_string())
}
これを元にwasm/pkg/wasm.d.tsファイルが自動的に作られるんだけど、any型ではTypeScriptを使う意味が無くなるので
#[wasm_bindgen(typescript_custom_section)]
を使って手作業で型付けする。
本番環境用のビルド
~/vite-react-rust-wasm-project/wasm$ wasm-pack build --release --target web
~/vite-react-rust-wasm-project$ vite build
静的サイトのデプロイ
package.jsonのscriptsの部分をこうして。
"scripts": {
"dev": "vite",
"build": "cd ./wasm && wasm-pack build --release --target web && cd .. && tsc && vite build",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview"
},
アプリのビルド
アプリをビルドするために、npm run build コマンドを実行します。
$ npm run build
デフォルトでは、ビルド結果は dist に置かれます。この dist フォルダを、お好みのプラットフォームにデプロイします。
GitHub Pages
リンクの通りなので省略
完成
(書いていないが)WebアプリケーションにするためにReactRouterとRecoilを追加した。
これでVite + React + TypeScript + WebAssembly + Rust + Recoil + React Router + WebSerialAPIで書いたアプリが完成した。
コピペしてください。
例えば東芝テレビリモコンの電源ボタンのコードを入れると
5601A900180015001800140018001400190013001900140019001400170040001700150018003F0019003E0018003E0019003F0019003E00170040001800140019003E001800150018003F00180014001800140019003F0018001400170016001700150018003F001800140018003F0018003F001800140019003F0018003F0018003E0019004F03
他にSONYテレビリモコンの電源ボタンのコードを入れると
5B0018002E001800180018002E001800170018002E00190017001800170018002E00180018001800170018001700180017004F03
これはダイキンエアコンのコード。
{"name":[417,448,418,450,417,450,417,449,418,448,417,25329,3450,1747,418,1315,419,446,419,449,417,450,417,1315,418,449,417,449,417,449,417,450,417,1314,418,450,417,1315,417,1315,418,448,418,1315,418,1315,417,1315,418,1315,417,1315,418,450,417,448,419,1312,419,449,417,449,417,451,416,449,419,448,417,449,417,450,417,448,419,448,417,449,417,1316,417,450,416,1314,419,448,418,449,417,449,418,1314,418,1314,419,449,417,450,417,448,419,447,418,450,417,448,419,448,417,449,417,449,418,448,418,449,418,449,417,448,419,448,417,449,418,449,417,1315,418,1314,418,1315,418,448,419,1313,419,448,419,1313,419,1313,420,34665,3450,1748,418,1314,419,447,418,450,416,450,417,1316,416,450,418,448,417,449,418,449,417,1315,418,449,418,1315,417,1315,417,451,416,1316,417,1314,418,1314,418,1316,416,1316,417,450,417,450,417,1313,418,451,416,449,417,449,418,449,416,450,417,449,417,450,416,449,417,450,416,451,416,449,419,1314,418,448,417,449,417,451,416,449,418,1317,416,450,415,450,417,449,418,448,417,450,416,450,417,451,416,448,417,450,417,449,417,450,417,450,417,449,418,448,417,453,414,449,417,449,417,450,416,450,416,1316,418,449,417,1315,417,449,418,1315,418,449,417,34670,3449,1750,416,1316,417,451,416,449,416,450,417,1315,418,450,416,450,415,451,417,449,417,1316,416,450,418,1315,416,1316,417,449,418,1315,418,1315,417,1316,417,1315,417,1315,418,450,416,450,417,1316,416,454,412,450,416,451,416,450,416,450,416,450,416,451,416,451,417,448,417,450,416,449,418,450,417,448,417,450,417,450,416,450,416,450,417,450,416,1317,416,1316,416,450,416,1317,417,1315,417,1316,417,449,418,448,417,452,414,451,416,1316,416,1316,417,450,416,1316,417,449,418,450,417,449,416,450,417,450,417,450,416,450,416,451,415,450,419,448,416,1316,417,1316,417,1315,418,1317,416,450,417,449,417,1315,417,450,416,450,420,448,415,450,416,450,417,450,416,450,416,450,417,449,418,1315,417,451,416,449,417,1316,416,451,416,450,416,451,415,1316,417,451,416,1316,416,450,418,450,415,450,416,451,416,451,416,449,417,450,416,450,417,450,416,450,416,450,416,1316,417,1317,417,447,418,450,416,451,416,451,416,449,416,450,417,450,417,449,416,450,416,452,414,451,416,450,416,451,415,451,416,451,415,450,416,451,416,1317,416,451,415,451,416,451,415,452,414,451,415,1317,417,1316,416,451,416,451,416,450,415,453,414,451,415,451,416,451,415,452,414,452,415,450,417,451,416,451,414,451,416,451,416,451,414,451,416,451,415,451,416,1317,416,451,415,1317,416,1316,417,1316,416,451,416]}
これはパナソニックエアコンのコード。
8800410014001000130032001300100013001000130010001300100014001000130010001300100013001000130010001400100013001000130032001300100013001000130010001400100013001000130010001300100014003100130032001300320013001000130010001300320013001000130010001400100013001000130010001300100014000F00140010001300100013001000130010001300100014001000130010001300100013001000140010001300100013001000130010001300100014001000130010001300100013001000130010001400100013001000130010001400100013003100140031001300100013001000140010001300100013001000130082018800410013001000140031001300100014001000130010001300100014000F001400100013001000130010001300100014000F00140010001300320013001000130010001300100014000F001400100013001000130010001300320013003200130032001300100013001000130032001300100013001000140010001300100013001000130010001300100014001000130010001300100013001000140010001300100013003200130010001300100014003100130032001300310014001000130010001300100013001000140031001300100014003100130032001300100013000F0013001000140010001300100013001000130010001300100014001000130031001400310013003200130032001300320013001000130032001300100013003200130031001400100013003100140031001300100013001000140010001300100013001000130010001400100013001000130010001300100013001000140010001300100013003200130032001300310014000F0014001000130010001300100013001000130010001400100013001000130010001300320013003200130031001400100013001000130010001300100013001000140010001300320013001000130010001300100014000F001400100013001000130010001300100014001000130010001300320013003200130010001300100013001000130011001300310013001000140010001300100013001000130010001300100014001000130010001300100013001000130011001300310013001000140010001300100013001000130032001300320013001000130032001300100013003200130032001300100013004F03
これは三菱電機エアコンのコード。
840044001200320012003100120011001200110010001200110033001200110012001100120031001100320013001000120032001200100013001000130031001200310013001000110032001300310012001100120011001200310011001200120011001000330012001100110012001200110012001100120010001300100013001000130010001300100012001100130010001200110011001200120011001200110012001000110012001300100013001000120011001200310013001000130010001300100012001100120011001200310013003100120010001300310012001100120010001100330012001000130031001200110012001100120011001200100013001000130031001000130012001000130010001300100012003200100033001200110010001300120011001200100013001000130010001200320012001000130010001300100013001000130010001100120012001100120011001200110012001000130010001100120013001000110012001300100012001100120011001000130012001100120011001200100013001000130010001100120013001000120011001200110012001100120011001200110012001000130031001200110012001100120011001200110012001000130031001200110012001000130031001300100013001000130010001300100012001100120011001200110012001100120011001200100013001000130010001300100013001000120011001200110012003100130010001300100012003100110012001200310013003100120011001200EB018500430011003200110033001200110012001100120010001300310012001100120011001200310013003100120010001300310012001100120010001100330012003100110012001100320013003100120011001000130012003100120011001000130012003100120011001100120010001300120011001200100013001000110012001100120013001000120011001200110011001200120011001000130012001000110012001300100013001000130010001200110012003100130010001100120011001200120011001100120010003300120032001200100012003200120011001200100013003100100012001300310010001300100013001200110010001200130011001200310011001200110012001000130012001100120031001100320013001000110012001100120011001200110012001100120010003300110012001100120011001200120011001000130010001300100012001100120011001200110012001100120011001200110012001000130010001300100013001000120011001200110012001100120011001200110012001000130010001300100013001000120013001000110012001100120011001200110012001100320011001200110012001100120012001100110012001000330011001200110012001000330011001200110012001100120011001200110012001000130010001300120011001000120011001200110012001100120011001200110012001200110010001300100033001100120011001200110032001100120011003300100033001100120012004F03
これは日立エアコンのコード。
6D0458078200400011003000120011001100100012001000120010001200100012001000110011001100110012001000120010001100100012003000100011001100110012001000120010001200100012001000110011001100100012001000120010001200100012001000120010001100110011001100110010001200100012003000120010001200300011003000110031001200300012002F0011003100120010001200300012002F001100310011003100110030001200300013002F0012002F00110031001200100011001100120010001100110011001000110011001100110012001000110011001200100012003000110030001100110013000F001200300012002F001200300011003100120010001100110012002F0013002F0013000F0012001000120010001200300013000F0012001000120030001100100012001000120030001200300013000F0011003100110030001200100012003000120030001200100011003000120030001100110012001000120030001200100011001100110011001200100011001100110030001200300012001000120030001000310011003100120010001200100011001100110031001200100012002F0011003100120010001200300012002F0013002F0012001000120030001100110012001000120030001000110013000F00110011001200100012001000110011001200100011001100110030001200300012002F0012003000120030001100300011003100120030001200100011001100110011001000120011001000120010001100110011001100110031001100300011003100110031001000310011003100120030001000310011001100120010001100110013000F00110011001100100012001000120010001200300011003100110030001200300013002F00100030001100310013002F001200100011001100100011001100110013000F0011001100120010001200100011003000110031001100310011003000110031001200300011003000120030001200100011001100120010001100110012001000110011001000110012001000110031001100310010003100110031001100310011003000110031001200300012002F001100310011001100110011001100310011001100110031001200100010001200110010001200300012003000120010001100310010001200100031001200300011001100110011001100110011003000120030001100310011003100100012001000310011003100110031001000120011001100110011001000110012001000120010001100110011001100110011001100110012001000110011001100300011003100110031001100300012003000110031001000310011003100110011001100110011001100110011001100110010001100110011001100110011003100110031001000310011003100110031001000310011003100110031001000120010001200100011001100110011001100120010001100110011003100100031001100310011003100100031001200300011003100100031001100110011003100120030001100110011001100110011001100110011001100110011001100110010001200100031001000320011003100110030001100310011003000110031001100110011001100110011001100110011001100100011001000110011001100120030001200300010003100110031001100310010003100110031001100110011001100100011001000320010001200100011001100110011003100110031001100300011003100110011001100310012002F0011003100110011001100110011001100120010001000120011001000110011001100110011001100110031001000310011003100110031001000310011003100110031001000310011001100120010001100110011001100100012001000110012001000110011001100310011003100100031001100310012003000100031001100310011003100100031001100310012003000100031001100310011003100100031001100310011001100120010001100110010001200100011001100110011001100110011001100310012002F001100310012003000110030001200300011003100120030001100110011001000120010001200100011001100120010001100110011001100100031001200300012003000100031001100310012003000110030001200300012001000120010001100110010001200110010001200100012001000120010001200300012002F00120030001200300012002F00110031001200300011003100110010001200100012001000120010001200100012001000110011001100110010004F03
ここからは余談。
赤外線リモコン信号
赤外線リモコン信号の構造はこのページが詳しい。
赤外線リモコン信号に興味があるならどうぞ活用してください。
Discussion