前言
聊天系统客户端是要存储消息的,因为所有所有的历史消息都从服务器拉的话一方面服务器压力大,另一方面也耗费用户流量。所以客户端存储消息是势在必行的。如何存储呢上一篇文章也写了,大概就是浏览器的话是localStorage或者IndexedDB。然后手机端和桌面端就是sqllite了。这样子消息的存储结构以及消息的增删改查也是需要一套的了。本篇文章将着重从自己的开源项目技术选型来进行分享。vuex进行增删改查。B站会录制视频同步分享。
目前已经写的文章有。并且有对应视频版本。
git项目地址 【IM即时通信系统(企聊聊)】点击可跳转
sprinboot单体项目升级成springcloud项目 【第一期】
前端项目技术选型以及页面展示【第二期】
分布式权限 shiro + jwt + redis【第三期】
给为服务添加运维模块 统一管理【第四期】
微服务数据库模块【第五期】
netty与mq在项目中的使用(第六期(废弃))】
分布式websocket即时通信(IM)系统构建指南【第七期】
分布式websocket即时通信(IM)系统保证消息可靠性【第八期】
分布式websocket IM聊天系统相关问题问答【第九期】
什么?websocket也有权限!这个应该怎么做?【第十期】
分布式ID是什么,以美团Leaf为例改造融入自己项目【第十一期】
IM聊天系统为什么需要做消息幂等?如何使用Redis以及Lua脚本做消息幂等【第12期】
微信发送一条消息经历哪些过程。企业微信以及钉钉的IM架构对比【第13期】
微信群为什么上限是500人,IM设计系统中的群聊的设计难点【第14期】
【分布式websocket】RocketMQ发送消息保证消息最终一致性需要做哪些处理?【第15期】
【分布式websocket】群聊中的各种难点以及解决推拉结合【第16期】
【分布式webscoket】未读消息如何设计?解决缓存与数据库数据一致性!推送未读消息流程【第17期】
IM系统客户端消息存储在手机电脑浏览器分别存储在什么地方?对消息加密策略?如何保证服务端消息和客户端消息一致性【第18期】
【分布式websocket】聊天系统消息加密如何做【第20期】
【分布式webscoket】IM聊天系统消息如何存储 如何分库分表以及Seata解决事务以及ShardingSphere-Scaling解决数据迁移【第21期】
客户端消息结构:
技术选型在浏览器端的localStorage,当然是有缺陷的。后续根据情况再进行优化。页面如下。
chats: [] 数组 存放每一个聊天用户
lastContent: 存放最后一条消息用于显示
tagrgetId: 标识唯一会话,可以考虑改成会话id,目前使用的是单聊是对方的id,群聊是群聊id
type : 标记是私聊还是群聊
unreadCount: 未读消息数量 (TODO)
messgaes :[] 存放每一个用户下面具体的聊天消息,数组
type: 用于标记消息是自己发的还是别人的,用于前端显示样式
group: 用于区分消息是群聊还是单聊 ,创建消息的时候会使用到
msgid: 消息唯一id
avatarUrl: 用于页面上面显示聊天头像框
content: 消息内容
tagrgetId: 标识唯一会话id
privateMsgMaxId: 拉取消息id
如下
客户端消息操作主要是 .
- 添加 1.上线后拉取未读消息要存储未读消息 2.发送消息需要添加消息3.收到消息需要添加
- 查询 进入聊天页面需要可以查到消息
- 修改 消息发送失败需要修改状态发送失败
- 删除 前端存储有限制只能维护一定时间的历史消息。更多的历史消息查询客户端
vuex基本概念概述
export default createStore({
state,
mutations,
actions,
getters,
modules: {}
})
大概就是这么几个部分。
总结来说,Getter 用于获取由 state 计算得出的数据;
Mutation 用于同步地改变 state,
Action 则封装了异步操作,并最终通过 commit 来间接触发 mutations 更新状态。
state 就是封装变量的地方。
Vuex 允许将应用程序的状态集中存储在一个共享的 store 中,避免了组件之间通过 props 和 events 进行状态传递的复杂性和繁琐性。这使得状态管理更加清晰和易于维护。通过 Vuex 管理状态,整个应用程序共享同一个状态树,确保了状态的一致性和同步性。
场景介绍
消息查询
// 创建一个计算属性,该属性基于其他响应式状态计算值
const computedChats = computed(() => {
let chat = null;
console.log("computedChats route.query.groupId", route.query.groupId);
if (state.current == 1) {
chat = {
targetId: state.toUser.openid,
};
} else {
chat = {
// targetId: state.toUser.openid,
targetId: state.groupId,
};
}
const idx = store.getters.findChatIdx(chat);
if (idx == null || idx == undefined) {
return [];
}
if (
store.state.chats[idx] == null ||
store.state.chats[idx] == undefined
) {
return [];
}
console.log("computedChats idx", idx);
console.log("computedChats 寻找成功啦", store.state.chats[idx]);
return store.state.chats[idx];
});
封装了一个计算属性。用于监听state里面的消息变化。逻辑大概是拿到当前会话的id,单聊的话就是对话的openid。然后去store里面去找一下。找不到的返回空数组。找到的话返回当前聊天下的所有信息.
对应前台页面
渲染一下这个compute属性
<list-scroll :scroll-data="computedChats.messages">
<div class="swiper-container">
<div
class="content"
v-for="(item, index) in computedChats.messages"
:key="index"
>
<div class="d-felx justify-start " v-if="item.type === 'self'">
<div style="display: flex;">
<van-image
width="35px"
height="35px"
fit="cover"
:src="userInfo.avatarUrl"
/>
<div class="font-18 content1">
<text>{{ item.content }}</text>
</div>
</div>
</div>
<div
style="display: flex; justify-content: flex-end;"
v-if="item.type === 'receive'"
>
<div class="font-18 content2">
<text>{{ item.content }}</text>
</div>
<div class="">
<van-image
width="35px"
height="35px"
fit="cover"
:src="toUser.avatarUrl"
/>
</div>
</div>
</div>
</div>
</list-scroll>
消息添加
分为离线数据添加 和在线数据添加。
离线数据添加需要使用到action,异步的去后台拉取然后插入。
在线数据直接调用mutation数据插入。
/**
* 插入消息.
* @param {*} state
* @param {*} msgInfo 当前消息
*/
insertMessage(state, msgInfo) {
console.log("insertMessage",msgInfo)
state.privateMsgMaxId = msgInfo.msgId;
state.groupMsgMaxId = msgInfo.msgId;
// 如果是已存在消息,则覆盖旧的消息数据
let chat = this.getters.findChat(msgInfo);
if (chat == null) {
this.commit("createChat", msgInfo);
chat = this.getters.findChat(msgInfo);
}
if(chat == null){
console.log("没有找到chat",chat);
return;
}
chat.messages.push(msgInfo);
this.commit("saveToStorage");
},
第一步,先更新一下这个最大的消息id。然后去store里面去找当前消息。找到之后给当前chat里面推送消息。并且同步的保存到Storgae里面.
/**
* state.chats 将更新后的存储.
* @param {*} state
*/
saveToStorage(state) {
let userId = state.userInfo.openid;
let key = "chats-" + userId;
let chatsData = {
privateMsgMaxId: state.privateMsgMaxId,
groupMsgMaxId: state.groupMsgMaxId,
chats: state.chats,
};
localStorage.setItem(key, JSON.stringify(chatsData));
},
这个存储的逻辑就是简单的将消息序列化后放到localStorage里面。
强调一下这个离线消息拉取的步骤;需要后台sql的配合。
async pullOffline(ctx) {
// 获取当前store中的privateMsgMaxId
const privateMsgMaxId = ctx.state.privateMsgMaxId+"";
console.log("privateMsgMaxId",ctx.state.privateMsgMaxId)
const res = await getChatContentAll(privateMsgMaxId);
const contentAll = res.content
for (var i = 0; i < contentAll.length; i++) {
ctx.commit("initInsertMessage", contentAll[i]);
}
}
调用后台接口getChatContentAll 然后获取未拉取的离线消息然后进行存储。