思路
记录用户查看朋友消息的最后时间,朋友可以根据这个时间来与自己发送的消息的发送时间作比较,据此来判断消息是否已被用户查看
模型
用户的已读回执需要持久化存储,故创建了新的Mongoose模型
Read:用户id,朋友id,最后查看时间
ReadService:根据两个id来查询最后查看时间;根据两个id与当前时间新建或更新记录
场景
用户登录
socket开始监听 receive-msg 事件
receive-msg 事件:触发后更新与对应好友之间的消息列表;如果当前正打开与该好友的聊天界面,则socket向服务端发送 save-read 事件
save-read 事件:服务端事件。触发后更新传入的已读回执,如果目标用户在线,向其客户端发送 read 事件
read 事件:触发后更新本地保存的朋友已读用户的最新已读时间
用户打开与好友的聊天界面
发送请求获取该好友已读当前用户的最新已读时间(注意请求参数中,userId是当前好友的id,而friendId是当前用户的id),根据这个时间来判断消息是否已读
socket开始监听 read 事件
socket向服务端发送 save-read 事件,更新当前用户已读该好友的已读时间
用户向好友发送消息
socket向服务端发送 chat 事件
chat 事件:服务端事件。触发后对传入消息进行保存。如果目标用户在线,向其客户端发送 receive-msg 事件
解释
打开与好友的聊天界面就去获取好友已读我的时间,依此来判断我发送的消息是否已读;同时去更新数据库中我已读好友的最新时间,因为我打开界面查看了最新的消息;此时好友若在线并且打开了与我的聊天界面,那么就通过socket来通知他更新我已读他的时间;若好友不在线或未打开界面也无妨,他再次上线并打开与我的聊天界面时,会去数据库获取最新的已读时间。
发送新的消息时,如果好友在线并打开了与我的聊天界面,那么会通知我的客户端更新好友的最后已读时间,因为他在线接收了我发送的消息。
代码
服务端socket
// socket服务端
/**
* 处理socket连接
* @param {*} server
*/
const socketIo = require('socket.io')
const messageService = require('../services/MessageService')
const userService = require('../services/UserService')
const readService = require('../services/ReadService')
// 创建一个Map来存储每个用户的socket实例
const clientSockets = new Map()
module.exports = (server) => {
// 连接socket
const io = socketIo(server, { cors: { origin: '*' } })
// 监听连接
io.on('connection', (socket) => {
// 存储当前用户id
let uid = ''
console.log('用户连接...')
// 监听用户连接
socket.on('login', async (userId) => {
uid = userId
// 更改数据库中你的状态
userService.updateStatus(userId, 'online')
// 告诉在线的好友你上线了
const ids = await userService.getFriendIds(userId)
ids.forEach((id) => {
if (clientSockets.has(id.toString())) {
clientSockets.get(id.toString()).emit(userId, 'online')
}
})
// 保存当前用户的socket实例
clientSockets.set(userId, socket)
console.log('当前用户:', clientSockets.size)
})
// 接收到当前用户客户端发送的消息,将消息保存至数据库,然后发送给目标用户和当前用户的客户端
socket.on('chat', async (msg) => {
// 保存消息至数据库
const savedMsg = await messageService.createMessage(msg.content, msg.senderId, msg.receiverId)
// 发送给当前用户
socket.emit('callback-msg', savedMsg)
// 发送给目标用户
if (clientSockets.has(msg.receiverId)) {
clientSockets.get(msg.receiverId).emit('receive-msg', savedMsg)
}
})
// 更新已读回执
socket.on('save-read', async ([userId, friendId]) => {
// 确定已读时间
const time = new Date()
// 更新回执
await readService.updateRead(userId, friendId, time)
// 如果朋友在线,通知其我已读
if (clientSockets.has(friendId)) {
clientSockets.get(friendId).emit('read', time)
}
})
// 断开连接
socket.on('disconnect', async () => {
console.log('用户断开连接...')
// 更改数据库中你的状态
userService.updateStatus(uid, 'offline')
// 告诉在线的好友你下线了
const ids = await userService.getFriendIds(uid)
ids.forEach((id) => {
if (clientSockets.has(id.toString())) {
clientSockets.get(id.toString()).emit(uid, 'offline')
}
})
// 清理存储的用户socket实例
clientSockets.delete(uid)
console.log('当前用户:', clientSockets.size)
})
})
}
客户端Chat组件(聊天界面)
/**
* 已读未读
*/
// 初始化时更新已读回执
function updateRead() {
socket.emit('save-read', [user.value._id, friend.value._id])
// 并绑定接收好友更新已读回执的通知
socket.on('read', (time) => {
lastReadAt.value = time
})
}
// 好友最后已读时间
const lastReadAt = ref()
// 获取最后已读时间
async function getLastRead() {
const res = await request.get('/read', {
params: { friendId: friend.value._id }
})
if (res && res.code === 200) {
lastReadAt.value = res.data
}
}
// 从缓存队列中恢复时/载入时
onActivated(async () => {
await getFriend()
load()
getLastRead()
receiveMsg()
updateRead()
})
onDeactivated(() => {
// 解绑事件
socket.off('callback-msg')
socket.off(friend.value._id)
socket.off('read')
})
客户端ChatList组件(对话窗口列表)
/**
* socket
*/
const socket = inject('socket')
// 接收到非当前好友的新消息,标记未读,更新最后消息
socket.on('receive-msg', (msg) => {
updateAside([msg.content, msg.senderId, msg.createdTime])
// 更新对应的消息列表
if (msgMap.value[msg.senderId]) {
msgMap.value[msg.senderId].messages.push(msg)
} else {
msgMap.value[msg.senderId] = {
messages: [msg],
isLastPage: true,
nextId: msg._id
}
}
// 如果是正在聊天的,滚动到底部,同时更新已读时间
if (msg.senderId === activeItem.value) {
bus.emit('bottom')
socket.emit('save-read', [msg.receiverId, msg.senderId])
}
})
参考
Web聊天室消息[已读未读]的实现 - xiepl1997 - 博客园 (cnblogs.com)