赞
踩
原版本功能实现方式较混乱,代码逻辑晦涩难懂,不利于开发者参考或复用。此实战项目在确保原项目功能保留的情况下进行完全重写并新增大量功能,以确保未来项目的可维护性和扩展性。
本地 JS 文件引入通常用于引入项目内的模块、工具函数等,而通过
node_modules
引入通常用于引入第三方模块,而 IM SDK 正是三方 js 包,并且会持续更新因此,这种引入形式更规范且方便一些。
原引入方式
本地引入:
import websdk from "../newSDK/Easemob-chat-4.5.0.js";
现引入方式
通过node_modules进行导入;
import EaseSDK from 'easemob-websdk/uniApp/Easemob-chat';
原 uni-app demo 中将 IM SDK 中的实例化统一放在
utils/WebIM.js
中,将配置放在utils/WebConfig.js
中,SDK 各接口的调用分布在各个组件或页面中,因此在后续接口更新或者维护中过于分散,如需复用则需要在各个页面中进行查找,因此将 IM 相关的调用实例化,以及 config 配置新建一个EaseIM
文件进行统一管理,涉及 IM 相关大部分代码书写至此文件。
项目目录结构 |
---|
EaseIM |
├── config # IM 相关配置文件 |
├── constant # IM 相关常量文件 |
├── emApis # IM 所有调用 api 集合 |
├── emListener # IM 所有监听回调集合 |
├── utils # IM 相关所需工具方法 |
└── index.js # IM 引入 SDK 以及实例化导出文件 |
//index.js 示例代码
import EaseSDK from 'easemob-websdk/uniApp/Easemob-chat';
import { EM_APP_KEY, EM_API_URL, EM_WEB_SOCKET_URL } from './config';
let EMClient = (uni.EMClient = {});
EMClient = new EaseSDK.connection({
appKey: EM_APP_KEY,
apiUrl: EM_API_URL,
url: EM_WEB_SOCKET_URL,
});
uni.EMClient = EMClient;
export { EaseSDK, EMClient };
在部分用户基于原 uni-app demo 进行二次开发过程中,对于 IM 相关的本地数据(消息+会话+好友列表等)的处理一直是较为头疼的点,这种头疼主要体现在进行数据结构调整,以及逻辑复用,源码阅读时。
这种情况的产生是由于原 demo 中将消息的存储在了 localstorage 当中,并且对应的消息存储还进行了一系列的逻辑处理以及封装,这种封装还与原始的消息结构完全不同,从而导致改动异常困难,而原 demo 中的会话列表的实现也是基于 localstorage 中存储的数据形成,但环信已经针对会话列表,聊天消息,好友列表,用户属性都进行了远端存储,因此调用远端数据进行处理无疑是更简单的方法,所以调整后提供良好的示例尤为重要。
由于原始代码篇幅较长不过多的贴源代码进行比较,原 demo 本地数据管理核心分别在
globaldata
、localstorage
,处于实际场景考虑,以及缓存数据存在多页面复用,因此将本地缓存的数据管理改用为vuex
进行全局状态管理,选用vuex
作为全局状态管理首先便于将不同的状态数据切分为不同的 module,第二数据变更时的调用脉络清晰有利于后续维护以及增加可读性。
下图为改用 vuex 后的 store 的目录结构
下列示例代码以
conversation
模块为例,conversation 的 store 里面主要存储以及管理当前用户的会话列表信息。
import { emConversation, emSilent } from '@/EaseIM/emApis'; import { EMClient } from '@/EaseIM'; import { CHAT_TYPE } from '@/EaseIM/constant'; import Vue from 'vue'; import MessageStore from './message'; const { fetchPinConversationFromServer, pinConversationItem, fetchConversationFromServer, removeConversationFromServer, sendChannelAck, } = emConversation(); const { getSilentModeForConversation, getSilentModeForConversationList, setSilentModeForConversation, clearRemindTypeForConversation, } = emSilent(); const ConversationStore = { state: { chattingId: '', //进入聊天页面聊天中的目标聊天用户信息 chattingChatType: CHAT_TYPE.SINGLE_CHAT, //当前聊天页面中的聊天类型类型 chattingTypingStatus: false, //当前聊天页面中是否正在输入 pinConversationList: [], conversationList: [], silentConversationMap: {}, }, mutations: { RESET_CONVERSATION_STORE: (state) => {}, SET_CHATING_USER_INFO: (state, payload) => {}, SET_CHATING_USER_INFO_TYPING_STATUS: (state, payload) => {}, SET_PIN_CONVERSATION_LIST: (state, pinConversationList) => {}, SET_SILENT_CONVERSATION_MAP: (state, payload) => {}, UPDATE_PIN_CONVERSATION_ITEM: (state, conversationItem) => {}, SET_CONVERSATION_LIST: (state, conversationList) => {}, DELETE_CONVERSATION_ITEM: (state, conversationId) => {}, UPDATE_CONVERSATION_ITEM: (state, payload) => {}, SET_CONVERSATION_ITEM_READ_SATUS: (state, payload) => {}, }, actions: { //主动更新lastMessage updateConversationLastMsg: async ({ commit, dispatch }, payload) => {}, fetchPinConversationList: async ({ commit }) => {}, fetchConversationList: async ({ commit, dispatch }) => {}, //处理会话置顶 pinConversationItem: async ({ commit }, params) => {}, deleteConversation: async ({ commit }, params) => {}, sendConversatonReadedAck: async ({ commit }, params) => {}, //获取会话免打扰状态 fetchSilentConversationList: async ({ commit }, params) => {}, //获取单个会话免打扰状态 fetchSilentConversation: ({ commit }, params) => {}, //设置会话免打扰 setConversationSilentMode: async ({ commit }, params) => {}, //调整会话已读状态 setConversationReadStatus: async ({ commit }, params) => {}, }, getters: { //排序会话列表 sortedConversationList(state) {}, //排序指定会话列表 sortedPinConversationList(state) {}, //会话免打扰信息 silentConversationMap(state) {}, //会话未读总数 calcAllUnReadNumFromConversation(state) {}, //聊天中用户ID chattingId(state) {}, chattingChatType(state) {}, chattingTypingStatus(state) {}, }, }; export default ConversationStore;
可以看到在此 store 文件中,有针对会话列表的常见增删改查,以及指定,会话列表的状态等进行了更为集中的管理,在各组件中都可以较为编辑的更新以及使用conversation
相关的数据。
下图为原会话列表组件以及获取的代码,谁看谁麻。
得益于集中维护,且各组件需要编辑复用数据的特性,因此将原 demo 逻辑中的,涉及全局状态管理的数据以及不同组件会经常使用的数据逻辑全部调整为vuex
进行管理,且vuex
中的数据变更是可以做到响应式的引起视图数据的更新,因此充斥在原始 demo 中大量的发布-订阅模式的event bus
代码可以进行删除,比如典型的这些代码。
//disp.fire 发布事件 //disp.on 订阅事件 //disp.off 卸载订阅 WebIM.conn .open(opt) .then(() => { //token获取成功,即可开始请求用户属性。 disp.fire('em.mian.profile.update'); disp.fire('em.mian.friendProfile.update'); }) .catch((err) => { console.log('>>>>>token获取失败', err); }); //监听加好友申请 disp.on('em.subscribe', this.onChatPageSubscribe); //监听解散群 disp.on('em.invite.deleteGroup', this.onChatPageDeleteGroup); //监听未读消息数 disp.on('em.unreadspot', this.onChatPageUnreadspot); //监听未读加群“通知” disp.on('em.invite.joingroup', this.onChatPageJoingroup); //监听好友删除 disp.on('em.contacts.remove', this.onChatPageRemoveContacts); //监听好友关系解除 disp.on('em.unsubscribed', this.onChatPageUnsubscribed); //页面卸载同步取消onload中的订阅,防止重复订阅事件。 disp.off('em.subscribe', this.onChatPageSubscribe); disp.off('em.invite.deleteGroup', this.onChatPageDeleteGroup); disp.off('em.unreadspot', this.onChatPageUnreadspot); disp.off('em.invite.joingroup', this.onChatPageJoingroup); disp.off('em.contacts.remove', this.onChatPageRemoveContacts); disp.off('em.unsubscribed', this.onChatPageUnsubscribed);
chat 聊天组件是指的原项目内的
components
下的chat
组件,原 uni-app demo 中 chat 组件的核心作用为实现聊天页面,通过在pages
中的某个页面级组件引入components/chat
后渲染展示其整个聊天页面,下面是原引入代码。
<template> <chat id="chat" :username="username" ref="chat" chatType="singleChat" @onClickInviteMsg="onClickMsg"></chat> </template> <script> import chat from "../../components/chat/chat.vue"; export default { data() { return { username: { your: "" } }; }, components: { chat },
尽管看起来只是简单的引入一个chat
组件,传入少数参数,但实际在二次开发中需要大量修改chat
中的逻辑。
由于本身聊天页就属于页面级组件因此在重构后将其“拎到”pages
中,并进行了逻辑重写和大量的功能增加。
在下面我们将以常见问题的形式指引大家如何去看到自己更加关注的代码逻辑,从而帮助我们找到集成中遇到的需要参考示例的代码片段。
假设我们只需要在已有项目中只需要实现核心的一个聊天页面功能,保障最基本的聊天功能即可,我们需要从这个 demo 中 copy 哪些代码就可以实现诉求功能呢?
npm install easemob-websdk
uview-ui
库,安装方式可以查看uView官网。
z-paging
一个 uni-app 分页组件。
全平台兼容,支持自定义下拉刷新、上拉加载更多,支持虚拟列表,支持自动管理空数据图、点击返回顶部,支持聊天分页、本地分页,支持展示最后更新时间,支持国际化等等。而在此示例中主要用到了该插件的聊天记录模式,来实现下拉加载更多聊天记录。
EaseIM
文件至你的项目中,建议与你的项目目录中pages
平级,在EaseIM
下的config
的index.js
文件中,请将常量EM_APP_KEY
的 appkey 修改为自己的 appkey、store
至你的项目中,如果你的项目里面恰好也用到了vuex
请自行进行合并。pages/emChatContainer
并也放入你的项目中的pages
目录下,不要忘记,在pages.json
中进行对应页面的配置注册。{ "path": "pages/emChatContainer/index", "style": { "navigationStyle": "custom", "navigationBarTextStyle": "white", "app-plus": { "bounce": "none" } } }, { "path": "pages/emChatContainer/emSelectUserCard/index", "style": { "navigationStyle": "custom", "navigationBarTextStyle": "white" } },
挂载监听回调
//App.vue <script> import { EMClient } from "@/EaseIM"; import { emConnectListener, emMountGlobalListener } from "@/EaseIM/emListener"; import { emConnect } from "@/EaseIM/emApis"; import { CONNECT_CALLBACK_TYPE, HANDLER_EVENT_NAME } from "@/EaseIM/constant"; import { emHandleReconnect } from "@/EaseIM/utils"; export default { onLaunch() { //传给监听callback回调 const connectedCallback = (type) => { console.log(">>>>>IM连接回调", type); if (type === CONNECT_CALLBACK_TYPE.CONNECT_CALLBACK) { this.onConnectedSuccess(); } if (type === CONNECT_CALLBACK_TYPE.DISCONNECT_CALLBACK) { this.onDisconnect(); } if (type === CONNECT_CALLBACK_TYPE.RECONNECTING_CALLBACK) { this.onReconnecting(); } }; /* 链接所需监听回调 */ emConnectListener(connectedCallback); /* 全局类型监听集合、消息、联系人、群组等... */ emMountGlobalListener(); this.handleAutoLoginEaseIM(); }, computed: { loginStoreStatus() { return this.$store.state.LoginStore.loginStatus; }, loginStoreUserBaseInfos() { return this.$store.state.LoginStore.loginUserBaseInfos; }, }, methods: { onConnectedSuccess() { const { loginUserId } = this.loginStoreUserBaseInfos || {}; const finalLoginUserId = loginUserId || EMClient.user; if (!this.loginStoreStatus) { this.fetchLoginUserNeedData(); uni.hideLoading(); console.log(">>>>>>开始跳转至会话列表页面"); uni.redirectTo({ url: "../home/home?myName=" + finalLoginUserId, }); } this.$store.commit("SET_LOGIN_USER_BASE_INFOS", { loginUserId: finalLoginUserId, }); this.$store.commit("SET_LOGIN_STATUS", true); }, onDisconnect() { const { closeEaseIM } = emConnect(); const { actionEMReconnect } = emHandleReconnect(); //断开回调触发后,如果业务登录状态为true则说明异常断开需要重新登录 if (!this.loginStatus) { uni.showToast({ title: "退出登录", icon: "none", duration: 2000, }); uni.redirectTo({ url: "../login/login", }); closeEaseIM(); } else { console.log(">>>>>需执行重登逻辑"); //执行通过token进行重新登录 actionEMReconnect(); } }, onReconnecting() { uni.showToast({ title: "IM 重连中...", icon: "none", }); }, //获取登录所需基础参数 async fetchLoginUserNeedData() { await this.$store.dispatch("fetchFriendList"); //获取好友用户属性 await this.$store.dispatch("fetchFriendUserInfoCollection"); //获取当前登录用户好友信息 await this.$store.dispatch("fetchLoginUserProfile"); await this.$store.dispatch("fetchBlockUserList"); // 在线状态订阅 await this.$store.dispatch("subscribePresenceStatus"); this.fetchJoinedGroupList(); //初始化缓存本地的新邀请列表 this.$store.commit("INIT_RECEIVE_INVITE_LIST"); }, //获取加入的群组列表 async fetchJoinedGroupList() { //获取群组列表 await this.$store.dispatch("fetchJoinedGroupList", { isInit: true, }); }, //自动登录 handleAutoLoginEaseIM() { const { actionEMReconnect } = emHandleReconnect(); const loginInfos = uni.getStorageSync(`EM_LOGIN_INFOS`); if (!loginInfos) return; actionEMReconnect(); }, }, }; </script>
接着你需要代码层面实现与环信服务建立连接相关的逻辑相关逻辑,类似如下代码。
import { emConnect } from '@/EaseIM/emApis'; <script> import { emConnect } from '@/EaseIM/emApis'; const { loginWithPassword, loginWithAccessToken } = emConnect(); export default { data() { return { /* 环信ID环信密码登录 */ loginEaseIMId: '', loginEaseIMPassword: '' }; }, methods: { async loginWithUserId() { try { const res = await loginWithPassword( this.loginEaseIMId, this.loginEaseIMPassword ); this.$store.commit('SET_LOGIN_USER_BASE_INFOS', { loginUserId: this.loginEaseIMId, }); this.setEMUserLoginInfosToStorage( this.loginEaseIMId.toLowerCase(), res.accessToken ); } catch (error) { console.log('>>>>>>', error); uni.showToast({ title: '登录失败', icon: 'none', }); } finally { this.loginEaseIMId = ''; this.loginEaseIMPassword = ''; } }, setEMUserLoginInfosToStorage(userId, token) { const params = { key: `EM_LOGIN_INFOS`, data: { userId: userId, token: token }, }; uni.setStorage({ ...params }); }, }, }; </script>
在有效建立连接后会自动触发监听回调中的onConnected
,同时也会触发上述的onConnectedSuccess
方法,这个表明长连接已经建立,可以进行聊天,那么接下来你可以点击按钮跳转至emChatContainer
组件也可以,在onConnected
中进行路由跳转,成功跳转之后则会如下图页面。
如果我们需要在自己的项目中通过引入该项目的会话组件进行二次开发,需要复用哪些代码完成一个会话列表界面,下面将简述一下需要依赖的代码逻辑。
如何在已有项目中实现单聊或群聊聊天功能?
中的,1~7 已完成,然后在原项目中将pages/conversation
组件按照原始目录 copy 至你的项目,并在pages.json
完整相关路由注册。会话列表以及聊天页面是最常见的一些组件复用需求,至于如果你还有其他组件复用疑问或者需求则可以在评论区提出。
该滚动插件在使用
聊天记录模式
模式时,可以追加消息,但为了实现编辑消息,不得不有一个replace的动作,因此不得不在其源码上新增了一个function,下面是修改的方法位置。
不推荐这么玩,因此如果不需要编辑消息可以忽略下面的修改代码。
//监听编辑消息更新item
onModifyMessage(data) {
/**
* @function updateChatRecordData
* @changeSrc "data-handle.js uni_modules/z-paging/components/z-paging/js/modules"
* @description 此方法为自己往插件库中新增的一个方法,主要为更新本地分页数据中已存在的消息体。
*/
// updateChatRecordData(data) {
// if (!this.useChatRecordMode) return;
// const _index = this.totalData.findIndex((o) => o.id === data.id);
// _index >= 0 && (this.totalData[_index] = data);
// }
this.$refs.paging.updateChatRecordData(data);
},
页面级滚动与 List 滚动重合冲突,因此 home 禁止滚动,调整了 List 中的高度,使其减去 navbar 以及顶部安全区,以及 tabbar 以及底部安全区。
setConversationListHeigth() { uni.getSystemInfo({ success: (res) => { //顶部安全区高度 const statusBarHeight = res.statusBarHeight; //底部安全区高度 const safeAreaBottom = res.safeAreaInsets.bottom; console.log('safeAreaBottom', safeAreaBottom); this.conversationListHeight = res.windowHeight - (tabBarHeight + navBarHeight + searchInputHeight + statusBarHeight + safeAreaBottom); }, }); },
迫不得已,最终改了 u-index-list 组件代码 在 customNavHeigth 后面多减了 100
init() {
// 设置列表的高度为整个屏幕的高度
//减去this.customNavHeight,并将this.scrollViewHeight设置为maxHeight
//解决当u-index-list组件放在tabbar页面时,scroll-view内容较少时,还能滚动
this.scrollViewHeight =
this.sys.windowHeight -
this.customNavHeight -
this.sys.safeAreaInsets.bottom;
if (this.sys.safeAreaInsets.bottom) {
this.scrollViewHeight = this.scrollViewHeight - 100;
} else {
this.scrollViewHeight = this.scrollViewHeight - 50;
}
},
要求主包 1.5m 结果代码 6m
因此想了一系列优化手段
运行时是否压缩代码
,这个效果很明显,直接至 2000+kb上传代码时自动压缩脚本文件
,此效果也很明显再次缩减部分大小。最后展示的占比图:
uView2.x
也出现了不再进行更新的情况,因此如果在进行参考或者复用逻辑进行二次开发的时候,也需要将此风险进行评估,不过如果在使用中有好的建议以及方案也欢迎进行友好交流。Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。