读万卷书不如行万里路,行万里路不如阅人无数,阅人无数不如名师指路。站着巨人身上,你可以看的更高、更远。
一、webSocket 初始化
initWebSocket 方法中,初始化 webSocket。
this.socket = new Socket('ws://192.168.11.198:12000/msg', {
userId: '1234',
token: 'token-demo',
sysCode: '001',
})
在文件中,引入 socket.js 文件(webSocket 的基本配置)。
export default class {
constructor (url, opts = {}) {
...
this.init(url, opts)
}
}
调用 init 初始化方法,连接 websocket,同时绑定系统事件。
init (url, opts) {
this.client = new WebSocket(url)
.....
return this.client
}
webSocket 相关事件
this.client.onopen = () => {} // 连接建立时触发
this.client.onmessage = (event) => {} // 客户端接收服务端数据时触发
this.client.onerror = () => {} // 通信发生错误时触发
this.client.onclose = () => {} // 连接关闭时触发
二、客户端与服务端建立连接
在建立连接时,调用 login 方法。
this.client.onopen = () => {
this.login(opts.userId, opts.sysCode, opts.token)
}
在登陆方法中,调用 send 方法。
login (userId, sysCode, token) {
const operation = 'Auth'
......
this.send(operation, data, token)
}
在 send 方法中, 首先检查 websocket 的连接状态。
this.client.readyState 为 1 时,表示 webSocket 已经连接成功,可以通讯。调用 this.client.send 方法发送信息。
/**
* 发送事件
* @param {string} operation 事件类型
* @param {object} data 事件参数
*/
send (operation, data) {
const readyState = this.client.readyState
return new Promise((resolve, reject) => {
if (this.client.readyState === 1) {
const message = {
operation,
data,
token: this.token,
}
this.client.send(JSON.stringify(message))
resolve()
} else {
reject(new Error(`当前连接状态为:${readyState}, 无法发送消息`))
}
})
}
接收来自服务端的消息。
this.client.onmessage = (event) => {
const resp = JSON.parse(event.data)
......
this.events[resp.operation](resp) // event 上绑定触发类型和结果。
}
在业务逻辑中,对绑定的登录事件进行回调。在登录成功后,回调服务器推送过来的消息,同时将 result 中返回的 token,传入调用心跳方法中。
this.socket.on('Auth', (result) => {
console.log('Auth 回调', result)
......
// 启动心跳
this.socket.heartStart(this.token)
})
on 绑定自定义事件。
on (operation, func) {
this.events[operation] = func
}
三、启动心跳(定时器)
采用定时器,在一定的时间间隔内,给服务端发送一次消息,说明客户端连接到服务端的。
heartStart (token) {
this.heartTimer = window.setInterval(() => {
this.heartBeat(token)
}, this.heartInterval)
this.heartBeat(token)
......
}
在心跳方法中,调用 send 方法。
heartBeat (token) {
const operation = 'HeartBeat'
...
this.send(operation, data, token)
}
同时在 heartStart 方法中,绑定回调事件。
heartStart (token) {
......
this.on('HeartBeat', () => {
console.log('接送到心跳回应')
})
}
接收来自服务端的消息。
this.client.onmessage = (event) => {
const resp = JSON.parse(event.data)
......
this.events[resp.operation](resp) // event 上绑定触发类型和结果。
}
最后绑定的 HeartBeat 事件回调中,打印信息:接送到心跳回应。
四、下拉获取分页数据
如何在滚动列表时,添加触底事件,向服务端发送获取该类型列表的下一页数据呢。
在触底事件中,只需添加下面代码:
this.socket.msgPull(this.token, type, row.id)
在消息分页 msgPull 方法中,调用 send 方法。
msgPull (token, type, pageId, size) {
const operation = 'Pull'
......
this.send(operation, data, token)
}
send 发送事件中,使用 this.client.send(JSON.stringify(message)) 发送信息给服务器。
在 this.client.onmessage 中接收服务端返回到客户端的信息。
this.client.onmessage = (event) {
const resp = JSON.parse(event.data)
......
this.events[resp.operation](resp)
}
在 Pull 回调中,获取从服务端回调过来的信息。
this.socket.on('Pull', (result) => {
console.log('Pull 回调', result)
})
敲敲黑板,重点来了。
1、如何监听列表滚动到底部时,调用 Pull 类型的 send 方法获取下一页数据;
2、如何区分当前滚动列表需要加载下一页的数据类型;
3、若该类型列表无更多数据,触底事件频繁调用获取下一页数据,如何优化呢?
调用 handleInfiniteOnLoad 方法时,会传入列表的类型,我们会将需要加载下一页数据的类型传给服务端的;在 Pull 操作类型的回调中,我们将返回的结果进行判断。
this.socket.on('Pull', (result) => {
this.loading = false
// end: '1' => 结束,没有下一页, '0' => 有下一页
if (result.data.end === '0') {
switch (result.data.type) {
case 'INFORM':
this.msgTable.informs = [...this.msgTable.informs, ...result.data.messages]
break
......
}
} else {
// 没有下一页时移除滚动事件
if (!this.noMoreFlagList.includes(result.data.type)) {
this.noMoreFlagList.push(result.data.type)
}
}
})
noMoreFlagList 为没有下一页的列表类型;该类型列表无更多数据,禁止提交 pull 操作类型的事件。
handleInfiniteOnLoad (type) {
let row
if (type === 'INFORM') {
row = this.msgTable.informs[this.msgTable.informs.length - 1]
}
......
// 匹配标识是否已没有下一页
if (!this.noMoreFlagList.includes(type)) {
this.loading = true
this.socket.msgPull(this.token, type, row.id)
}
},
五、后端主动推送消息
当后端主动推送消息时,我们又应该如何接收呢?
在服务端推送消息给客户端时,会获取消息的类型和数据,对应的在消息回调中可以获取相应的数据。
initWebSocket () {
......
// 后端主动推送消息
this.socket.on('Push', (result) => {
console.log('push', result)
this.$message.info({
message: '您好',
description: '您有新的未读消息,请注意查收',
})
switch (result.data.type) {
case 'INFORM':
this.msgTable.informs.unshift(result.data)
break
......
}
// 未读总数+1
this.totalUnreadCount++
})
},
六、已读消息处理
首先用户点击浏览器,调用 handleRead 事件。
handleRead (msgId, status) {
// 已读消息则不再发起阅读
if (status === '0' && !this.readedList.includes(msgId)) {
this.socket.msgRead(this.token, msgId)
}
},
在 msgRead 方法中,调用【已读】类型的 send 方法。
msgRead (token, msgId) {
const operation = 'Read'
......
this.send(operation, data, token)
}
在业务逻辑中,对已读进行回调,将返回的结果 id 放入 readedList 已读列表中,同时对未读数量 totalUnreadCount 进行 -1 操作。
this.socket.on('Read', (result) => {
this.readedList.push(result.data.msgId)
// 未读总数-1
this.totalUnreadCount--
})
页面展示已读与未读
// item.vue 组件
<div
class="z-msg__item"
:class="{'z-msg__item--read': (itemData.status === '1' || judgeReaded(itemData.id))}"
@click="itemClick(itemData)"
>
......
</div>
status 为 1 或者 readedList 数组中存在点击过的消息 Id时,则为已读状态;
// 处理已读
judgeReaded (msgId) {
if (this.readedList.includes(msgId)) {
return true
} else {
return false
}
},
z-msg__item--read {
opacity: 0.6;
}
注意:首先通过 userId 和 token 与用户相关的字段,与服务端建立连接(用于身份标识,这里的 userId 和 token 是登录系统后的用户信息)。在建立连接之后的回调中,返回的 result 里面有个 token 字段。这个由服务端返回的字段,在后续的 HeartBeat、Pull、Push、Read 类型的 send 方法中,作为客户端与服务端的传递消息的桥梁。