环信 uni-app-demo 升级改造计划——整体代码重构优化(二)

概述

本次关于 uni-app 代码整体重构工作,基于上一期针对 uni-app 官网 demo 从 vue2 迁移 vue3 框架衍生而来,在迁移过程中有明显感知,目前的项目存在的问题为,项目部分代码风格较为不统一,命名不够规范,注释不够清晰、可读性差、以造成如果复用困难重重,本地重构期望能够充分展示 api 在实际项目中的调用方式,尽可能达到示例代码可移植,或能辅助进行即时通讯功能二次开发的能力。

目的

  • 使代码更加可读。
  • 简化或去除冗余代码。
  • 部分组件以及逻辑重命名、重拆分、重合并。
  • 增加全局状态管理。
  • 修改 SDK 引入方式为通过 npm 形式引入
  • 收束 SDK 大部分 API 到统一文件、方便管理调用。
  • 升级 SDK api 至最新的调用方式(监听、发送消息)
  • 增加会话列表接口、消息漫游接口。
  • SDK 指环信 IM uni-app SDK

重构计划

一、修改原 WebIM 的导出导入使用方式。

目的

  1. 现有 uniSDK 已支持 npm 形式导入。
  2. 原有实例化代码与 config 配置较为混乱不够清晰。
  3. 分离初始化以及配置形成独立文件方便管理。

实现

  1. 项目目录中创建 EaseIM 文件夹并创建 index.js,在 index.js 中完成导入 SDK 并实现实例化并导出。
  2. EaseIM -> config 文件夹并将 SDK 中相关配置在此文件中书写并导出供实例化使用。
    环信 uni-app-demo 升级改造计划——整体代码重构优化(二)_第1张图片

影响(无影响)

二、引入 pinia 进行状态管理

Pinia 文档

Uni-app Pinia 文档

pinia 还能通过$reset()方法即可完成对某个 store 的初始化,利用该方法可以非常方便的在切换账号时针对缓存在 stores 中的数据初始化,防止切换后的账号与上一个账号的数据造成冲突。

目的

  1. 存放 SDK 部分数据以及状态(登录状态、会话列表数据、消息数据)
  2. 方便各组件状态或数据取用避免数据层层传递。
  3. 用以平替原有本地持久化数据存储。
  4. 可以替代原有 disp 发布订阅管理工具,因为 store 中状态改变,各组件可以进行重新计算或监听,无需通过发布订阅通知改变状态。

实现

  1. 在 mian.js 中引入 pinia,并挂载
//Pinia
import * as Pinia from 'pinia';
export function createApp() {
  const app = createSSRApp(App);
  app.use(Pinia.createPinia());
  return {
    app,
    Pinia,
  };
}
  1. 项目目录中新建 stores 并创建各个所需 store,类似目录如下:
    环信 uni-app-demo 升级改造计划——整体代码重构优化(二)_第2张图片

影响(无影响)

三、重新梳理 App.vue 根组件中代码

目的

  1. 简化项目中 App.vue 根组件中的冗长代码。
  2. 迁移根组件中的监听代码。
  3. globalData,method 中代码转为 stores 中或剔除。
  4. disp 代码剔除。

实现

  1. App.vue 中的监听嵌入至 EaseIM 文件夹下的 listener 集中管理,并在 App.vue 中重新进行挂载
  2. import '@/EaseIM';从而实现实例化 SDK。
  3. 将需要 IM 连接成功后调用的数据,合为一个方法中,并在 onConnected 触发后调用。
  4. 部分关于 SDK 调用的代码迁入至 EaseIM 文件夹下的 imApis 文件夹中
  5. 部分有关 SDK 的工具方法代码迁入至 EaseIM 文件夹下的 utils 文件夹中

影响

App.vue 改动相对较大,主要为监听的迁移,一部分方法迁移至 stores 中,并且需要重新进行监听的挂载。具体代码可在后续迁移前后比对中看到,或者文尾的看到 github 中看到代码地址。

四、优化 login 页面代码

目的

原有 login 组件登录部分代码比较冗长并且可读性较差因此进行优化。

实现

  1. 删除原有操作 input 的代码,改为通过 v-model 双向绑定。
  2. 拆分登录代码为通过 username+password,以及手机号+验证码两个登录方法。
  3. 增加登录存储 token 方法,方便后续重连时通过用户名+token 形式进行重新登录。
  4. 登录成功之后将登录的 id 以及手机号信息 set 进入到 stores 中。

影响(无影响)

五、增加 home 页面

目的

  1. 作为 Conversation、Contacts、Me 三个核心页面容器组件,方便页面切换管理。
  2. 作为 Tabbar 的容器组件

实现

  1. 去除原有会话、联系人、我的(原 setting 页面)pages.json 的路由配置,增加 home 页面路由相关配置。
  2. pages 中增加 home 组件,并以组件化的形式引入三个核心页面组件。
  3. 项目根目录中新建 layout 文件夹并增加 tabbar 组件,将三个页面中的 tabbar 功能抽离至 tabbar 组件中,并增加相应切换逻辑。

影响

此改动主要影响为要涉及到将原 setting 组件改为 Me 组件,并将三个原有页面 pages.json 删除并在 home 中引入,并无其他副作用。

六、重构 Conversation、Contacts、Me 等基础组件

目的

  1. 将原有数据(会话列表数据,联系人数据,昵称头像数据)来源切换为从 SDK 接口+stores 中获取。
  2. 去除组件内的 disp 发布订阅相关代码,以及 WebIM 的使用。
  3. 调整原组件代码中的不合理的命名,去除不再使用的方法简化该组件代码。

实现

以 Conversation 组件举例
  1. 以 SDK 接口 getConversationlist 获取会话列表数据,缓存至 stores 中并做排序处理,在组件中使用计算属性获取作为新的会话列表数据来源。
  2. 由于会话列表的更新是动态的,因此不再需要 disp 订阅一系列的事件进行处理,因此相关代码可以开始进行删除。
  3. 原有的通过会话列表跳转至联系人页面或者其他群组页面命名改为单词从 into 改为 entry 并改为驼峰命名,经过改造该组件用不到的方法则完全删除。

影响

主要影响则是这些组件内的逻辑代码会有从结构以及数据源会有较大变化,需要边改造边验证,并且会与 stores、EaseIM 等组件有较大的关系,需要耐心进行调整。

七、增加 emChatContainer 组件

目的

  1. 新增此组件命名更为语义化,能够通过组件名看出其实际功能为 emChat 聊天页组件容器。
  2. 合并原有 singleChatEntry 组件以及 groupChatEntry 组件,两个相似功能组件至统一的一个 emChatContainer 内。

实现

  1. 在 pages 下新建一个名为 emChatContainer 的组件,并先将 components 下的 chat 组件参考 singleChatEntry 组件引入,并在 pages 中配置对应路由路径映射。
  2. 观察发现该组件作为 chat 组件容器,主要向下传递两个核心参数,1)目标 ID(也就是聊天的目标环信 ID)。2)chatType(也就是目标聊天的类型,常规为单聊、群聊。),且这两个核心参数经常被 chat 组件中的各个子组件用到,一层层向下传递较为繁琐,因此使用到 Vue 组件传参方法之一的,provide、inject 方式将参数注册并向下传递下去。
  3. 完成合并之后将 singleChatEntry、groupChatEntry 删去,并且将原有用到向该组件跳转的方法路径全部指向 emChatContainer,且在 pages.json 中删除对应的页面路径。

影响

从会话进入到聊天页、从联系人、群组页面进入到聊天页的路由跳转路径全部改为 emChatContainer,并且将会改变 chat 组件使用 targetId(聊天目标 ID)以及 chatType 的方式,因为需要改为通过 inject 接收。

八、emChat 组件重构

目的

  1. 改写该组件下不合理的文件命名。
  2. 删除非必要的 js 文件或组件。
  3. 该组件内各个功能子组件进行局部代码重构。

实现

  1. 配合 emChatContainer 将 chat 组件改名为 emChat。
  2. 删除其组件内的 msgpackager.js、msgstorage.js、msgtype.js、pushStorage.js,这几个 js 文件。
  3. messagelist inputbar 改为驼峰命名。
  4. messageList 组件内的消息列表来源改为从 stores 中获取,增加下拉通过 getHistroyMessage 获取历史消息。
  5. 子组件内接收目标 id 以及消息类型改为通过 inject 接收。
  6. msgType 从 EaseIM/constant 中获取。
  7. 发送消息 API 全部改为 SDK4.xPromise 写法,在 EaseIM/imApis/emMessages 统一并导出,在需要的发送的组件中导入调用,剔除原有发送消息的方式。

影响

该组件调整难度最大,因为牵扯的组件,以及需要新增的调整的代码较多,需要逐个组件修改并验证,具体代码将在下方局部展示。详情请参看源码地址。

九、新增重连中提示监听回调

目的

能够在 IM websocket 断开的时候有相应的回调出来,并给到用户相应的提示。

实现

在 addEventHandler 监听中增加 onReconnecting 监听回调,并且在实际触发的时候增加 Toast 提示监听 IM 正在重连中。

PS:onReconnecting 属于实验性回调。

影响(无影响)

重构前后代码片段展示

一、重构前后项目目录展示

重构前项目目录结构

环信 uni-app-demo 升级改造计划——整体代码重构优化(二)_第3张图片

重构后项目目录结构

环信 uni-app-demo 升级改造计划——整体代码重构优化(二)_第4张图片

二、重构前后 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 组件代码片段展示






六、重构后新增 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 };

九、重构前后发送消息代码片段展示

重构前发送文本消息组件