Wasm
Pharにしたアプリケーションは、ブラウザの中でも動きます。service workerがWebAssemblyにコンパイルされたPHP(php-cgi-wasm)を起動し、ブラウザのリクエストをすべてapp.pharに渡します。サーバーにPHPランタイムもアプリケーションサーバーも要りません。静的ファイルを置ける場所さえあれば動きます。
ブラウザ ──fetch──> service worker ──> php-cgi-wasm ──> app.phar (BEAR.Sunday)
(wasm/sw.js) └─ pdo_sqlite
リソースはHTMLを返し、_linksは<a>と<form>になります。状態はSQLite(pdo_sqlite)に書かれ、ブラウザのIndexedDBが保持します。
動くデモはkoriym/wasm-todo(公開ページ)です。
BEAR.Package 1.24以降が必要です。ProdModuleに書くReadOnlyAppModuleはPharと同じです。
ビルド
アーカイブはビルドマシンで作ります。Pharと同じです。
composer compile # bin/compile.php が prod-html-app をコンパイルして app.phar にパック
HTMLで答えるために、HtmlModuleがQiqModuleをインストールします。テンプレートはvar/qiq/templateに置きます。ProdModuleに入れるQiqProdModuleはコンパイルステップを登録するので、テンプレートはビルドでvar/build/{context}/qiqにコンパイルされ、アーカイブがそれを運びます。起動してからテンプレートをコンパイルすることはありません。
wasmの中でパックはできません。Compiler::phar()はphar.readonly=0の子プロセスを使い、wasmにプロセスはないからです。wasmがするのは、出来上がったアーカイブの起動だけです。
動かす
index.htmlがservice workerを登録します。workerはapp.pharをwasmの仮想ファイルシステムに置き、すべてのリクエストをPHPに渡します。
ブラウザはservice workerを使い捨てます。killされて再起動したときのファイルシステムは空です。ライブラリのfiles:プリロードを使い、起動のたびに/index.phpに入れ直します。
import { PhpCgiWebBase } from 'php-cgi-wasm/PhpCgiWebBase.mjs';
import Php85CgiWorker from 'php-cgi-wasm/php8.5-cgi-worker.mjs';
import phar from 'php-wasm-phar';
import sqlite from 'php-wasm-sqlite';
const php = new PhpCgiWebBase(
Promise.resolve({ default: Php85CgiWorker }),
{
version: '8.5',
sharedLibs: [phar, sqlite],
files: [{ parent: '/', name: 'index.php', url: 'app.phar' }],
env: { CONTEXT: 'prod-html-app' },
}
);
self.addEventListener('fetch', event => php.handleFetchEvent(event));
JavaScriptはこれだけです。
pharとsqliteの拡張はphp-cgi-wasmの既定ビルドに入っていません。php-wasm-pharとphp-wasm-sqliteをsharedLibsで渡します。
バンドルはNode.jsで行い、出来たものを静的サーバーに置きます。
cd wasm
npm install
npm run build # sw.js をバンドルし、dist/ にアセットを集める
npx serve dist # 任意の静的サーバー
サブパス
GitHub Pagesはhttps://{user}.github.io/{repo}/のようにサブパスで配信します。workerは自身のURLからbasePathを導出し、エントリポイントがBASE_PATHを剥がしてからルーティングします。リソースが返すリンクは相対なので、どのサブパスでも正しいままです。303のLocationも相対で、要求URLを基準に解決されます。だからルートはすべて同じ深さに置きます。静的ホストにはアプリのルートに対応するファイルがないので、404.htmlが同じブートストラップを返し、ディープリンクの初回訪問もworkerに引き継がれます。
永続化
wasmの仮想ファイルシステムのうち、/persistはIndexedDBに同期されるマウントです。ここに置いたSQLiteのデータベースはリロードしても残ります。デモではTodoRepositoryが/persist/todo.dbに書きます。
wasmの制限
MySQLは動きません。ブラウザのwasmにはraw TCPソケットがなく、mysqliもpdo_mysqlも接続できません。ストレージはブラウザの中で動くSQLiteかPGlite、あるいはリモートのCloudflare D1です。制限はTCPであって、HTTPではありません。
デモ
koriym/wasm-todoは、リソース4つ(Todos/Todoと、遷移のtodo-toggle/todo-delete)、Qiqのテンプレート、TodoRepository、service worker、ビルドスクリプトの構成です。ページリソースが実装するのはonGetとonPostだけで、状態を変える遷移はそれぞれリソースになっています。だからフォームはPOSTだけで足ります。GitHub Actionsがpharをビルドし、esbuildでバンドルし、GitHub Pagesが配信します。