视频回放: 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
对象有效,表示用户登录。
大家需要关注下 create
和 enter
的区别,还有 leave
和 delete
的区别。
附上文档: 实时数据同步 LiveQuery 开发指南7
在 Ticket 中具体的使用 15:00
当前使用的代码版本 5c228a0
。
工单详情页回复列表和操作列表的更新
主要代码在 modules/Ticket.js
14
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.js
2
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')})`})
}
})
...
}
}
如果当前登录用户是「技术支持人员」(isCustomerService
为 true
),则关注的事件会多一些:
- 新的工单:新产生的
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
操作。