🚀

Rust + WebAssembly + SolidJS なサイトを Cloudflare Pages にデプロイする

2022/10/14に公開

はじめに

最近 Rust に再入門して WebAssembly を作って遊んでいます。

https://zenn.dev/a24k/articles/20221012-wasmple-simple-console

スタックとしては、Rust + WebAssembly + SolidJS という感じなのですが、Rust 側と JavaScript 側とそれぞれビルドが必要だったりして、なんかちょっと面倒です。普通に npm run dev したら、Rust 側も含めてホットリロードされると良いなぁ・・・ということで、今回は以下のコミットを題材に、ローカルでのビルド設定から GitHub Actions を利用したデプロイまで、見ていきたいと思います。

https://github.com/a24k/wasmple/tree/e29af52baa06600548ac988af25e61c09e45d092

ローカルでの開発とビルド

プロジェクト内には、JavaScript 側のファイルと Rust 側のファイルが共存することになります。大きくなってきたら、階層を整理したり・リポジトリ分けたり・・・という話にもなるかとは思いますが、いったんルート / 直下に JavaScript 側(npm)のプロジェクトを配置しています。Rust 側(Cargo)のプロジェクトは /wasmple 配下に配置しています。

npm run build

まずは、npm scripts を使って、Rust 側と JavaScript 側のビルドが一気に出来る様にしてみましょう。こんな感じ。

"scripts": {
  "build:wasm": "cd ./wasmple && cargo build --target wasm32-unknown-unknown --release",
  "postbuild:wasm": "wasm-snip -o ./wasmple/target/wasm32-unknown-unknown/wasmple.wasm ./wasmple/target/wasm32-unknown-unknown/release/wasmple.wasm",
  "build:js": "vite build",
  "build": "run-s build:wasm build:js"
}

Rust 側は、cargo build --release をした後に、wasm-snip をかけています。

https://github.com/rustwasm/wasm-snip

ビルド直後の .wasm ファイルは、こんな小さなプロジェクトでも MB 単位になったりしているのですが、snip すると不要なコードが削除されてかなり小さくなります。Web 上でロードするものなので、小さいに越したこと無いです。他にもいくつか似たようなツールがありそうなのですが、これが一番効果がありそうだったので、いったんこれだけかけています。この辺りは、.wasm のサイズが大きくなってきたら、もう少し研究したいところです。

build phase .wasm size
cargo build --release 1,743,589 bytes
wasm-snip 50,543 bytes

JavaScript 側は、単なる vite build ですね。Rust 側のビルド完了後に、JavaScript 側のビルドが走る様になっています。JavaScript 内でアセットとして .wasm ファイルを参照しているので、一緒に dist/ に書き出されますし、assetsInlineLimit よりも小さい場合は JavaScript 内に base64 で埋め込まれたりもします。良い感じです。

https://github.com/a24k/wasmple/blob/e29af52baa06600548ac988af25e61c09e45d092/src/wasmple.js#L1

https://vitejs.dev/guide/assets.html#explicit-url-imports

npm run dev

ビルドが出来たので、ホットリロードの方も組んでみましょう。結局こんな感じになりました。

"scripts": {
  "dev:wasm": "cd ./wasmple && cargo build --target wasm32-unknown-unknown",
  "dev:copy": "cpx ./wasmple/target/wasm32-unknown-unknown/debug/wasmple.wasm ./wasmple/target/wasm32-unknown-unknown/",
  "watch:wasm": "cd ./wasmple && cargo watch -x 'build --target wasm32-unknown-unknown'",
  "watch:copy": "cpx ./wasmple/target/wasm32-unknown-unknown/debug/wasmple.wasm ./wasmple/target/wasm32-unknown-unknown/ --watch",
  "watch:js": "vite --clearScreen false",
  "predev": "run-s dev:wasm dev:copy",
  "dev": "run-p watch:wasm watch:copy watch:js",
}

開発中は、snip しない .wasm を利用しています。今のところはデバッガ繋ぐわけでも無いので、あんまり変わらない気もします。watch を始める時に、.wasm 側が整っていないとエラーが出てちょっとびっくりするので、一度 Rust 側のビルドを通してから watch に移行しています。

Rust 側は、cargo watch を使って変更監視をしており、変更があり次第ビルドが走る様になっています。.wasm が更新されると、JavaScript から参照しているパスにコピーされた後に、連鎖して vite 側のホットリロードが走ります。これで、どちらのコードを編集してもホットリロードが走るようになりました。まぁまぁ快適。

GitHub Actions でのビルドとデプロイ

ローカルでのビルドはサクッと出来る様になったので、今度は Continuous Delivery の設定をしてみます。最近割と好きな Cloudflare Pages にデプロイしたいと思うのですが、Cloudflare Pages のビルド環境には Rust が入っていない様です。ねぇ。

https://developers.cloudflare.com/pages/platform/build-configuration/

仕方がない(?)ので、GitHub Actions でビルドすることにします。ワークフローこんな感じです。

https://github.com/a24k/wasmple/blob/e29af52baa06600548ac988af25e61c09e45d092/.github/workflows/cd-cfpages.yml

実行のたびに依存 crate をダウンロードしてくるところからやるのは時間がかかるので、ある程度キャッシュされるように Swatinem/rust-cache というアクションを使っています。

https://github.com/Swatinem/rust-cache

また、途中で wasm-snip のインストールを行っているのですが、(上記キャッシュの影響等で)既にバイナリが存在すると cargo install が失敗するので、continue-on-error: true として実行が止まらない様にしています。

Cloudflare Pages へのデプロイについては、公式のアクション cloudflare/pages-action があるので、こちらを利用します。設定の手順については、@nwtgck さんの記事が詳しいです。感謝。

https://github.com/cloudflare/pages-action

https://zenn.dev/nwtgck/articles/1fdee0e84e5808

おわりに

これで、ローカルでの開発からデプロイまで整いました。push する度に Cloudflare Pages にデプロイされて、その都度プレビュー用の url が払い出されるので、記事から参照する際にも実際のサイトでデモすることが出来て良さそうです。なかなか快適なので、引き続き遊んでみることにします。

Discussion