CouchDBと同期が必要なReact Nativeアプリを開発している。PouchDBはJSで組まれたCouchDBと同期可能なデータベースライブラリで、ブラウザを主眼としているが、React Nativeでも動かすことが出来る。
そのために自分はreact-native-sqlite-2 と pouchdb-adapter-react-native-sqliteを作った。
しかしそれだけではRNで動かすには不十分だった。
例えばRNはFileReader.readAsArrayBuffer
をまだサポートしていないためAttachmentsの取扱いに難があった。将来的に彼らが対応してくれることを願う。
そしてpouchdb-react-nativeプロジェクトの活動が最近見られないようだ。なので自分で何とかすることにして、今回動かすことに成功した。本稿ではどうやったか説明する。
React Native上でPouchDBを動かす手順
まずは実際にどうやってPouchDBを動かすか、手順を説明したい。
自分は今回、RN用にPouchDBのコアモジュールをハックしたパッケージを2つ作成した。
こちらがデモアプリ。
これを拙作アプリInkdropにて採用するつもり。
依存モジュールのインストール
まずはPouchDBのコアパッケージをインストールする:
npm i pouchdb-adapter-http pouchdb-mapreduce
次に、React Native用に手を加えたPouchDBのパッケージをインストールする:
npm i @craftzdog/pouchdb-core-react-native @craftzdog/pouchdb-replication-react-native
そしてSQLite3エンジンのモジュールをインストールする:
npm i pouchdb-adapter-react-native-sqlite react-native-sqlite-2
react-native link react-native-sqlite-2
PouchDBで必要なpolyfill用のパッケージを入れる:
npm i base-64 events
Polyfillを作る
PouchDB内で使用される関数群をpolyfillするjsファイルを作る:
import {decode, encode} from 'base-64'
if (!global.btoa) {
global.btoa = encode;
}
if (!global.atob) {
global.atob = decode;
}
// node依存のコードが実行されないようにする
process.browser = true
それをindex.js
の先頭でimport
してやる。
PouchDBを初期化する
以下のようなpouchdb.js
を作る:
import PouchDB from '@craftzdog/pouchdb-core-react-native'
import HttpPouch from 'pouchdb-adapter-http'
import replication from '@craftzdog/pouchdb-replication-react-native'
import mapreduce from 'pouchdb-mapreduce'
import SQLite from 'react-native-sqlite-2'
import SQLiteAdapterFactory from 'pouchdb-adapter-react-native-sqlite'
const SQLiteAdapter = SQLiteAdapterFactory(SQLite)
export default PouchDB
.plugin(HttpPouch)
.plugin(replication)
.plugin(mapreduce)
.plugin(SQLiteAdapter)
必要に応じてpouchdb-find
とかのプラグインも追加する。
PouchDBを使う
あとはいつもの通り使用する:
import PouchDB from './pouchdb'
function loadDB () {
return new PouchDB('mydb.db', { adapter: 'react-native-sqlite' })
}
PouchDBにどのように手を加えたか
React Nativeで動作させるためには、PouchDBのコアモジュールからFileReader.readAsArrayBuffer
が呼び出されるのを阻止しなければならない。
それは、全てのattachmentsをBlob
の代わりにBase64エンコードで取り扱うという事だ。
それはごく少量の行への変更で実現できる。
readAsArrayBuffer
が呼ばれている場所
PouchDBは各ドキュメントのMD5ダイジェストを計算しようとする。その際にreadAsArrayBuffer
が必要となる。
pouchdb-binary-utils/lib/index-browser.js
にて:
72 function readAsBinaryString(blob, callback) {
73 if (typeof FileReader === 'undefined') {
74 // fix for Firefox in a web worker
75 // https://bugzilla.mozilla.org/show_bug.cgi?id=901097
76 return callback(arrayBufferToBinaryString(
77 new FileReaderSync().readAsArrayBuffer(blob)));
78 }
79
80 var reader = new FileReader();
81 var hasBinaryString = typeof reader.readAsBinaryString === 'function';
82 reader.onloadend = function (e) {
83 var result = e.target.result || '';
84 if (hasBinaryString) {
85 return callback(result);
86 }
87 callback(arrayBufferToBinaryString(result));
88 };
89 if (hasBinaryString) {
90 reader.readAsBinaryString(blob);
91 } else {
92 reader.readAsArrayBuffer(blob);
93 }
94 }
この関数は以下のpouchdb-md5/lib/index-browser.js
から呼ばれる:
24 function appendBlob(buffer, blob, start, end, callback) {
25 if (start > 0 || end < blob.size) {
26 // only slice blob if we really need to
27 blob = sliceBlob(blob, start, end);
28 }
29 pouchdbBinaryUtils.readAsArrayBuffer(blob, function (arrayBuffer) {
30 buffer.append(arrayBuffer);
31 callback();
32 });
33 }
どうすればこれを避けられるか?
Attachmentsの保存部
getAttachment
メソッドのbinary
オプションを常に無効にする。
pouchdb-core/src/adapter.js
を以下のように変更する:
714 if (res.doc._attachments && res.doc._attachments[attachmentId]
715 opts.ctx = res.ctx;
716 // force it to read attachments in base64
717 opts.binary = false;
718 self._getAttachment(docId, attachmentId,
719 res.doc._attachments[attachmentId], opts, callback);
720 } else {
この変更により、attachmentsは常にbase64で取得されることに注意されたい。
Pull Replication
リモートデータベースからのレプリケーションの際、取得されたattachmentsを blobからbase64に変換してやる必要がある。
pouchdb-replication/lib/index.js
を以下のように変更してやる:
function getDocAttachmentsFromTargetOrSource(target, src, doc) {
var doCheckForLocalAttachments = pouchdbUtils.isRemote(src) && !pouchdbUtils.isRemote(target);
var filenames = Object.keys(doc._attachments);
function convertBlobToBase64(attachments) {
return Promise.all(attachments.map(function (blob) {
if (typeof blob === 'string') {
return blob
} else {
return new Promise(function (resolve, reject) {
var reader = new FileReader();
reader.readAsDataURL(blob);
reader.onloadend = function() {
const uri = reader.result;
const pos = uri.indexOf(',')
const base64 = uri.substr(pos + 1)
resolve(base64)
}
});
}
}));
}
if (!doCheckForLocalAttachments) {
return getDocAttachments(src, doc)
.then(convertBlobToBase64);
}
return target.get(doc._id).then(function (localDoc) {
return Promise.all(filenames.map(function (filename) {
if (fileHasChanged(localDoc, doc, filename)) {
return src.getAttachment(doc._id, filename);
}
return target.getAttachment(localDoc._id, filename);
}))
.then(convertBlobToBase64);
}).catch(function (error) {
/* istanbul ignore if */
if (error.status !== 404) {
throw error;
}
return getDocAttachments(src, doc)
.then(convertBlobToBase64);
});
}
これで動く。
@craftzdog/pouchdb-core-react-native
と @craftzdog/pouchdb-replication-react-native
が必要なのはそのため。
もし問題を見つけたらここにPRを送って欲しい。