🔌

React+TypeScript+WebAssembly+RustでWebアプリを作ってみる

2023/08/21に公開

https://ak1211.com/7727/

以前にTauri, Rust, TypeScriptで書いたソースコードを流用してWebアプリにしてみた。

前はPureScript + Halogenで書かれたソースを TypeScript + Rustに書き換えたんだけど、今回もう一度書き換えてReact + TypeScript + Wasm + Rustの構成にする。

作った成果物

https://ak1211.github.io/vite-react-rust-wasm-project/

https://github.com/ak1211/vite-react-rust-wasm-project

開発環境

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を使ったほうが便利でしょ。

https://vitejs.dev/guide/

プロジェクト名を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のセットアップ

https://developer.mozilla.org/ja/docs/WebAssembly/Rust_to_Wasm

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を書き換える。

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 にコンパイルする

とりあえずこれでいいか。

Cargo.toml
[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はこうなる。

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との界面。

wasm/src/lib.rs
// 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)]
を使って手作業で型付けする。

https://rustwasm.github.io/wasm-bindgen/reference/attributes/on-rust-exports/typescript_custom_section.html

本番環境用のビルド

https://rustwasm.github.io/docs/wasm-pack/commands/build.html

~/vite-react-rust-wasm-project/wasm$ wasm-pack build --release --target web

https://ja.vitejs.dev/guide/build.html

~/vite-react-rust-wasm-project$ vite build

静的サイトのデプロイ

https://ja.vitejs.dev/guide/static-deploy.html

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で書いたアプリが完成した。

https://ak1211.github.io/vite-react-rust-wasm-project/

コピペしてください。

例えば東芝テレビリモコンの電源ボタンのコードを入れると

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

ここからは余談。

赤外線リモコン信号

赤外線リモコン信号の構造はこのページが詳しい。
http://elm-chan.org/docs/ir_format.html

https://ak1211.com/7727/
https://ak1211.com/7586/
https://ak1211.com/7141/

赤外線リモコン信号に興味があるならどうぞ活用してください。

Discussion