SQLite WasmとOPFSを実際に使う際に調べたこと(備忘録)
はじめに
SQLite Wasm
と OPFS
を用いてWebフロントエンドでのデータ永続化を簡単に実現させるための NeverChange
というライブラリを作成している
こちらに関する詳細は以下のZennの記事に以前書かせていただいている。
今回この NeverChange
の開発をするにあたり、SQLite Wasm + OPFS
での動作環境について調べた内容をこちらのスクラップにまとめていく。
なお、 SQLite Wasm
自体は以下のリポジトリを対象として書いていく。
ただ調べたところ他にも SQLite Wasm
の実装はあり別の実装では内部の実装内容が異なっていそうだったので、これから書いていくような内容を行わずに SQLite Wasm
を動かしたいという場合、それらのライブラリを使うことで解決できるかもしれない。
SQLite WasmとOPFSを有効化させるためには
まず SQLite Wasm
では内部処理で SharedArrayBuffer
を用いているが、これを動かすためには特定のヘッダー設定が必要となる。具体的には以下。
-
Cross-Origin-Opener-Policy
をsame-origin
-
Cross-Origin-Embedder-Policy
をrequire-corp
まずこれを実現できないとSQLite Wasm
+ OPFS
という設定で動かすことはできない。
そして実際にサイトにデプロイして動かすには、いかにしてこのヘッダー設定を達成するかが鍵となる。
workerで動かす必要がある
またOPFSを有効にして動かすにはworkerで動かす必要があるが、SQLite Wasm
のREADMEにはわかりやすいサンプルが載っているので、そちらを踏襲すれば問題ない。
※以下はSQLite wasmのREADMEに記載されていたサンプルを引用したもの
import { sqlite3Worker1Promiser } from '@sqlite.org/sqlite-wasm';
const log = console.log;
const error = console.error;
const initializeSQLite = async () => {
try {
log('Loading and initializing SQLite3 module...');
const promiser = await new Promise((resolve) => {
const _promiser = sqlite3Worker1Promiser({
onready: () => resolve(_promiser),
});
});
log('Done initializing. Running demo...');
const configResponse = await promiser('config-get', {});
log('Running SQLite3 version', configResponse.result.version.libVersion);
const openResponse = await promiser('open', {
filename: 'file:mydb.sqlite3?vfs=opfs',
});
const { dbId } = openResponse;
log(
'OPFS is available, created persisted database at',
openResponse.result.filename.replace(/^file:(.*?)\?vfs=opfs$/, '$1'),
);
// Your SQLite code here.
} catch (err) {
if (!(err instanceof Error)) {
err = new Error(err.result.message);
}
error(err.name, err.message);
}
};
initializeSQLite();
実際にホスティング環境で動かす方法
ここからはよく利用されているホスティング環境を含めたいくつかのユースケースにおける動かし方について触れていく。
なお、サーバー環境も含めてデプロイできるサービスであれば、独自にヘッダー周りは設定できると思うのでここでは割愛する。
(例:EC2にデプロイして利用するケースなど)
Viteを用いた開発環境のセットアップ
まずは開発環境周り。私は SQLite Wasm + OPFS
での開発時は基本的に Vite
を用いていた。
Vite
を利用する場合設定ファイルに以下のような記述を書けば実行可能となる。
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
server: {
headers: {
'Cross-Origin-Opener-Policy': 'same-origin',
'Cross-Origin-Embedder-Policy': 'require-corp',
},
},
optimizeDeps: {
exclude: ['@sqlite.org/sqlite-wasm'],
},
});
optimizeDeps
に SQLite Wasm
を入れる理由だが、SQLite Wasm
にはWasmファイルが含まれているが、Viteの依存関係の最適化機能はJavaScriptモジュールの処理を想定したものなので、ここに含めてしまうとうまく動かない、というのが理由だった気がする。ここは認識がアバウトです。
(だが、optimizeDeps
に入れないといずれにせよエラーにはなる)
Netlifyで動かす方法
Netlify
で SQLite Wasm + OPFS
のアプリを動かすのはとても簡単なので、こういった無料から利用を開始できる静的ファイルを動かせる系のホスティング環境ならオススメの選択肢となる。
Netlify
では _headers
ファイルという独自の設定ファイルをルートディレクトリに配置することで、ヘッダーなどの設定を反映させることができる。
_headers
ファイルには以下のような記述を書き、これをデプロイしたプロジェクトのルートに配置するだけでOK。
/*
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
GitHub Pagesで動かす方法
GitHub Pagesを使用する場合、Cross-Origin-Opener-Policy: same-origin
と Cross-Origin-Embedder-Policy: require-corp
ヘッダーを設定できないため、そのままでは利用できない。
そこで以下の coi-serviceworker.js
を利用する形となる。
これはservice workerを介してヘッダーを設定してくれるライブラリで、GitHub Pagesなどで利用する際はこれを使うのが定番?らしい。
インストール後、HTMLなどに以下のような記述を書けばヘッダーの適用をしてくれる。
<!DOCTYPE html>
<html>
<head>
<title>NeverChange Example</title>
<!-- coi-serviceworker.js is required for GitHub Pages -->
<script src="/your-project-name/coi-serviceworker.js"></script>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
なお、以下のようにルート以下のディレクトリに配置した場合、何故か無限に読み込みループが発生する事態となったので注意。
<script src="/your-project-name/directory/coi-serviceworker.js"></script>
実際にGitHub Pagesにデプロイしているサンプルアプリのリポジトリを公開しているので、より実践的な構成はこちらが参考になると思われる。
Safariだと動かない
そしてここで残念なお知らせだが、このサービスワーカーを用いた仕組みは Safari では正しく機能しないため、Safari(iPhone、Macどちらも)環境では動作させることができない。
例えばiPhoneのSafariでも動作するWebアプリを作成したいとなった場合、Netlify
を選ぶのがオススメかと思われる。
他のサイト事例について
今のところ、Netlify
と GitHub Pages
しか検証できていないが他にも似たようなサービスはたくさんあるので何か分かり次第追加していく予定。
逆にご存知のケースがあればコメントいただけるとありがたいです。
余談:Duck DB Wasm + OPFSの場合、もっと簡単に動く
SQLite Wasm
とは別に DuckDB Wasm
がある。
利用用途としては異なるが、こちらも利用することで SQLite Wasm
同様にWebフロントエンドに完結した形でDBの利用が可能となる。
(実際に私自身も DuckDB Wasm + OPFS
の構成でログ分析用のアプリを書いて利用しているがとても便利)
そして Duck DB Wasm
で OPFS
を利用する場合、ここまで書いてきたようなヘッダーの設定を適用する必要はなく、普通に動く
これは Duck DB Wasm
側での実装が SharedArrayBuffer
に依存していない実装になっているのかと想像しているが詳細までは追えていない。