第六课:本地和远程PouchDB和Cloudant后台
虽然我们完成了应用的大部分功能,但是本节课将是最大最难的那个。我们将使用PouchDB来存储信息而不是直接将他扔到信息数组里面去。
PouchDB是一个浏览器内的NoSQL数据库,灵感来自CouchDB项目。他最大的功能是允许存储离线数据,当应用再次上线之后会自动从远程数据库同步数据。和使用Ionic提供的SqlStorage一样,使用PouchDB保证你存储的本地数据不会被随机擦除。
普及NoSQL不是本书的目的,但是为了给你一点基础,NoSQL数据库通常以类JSON的键值对风格格式来存储数据,不同的样式要不同的去思考。所以,所以当你来到NoSQL的时候,需要放弃你可能已有大量的的SQL固有观念(假设你之前用过)。我们存储的数据非常简单,所以我们不需要担心如何合理的使用NoSQL数据库。
所以PouchDB会负责本地数据的存储,但是我们还要用到Cloudant来创建一个远程数据库。我们将同步本地的PouchDB数据库和远程的Cloudant数据库,这样无论何时应用上线的时候都可以从Cloudant获取最新数据,任何本地新数据都将推送到Cloudant。幸运的是,这两个工具都被设计为相互协作且设置好了远程同步,这在平常事一个非常复杂的任务,现在却变得非常简单了。
Cloudant是一个DBaas(Database as a Service数据库即是服务),所以我们只要创建一个帐号(低端使用免费)来使用,当然也要设置数据库。使用类似Cloudant这样的的东西非常简单,且扩展非常简单(因为所有后端架构都帮你处理好了),但是如果你喜欢的话,你也可以轻松的使用类似CouchDB或者Couchbase安装到你的服务端来和PouchDB同步。这个我不会深入,因为差别不大。
创建Cloudant Database
在进入编码之前,我们要在IBM Bluemix的Cloudant上设置我们的后端。Bluemix让我们可以访问IBM的Open Cloud Architecture,他可以用来创建,部署和管理基于云的应用。他提供大量可用的服务,其中一个就是Cloudant。
首先你的创建一个IBM Bluemix帐号。当你创建好帐号登录进去之后,可以看到这样的界面:
选择Create App选项,选择Mobile,给你的应用取个名字然后点击Finish。之后就会看到这样的画面:
从左边的Service菜单选择Cloudant NoSQL DB,然后在Cloudant Dashboard上点击View your Data。你现在应该来带了Cloudant的仪表盘了。点击右上的Create Database选项创建一个新的数据库然后命名为camperchat或者其他你喜欢的。
现在,选择刚才新建的数据库查看具体细节,应该可以看到如下画面:
点击右上的API连接就得到了你的Cloudant Database URL了,这个我们要稍后提供给PouchDB的,所以最好拿个本子记下来吧。同时需要去Permissions部分生产一个API密钥(确保给他提供Write和Replicate权限) — 他可以让PouchDB访问你的数据库,使用API密钥比用你的用户名和密码好一些。请记住密码因为一旦离开屏幕你就再也找不到了。
这里我们还有一件事情要做。我们需要激活CORS(Cross Origin Resource Sharing)这样我们就可以从我们的应用中向数据库发起请求了。来到左边菜单的Account,选择CORS然后选择All Domains(*) 选项。
这样就在Cloudant上设置好了,可以跟PouchDB一起使用了。
整合PouchDB
之前我们创建好了Data服务,也只是存放了一点Facebook API返回的值。现在我们要扩展Data服务用来操作PouchDB的数据存储和获取。
> 修改 src/providers/data.ts 为如下:
import { Injectable } from '@angular/core';
import PouchDB from 'pouchdb';
@Injectable()
export class Data {
fbid: number;
username: string;
picture: string;
db: any;
data: any;
cloudantUsername: string;
cloudantPassword: string;
remote: string;
constructor(){
this.db = new PouchDB('camperchat');
this.cloudantUsername = 'YourAPIUsernameHere';
this.cloudantPassword = 'YourAPIPasswordHere';
this.remote = 'https://YOUR-URL-HERE-bluemix.cloudant.com/camperchat';
//Set up PouchDB
let options = {
live: true,
retry: true,
continuous: true,
auth: {
username: this.cloudantUsername,
password: this.cloudantPassword
}
};
this.db.sync(this.remote, options);
}
addDocument(message){
}
getDocuments(){
}
handleChange(change){
}
}
我们已经通过npm安装好了PouchDB,想要在这个服务里使用的话我们得先导入:
import PouchDB from 'pouchdb';
在构造器中,我们处理了PouchDB的设置和远程Cloudant数据库的同步。首先我们我们新建了一个PouchDB,或者获取一个已存在的引用:
this.db = new PouchDB('camperchat');
然后我们定义了一些用于连接到Cloudant数据库的变量:
this.cloudantUsername = 'YourAPIUsernameHere';
this.cloudantPassword = 'YourAPIPasswordHere';
this.remote = 'https://YOUR-URL-HERE-bluemix.cloudant.com/camperchat';
请记得要用你自己的Cloudant仪表盘上的参数替换上面这些值。接着我们创建了一个options对象来配置到Cloudant数据库的连接,然后我们调用了sync方法:
this.db.sync(this.remote, options);
这样将会设置从PouchDB数据库复制到Cloudant数据库,同时也会设置从Cloudant数据库到PouchDB数据库的复制。现在,如果我们给PouchDB添加一些数据的时候将会自动反射到远程Cloudant数据库,如果我们在远程Cloudant修改或者添加一些数据的时候,将会自动反射到我们的本地数据库。
在那之后我们创建了三个空函数,我们现在就来实现。
addDocument
> 修改 addDocument 为如下:
addDocument(message) {
this.db.put(message);
}
我们只需要简单的调用数据库的put就可以向PouchDB添加文档(NoSQL条款里一个数据对象叫做一个“文档”,所以可以将文档想象成一小片数据,而不是一个Word文档)。这样我们可以向这个函数传入任何数据以存储到数据库。
getDocument
> 修改 getDocument 为如下:
getDocuments(){
return new Promise(resolve => {
this.db.allDocs({
include_docs: true,
limit: 30,
descending: true
}).then((result) => {
this.data = [];
let docs = result.rows.map((row) => {
this.data.push(row.doc);
});
this.data.reverse();
resolve(this.data);
this.db.changes({live: true, since: 'now', include_docs:true}).on('change', (change) => {
this.handleChange(change);
});
}).catch((error) => {
console.log(error);
});
});
}
这个函数用户获取数据库中的所有文档(记住,文档只是一个数据对象)。这是一个一部操作,所以我们将他包装到一个promise里,然后在数据返回的时候在resolve。他允许我们在应用内其他地方使用getDocuments().then()语法。通过调用allDocs可以获取所有文档,这里我们也提供了一些选项:
include_docs: true,
limit: 30,
descending: true
这里的include_docs看起来有点迷糊,但是我们还是要指定这个这样文档内的所有数据都将被返回(信息,图片等)。如果没有提供这个参数的话只会返回文档的id。我们也设置了一个30的显示,和一个降序,这样一来只返回最新的30个文档(聊天信息)。
我们将这些结果传入到我们的处理器,每个返回的行我们都会压入到this.data数字。我们想让这些信息倒序这样的话最新的消息将会显示在最下面。之后我们resolve我们创建的promise然后回传数据,然后设置changes监听器。
changes监听器在每次侦测到数据库变更的时候(例如当其他用户添加了一个聊天信息的时候)都将调用this.handleChange函数,change本身也会被传入到函数里。我们现在来定义。
handleChange
> 修改 handleChange 为如下:
handleChange(change) {
let changedDoc = null;
let changedIndex = null;
this.data.forEach((doc, index) => {
if(doc._id === change.id){
changedDoc = doc;
changedIndex = index;
}
});
//A document was deleted
if(change.deleted){
this.data.splice(changedIndex, 1);
}
else {
//A document was updated
if(changedDoc){
this.data[changedIndex] = change.doc;
}
//A document was added
else {
this.data.push(change.doc);
}
}
}
我们将传入的change反射到我们的this.data数组,但是有点技巧。传入进来的change对象可能是一个文档更新了,新建了一个文档,或者删除了一个文档。
检查文档删除非常简单只要检查他是否有deleted属性就可以了。但是辨别他是否是更新,我们需要检查我们是否有了相同id的文档(如果找到了的话就更新他),如果没有的话我们就知道是新增文档了(我们需要将他添加到数组)。
现在我们完全设置好了Data服务,但是如果要使用他的话还有些事情要做。我们需要使用provider来存储新数据,而不是像现在这样直接将他添加到messages数组,同时在应用打开的时候我们需要加载最新的信息。我们现在就来完成这些。
> 修改 src/page/home/home.ts 如下:
import { Component, ViewChild } from '@angular/core';
import { Data } from '../../providers/data';
@Component({
selector: 'page-home',
templateUrl: 'home.html'
})
export class HomePage {
@ViewChild('chat') chat: any;
chatMessage: string = '';
messages: any = [];
constructor(public dataService: Data){
this.dataService.getDocuments().then((data) => {
this.messages = data;
this.chat.scrollToBottom();
});
}
sendMessage(): void {
let message = {
'_id': new Date().toJSON(),
'fbid': this.dataService.fbid,
'username': this.dataService.username,
'picture': this.dataService.picture,
'message': this.chatMessage
};
this.dataService.addDocument(message);
this.chatMessage = '';
}
}
现在我们在构造器中调用了getDocument()函数来加载信息数据,一旦完成之后我们通过@ViewChild获取滚动内容的引用,将他滚动到底部。
最后,在sendMessage函数中,唯一的变更是我们调用了addDocument函数而不是直接把信息压入到messages数组。
终于完成了!【译者:我尿也憋坏了】现在聊天应用的全部功能都开发完了。他能够正常工作了,但是也丑爆了。下节课我们让他变漂亮点甚至给他加点动画的啥的!