Hi,亲爱的用户:

最近我们正以「LeanCloud Web 应用开发实践」为主题,进行一系列的直播及文章分享。并将重写后的 LeanTicket (工单)作为范例,介绍使用 LeanCloud 开发 Web 应用的一些最佳实践,内容包括且不限于:

  1. 使用 LeanCloud 账号系统以及三方 OAuth 授权。
  2. 使用 ACL 保障数据安全。
  3. 使用云引擎的云函数和 hook 函数简化客户端逻辑。
  4. LiveQuery 的使用。
  5. 与微信或其他外部系统对接。
  6. 邮件的发送即接收。
  7. 批量处理/订正数据的实践。
  8. 使用数据缓存提高响应速度,减少存储服务查询次数。

欢迎您加入微信群(文末扫码)进行交流。

第五期课程:使用 LiveQuery 实现多端数据实时同步

围绕一个工单,客服人员可能要与用户进行多次沟通。为了让每一次回复都能及时传达到对方,我们使用 LiveQuery 来实时同步数据,让每一条新增的回复内容都能实时地同时出现在移动端和 PC 端。

内容:

  • 如何使用 LiveQuery
  • 使用 LiveQuery 需要注意的地方

讲师

LeanCloud 工程师 陈伟

直播时间

2017 年 8 月 29 日(周二) 20:00

课程地址:https://www.douyu.com/158796942

扫描下方二维码即可加入「LeanCloud Web 应用开发实践」,如有疑问也可加微信「 guixiaoTL」咨询。


往期回顾(末尾文字讲解):

第一期课程:示例应用(LeanTicket)开发环境搭建及功能简介

第二期课程:理清 Web 应用的登录状态

第三期课程:使用 OAuth 接入第三方用户信息

第四期课程:使用 ACL 保护数据安全

本主题已置顶,它将始终显示在它所属分类的顶部。可由职员对所有人解除置顶,或者由用户自己取消置顶。

视频回放: https://www.bilibili.com/video/av13991046/20

LiveQuery 简单介绍 00:00:00

使用 LiveQuery 可以很方便的实现多端数据实时同步,服务端会主动推送变更的数据到客户端,免去了客户端主动轮训获取数据的痛苦。

在 Ticket 中的使用场景 00:40

主要在两个场景使用:

  • 工单详情页回复和操作列表的更新:有新的回复或操作,工单详情页会自动添加上最新的内容。
  • 浏览器通知:有新的关于自己的工单,浏览器弹出通知提示。

客户端实时更新数据的实现方式 04:43

基本上有两种方式:

  • 客户端定时轮训:客户端可以每 5 秒(或其他时间)定期到服务端查询数据。
    • 优势:依赖的技术基础简单,HTTP 的请求响应模型即可。
    • 劣势:最坏情况客户端数据更新延迟要等待一个轮训周期(比如 5 秒);数据变动少时,客户端与服务端有很多无意义的通信,以及服务端要做很多无意义的数据检索。
  • 长链接 + 服务端推送:客户端与服务端建立长链接,服务端有数据变化主动通知客户端,或直接将数据推送到客户端。
    • 优势:客户端可以更实时的感知到数据变化;减少无意义的轮训通信和服务端数据检索。
    • 劣势:普通 HTTP 的请求响应模型很难满足,需要 WebSocket 等其他手段实现;服务端需要维持较多的长链接。

LiveQuery 使用的是后者来实现。

使用方式 07:47

使用 LiveQuery 有三个关键点:

确定数据范围

单个客户端所关注的数据一般是服务端数据库所有数据的一个子集,比如对于 LeanTicket 应用来说,一个客户端关心的数据可能是「我提交的工单」。

确定数据范围可以很自然的联想到 AV.Query,一个客户端使用的查询基本上就是在确定自己感兴趣的数据子集,所以 LiveQuery 的使用入口就是从构建一个 AV.Query 开始。

订阅确认

当构建了 AV.Query 对象之后,可以通过下面的方法获取 liveQuery 对象:

query.subscribe().then(liveQuery => {
  //...
})

此时可以认为已经向服务端建立了长链接,并确认了订阅。

响应数据变更

上面的过程完成之后,如果数据库相关数据发生变化,服务端就会把数据的最新结果推送到客户端。客户端可以通过类似下面的方法来获取数据,并改变客户端界面渲染:

liveQuery.on("create", (avObj) => {
  // ...
})

响应事件

数据变动有不同的类型,对此 LiveQuery 定义了几种响应事件:

  • create: 符合查询条件的对象创建。
  • update: 符合查询条件的对象属性修改。
  • enter : 对象被修改后,从不符合查询条件变成符合。
  • leave : 对象被修改后,从符合查询条件变成不符合。
  • delete: 对象删除。
  • login : 只对 _User 对象有效,表示用户登录。

大家需要关注下 createenter 的区别,还有 leavedelete 的区别。

附上文档: 实时数据同步 LiveQuery 开发指南7

在 Ticket 中具体的使用 15:00

当前使用的代码版本 5c228a0

工单详情页回复列表和操作列表的更新

主要代码在 modules/Ticket.js14

import AV from 'leancloud-storage/live-query'
...

  getReplyQuery(ticket) {
    const replyQuery = new AV.Query('Reply')
    .equalTo('ticket', ticket)
    .include('author')
    .include('files')
    replyQuery.subscribe({subscriptionId: UUID}).then(liveQuery => {
      this.replyLiveQuery = liveQuery
      this.replyLiveQuery.on('create', reply => {
        reply.fetch({include: 'author,files'})
        .then(() => {
          const replies = this.state.replies
          replies.push(reply)
          this.setState({replies})
        })
      })
    })
    return replyQuery
  }

  getOpsLogQuery(ticket) {
    ...
  }

getReplyQuery 为例:

  • 首先构建 AV.Query 来查询一个 Ticket 的所有回复:

  const replyQuery = new AV.Query('Reply')
    .equalTo('ticket', ticket)
    .include('author')
    .include('files')
  • 然后通过 query 对象「订阅」得到一个 liveQuery 对象:

    replyQuery.subscribe({subscriptionId: UUID}).then(liveQuery => {
      ...
    }

关于 subscriptionId 在后面「一些细节」部分详细介绍。

  • 因为 Reply 不存在修改和删除,所以只需要关心「对象创建」事件即可:

      this.replyLiveQuery.on('create', reply => {
        reply.fetch({include: 'author,files'})
        .then(() => {
          const replies = this.state.replies
          replies.push(reply)
          this.setState({replies})
        })
      })

把服务端推送过来的对象先通过 fetch 来补全关联信息,然后将对象加入到客户端已有的回复列表中,由 React 来重新渲染页面即可。

对于 getOpsLogQuery 方法处理方式类似,主要用来处理「工单操作」相关的信息展现。

除此之外,如果客户端离开工单详情页,意味着不再对某个 Ticket 的回复内容感兴趣,此时应该通知服务端「取消订阅」:

  componentWillUnmount() {
    if (this.replyLiveQuery) {
      Promise.all([
        this.replyLiveQuery.unsubscribe(),
        this.opsLogLiveQuery.unsubscribe()
      ])
      .catch(this.context.addNotification)
    }
  }

主要就是对 liveQuery 对象的 unsubscribe 方法的调用。

浏览器通知中的应用 25:30

主要代码在 modules/App.js2

  updateLiveQuery(isCustomerService) {
    ...
    if (isCustomerService) {
      ...
        liveQuery.on('create', (ticket) => {
          this.notify({title: '新的工单', body: `${ticket.get('title')} (#${ticket.get('nid')})`})
        })
        liveQuery.on('enter', (ticket, updatedKeys) => {
          if (updatedKeys.indexOf('assignee') !== -1) {
            this.notify({title: '转移工单', body: `${ticket.get('title')} (#${ticket.get('nid')})`})
          }
        })
        liveQuery.on('update', (ticket, updatedKeys) => {
          if (updatedKeys.indexOf('latestReply') !== -1
              && ticket.get('latestReply').author.username !== AV.User.current().get('username')) {
            this.notify({title: '新的回复', body: `${ticket.get('title')} (#${ticket.get('nid')})`})
          }
        })
      ...
    } else {
      ...
        liveQuery.on('update', (ticket, updatedKeys) => {
          if (updatedKeys.indexOf('latestReply') !== -1
              && ticket.get('latestReply').author.username !== AV.User.current().get('username')) {
            this.notify({title: '新的回复', body: `${ticket.get('title')} (#${ticket.get('nid')})`})
          }
        })
      ...
    }
  }

如果当前登录用户是「技术支持人员」(isCustomerServicetrue),则关注的事件会多一些:

  • 新的工单:新产生的 Ticket 对象,并且负责人是自己,所以事件是 create
  • 转移工单:工单对象的 assignee 属性变更(因为 assignee 属性在 updatedKeys 中),变更之后值是当前登录用户,所以该 Ticket 「对象被修改后,从不符合查询条件变成符合」,所以使用事件是 enter
  • 新的回复:为了方便使用,Ticket 对象有一个属性 latestReply 总是保存了最近一次回复的概要信息,通过这样的数据冗余在很多地方可以减少对 Reply 的查询(关于 latestReply 的赋值请参考 Reply afterSave hook1)。所以如果有新的回复 latestReply 就会发生变化,而且通过 ticket.get('latestReply').author.username 可以获取到回复者,当判断回复者不是自己时就弹出通知。对于「新的回复」使用的是事件 update

对于用户来说,相关事件会简单很多,主要就是「新的回复」,处理方式和上面类似。

同样也有关于「取消订阅」的部分:

  componentWillUnmount() {
    this.ticketsLiveQuery.unsubscribe()
  }

其他关于浏览器通知部分的具体代码和 API 这里不展开说明,可以参考 使用 Web Notifications2 文档。

一些细节 38:47

使用 LiveQuery 时需要注意一些细节(有些只是浏览器客户端需要注意):

  • 记得取消订阅:当客户端因为页面变更,不再关心某个数据集订阅时,记得取消订阅,否则订阅连接仍然存在,会对服务端产生不必要的消耗,影响到 LiveQuery 相关的计费。
  • 已修复,无需特殊处理。(单页面 Web 应用需要指定 subscriptionId:LeanTicket 做了 处理,让每个浏览器页面都有唯一的 UUID,然后所有使用 LiveQuery 进行订阅时都传入这个 UUID,目的是让服务端认为每个浏览器页面都是单独的客户端。否则所有页面都是用同样的 subscriptionId(默认一个浏览器内保持一样),服务器会发现同样的客户端已经建立连接,则会拒绝其他的连接。导致其他页面不断重试,最终使得这些页面的 LiveQuery 功能失效。)
  • JS 客户端引入包都使用 leancloud-storage/live-query:因为前端会使用 webpack 等工具压缩客户端代码,如果有些地方因为不使用 LiveQuery 而引入 leancloud-storage ,最终可能导致客户端代码存在两份 leancloud-storage 代码,增大了客户端代码体积。
  • pointer 需要单独 fetch:服务端推送的数据是不包含 pointer 的内容的。如果需要,客户端请单独执行 fetch 操作。

This topic is now unpinned. It will no longer appear at the top of its category.