第六课:本地和远程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帐号。当你创建好帐号登录进去之后,可以看到这样的界面:
Bluemix
选择Create App选项,选择Mobile,给你的应用取个名字然后点击Finish。之后就会看到这样的画面:
Bluemix
从左边的Service菜单选择Cloudant NoSQL DB,然后在Cloudant Dashboard上点击View your Data。你现在应该来带了Cloudant的仪表盘了。点击右上的Create Database选项创建一个新的数据库然后命名为camperchat或者其他你喜欢的。
Bluemix
现在,选择刚才新建的数据库查看具体细节,应该可以看到如下画面:
Bluemix
点击右上的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 为如下:

  1. import { Injectable } from '@angular/core';
  2. import PouchDB from 'pouchdb';
  3. @Injectable()
  4. export class Data {
  5. fbid: number;
  6. username: string;
  7. picture: string;
  8. db: any;
  9. data: any;
  10. cloudantUsername: string;
  11. cloudantPassword: string;
  12. remote: string;
  13. constructor(){
  14. this.db = new PouchDB('camperchat');
  15. this.cloudantUsername = 'YourAPIUsernameHere';
  16. this.cloudantPassword = 'YourAPIPasswordHere';
  17. this.remote = 'https://YOUR-URL-HERE-bluemix.cloudant.com/camperchat';
  18. //Set up PouchDB
  19. let options = {
  20. live: true,
  21. retry: true,
  22. continuous: true,
  23. auth: {
  24. username: this.cloudantUsername,
  25. password: this.cloudantPassword
  26. }
  27. };
  28. this.db.sync(this.remote, options);
  29. }
  30. addDocument(message){
  31. }
  32. getDocuments(){
  33. }
  34. handleChange(change){
  35. }
  36. }

我们已经通过npm安装好了PouchDB,想要在这个服务里使用的话我们得先导入:

  1. import PouchDB from 'pouchdb';

在构造器中,我们处理了PouchDB的设置和远程Cloudant数据库的同步。首先我们我们新建了一个PouchDB,或者获取一个已存在的引用:

  1. this.db = new PouchDB('camperchat');

然后我们定义了一些用于连接到Cloudant数据库的变量:

  1. this.cloudantUsername = 'YourAPIUsernameHere';
  2. this.cloudantPassword = 'YourAPIPasswordHere';
  3. this.remote = 'https://YOUR-URL-HERE-bluemix.cloudant.com/camperchat';

请记得要用你自己的Cloudant仪表盘上的参数替换上面这些值。接着我们创建了一个options对象来配置到Cloudant数据库的连接,然后我们调用了sync方法:

  1. this.db.sync(this.remote, options);

这样将会设置从PouchDB数据库复制到Cloudant数据库,同时也会设置从Cloudant数据库到PouchDB数据库的复制。现在,如果我们给PouchDB添加一些数据的时候将会自动反射到远程Cloudant数据库,如果我们在远程Cloudant修改或者添加一些数据的时候,将会自动反射到我们的本地数据库。
在那之后我们创建了三个空函数,我们现在就来实现。
addDocument
> 修改 addDocument 为如下:

  1. addDocument(message) {
  2. this.db.put(message);
  3. }

我们只需要简单的调用数据库的put就可以向PouchDB添加文档(NoSQL条款里一个数据对象叫做一个“文档”,所以可以将文档想象成一小片数据,而不是一个Word文档)。这样我们可以向这个函数传入任何数据以存储到数据库。
getDocument
> 修改 getDocument 为如下:

  1. getDocuments(){
  2. return new Promise(resolve => {
  3. this.db.allDocs({
  4. include_docs: true,
  5. limit: 30,
  6. descending: true
  7. }).then((result) => {
  8. this.data = [];
  9. let docs = result.rows.map((row) => {
  10. this.data.push(row.doc);
  11. });
  12. this.data.reverse();
  13. resolve(this.data);
  14. this.db.changes({live: true, since: 'now', include_docs:true}).on('change', (change) => {
  15. this.handleChange(change);
  16. });
  17. }).catch((error) => {
  18. console.log(error);
  19. });
  20. });
  21. }

这个函数用户获取数据库中的所有文档(记住,文档只是一个数据对象)。这是一个一部操作,所以我们将他包装到一个promise里,然后在数据返回的时候在resolve。他允许我们在应用内其他地方使用getDocuments().then()语法。通过调用allDocs可以获取所有文档,这里我们也提供了一些选项:

  1. include_docs: true,
  2. limit: 30,
  3. descending: true

这里的include_docs看起来有点迷糊,但是我们还是要指定这个这样文档内的所有数据都将被返回(信息,图片等)。如果没有提供这个参数的话只会返回文档的id。我们也设置了一个30的显示,和一个降序,这样一来只返回最新的30个文档(聊天信息)。
我们将这些结果传入到我们的处理器,每个返回的行我们都会压入到this.data数字。我们想让这些信息倒序这样的话最新的消息将会显示在最下面。之后我们resolve我们创建的promise然后回传数据,然后设置changes监听器。
changes监听器在每次侦测到数据库变更的时候(例如当其他用户添加了一个聊天信息的时候)都将调用this.handleChange函数,change本身也会被传入到函数里。我们现在来定义。
handleChange
> 修改 handleChange 为如下:

  1. handleChange(change) {
  2. let changedDoc = null;
  3. let changedIndex = null;
  4. this.data.forEach((doc, index) => {
  5. if(doc._id === change.id){
  6. changedDoc = doc;
  7. changedIndex = index;
  8. }
  9. });
  10. //A document was deleted
  11. if(change.deleted){
  12. this.data.splice(changedIndex, 1);
  13. }
  14. else {
  15. //A document was updated
  16. if(changedDoc){
  17. this.data[changedIndex] = change.doc;
  18. }
  19. //A document was added
  20. else {
  21. this.data.push(change.doc);
  22. }
  23. }
  24. }

我们将传入的change反射到我们的this.data数组,但是有点技巧。传入进来的change对象可能是一个文档更新了,新建了一个文档,或者删除了一个文档。
检查文档删除非常简单只要检查他是否有deleted属性就可以了。但是辨别他是否是更新,我们需要检查我们是否有了相同id的文档(如果找到了的话就更新他),如果没有的话我们就知道是新增文档了(我们需要将他添加到数组)。
现在我们完全设置好了Data服务,但是如果要使用他的话还有些事情要做。我们需要使用provider来存储新数据,而不是像现在这样直接将他添加到messages数组,同时在应用打开的时候我们需要加载最新的信息。我们现在就来完成这些。
> 修改 src/page/home/home.ts 如下:

  1. import { Component, ViewChild } from '@angular/core';
  2. import { Data } from '../../providers/data';
  3. @Component({
  4. selector: 'page-home',
  5. templateUrl: 'home.html'
  6. })
  7. export class HomePage {
  8. @ViewChild('chat') chat: any;
  9. chatMessage: string = '';
  10. messages: any = [];
  11. constructor(public dataService: Data){
  12. this.dataService.getDocuments().then((data) => {
  13. this.messages = data;
  14. this.chat.scrollToBottom();
  15. });
  16. }
  17. sendMessage(): void {
  18. let message = {
  19. '_id': new Date().toJSON(),
  20. 'fbid': this.dataService.fbid,
  21. 'username': this.dataService.username,
  22. 'picture': this.dataService.picture,
  23. 'message': this.chatMessage
  24. };
  25. this.dataService.addDocument(message);
  26. this.chatMessage = '';
  27. }
  28. }

现在我们在构造器中调用了getDocument()函数来加载信息数据,一旦完成之后我们通过@ViewChild获取滚动内容的引用,将他滚动到底部。
最后,在sendMessage函数中,唯一的变更是我们调用了addDocument函数而不是直接把信息压入到messages数组。
终于完成了!【译者:我尿也憋坏了】现在聊天应用的全部功能都开发完了。他能够正常工作了,但是也丑爆了。下节课我们让他变漂亮点甚至给他加点动画的啥的!