
九保すこひです(フリーランスのITコンサルタント、エンジニア)
さてさて、この間知ったのですが、どうやらJavaScript
でwasm
(Webassembly
)であることができるのを知りました。
それが・・・・・・
SQLiteを操作することができる
というものです。
つまり、wasm
を使うことで「ブラウザだけでSQLite」を動かすことができるんですね。
参考ページ: WebAssemblyとは?
そこで
なんか面白そうだったので、「Laravel + React」の環境で使ってみることにしました。
機能は「(ウェブなのに)ローカルだけで完結するブックマーク」です。(需要は度外視ですが、もしかすると秘密のブックマークなんかにも使えるかもですね)
ぜひ何かの参考になりましたら嬉しいです。
「TikTok で英語リスニング
トレーニング中。
海外案件狙うぞ(できれば円安中に…)」
目次 [非表示]
パッケージをインストールする
では、今回の機能で利用するパッケージ2つをインストールしておきましょう。
以下のコマンドを実行してください。
npm i sql.js vite-plugin-static-copy --save-dev
Vite で sql-wasm.wasm がコピーできるようにする
パッケージがインストールできたら、次にwasm
ファイルがインターネット上からアクセスできるようにファイルをコピーできるようにします。
vite.config.js
import { defineConfig } from 'vite';
import laravel from 'laravel-vite-plugin';
import react from '@vitejs/plugin-react';
import { viteStaticCopy } from 'vite-plugin-static-copy' //
ここを追加しました
export default defineConfig({
plugins: [
laravel({
input: 'resources/js/app.jsx',
ssr: 'resources/js/ssr.jsx',
refresh: true,
}),
react(),
viteStaticCopy({ //
ここを追加しました
targets: [
{
src: 'node_modules/sql.js/dist/sql-wasm.wasm',
dest: 'wasm'
}
]
})
]
// 省略
});
これでsql.js
の中にあるsql-wasm.wasm
がビルドする度にコピーされる(=つまり、パッケージにアップデートがあっても自動で新しいものに置き換わる)ということになります
では、早速以下のコマンドでファイルをコピーしてみましょう。
npm run build
すると、以下のようにファイルがコピーされますので、「http://*******/build/wasm/sql-wasm.wasm」でアクセスできるようになります。
ルートをつくる
では、準備は完了しましたので、ここからはコードを書いていきます
まずはルートです。
routes/web.php
// 省略
Route::get('wasm_sqlite_bookmark', fn() => Inertia::render('WasmSqliteBookmark/Index'));
ビューをつくる
では、今回のメインどころのビューです。
ちょっとコードが長いですが、そこまで複雑な部分はないのでぜひ順に見てください。
resources/js/Pages/WasmSqliteBookmark/Index.jsx
import initSqlJs from 'sql.js'
import {useEffect, useRef, useState} from "react";
export default function Index() {
// DB
const db = useRef(null);
useEffect(() => {
initializeDb();
}, []);
const initializeDb = (data = null) => {
// Wasm の指定
const config = {
locateFile() {
return '/build/wasm/sql-wasm.wasm'
}
}
// SQLite を初期化
initSqlJs(config)
.then(SQL => {
db.current = new SQL.Database(data);
if(! data) { // 初期の場合はテーブルを作成
const sql = `CREATE TABLE bookmarks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title varchar(255) NOT NULL,
url varchar(255) NOT NULL,
created_at datetime NOT NULL
);`
db.current.run(sql);
}
loadBookmarks();
});
};
// ブックマーク
const [bookmarks, setBookmarks] = useState([]);
const handleFileSelected = e => { // ファイルが選択されたとき
const files = e.target.files;
if(files.length > 0) {
const file = files[0];
const reader = new FileReader();
reader.onload = () => {
const data = new Uint8Array(reader.result); // ファイルを読み込んでデータ化
initializeDb(data); // DB を初期化
}
reader.readAsArrayBuffer(file);
}
};
const loadBookmarks = () => { // SQLite からブックマークを読み込む
let newBookmarks = [];
const stmt = db.current.prepare('SELECT id, title, url, created_at FROM bookmarks;');
stmt.getAsObject();
while(stmt.step()) {
const row = stmt.getAsObject();
newBookmarks.push(row);
}
setBookmarks(newBookmarks);
};
// パラメータ
const [title, setTitle] = useState('');
const [url, setUrl] = useState('');
const handleSaveBookmarkClick = () => {
// バリデーションは省略してます
const sql = `INSERT INTO
bookmarks (title, url, created_at)
VALUES (?, ?, datetime('now'));`;
const params = [title, url];
db.current.run(sql, params);
// パラメータを初期化
setTitle('');
setUrl('');
loadBookmarks();
};
// ブックマークのエクスポート
const handleDownloadClick = () => {
const data = db.current.export();
const blob = new Blob([data], {
type: 'application/octet-stream'
});
const downloadUrl = window.URL.createObjectURL(blob);
const link = document.createElement('a');
const dateTime = new Date().toLocaleString().replace(/:/g, '-');
const filename = `bookmark_${dateTime}.sqlite`;
// ダウンロード
link.href = downloadUrl;
link.download = filename;
document.body.appendChild(link);
link.style = 'display:none';
link.click();
link.remove();
setTimeout(() => {
return window.URL.revokeObjectURL(downloadUrl); // メモリ解放
}, 1000);
};
return (
<div className="p-5">
<div className="mb-3">
<button type="button" className="text-white bg-blue-700 font-medium rounded text-sm px-3 py-2.5 mr-2 mb-2" onClick={handleDownloadClick}>
ブックマーク(SQLite)をダウンロード
</button>
<label className="text-white bg-gray-700 font-medium rounded text-sm px-3 py-2.5 mr-2 mb-2 cursor-pointer">
<input
type="file"
className="hidden"
onChange={handleFileSelected} />
ファイルからブックマークを読み込む
</label>
</div>
<hr />
<div className="px-3 py-5">
{bookmarks.length > 0 && (
bookmarks.map(bookmark => (
<div key={bookmark.id} className="mb-3">
📝 <a href={bookmark.url} className="text-blue-700 font-medium font-bold" target="_blank">{bookmark.title}</a>
</div>
))
) || (
bookmarks.length === 0 && (
<small>ブックマークが見つかりません。以下から追加してください。</small>
))}
</div>
<div className="border border-1 bg-gray-200 p-3">
<small>ブックマークを追加する</small>
<div className="mb-3">
<input type="text" value={title} onChange={e => setTitle(e.target.value)} placeholder="タイトル" />
</div>
<div className="mb-3">
<input type="text" value={url} onChange={e => setUrl(e.target.value)} placeholder="URL" />
</div>
<button
type="button"
className="text-white bg-orange-500 font-medium rounded text-sm px-2 py-1.5 mr-2 mb-2"
onClick={handleSaveBookmarkClick}>
追加する
</button>
</div>
</div>
);
}
では、重要な部分をひとつずつ紹介してきます。
DB
initSqlJs()
は、sql.js
の関数です。
これを呼び出すことで、SQLite
が使えるようになるのですが、そのためには先ほどVite
の方で自動コピーできるようした「wasmファイル」を指定しないといけません。
それが、「Wasm の指定」にあるlocateFile
です。
そして、initializeDb()
は、以下2つのパターンで実行されます。
- はじめにページにアクセスされたとき
- すでに保存したブックマークファイル(SQLite)が選択されたとき
そのため、引数の初期値はnull
になっています。(つまり、ファイルが選択されたときはdata
に中身が入っている状態になります)
ブックマーク
このセクションは、SQLite
からブックマークのデータを読み込む機能になります。
後で「ブックマークのエクスポート」部分にも出てきますが、SQLite
の保存データはUint
のため、読み込んだファイルは形式を変換するようになっています。
そして、SQLite
からブックマーク・データを読み込むのがloadBookmarks()
です。
といっても、ここはSQL
文を発行してbookmarks
の中身をsetBookmarks()
で変更しているだけです。
パラメータ
ここは、新しいブックマークをSQLite
へ追加する部分です。
流れとしては、以下のとおりです。
- INSET 文を発行してデータを追加
- パラメータはもういらないので初期化
- SELECT 文を発行して新しいデータを読み込む
※ なお、コメントにも書いてありますが、バリデーションは全く書いてません。ご注意ください。m(_ _)m
ブックマークのエクスポート
wasm
はブラウザ上で動いていますので、ページがリロードされるとその内容は失われてしまいます。
そのため、追加したブックマークをファイルとして保存するのがこの部分です。
コードは少し長いですが、DB
からデータを取得し、それをJavaScript
ダウンロードできるようにしているだけです。
なお、「メモリ解放」の部分は気をつけてください。
どうもこれがないとメモリが開放されず、最悪の場合ブラウザが動かなくなってしまうようです。(キャッチ・アンド・リリースです!)
以下のページがとても参考になりました。
ありがとうございます!
参考ページ: JavaScript】 createObjectURL()した後にrevokeObjectURL()が必要な理由
これで作業は完了です
お疲れ様でした
テストしてみる
では、実際にテストしてみましょう
まずはVite
を起動して「http://******/wasm_sqlite_bookmark」へアクセスします。
すると、以下のように表示されます。
では、まだブックマークがないので以下のように入力して「追加する」ボタンをクリックしてみましょう。
すると・・・・・・
はい
新しいリンクが登場しました。
では、この要領で3つほどブックマークを追加してみます。
※ 最後だけはこのブログでブックマークしました
そして、ブックマークのダウンロード(エクスポート)ができるかをボタンをクリックして確認してみましょう。
どうなるでしょうか・・・・・・
はい
ダウンロードが自動で開始され、ブラウザに表示がでました。
では、一旦ページをリロードし、初期化します。
そして、ファイルからブックマークを読み込むため、ボタンをクリック&先ほどダウンロードしたSQLite
のファイルを選択してみます。
うまくいくでしょうか・・・・・・
はい
先ほど追加した内容を復活させることができました。
すべて成功です
デモページを用意しました
せっかくなのでデモページを用意しました。
ぜひ実際に試してみてください。
※ 冒頭でも書きましたが、サーバー側にデータが送られることはありませんので、秘密のブックマークとして普段遣いしてもらっても問題ありません。
企業様へのご提案
今回のようにwasm
(Webassembly
)を使うと、以下のメリットがあります。
- コードが分離されているのでセキュリティに強い
- バイナリ化されている(機械が読みやすい内容になっている)ので高速に実行できる
もしWebassembly
を使った開発をご希望でしたら、いつでもお気軽にご相談ください。
お待ちしております。
おわりに
ということで、今回はWebassembly
のSQLite
を使ってブックマーク機能をつくってみました。
個人的にはWebassembly
はWasmer
を使って「どんな環境でも実行できる」ようできるため、注目しているテクノロジーのひとつです。
さらに、Webassembly
は最近人気になっているRust
言語からでもつくることができる(※以下ページを参考にしてみてください)ので、これからの流れにのっているんじゃないかと考えています。
参考ページ: WebAssembly を Rust でつくり、JavaScript でつかう
未来予想はそこまで得意じゃないですけど、ぜひ皆さんも研究してみてくださいね。
ではでは〜
「口呼吸 → 鼻呼吸
で体調復活」