はじめに
2024年の暮れ、Svelte の Advent of SvelteにてDay 22: self-contained appsが公開されました。これは Svelte で実装したものを1つの index.html
ファイルにまとめる機能です。
便利な点は、index.html
ファイルを共有するだけでアプリを配布できることです。
例えば、 Web 上で提供されるサービスは、提供者の都合で見た目や動作が変わってしまうことがあります。しかし、index.html
ファイルを共有する形式であれば、利用者の都合に合わせて一定のバージョンを保持できます。(こういった需要がどの程度あるかは未知数ですが...)
動機
self-contained apps ですが、WebAssembly には対応していません(ビルドしても wasm
ファイルは独立したままです)。もし WebAssembly にも対応できれば、高度な機能を備えたアプリを index.html
だけで共有できるのでは?という発想から検証してみました。
この記事の前提知識
- WebAssembly の概要程度の知識
- Linux をかじった程度の知識
- Svelte の基礎知識
方針検討
JavaScript で wasm ファイルを読み込む際は WebAssembly.instantiateStreaming() を使うのが一般的です。第一引数の source
は Response 型であり、通常はfetch() を使って取得します。
fetch()
の第一引数は URL なので データURL 形式を使用できます。つまり、wasm
ファイルを Base64 エンコードすれば index.html
内に埋め込むことが可能になります。
const wasm = await WebAssembly.instantiateStreaming(
fetch(`data:application/wasm;base64,AGFzbQEAAAABkYCAgA...ICw==`)
);
実践
以下の手順で検証します。
- WebAssembly 環境の構築
- 簡単な WebAssembly アプリを実装
- 簡単な Svelte アプリを実装
1. WebAssembly 環境の構築
Emscripten をインストールします。(Ubuntu環境を前提)
$ sudo apt update
$ sudo apt upgrade
$ sudo apt install cmake
$ sudo apt install emscripten
$ emcc -v
emcc (Emscripten gcc/clang-like replacement + linker emulating GNU ld) 3.1.5 ()
Ubuntu clang version 13.0.1-2ubuntu2.2
Target: wasm32-unknown-emscripten
Thread model: posix
InstalledDir: /usr/bin
2. 簡単な WebAssembly アプリを実装
C言語のコードを作成します。
#include <stdio.h>
int add100(int num) {
return num + 100;
}
wasmファイルを生成します。
$ emcc sample.c -o add100.mjs -s EXPORTED_RUNTIME_METHODS=cwrap -s EXPORTED_FUNCTIONS=['_add100'] -s ENVIRONMENT=web -s SINGLE_FILE --no-entry
ここで重要なのは -s SINGLE_FILE
です。これにより wasm
ファイルが Base64 文字列に変換されて add100.mjs
に埋め込まれます。
参考:https://emscripten.org/docs/tools_reference/settings_reference.html#single-file
また、Svelte で import できるようにするため add100.mjs
のように拡張子を mjs
に設定しました。
3. 簡単な Svelte アプリを実装
あとはこれを Svelte アプリとして実装してビルドするだけです。
Svelte プロジェクトを作成します。
$ npx sv create myapp
Need to install the following packages:
sv@0.6.18
Ok to proceed? (y) y
┌ Welcome to the Svelte CLI! (v0.6.18)
│
◇ Which template would you like?
│ SvelteKit minimal
│
◇ Add type checking with Typescript?
│ Yes, using Typescript syntax
│
◆ Project created
│
◇ What would you like to add to your project? (use arrow keys / space bar)
│ none
│
◇ Which package manager do you want to install dependencies with?
│ npm
│
◆ Successfully installed dependencies
│
◇ Project next steps ─────────────────────────────────────────────────────╮
│ │
│ 1: cd myapp │
│ 2: git init && git add -A && git commit -m "Initial commit" (optional) │
│ 3: npm run dev -- --open │
│ │
│ To close the dev server, hit Ctrl-C │
│ │
│ Stuck? Visit us at https://svelte.dev/chat │
│ │
├──────────────────────────────────────────────────────────────────────────╯
│
└ You're all set!
$ cd myapp
$ npm i -D @sveltejs/adapter-static
$ npm install
self-contained apps を有効化します。
import adapter from '@sveltejs/adapter-static';
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
/** @type {import('@sveltejs/kit').Config} */
const config = {
// Consult https://svelte.dev/docs/kit/integrations
// for more information about preprocessors
preprocess: vitePreprocess(),
kit: {
// adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list.
// If your environment is not supported, or you settled on a specific environment, switch out the adapter.
// See https://svelte.dev/docs/kit/adapters for more information about adapters.
adapter: adapter(),
output: {
bundleStrategy: 'inline'
},
router: {
type: 'hash'
}
}
};
export default config;
add100.mjs
をコピーします。
$ cp [WebAssembly実装ディレクトリ]/add100.mjs src/routes/
アプリの HTML を src/routes/+page.svelte
に実装します。
<script>
import { onMount } from 'svelte';
import initWasm from './add100.mjs';
// 初期化関数
async function init() {
const module = await initWasm();
// モジュール読み込みが完了するまで待つ
await new Promise(resolve => {
if (module.onRuntimeInitialized) {
module.onRuntimeInitialized = resolve;
} else {
resolve();
}
});
// add100 関数を読み込む
const add100 = module.cwrap(
"add100",
"number",
["number"]
);
// ボタンをクリックされたら add100 を実行するように設定
const buttonElm = document.querySelector("#button");
buttonElm.addEventListener("click", () => {
const inputElm = document.querySelector("#input");
const result = add100(parseInt(inputElm.value));
inputElm.value = result;
});
}
onMount(init);
</script>
<input id="input" type="number">
<button id="button">add 100</button>
ビルドします。
$ npm run build
index.html 単体で機能することを確認するため、あえてコピーします。
$ cp build/index.html [コピー先]/index.html
結果確認
index.html
ファイルをダブルリックしてブラウザを起動して動作確認してみます。
「add 100」ボタンをクリックしてみます。
もう一回クリックしてみます。
ちゃんと WebAssembly が機能してますね!
まとめ
Svelte の self-contained apps で WebAssembly を実現する手順を検討しました。
結果、emcc
コマンドのオプションを工夫することで実現可能であることがわかりました。
ローカル専用で WebAssembly を使ったアプリを手軽に作って共有できそうですね!