概述
本次关于 uni-app 代码整体重构工作,基于上一期针对 uni-app 官网 demo 从 vue2 迁移 vue3 框架衍生而来,在迁移过程中有明显感知,目前的项目存在的问题为,项目部分代码风格较为不统一,命名不够规范,注释不够清晰、可读性差、以造成如果复用困难重重,本地重构期望能够充分展示 api 在实际项目中的调用方式,尽可能达到示例代码可移植,或能辅助进行即时通讯功能二次开发的能力。
目的
- 使代码更加可读。
- 简化或去除冗余代码。
- 部分组件以及逻辑重命名、重拆分、重合并。
- 增加全局状态管理。
- 修改 SDK 引入方式为通过 npm 形式引入
- 收束 SDK 大部分 API 到统一文件、方便管理调用。
- 升级 SDK api 至最新的调用方式(监听、发送消息)
- 增加会话列表接口、消息漫游接口。
-
SDK 指环信 IM uni-app SDK
重构计划
一、修改原 WebIM 的导出导入使用方式。
目的
- 现有 uniSDK 已支持 npm 形式导入。
- 原有实例化代码与 config 配置较为混乱不够清晰。
- 分离初始化以及配置形成独立文件方便管理。
实现
- 项目目录中创建 EaseIM 文件夹并创建 index.js,在 index.js 中完成导入 SDK 并实现实例化并导出。
- EaseIM -> config 文件夹并将 SDK 中相关配置在此文件中书写并导出供实例化使用。
影响(无影响)
二、引入 pinia 进行状态管理
pinia 还能通过$reset()方法即可完成对某个 store 的初始化,利用该方法可以非常方便的在切换账号时针对缓存在 stores 中的数据初始化,防止切换后的账号与上一个账号的数据造成冲突。
目的
- 存放 SDK 部分数据以及状态(登录状态、会话列表数据、消息数据)
- 方便各组件状态或数据取用避免数据层层传递。
- 用以平替原有本地持久化数据存储。
- 可以替代原有 disp 发布订阅管理工具,因为 store 中状态改变,各组件可以进行重新计算或监听,无需通过发布订阅通知改变状态。
实现
- 在 mian.js 中引入 pinia,并挂载
//Pinia
import * as Pinia from 'pinia';
export function createApp() {
const app = createSSRApp(App);
app.use(Pinia.createPinia());
return {
app,
Pinia,
};
}
影响(无影响)
三、重新梳理 App.vue 根组件中代码
目的
- 简化项目中 App.vue 根组件中的冗长代码。
- 迁移根组件中的监听代码。
- globalData,method 中代码转为 stores 中或剔除。
- disp 代码剔除。
实现
- App.vue 中的监听嵌入至 EaseIM 文件夹下的 listener 集中管理,并在 App.vue 中重新进行挂载
- import '@/EaseIM';从而实现实例化 SDK。
- 将需要 IM 连接成功后调用的数据,合为一个方法中,并在 onConnected 触发后调用。
- 部分关于 SDK 调用的代码迁入至 EaseIM 文件夹下的 imApis 文件夹中
- 部分有关 SDK 的工具方法代码迁入至 EaseIM 文件夹下的 utils 文件夹中
影响
App.vue 改动相对较大,主要为监听的迁移,一部分方法迁移至 stores 中,并且需要重新进行监听的挂载。具体代码可在后续迁移前后比对中看到,或者文尾的看到 github 中看到代码地址。
四、优化 login 页面代码
目的
原有 login 组件登录部分代码比较冗长并且可读性较差因此进行优化。
实现
- 删除原有操作 input 的代码,改为通过 v-model 双向绑定。
- 拆分登录代码为通过 username+password,以及手机号+验证码两个登录方法。
- 增加登录存储 token 方法,方便后续重连时通过用户名+token 形式进行重新登录。
- 登录成功之后将登录的 id 以及手机号信息 set 进入到 stores 中。
影响(无影响)
五、增加 home 页面
目的
- 作为 Conversation、Contacts、Me 三个核心页面容器组件,方便页面切换管理。
- 作为 Tabbar 的容器组件
实现
- 去除原有会话、联系人、我的(原 setting 页面)pages.json 的路由配置,增加 home 页面路由相关配置。
- pages 中增加 home 组件,并以组件化的形式引入三个核心页面组件。
- 项目根目录中新建 layout 文件夹并增加 tabbar 组件,将三个页面中的 tabbar 功能抽离至 tabbar 组件中,并增加相应切换逻辑。
影响
此改动主要影响为要涉及到将原 setting 组件改为 Me 组件,并将三个原有页面 pages.json 删除并在 home 中引入,并无其他副作用。
六、重构 Conversation、Contacts、Me 等基础组件
目的
- 将原有数据(会话列表数据,联系人数据,昵称头像数据)来源切换为从 SDK 接口+stores 中获取。
- 去除组件内的 disp 发布订阅相关代码,以及 WebIM 的使用。
- 调整原组件代码中的不合理的命名,去除不再使用的方法简化该组件代码。
实现
以 Conversation 组件举例
- 以 SDK 接口 getConversationlist 获取会话列表数据,缓存至 stores 中并做排序处理,在组件中使用计算属性获取作为新的会话列表数据来源。
- 由于会话列表的更新是动态的,因此不再需要 disp 订阅一系列的事件进行处理,因此相关代码可以开始进行删除。
- 原有的通过会话列表跳转至联系人页面或者其他群组页面命名改为单词从 into 改为 entry 并改为驼峰命名,经过改造该组件用不到的方法则完全删除。
影响
主要影响则是这些组件内的逻辑代码会有从结构以及数据源会有较大变化,需要边改造边验证,并且会与 stores、EaseIM 等组件有较大的关系,需要耐心进行调整。
七、增加 emChatContainer 组件
目的
- 新增此组件命名更为语义化,能够通过组件名看出其实际功能为 emChat 聊天页组件容器。
- 合并原有 singleChatEntry 组件以及 groupChatEntry 组件,两个相似功能组件至统一的一个 emChatContainer 内。
实现
- 在 pages 下新建一个名为 emChatContainer 的组件,并先将 components 下的 chat 组件参考 singleChatEntry 组件引入,并在 pages 中配置对应路由路径映射。
- 观察发现该组件作为 chat 组件容器,主要向下传递两个核心参数,1)目标 ID(也就是聊天的目标环信 ID)。2)chatType(也就是目标聊天的类型,常规为单聊、群聊。),且这两个核心参数经常被 chat 组件中的各个子组件用到,一层层向下传递较为繁琐,因此使用到 Vue 组件传参方法之一的,provide、inject 方式将参数注册并向下传递下去。
- 完成合并之后将 singleChatEntry、groupChatEntry 删去,并且将原有用到向该组件跳转的方法路径全部指向 emChatContainer,且在 pages.json 中删除对应的页面路径。
影响
从会话进入到聊天页、从联系人、群组页面进入到聊天页的路由跳转路径全部改为 emChatContainer,并且将会改变 chat 组件使用 targetId(聊天目标 ID)以及 chatType 的方式,因为需要改为通过 inject 接收。
八、emChat 组件重构
目的
- 改写该组件下不合理的文件命名。
- 删除非必要的 js 文件或组件。
- 该组件内各个功能子组件进行局部代码重构。
实现
- 配合 emChatContainer 将 chat 组件改名为 emChat。
- 删除其组件内的 msgpackager.js、msgstorage.js、msgtype.js、pushStorage.js,这几个 js 文件。
- messagelist inputbar 改为驼峰命名。
- messageList 组件内的消息列表来源改为从 stores 中获取,增加下拉通过 getHistroyMessage 获取历史消息。
- 子组件内接收目标 id 以及消息类型改为通过 inject 接收。
- msgType 从 EaseIM/constant 中获取。
- 发送消息 API 全部改为 SDK4.xPromise 写法,在 EaseIM/imApis/emMessages 统一并导出,在需要的发送的组件中导入调用,剔除原有发送消息的方式。
影响
该组件调整难度最大,因为牵扯的组件,以及需要新增的调整的代码较多,需要逐个组件修改并验证,具体代码将在下方局部展示。详情请参看源码地址。
九、新增重连中提示监听回调
目的
能够在 IM websocket 断开的时候有相应的回调出来,并给到用户相应的提示。
实现
在 addEventHandler 监听中增加 onReconnecting 监听回调,并且在实际触发的时候增加 Toast 提示监听 IM 正在重连中。
PS:onReconnecting 属于实验性回调。
影响(无影响)
重构前后代码片段展示
一、重构前后项目目录展示
重构前项目目录结构
重构后项目目录结构
二、重构前后 SDK 引入以及初始化展示
重构前 SDK 引入初始化代码片段
import websdk from 'easemob-websdk/uniApp/Easemob-chat';
import config from './WebIMConfig';
console.group = console.group || {};
console.groupEnd = console.groupEnd || {};
var window = {};
let WebIM = (window.WebIM = uni.WebIM = websdk);
window.WebIM.config = config;
WebIM.conn = new WebIM.connection({
appKey: WebIM.config.appkey,
url: WebIM.config.xmppURL,
apiUrl: WebIM.config.apiURL,
});
export default WebIM;
重构后 SDK 引入初始化代码片段
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 };
三、重构前后 App.vue 组件代码片段展示
该组件代码过于长,为防止水文嫌疑,因此截取部分代码展示[手动狗头]
重构前 App.vue 组件代码片段
重构后 App.vue 组件代码片段
可以看到比原有 App.vue 组件有明显的代码简化。
四、重构前后会话列表(conversation)组件代码片段对比
重构前会话列表代码片段展示
PS:template 中代码变化不大,为缩减长度暂时省去 template 相关代码
重构后会话列表代码片段展示
五、重构后新增 Tabbar 组件代码片段展示
{{ unReadSpotNum + unReadTotalNotNum }}
会话
联系人
我的
六、重构后新增 home 组件代码片段展示
没有使用 Vue 中的动态组件(component)实现是因为 uni-app 打包到某些平台不支持。
七、重构后新增 emChatContainer 组件代码展示
八、重构后新增 EaseIM 文件部分代码片段
config(针对 IM 相关配置文件)
export const EM_API_URL = 'https://a1.easemob.com';
export const EM_WEB_SOCKET_URL = 'wss://im-api-wechat.easemob.com/websocket';
export const EM_APP_KEY = 'easemob#easeim';
constant(IM 相关常量)
export const CHAT_TYPE = {
SINGLE_CHAT: 'singleChat',
GROUP_CHAT: 'groupChat',
};
export const HANDLER_EVENT_NAME = {
CONNECT_EVENT: 'connectEvent',
MESSAGES_EVENT: 'messagesEvent',
CONTACTS_EVENT: 'contactsEvent',
GROUP_EVENT: 'groupEvent',
};
export const CONNECT_CALLBACK_TYPE = {
CONNECT_CALLBACK: 'connected',
DISCONNECT_CALLBACK: 'disconnected',
RECONNECTING_CALLBACK: 'reconnecting',
};
export const MESSAGE_TYPE = {
IMAGE: 'img',
TEXT: 'txt',
LOCATION: 'location',
VIDEO: 'video',
AUDIO: 'audio',
EMOJI: 'emoji',
FILE: 'file',
CUSTOM: 'custom',
};
imApis(SDK 接口管理)
import { EMClient } from '../index';
const emContacts = () => {
const fetchContactsListFromServer = () => {
return new Promise((resolve, reject) => {
EMClient.getContacts()
.then((res) => {
const { data } = res;
resolve(data);
})
.catch((error) => {
reject(error);
});
});
};
const removeContactFromServer = (contactId) => {
if (contactId) {
EMClient.deleteContact(contactId);
}
};
const addContact = (contactId, applyMsg) => {
if (contactId) {
EMClient.addContact(contactId, applyMsg);
}
};
const acceptContactInvite = (contactId) => {
if (contactId) {
EMClient.acceptContactInvite(contactId);
}
};
const declineContactInvite = (contactId) => {
if (contactId) {
EMClient.declineContactInvite(contactId);
}
};
return {
fetchContactsListFromServer,
removeContactFromServer,
acceptContactInvite,
declineContactInvite,
addContact,
};
};
export default emContacts;
listener(SDK 监听回调管理)
import { EMClient } from '../index';
import { CONNECT_CALLBACK_TYPE, HANDLER_EVENT_NAME } from '../constant';
export const emConnectListener = (callback, listenerEventName) => {
console.log('>>>>连接监听已挂载');
const connectListenFunc = {
onConnected: () => {
console.log('connected...');
callback && callback(CONNECT_CALLBACK_TYPE.CONNECT_CALLBACK);
},
onDisconnected: () => {
callback && callback(CONNECT_CALLBACK_TYPE.DISCONNECT_CALLBACK);
console.log('disconnected...');
},
onReconnecting: () => {
callback && callback(CONNECT_CALLBACK_TYPE.RECONNECTING_CALLBACK);
},
};
EMClient.removeEventHandler(
listenerEventName || HANDLER_EVENT_NAME.CONNECT_EVENT
);
EMClient.addEventHandler(
listenerEventName || HANDLER_EVENT_NAME.CONNECT_EVENT,
connectListenFunc
);
};
utils(IM 相关工具函数文件)
/* 用以获取消息存储格式时的key */
const getEMKey = (loginId, fromId, toId, chatType) => {
let key = '';
if (chatType === 'singleChat') {
if (loginId === fromId) {
key = toId;
} else {
key = fromId;
}
} else if (chatType === 'groupChat') {
key = toId;
}
return key;
};
export default getEMKey;
index.js(引入 SDK 并初始化 SDK 并导出)
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 };
九、重构前后发送消息代码片段展示
重构前发送文本消息组件
重构后发送文本消息组件
十、重构前后消息列表(messageList)代码展示
重构前消息列表代码
本应用仅用于环信产品功能开发测试,请勿用于非法用途。任何涉及转账、汇款、裸聊、网恋、网购退款、投资理财等统统都是诈骗,请勿相信!
重构前消息列表代码
本应用仅用于环信产品功能开发测试,请勿用于非法用途。任何涉及转账、汇款、裸聊、网恋、网购退款、投资理财等统统都是诈骗,请勿相信!
还有更多重构代码篇幅有限不便一一展示,感兴趣请至片尾点击 github 地址查看。
重构过程中遇到的部分问题记录
问题一、打包至微信小程序中三大页面组件样式丢失。
问题简述:该问题在 H5 以及 app 中运行均正常展示,但测试发现运行至微信小程序中,会话列表、联系人、我的页面三个页面样式无法加载,效果如下图:
排查解决:发现这三个组件由原来页面级跳转改为了动态切换组件,但是在 pages.json 中仍然配置有该三大组件的路由映射地址,导致打包运行至微信小程序中时,样式出现丢失未能加载。去掉 pages.json 中仍存在的路由映射地址即可恢复正常。
问题二、打包至微信小程序时发现 emoji 表情图片无法正常加载展示。
问题简述:打包至微信小程序时点击 emoji 发送,发送框无法展示 emoji 映射的静态资源图片,效果如下图:
问题三、打包至微信小程序发送图片时发现截取图片类型时异常,导致发送失败。
问题描述:打包至微信小程序时发现发送图片功能异常,导致消息发送失败。
排查解决:经排查发现微信小程序微信小程序从基础库 2.21.0 开始, wx.chooseImage 停止维护,需使用 uni.chooseMedia 代替。
因此经过处理,判断如果是微信小程序平台,字节平台,京东平台使用 uni.chooseMedia 去进行文件的选取。
当然还需要注意 uni.chooseMedia 与 uni.chooseImage 返回的字段不一致,因此在后续发送时也需要针对性的进行处理。
问题四、运行至原生客户端(安卓、IOS)平台,发送语音、发送图片、拍照发送图片等功能提示 XXX 模块未加载。
问题描述:运行至原生端,在点击附件类消息发送时,例如发送语音、发送图片、拍照发送图片等功能提示 XXX 模块未加载。
排查解决:在 HB 中进行云打包之前,请记得 manifest.json / App 模块配置中勾选如下模块。
最终小结
非常高兴能够对 webim-uniapp-demo 重构,这个事情是我一直都想要做的事情,因为原有的项目代码已经不能很好的帮助想要拿此项目作为参考或者复用的的开发者完成高效的 IM 功能开发。
在重构过程中受益匪浅。因为在这个过程中,我通过对整个 demo 的代码重新整理和改写,不仅使自己对 SDK 的集成有了更深入的认识,更让我意识到 IM 相关功能相比于传统业务项目来说具有更多的灵活性。这种灵活性可能是因为 IM 功能通常需要处理实时性和互动性方面的需求,而这些特点也让开发者有更多的空间去创造新的功能和体验。此外,在这个过程中,我还了解到一些 SDK 的最佳实践,对 Vue3 中的组合式 API 使用也让我感受到了 Vue3 语法的灵活性,im 监听以及 api 的拆分以及仿 hook 的使用方式,有助于后续的扩展维护。
任何项目的经验都是宝贵的,但愿帮助我将来在其他的项目中更好地开发出稳定、可靠和高效的应用程序。
如果你有使用到环信 uni-app-demo,如果改写后的代码能够对你有所帮助,那么这件事情真是泰裤辣!
友情链接
环信 uni-app Demo升级改造计划——Vue2迁移到Vue3(一)
最后多说一句,如果觉得有帮助请点赞支持一下!本 demo 还有三期计划(增加音视频功能),敬请期待!