PouchDB是一个受Apache CouchDB启发的开源JavaScript数据库,能够在浏览器中良好运行。 PouchDB帮助Web开发人员构建脱机Web应用,并使其具有与在线应用同样的用户体验。 它使应用程序可以在脱机时在本地存储数据,在应用程序重新联机时将其与CouchDB或者兼容的服务器同步。本文参考Apache官方资料,通过演示程序介绍了如何在浏览器中构建PouchDB,并使其与后端的Docker CouchDB同步,同时介绍了如何通过Design Document实现CouchDB中的数据过滤。
准备工作
在Docker上创建CouchDB容器。
下载Apache的PouchDB入门程序模板并解压。
下载PouchDB运行库。
构建CouchDB容器
1 拉取CouchDB镜像
docker pull apache/couchdb
2 运行CouchDB镜像
docker run -p5984:5984 -d --name couchdb -e COUCHDB_USER=admin -e /
COUCHDB_PASSWORD=password apache/couchdb
以上命令创建了一个名为couchdb的容器,同时为CouchDB创建了一个管理员账户admin,密码为password
3 创建_users和__replicator数据库
如果执行 docker logs couchdb,会发现CouchDB服务每隔5秒会打印若干行错误日志,通过创建 _users和_replicator数据库解决这个问题,详细请参照https://github.com/apache/couchdb-docker/issues/54
docker exec -ti couchdb curl -X PUT http://admin:password@127.0.0.1:5984/_users
docker exec -ti couchdb curl -X PUT http://admin:password@127.0.0.1:5984/_replicator
4 创建实验用todos数据库
docker exec -ti couchdb curl -X PUT http://admin:password@127.0.0.1:5984/todos
5 CORS设置
在浏览器中访问http://localhost:5984/_utils,用户名admin密码password
点击Configuration
开启CORS
编写JavaScript脚本
解压下载的PouchDB入门程序模板,拷贝PouchDB运行库(pouchdb-7.1.1.js)到js目录, 打开 index.html文件,添加以下三行
<script src="js/pouchdb-7.1.1.js">
</script><script src="js/base.js">
</script><script src="js/app.js"></script>
保存index.html后打开 app.js文件
按照Apach的官方文档指引,在第10行添加以下代码,
var db = new PouchDB("todos");
var remoteCouch = 'http://admin:password@localhost:5984/todos';
进一步完成其它代码,完整代码如下
window.onload = (function () {
"use strict";
var ENTER_KEY = 13;
var newTodoDom = document.getElementById("new-todo");
var syncDom = document.getElementById("sync-wrapper");
// EDITING STARTS HERE (you dont need to edit anything above this line)
var db = new PouchDB("todos");
var remoteCouch = 'http://admin:password@localhost:5984/todos';
db.changes({
since: 'now',
live: true
}).on('change', showTodos);
// We have to create a new todo document and enter it in the database
function addTodo(text) {
var todo = {
_id: new Date().toISOString(),
title: text,
completed: false,
};
db.put(todo, function callback(err, result) {
if (!err) {
console.log("Successfully posted a todo!");
}
});
}
// Show the current list of todos by reading them from the database
function showTodos() {
db.allDocs({include_docs: true, descending: true}, function(err, doc) {
redrawTodosUI(doc.rows);
});
}
function checkboxChanged(todo, event) {
todo.completed = event.target.checked;
db.put(todo);
}
// User pressed the delete button for a todo, delete it
function deleteButtonPressed(todo) {
db.remove(todo);
}
// The input box when editing a todo has blurred, we should save
// the new title or delete the todo if the title is empty
function todoBlurred(todo, event) {
var trimmedText = event.target.value.trim();
if (!trimmedText) {
db.remove(todo);
} else {
todo.title = trimmedText;
db.put(todo);
}
}
// Initialise a sync with the remote server
function sync() {
syncDom.setAttribute('data-sync-state', 'syncing');
var opts = {live: true};
db.replicate.to(remoteCouch, opts, syncError);
db.replicate.from(remoteCouch, opts, syncError);
}
// EDITING STARTS HERE (you dont need to edit anything below this line)
// There was some form or error syncing
function syncError() {
syncDom.setAttribute("data-sync-state", "error");
}
// User has double clicked a todo, display an input so they can edit the title
function todoDblClicked(todo) {
var div = document.getElementById("li_" + todo._id);
var inputEditTodo = document.getElementById("input_" + todo._id);
div.className = "editing";
inputEditTodo.focus();
}
// If they press enter while editing an entry, blur it to trigger save
// (or delete)
function todoKeyPressed(todo, event) {
if (event.keyCode === ENTER_KEY) {
var inputEditTodo = document.getElementById("input_" + todo._id);
inputEditTodo.blur();
}
}
// Given an object representing a todo, this will create a list item
// to display it.
function createTodoListItem(todo) {
var checkbox = document.createElement("input");
// checkbox.className = "toggle";
checkbox.type = "checkbox";
checkbox.addEventListener("change", checkboxChanged.bind(this, todo));
var label = document.createElement("label");
label.appendChild(document.createTextNode(todo.title));
label.addEventListener("dblclick", todoDblClicked.bind(this, todo));
var deleteLink = document.createElement("button");
deleteLink.className = "destroy";
deleteLink.addEventListener("click", deleteButtonPressed.bind(this, todo));
var divDisplay = document.createElement("div");
divDisplay.className = "view";
divDisplay.appendChild(checkbox);
divDisplay.appendChild(label);
divDisplay.appendChild(deleteLink);
var inputEditTodo = document.createElement("input");
inputEditTodo.id = "input_" + todo._id;
inputEditTodo.className = "edit";
inputEditTodo.value = todo.title;
inputEditTodo.addEventListener("keypress", todoKeyPressed.bind(this, todo));
inputEditTodo.addEventListener("blur", todoBlurred.bind(this, todo));
var li = document.createElement("li");
li.id = "li_" + todo._id;
li.appendChild(divDisplay);
li.appendChild(inputEditTodo);
if (todo.completed) {
li.className += "complete";
checkbox.checked = true;
}
return li;
}
function redrawTodosUI(todos) {
var ul = document.getElementById("todo-list");
ul.innerHTML = "";
todos.forEach(function (todo) {
ul.appendChild(createTodoListItem(todo.doc));
});
}
function newTodoKeyPressHandler(event) {
if (event.keyCode === ENTER_KEY) {
addTodo(newTodoDom.value);
newTodoDom.value = "";
}
}
function addEventListeners() {
newTodoDom.addEventListener("keypress", newTodoKeyPressHandler, false);
}
addEventListeners();
showTodos();
if (remoteCouch) {
sync();
}
})();
在浏览器中访问index.html, 通过http://localhost:5984/_utils查看CouchDB中的todos数据库,你会发现在浏览器中更改的数据会及时反应到CouchDB中,同时如果你在CouchDB中增加,删除,修改文档,你会发现index.html页面中的数据也会发生相应的变化。
增加数据库用户
在以上实验中我们使用admin账户进行连接,admin账户是CouchDB服务器管理账户,是个超级账户,它不同于todos数据库管理员账户和用户账户,我们不应该使用这个账户做数据库连接与同步,下面指令为todos数据库增加一个普通用户账户。
为CouchDB新增加一个pouchclient账户, 密码为password
docker exec -ti my-couchdb /bin/bash
curl -X PUT http://localhost:5984/_users/org.couchdb.user:pouchclient \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{"name": "pouchclient", "password": "password", "roles": [], "type": "user"}'
把pouchclient用户添加到todos数据库的user中
curl -X PUT http://localhost:5984/todos/_security \
-u admin:password \
-H "Content-Type: application/json" \
-d '{"admins": { "names": [], "roles": [] }, "members": { "names": ["todoclient"], "roles": [] } }'
修改JavaScript代码尝试用新的账户pouchclient和CouchDB做同步
CouchDB服务器端数据过滤
有时候我们不希望把CouchDB的数据完全同步到客户端的PouchDB中,可以通过在CouchDB中增加design document实现数据过滤功能
在浏览器中访问todos数据库,并创建一个document
{
"_id": "_design/app",
"_rev": "14-9a0ad35ea6fb92d8b530a1d54f6b79bf",
"filters": {
"by-complete": "function (doc) {return doc.completed == true;}"
},
"language": "javascript"
}
上述文档中的return doc.completed == true表示只有当文档中的competed字段的数值为true的时候才和前端同步数据
回到app.js,更新同步代码模块为
// Initialise a sync with the remote server
function sync() {
syncDom.setAttribute('data-sync-state','syncing');
var opts = {live:true};
db.replicate.to(remoteCouch, opts, syncError);
db.replicate.from(remoteCouch, {live:true, filter:'app/by-complete'}, syncError);
}
filter:'app/by-complete' 指明服务器端的过滤器为app/by-complete, 其中的‘app’来自design document("_id": "_design/app"),by-complete为我们在文档中定义的function名字("by-complete": "function (doc) {return doc.completed == true;}")
刷新index.html, 同时访问http://localhost:5984/_utils/#/database/todos/_all_docs尝试在todos数据库中增加修改文档,你会发现todos数据库中只有competed为ture的documnt才会推送PouchDB中。