九保すこひ@フリーランスエンジニア|累計300万PVのブログ運営中
さてさて、この間知ったのですが、どうやら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 でつかう
未来予想はそこまで得意じゃないですけど、ぜひ皆さんも研究してみてくださいね。
ではでは〜❗
「口呼吸 → 鼻呼吸
で体調復活❗」