はじめに
電波が不安定な場所でもフォームの入力を失いたくない、複数のデバイス間でデータを同期させたい——そんなオフライン対応のWebアプリを作ろうとすると、たいてい「ローカル保存」と「サーバーとの同期」を自前で組み合わせる羽目になります。IndexedDBに書き込む処理、オンライン復帰を検知する処理、差分だけを送る処理……これを手作りするのは意外と骨が折れます。
PouchDBは、この面倒な部分をライブラリ側に任せられるブラウザ内蔵のデータベースです。ローカルへの保存はもちろん、CouchDBという互換サーバーとの間で双方向のレプリケーション(同期)をsync()メソッド1つで開始できます。
とはいえ、読むより触った方が早いと思います。メモを追加・削除できるだけの小さなアプリが以下に置いてあるので、先に挙動を見たい方はこちらからどうぞ。
PouchDBとは
PouchDBは、JavaScriptで実装されたオープンソースのデータベースです。ブラウザ上ではIndexedDBを、Node.js上ではLevelDBを内部ストレージとして利用し、CouchDBと同じHTTP API・レプリケーションプロトコルを話せるように設計されています。もともとはCouchDBチームの有志が始めたプロジェクトで、現在はApache Software Foundationの傘下(apache/pouchdb)で開発が続けられています。
主な特徴
- オフラインファースト - ネットワークが無い状態でもローカルにドキュメントを保存でき、オンラインに戻ったタイミングで自動的に差分だけを同期できます
- CouchDBとの双方向レプリケーション -
db.sync()を呼ぶだけで、ローカルのPouchDBとリモートのCouchDB互換サーバーの間の同期が継続的に走ります - 追加サーバー不要のドキュメントDB -
new PouchDB('name')と書くだけでブラウザ内にドキュメント指向のデータベースが立ち上がり、_idをキーにしたCRUDがすぐに使えます - プラグインによる拡張 -
pouchdb-findでMongoDB風のクエリを、他のプラグインで全文検索や暗号化などを追加できます
インストール
npm install pouchdb
クエリ機能を使う場合はpouchdb-findも合わせて入れます。
npm install pouchdb pouchdb-find
ビルドツールを使わない場合は、公式が配布しているブラウザ向けバンドルを<script>タグで読み込むこともできます。
PouchDBのサンプルを動かす
以下はPouchDBのput()・allDocs()・remove()だけで作ったメモ帳です。入力欄にテキストを書いて「追加」を押すとドキュメントがput()で保存され、一覧がallDocs()で再取得されます。削除ボタンは対象ドキュメントをget()してからremove()します。
import PouchDB from 'pouchdb'
const db = new PouchDB('memos')
// 追加(_idを指定しなければ自動生成される)
await db.put({ _id: 'memo1', text: 'PouchDBを試す' })
// 全件取得
const result = await db.allDocs({ include_docs: true })
console.log(result.rows.map(row => row.doc))
// 削除には最新の_revを含むドキュメントが必要
const doc = await db.get('memo1')
await db.remove(doc)
実際に動かせるものが下です。メモを何件か追加してから、あえて同じ文言をもう一度追加してみてください。PouchDBは_idが同じでも別のリビジョンとして扱えるため、_idにタイムスタンプを使っている今回の実装では重複が普通に許容されることが分かります。
put()・allDocs()・remove()の3つだけで、ページをリロードしてもデータが消えない永続化されたリストが作れることが分かったと思います(このサンプルはiframe内なので実際にリロードしても保持されるとは限りませんが、同一セッション内では動作を確認できます)。
基本的な使い方
単一ドキュメントの作成・取得・更新・削除の一連の流れは次のように書きます。
import PouchDB from 'pouchdb'
const db = new PouchDB('mydb')
// 作成
await db.put({ _id: 'user:1', name: 'Taro', age: 28 })
// 取得
const doc = await db.get('user:1')
// 更新(取得したドキュメントの_revを含めて上書きする)
await db.put({ ...doc, age: 29 })
// 削除
await db.remove(await db.get('user:1'))
PouchDBのドキュメントは更新のたびに_revという値を持ち直します。更新や削除の際は、最新の_revを含んだドキュメントを渡す必要がある点がCouchDB系データベースの特徴です。
実践的なユースケース
PouchDBのchanges APIでリアルタイムに一覧を更新する
db.changes({ live: true, since: 'now' })を使うと、ドキュメントの追加・更新・削除をリアルタイムに検知できます。ボタンのクリックハンドラとは別の場所に変更通知の受け口を用意し、そこでUIを更新するという設計にできるのがポイントです。複数タブからの変更や、後述するsync()で受信した変更をUIに即座に反映したい場合に使います。
import PouchDB from 'pouchdb'
const db = new PouchDB('tasks')
db.changes({
since: 'now',
live: true,
include_docs: true,
}).on('change', (change) => {
console.log('変更を検知:', change.doc)
})
以下のサンプルでは、ボタンを押すたびにput()でタスクを追加していますが、一覧への反映はchanges()のリスナー1つが一手に引き受けています。
「タスクA追加」と「タスクB追加」、どちらを押してもchangesのリスナーが検知して件数が増え、一覧に追記されます。追加処理と表示更新のロジックが分離できるので、複数の場所からドキュメントを書き換えるアプリでも、UI更新のコードを1箇所にまとめられます。
pouchdb-findで条件を指定して絞り込む
標準のallDocs()は_idの範囲検索しかできませんが、pouchdb-findプラグインを使うとMongoDB風のselector構文で任意のフィールドを検索できます。createIndex()でインデックスを張ってからfind()を呼ぶのが基本の流れです。
import PouchDB from 'pouchdb'
import PouchDBFind from 'pouchdb-find'
PouchDB.plugin(PouchDBFind)
const db = new PouchDB('products')
await db.createIndex({ index: { fields: ['price'] } })
const result = await db.find({
selector: { price: { $gte: 1000 } },
sort: ['price'],
})
console.log(result.docs)
下のサンプルは商品リストを事前に投入しておき、「最低価格」の欄を書き換えるたびにfind()で絞り込みを実行し直します。
最低価格に「5000」と入力すると、キーボードとモニターだけが残ります。selectorの$gteを$ltに変えれば、逆に指定額より安い商品だけを絞り込む検索にもできます。
CouchDBとの双方向レプリケーション
PouchDB最大の特徴は、CouchDB互換のレプリケーションプロトコルを話せることです。db.sync(remoteDB, { live: true })を呼ぶだけで、ローカルのPouchDBとリモートのCouchDB(またはCouchDB互換サーバー)の間で継続的な双方向同期が始まります。オフライン中に貯まった変更は、オンライン復帰時に自動でまとめて送信されます。
このサンプルは実際のCouchDBサーバーへの接続が必要なため、この記事内では実行できる形では用意していません。コードの形だけ紹介します。
import PouchDB from 'pouchdb'
const localDB = new PouchDB('localdb')
const remoteDB = new PouchDB('https://example.com/mydb')
// 双方向・継続的な同期を開始
const sync = localDB.sync(remoteDB, {
live: true,
retry: true,
})
sync
.on('change', (info) => console.log('同期しました:', info))
.on('paused', () => console.log('同期が一時停止(オフラインなど)'))
.on('active', () => console.log('同期を再開'))
.on('error', (err) => console.error('同期エラー:', err))
一方向だけでよければreplicate.to() / replicate.from()も用意されています。用途に応じて使い分けられます。
まとめ
PouchDBは、ブラウザ(やNode.js)の中に軽量なドキュメント指向データベースを立ち上げ、CouchDBとのレプリケーションまで面倒を見てくれるライブラリです。put()・get()・remove()・allDocs()といった基本操作はシンプルで、changes()によるリアルタイム検知やpouchdb-findによるクエリを組み合わせれば、オフライン対応アプリに必要な機能の大部分をカバーできます。ローカル保存とサーバー同期を毎回自前で組み立てる前に、一度PouchDBのsync()を試してみてはいかがでしょうか。