はじめに
ブラウザにデータを保存しようとして、IndexedDBのonupgradeneededやトランザクションまわりのコードにうんざりしたことはないでしょうか。かといってlocalStorageは同期処理でUIをブロックするうえ、文字列しか保存できず、容量もわずか5MB程度しかありません。
Localforageは、この悩みを解決してくれるライブラリです。IndexedDB・WebSQL・localStorageという3つの異なるストレージ機構を、localStorageライクなシンプルなAPIひとつでラップし、内部で最適な方式を自動的に選んでくれます。しかもすべて非同期のPromiseベースなので、書き味も現代的です。
とはいえ、読むより触った方が早いと思います。メモを保存・復元・削除できるサンプルを用意したので、先に挙動を見たい方はこちらからどうぞ。
Localforageとは
Localforageは、Mozillaが中心となって開発したJavaScriptのオフラインストレージライブラリです。ブラウザが対応しているストレージ機構のうち、IndexedDB→WebSQL→localStorageの優先順位で自動的に最適なものを選び、共通のAPIで読み書きできるようにします。
主な特徴
- localStorage風のシンプルなAPI -
setItem/getItem/removeItem/clearなど、見慣れたメソッド名で使えます - 完全非同期 - すべてのメソッドがPromiseを返すため、メインスレッドをブロックしません(コールバックスタイルにも対応)
- 多様なデータ型をそのまま保存 - 文字列だけでなく、オブジェクト・配列・Blob・ArrayBufferなどをJSON変換の手間なく保存・復元できます
- ストレージの自動フォールバック - IndexedDBが使えない環境ではWebSQLやlocalStorageに自動的に切り替わります
- 依存ライブラリなし - 単体で完結しており、バンドルサイズも軽量です
インストール
npmを使う場合は以下のコマンドでインストールできます。
npm install localforage
Yarnの場合は次のとおりです。
yarn add localforage
CDN経由でそのまま読み込むことも可能です。
<script src="https://cdn.jsdelivr.net/npm/localforage/dist/localforage.min.js"></script>
Localforageのサンプルを動かす
下のサンプルは、テキストエリアの内容をLocalforageのsetItemで保存し、getItemで読み込み、removeItemで削除するメモ帳です。「保存」を押したあとページ内の「読み込み」を押すと、変数を経由せずストレージから値が復元されることが確認できます。
要点だけを抜き出すと、Localforageの基本操作は次の3行に集約されます。
import localforage from 'localforage'
await localforage.setItem('memo', 'こんにちは')
const value = await localforage.getItem('memo') // 'こんにちは'
await localforage.removeItem('memo')
実際に動かせるものが下です。テキストを入力して「保存」を押し、「削除」してから「読み込み」を押すと、nullが返って空になる様子も確認できます。
setItemとgetItemはどちらもPromiseを返すため、awaitだけで完結する点がポイントです。内部ではIndexedDBが使える環境ならIndexedDBに、使えなければlocalStorageなどにフォールバックして保存されるため、呼び出す側はストレージの種類を意識する必要がありません。
基本的な使い方
Localforageはconfig()でデータベース名やドライバの優先順位をカスタマイズできます。設定しない場合はデフォルト値が使われるため、最小構成では呼び出さなくても問題ありません。
import localforage from 'localforage'
// ドライバの優先順位やDB名を設定(任意)
localforage.config({
driver: [localforage.INDEXEDDB, localforage.WEBSQL, localforage.LOCALSTORAGE],
name: 'my-app',
storeName: 'keyvaluepairs',
})
await localforage.setItem('key', 'value')
const value = await localforage.getItem('key')
await localforage.removeItem('key')
await localforage.clear() // ストア内のすべてのデータを削除
実践的なユースケース
TODOリストをそのまま保存する(オブジェクト・配列の永続化)
localStorageは文字列しか保存できないため、配列やオブジェクトを扱う際はJSON.stringify/JSON.parseが必須でした。LocalforageのsetItem/getItemはオブジェクトや配列をそのまま保存・復元できるため、この変換コードが不要になります。
// localforageはJSONだけでなくBlobやArrayBufferもそのまま保存できる
const todos = ['牛乳を買う', 'レポート提出']
await localforage.setItem('todos', todos)
const saved = await localforage.getItem('todos')
console.log(saved) // ['牛乳を買う', 'レポート提出'] 配列のまま復元される
下のサンプルでは、入力したタスクを配列としてsetItemに保存し、「削除」ボタンではspliceした配列を再度setItemで書き戻しています。
追加・削除のたびに配列をgetItemで読み出し、加工してからsetItemで書き戻すだけでTODOアプリの永続化が完成します。JSONへの変換コードを一切書かずに済む分だけ、実装がすっきりする点を体感できるはずです。
名前空間を分けて複数のストアを使う(createInstance)
1つのアプリの中で「ユーザー設定」と「APIレスポンスのキャッシュ」のように性質の異なるデータを扱う場合、同じキー名を使うと上書き事故が起きかねません。LocalforageのcreateInstance()を使うと、独立した名前空間を持つストアを複数作成でき、同じキー名でも衝突しません。
// createInstanceで独立した名前空間のストアを作れる
const userStore = localforage.createInstance({ name: 'user-store' })
const cacheStore = localforage.createInstance({ name: 'cache-store' })
await userStore.setItem('name', 'たくま')
await cacheStore.setItem('lastFetched', Date.now())
// 同じキー名(name)を使っても別ストアなので衝突しない
下のサンプルでは、userStoreとcacheStoreという2つの独立したインスタンスに対してそれぞれ書き込みを行い、両方の中身を同時に表示しています。
createInstanceに渡すname(またはstoreName)を変えるだけで、内部的には別のIndexedDBデータベースとして分離されます。認証情報のキャッシュとAPIレスポンスのキャッシュを別々にclear()したい、といった場面でも役立ちます。
まとめ
Localforageを使うと、IndexedDBの複雑なAPIを直接書くことなく、localStorage感覚のシンプルな呼び出しでオフラインストレージを扱えます。すべてのメソッドがPromiseベースであること、オブジェクトや配列をそのまま保存できること、createInstanceでストアを分離できることが大きな特徴です。
localStorageの容量や同期処理に限界を感じたら、まずは今のsetItem/getItemをLocalforageに置き換えてみることから始めてみてください。
