CouchDB + PouchDB でPWAを作ろう！

PWA作ってますか？それとも使ってますか？  
今、皆さんが見ているQiitaもChrome等ではアドレスバーにinstallアイコンが表示されているので、既に知らず知らずのうちに使っていることでしょう。


先日、私は[kanban-board-app](https://github.com/shellyln/kanban-board-app)というPWAのコードをリリースしました。
動作は[こちら](https://shellyln.github.io/knbn/#/)で確認できます。
その際に使用した[CouchDB](https://couchdb.apache.org) + [PouchDB](https://pouchdb.com/)の構成について紹介したいと思います。



# そもそもPWAって？
乱立するPWA記事で既に見飽きていることでしょうが、念のため説明のリンクを貼っておきたいと思います。
[はじめてのプログレッシブウェブアプリ](https://developers.google.com/web/fundamentals/codelabs/your-first-pwapp/?hl=ja)

PWAはオフライン状態でもコンテンツや機能を提供し続けることが必要で、それを実現する技術(API)が[Service Worker](https://developer.mozilla.org/ja/docs/Web/API/Service_Worker_API)と[IndexedDB](https://developer.mozilla.org/ja/docs/Web/API/IndexedDB_API)です。
誤解を恐れず単純化して説明すると、Service Workerは固定のページ、画像、スクリプト等をキャッシュし、IndexedDBは動的コンテンツをキャッシュします。

> * 註：Service Workerは動的コンテンツをローカルマシンから配信することもできる高度な機能を持っています。
> * 註：IndexedDB以外にも、ローカルコンテンツのキャッシュに利用できる機能は存在します (非標準のWeb SQL等)。
> * 註：IndexedDB自体が透過的なキャッシュとして働くわけではありません。リモートのコンテンツを自身で格納する必要があります。



# PWAを作ろう！

幸いなことに、[Create React App](https://github.com/facebook/create-react-app)等のツールを使えば、Service Workerに対応したアプリのコードを生成してくれます。`serviceWorker.register`を呼ぶように変更するだけです。


```ts:src/index.tsx
...


// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA

// serviceWorker.unregister();
serviceWorker.register();
```

> * 註：`public/manifest.json`も自分のアプリの説明に変更しましょう。



# 問題は動的コンテンツのキャッシュ

非透過的な方法(自身でリモートから取得、ローカルを更新、リモート取得に失敗したらローカルから取得)でデータを管理するのは非常に苦痛であり、工数も多く掛かるので、リモートとローカルを自動同期して、どこにデータがあるかを意識せずに済む仕組みにしたいと思うのは当然のことと思います。
最近の流れでは、認証含めてFirebaseに丸投げし、この自動同期には[Firestore](https://firebase.google.com/docs/firestore?hl=ja)や[Firebase Realtime Database](https://firebase.google.com/docs/database/?hl=ja)
で済ませてしまうのが、一つのトレンドかと思います。

しかし、様々な理由(ロックインされたくない、サービス化しない、イントラでのみ使う、等々)でFirebaseを使用しないという選択もあり得ます。


# CouchDB + PouchDB という選択肢

[CouchDB](https://couchdb.apache.org)はオープンソースのNoSQLドキュメントデータベースで、すべての操作がREST APIのみで提供されています。
立ち位置としては、MongoDB等と同じですが、MongoDBのようにRDB紛いの複雑な(表)結合操作は行なえません。
JavaScriptによるMap/Reduce関数によってビューを定義することもできますが、Firebase Realtime Databaseのように動的な条件を扱うことはできません。

[PouchDB](https://pouchdb.com/)は、JavaScriptのドキュメントデータベースライブラリで、CouchDBの各APIと同等な機能をJavaScript APIとして提供しています。
ブラウザで使用する場合のバックエンドはIndexedDB、Nodeで使用する場合のデフォルトのバックエンドはLevelDBです。
PouchDBのもう1つの特徴は、CouchDBのレプリケーションAPIによってリモートのCouchDBと自動同期できることです。


CouchDBとPouchDBを同期する最低限のコード (PouchDBサイトより引用)

```js
var db = new PouchDB('dbname');

db.put({
  _id: 'dave@gmail.com',
  name: 'David',
  age: 69
});

db.changes().on('change', function() {
  console.log('Ch-Ch-Changes');
});

db.replicate.to('http://example.com/mydb');
```

これだけのコードで自動同期が実現できます！
あとは、常にローカル側を参照するだけです。


# CouchDBの課題

上述しましたが、CouchDBでは動的なビューを提供できず、さらに、ユーザーのアクセス権限をデータベース単位でしか設定できないので、1つのデータベースに複数ユーザーの、個々人の(共有しない)データを格納することができません。
結果として、ユーザー毎にデータベースを作成することになります。
また、複数ユーザー共有のデータがある場合、もう1つ、ユーザーからは読み取り専用のデータベースを個人毎に提供し、バックエンド側から更新することになります。

> * 註：1つのサーバーインスタンスに複数のデータベースを持つことができます。「データベース」と言ってもRDBのテーブル位の粒度のオブジェクトです。


# PouchDBの課題

一部のCouchDB互換サーバー([IBM Cloudant](https://www.ibm.com/jp-ja/cloud/cloudant))との同期では、同期対象のドキュメントidを指定する必要があります。
以下のように、同期開始前にリモートのid一覧を取得することで対処できます。


[https://github.com/shellyln/kanban-board-app/blob/master/src/lib/db.ts](https://github.com/shellyln/kanban-board-app/blob/master/src/lib/db.ts)

```ts
...

const localDocs = await localDb.allDocs({});
const remoteDocs = remoteDb ? await remoteDb.allDocs({}) : null;
const idSet = new Set<string>();
for (const doc of localDocs.rows) {
    idSet.add(doc.id);
}
if (remoteDocs) {
    for (const doc of remoteDocs.rows) {
        idSet.add(doc.id);
    }
}

rep = localDb.sync(remoteDb, {
    live: true,
    retry: true,
    doc_ids: Array.from(idSet.values()),
})

...
```


# さいごに

PWA簡単です！是非作ってみましょう！
~~(私が一番辛かったのは、各サイズのアイコン作成というのは内緒です)~~