乐趣区

关于前端:环信-uniappdemo-升级改造计划整体代码重构优化二

概述

本次对于 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 中相干配置在此文件中书写并导出供实例化应用。

影响(无影响)

二、引入 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,相似目录如下:

影响(无影响)

三、从新梳理 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 属于实验性回调。

影响(无影响)

重构前后代码片段展现

一、重构前后我的项目目录展现

重构前我的项目目录构造

重构后我的项目目录构造

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

<script>
import WebIM from '@/utils/WebIM.js';
import msgStorage from '@/components/chat/msgstorage';
import _chunkArr from './utils/chunkArr';
import msgType from '@/components/chat/msgtype';
import disp from '@/utils/broadcast';
import {onGetSilentConfig} from './components/chat/pushStorage';
let logout = false;

function ack(receiveMsg) {
  // 解决未读音讯回执
  var bodyId = receiveMsg.id; // 须要发送已读回执的音讯 id

  var ackMsg = new WebIM.message('read', WebIM.conn.getUniqueId());
  ackMsg.set({
    id: bodyId,
    to: receiveMsg.from,
  });
  WebIM.conn.send(ackMsg.body);
}

function onMessageError(err) {if (err.type === 'error') {
    uni.showToast({title: err.errorText,});
    return false;
  }

  return true;
}

function getCurrentRoute() {let pages = getCurrentPages();
  if (pages.length > 0) {let currentPage = pages[pages.length - 1];
    return currentPage.route;
  }
  return '/';
}

// 蕴含陌生人版本
// 该办法用以计算本地存储音讯的未读总数。function calcUnReadSpot(message) {let myName = uni.getStorageSync('myUsername');
  let pushObj = uni.getStorageSync('pushStorageData');
  let pushAry = pushObj[myName] || [];
  uni.getStorageInfo({success: function (res) {
      let storageKeys = res.keys;
      let newChatMsgKeys = [];
      let historyChatMsgKeys = [];
      storageKeys.forEach((item) => {if (item.indexOf(myName) > -1 && item.indexOf('rendered_') == -1) {newChatMsgKeys.push(item);
        }
      });
      let count = newChatMsgKeys.reduce(function (result, curMember, idx) {let newName = curMember.split(myName)[0];
        let chatMsgs;
        chatMsgs = uni.getStorageSync(curMember) || [];
        // 过滤消息来源与以后登录 ID 统一的音讯,不计入总数中。chatMsgs = chatMsgs.filter((msg) => msg.yourname !== myName);
        if (pushAry.includes(newName)) return result;
        return result + chatMsgs.length;
      }, 0);
      getApp().globalData.unReadMessageNum = count;
      disp.fire('em.unreadspot', message);
    },
  });
}

function saveGroups() {
  var me = this;
  return WebIM.conn.getGroup({
    limit: 50,
    success: function (res) {
      uni.setStorage({
        key: 'listGroup',
        data: res.data,
      });
    },
    error: function (err) {console.log(err);
    },
  });
}

export default {
  globalData: {
    phoneNumber: '',
    unReadMessageNum: 0,
    userInfo: null,
    userInfoFromServer: null, // 用户属性从环信服务器获取
    friendUserInfoMap: new Map(), // 好友属性
    saveFriendList: [],
    saveGroupInvitedList: [],
    isIPX: false, // 是否为 iphone X
    conn: {
      closed: false,
      curOpenOpt: {},

      open(opt) {
        uni.showLoading({
          title: '正在初始化客户端..',
          mask: true,
        });
        const actionOpen = () => {
          this.curOpenOpt = opt;
          WebIM.conn
            .open(opt)
            .then(() => {
              //token 获取胜利,即可开始申请用户属性。disp.fire('em.mian.profile.update');
              disp.fire('em.mian.friendProfile.update');
            })
            .catch((err) => {console.log('>>>>>token 获取失败', err);
            });
          this.closed = false;
        };
        if (WebIM.conn.isOpened()) {WebIM.conn.close();
          setTimeout(() => {actionOpen();
          }, 300);
        } else {actionOpen();
        }
      },

      reopen() {if (this.closed) {//this.open(this.curOpenOpt);
          WebIM.conn.open(this.curOpenOpt);
          this.closed = false;
        }
      },
    },
    onLoginSuccess: function (myName) {uni.hideLoading();
      uni.redirectTo({url: '../conversation/conversation?myName=' + myName,});
    },
  onLaunch() {
    var me = this;
    var logs = uni.getStorageSync('logs') || [];
    logs.unshift(Date.now());
    uni.setStorageSync('logs', logs);

    disp.on('em.main.ready', function () {calcUnReadSpot();
    });
    uni.WebIM.conn.listen({onOpened(message) {
        if (getCurrentRoute() == 'pages/login/login' ||
          getCurrentRoute() == 'pages/login_token/login_token') {
          me.globalData.onLoginSuccess(uni.getStorageSync('myUsername').toLowerCase());
          me.fetchFriendListFromServer();}
      },

      onReconnect() {
        uni.showToast({
          title: '重连中...',
          duration: 2000,
        });
      },

      onSocketConnected() {
        uni.showToast({
          title: 'socket 连贯胜利',
          duration: 2000,
        });
      },

      onClosed() {
        uni.showToast({
          title: '退出登录',
          icon: 'none',
          duration: 2000,
        });
        uni.redirectTo({url: '../login/login',});
        me.globalData.conn.closed = true;
        WebIM.conn.close();},
      onTextMessage(message) {console.log('onTextMessage', message);

        if (message) {if (onMessageError(message)) {msgStorage.saveReceiveMsg(message, msgType.TEXT);
          }

          calcUnReadSpot(message);
          ack(message);
          onGetSilentConfig(message);
        }
      },
      onPictureMessage(message) {console.log('onPictureMessage', message);

        if (message) {if (onMessageError(message)) {msgStorage.saveReceiveMsg(message, msgType.IMAGE);
          }

          calcUnReadSpot(message);
          ack(message);
          onGetSilentConfig(message);
        }
      },
    });
    this.globalData.checkIsIPhoneX();},

  methods: {async fetchUserInfoWithLoginId() {
      const userId = await uni.WebIM.conn.user;
      if (userId) {
        try {const { data} = await uni.WebIM.conn.fetchUserInfoById(userId);
          this.globalData.userInfoFromServer = Object.assign({}, data[userId]);
        } catch (error) {console.log(error);
          uni.showToast({
            title: '用户属性获取失败',
            icon: 'none',
            duration: 2000,
          });
        }
      }
    },
    async fetchFriendInfoFromServer() {let friendList = [];
      try {const res = await uni.WebIM.conn.getContacts();
        friendList = Object.assign([], res?.data);
        if (friendList.length && friendList.length < 99) {const { data} = await uni.WebIM.conn.fetchUserInfoById(friendList);
          this.setFriendUserInfotoMap(data);
        } else {let newArr = _chunkArr(friendList, 99);
          for (let i = 0; i < newArr.length; i++) {const { data} = await uni.WebIM.conn.fetchUserInfoById(newArr[i]);
            this.setFriendUserInfotoMap(data);
          }
        }
      } catch (error) {console.log(error);
        uni.showToast({
          title: '用户属性获取失败',
          icon: 'none',
        });
      }
    },
    setFriendUserInfotoMap(data) {if (Object.keys(data).length) {for (const key in data) {if (Object.hasOwnProperty.call(data, key)) {const values = data[key];
            Object.values(values).length &&
              this.globalData.friendUserInfoMap.set(key, values);
          }
        }
      }
    },
    async fetchFriendListFromServer() {uni.removeStorageSync('member');
      try {const { data} = await WebIM.conn.getContacts();
        console.log('>>>>>>App.vue 拉取好友列表');
        if (data.length) {
          uni.setStorage({
            key: 'member',
            data: [...data],
          });
        }
      } catch (error) {console.log('>>>>> 好友列表获取失败', error);
      }
    },
  },
};
</script>
<style lang="scss">
@import './app.css';
</style>

重构后 App.vue 组件代码片段

能够看到比原有 App.vue 组件有显著的代码简化。

<script>
/* EaseIM */
import '@/EaseIM';
import {emConnectListener, emMountGlobalListener} from '@/EaseIM/listener';
import {emUserInfos, emGroups, emContacts} from '@/EaseIM/imApis';
import {CONNECT_CALLBACK_TYPE} from '@/EaseIM/constant';
import {useLoginStore} from '@/stores/login';
import {useGroupStore} from '@/stores/group';
import {useContactsStore} from '@/stores/contacts';
import {EMClient} from './EaseIM';

export default {setup() {const loginStore = useLoginStore();
    const groupStore = useGroupStore();
    const contactsStore = useContactsStore();
    /* 链接所需监听回调 */
    // 传给监听 callback 回调
    const connectedCallback = (type) => {console.log('>>>>> 连贯胜利回调', type);
      if (type === CONNECT_CALLBACK_TYPE.CONNECT_CALLBACK) {onConnectedSuccess();
      }
      if (type === CONNECT_CALLBACK_TYPE.DISCONNECT_CALLBACK) {onDisconnect();
      }
      if (type === CONNECT_CALLBACK_TYPE.RECONNECTING_CALLBACK) {onReconnecting();
      }
    };
    //IM 连贯胜利
    const onConnectedSuccess = () => {
      const loginUserId = loginStore.loginUserBaseInfos.loginUserId;
      if (!loginStore.loginStatus) {fetchLoginUserNeedData();
      }
      loginStore.setLoginStatus(true);
      uni.hideLoading();
      uni.redirectTo({url: '../home/index?myName=' + loginUserId,});
    };
    //IM 断开连接
    const onDisconnect = () => {
      // 断开回调触发后,如果业务登录状态为 true 则阐明异样断开须要从新登录
      if (!loginStore.loginStatus) {
        uni.showToast({
          title: '退出登录',
          icon: 'none',
          duration: 2000,
        });
        uni.redirectTo({url: '../login/login',});
        EMClient.close();} else {
        // 执行通过 token 机型从新登录
        const loginUserId = uni.getStorageSync('myUsername');
        const loginUserToken =
          loginUserId && uni.getStorageSync(`EM_${loginUserId}_TOKEN`);
        EMClient.open({user: loginUserId, accessToken: loginUserToken.token});
      }
    };
    //IM 重连中
    const onReconnecting = () => {
      uni.showToast({
        title: 'IM 重连中...',
        icon: 'none',
      });
    };
    // 挂载 IM websocket 连贯胜利监听
    emConnectListener(connectedCallback);
    const {fetchUserInfoWithLoginId, fetchOtherInfoFromServer} =
      emUserInfos();
    const {fetchJoinedGroupListFromServer} = emGroups();
    const {fetchContactsListFromServer} = emContacts();
    // 获取登录所需根底参数
    const fetchLoginUserNeedData = async () => {
      // 获取好友列表
      const friendList = await fetchContactsListFromServer();
      await contactsStore.setFriendList(friendList);
      // 获取群组列表
      const joinedGroupList = await fetchJoinedGroupListFromServer();
      joinedGroupList.length &&
        (await groupStore.setJoinedGroupList(joinedGroupList));
      if (friendList.length) {
        // 获取好友用户属性
        const friendProfiles = await fetchOtherInfoFromServer(friendList);
        contactsStore.setFriendUserInfotoMap(friendProfiles);
      }
      // 获取以后登录用户好友信息
      const profiles = await fetchUserInfoWithLoginId();
      await loginStore.setLoginUserProfiles(profiles[EMClient.user]);
    };
    // 挂载全局所需监听回调【好友关系、音讯监听、群组监听】emMountGlobalListener();},
};
</script>
<style lang="scss">
@import './app.css';
</style>

四、重构前后会话列表(conversation)组件代码片段比照

重构前会话列表代码片段展现

PS:template 中代码变动不大,为缩减长度临时省去 template 相干代码

<script setup>
import {reactive, computed} from 'vue';
import {onLoad, onShow, onUnload} from '@dcloudio/uni-app';
import swipeDelete from '@/components/swipedelete/swipedelete';
import msgtype from '@/components/chat/msgtype';
import dateFormater from '@/utils/dateFormater';
import disp from '@/utils/broadcast';
const WebIM = uni.WebIM;
let isfirstTime = true;
const conversationState = reactive({
  //       msgtype,
  search_btn: true,
  search_chats: false,
  show_mask: false,
  yourname: '',
  unReadSpotNum: 0,
  unReadNoticeNum: 0,
  messageNum: 0,
  unReadTotalNotNum: 0,
  conversationList: [],
  show_clear: false,
  member: '',
  isIPX: false,
  gotop: false,
  input_code: '',
  groupName: {},
  winSize: {},
  popButton: ['删除该聊天'],
  showPop: false,
  currentVal: '',
  pushConfigData: [],
  defaultAvatar: '/static/images/theme2x.png',
  defaultGroupAvatar: '/static/images/groupTheme.png',
});
onLoad(() => {disp.on('em.subscribe', onChatPageSubscribe);
  // 监听遣散群
  disp.on('em.invite.deleteGroup', onChatPageDeleteGroup);
  // 监听未读音讯数
  disp.on('em.unreadspot', onChatPageUnreadspot);
  // 监听未读加群“告诉”disp.on('em.invite.joingroup', onChatPageJoingroup);
  // 监听好友删除
  disp.on('em.contacts.remove', onChatPageRemoveContacts);
  // 监听好友关系解除
  disp.on('em.unsubscribed', onChatPageUnsubscribed);
  if (!uni.getStorageSync('listGroup')) {listGroups();
  }
  if (!uni.getStorageSync('member')) {getRoster();
  }

  readJoinedGroupName();});
onShow(() => {uni.hideHomeButton && uni.hideHomeButton();
  setTimeout(() => {getLocalConversationlist();
  }, 100);
  conversationState.unReadMessageNum =
    getApp().globalData.unReadMessageNum > 99
      ? '99+'
      : getApp().globalData.unReadMessageNum;
  conversationState.messageNum = getApp().globalData.saveFriendList.length;
  conversationState.unReadNoticeNum =
    getApp().globalData.saveGroupInvitedList.length;
  conversationState.unReadTotalNotNum =
    getApp().globalData.saveFriendList.length +
    getApp().globalData.saveGroupInvitedList.length;
  if (getApp().globalData.isIPX) {conversationState.isIPX = true;}
});
const showConversationAvatar = computed(() => {const friendUserInfoMap = getApp().globalData.friendUserInfoMap;
  return (item) => {if (item.chatType === 'singleChat' || item.chatType === 'chat') {
      if (friendUserInfoMap.has(item.username) &&
        friendUserInfoMap.get(item.username)?.avatarurl
      ) {return friendUserInfoMap.get(item.username).avatarurl;
      } else {return conversationState.defaultAvatar;}
    } else if (item.chatType.toLowerCase() === 'groupchat' ||
      item.chatType === 'chatRoom'
    ) {return conversationState.defaultGroupAvatar;}
  };
});
const showConversationName = computed(() => {const friendUserInfoMap = getApp().globalData.friendUserInfoMap;
  return (item) => {if (item.chatType === 'singleChat' || item.chatType === 'chat') {
      if (friendUserInfoMap.has(item.username) &&
        friendUserInfoMap.get(item.username)?.nickname
      ) {return friendUserInfoMap.get(item.username).nickname;
      } else {return item.username;}
    } else if (
      item.chatType === msgtype.chatType.GROUP_CHAT ||
      item.chatType === msgtype.chatType.CHAT_ROOM ||
      item.chatType === 'groupchat'
    ) {return item.groupName;}
  };
});
const handleTime = computed(() => {return (item) => {return dateFormater('MM/DD/HH:mm', item.time);
  };
});

const listGroups = () => {
  return uni.WebIM.conn.getGroup({
    limit: 50,
    success: function (res) {
      uni.setStorage({
        key: 'listGroup',
        data: res.data,
      });
      readJoinedGroupName();
      getLocalConversationlist();},
    error: function (err) {console.log(err);
    },
  });
};

const getRoster = async () => {const { data} = await WebIM.conn.getContacts();
  if (data.length) {
    uni.setStorage({
      key: 'member',
      data: [...data],
    });
    conversationState.member = [...data];
    //if(!systemReady){disp.fire('em.main.ready');
    //systemReady = true;
    //}
    getLocalConversationlist();
    conversationState.unReadSpotNum =
      getApp().globalData.unReadMessageNum > 99
        ? '99+'
        : getApp().globalData.unReadMessageNum;}
  console.log('>>>> 好友列表获取胜利', data);
};

const readJoinedGroupName = () => {const joinedGroupList = uni.getStorageSync('listGroup');
  const groupList = joinedGroupList?.data || joinedGroupList || [];
  let groupName = {};
  groupList.forEach((item) => {groupName[item.groupid] = item.groupname;
  });
  conversationState.groupName = groupName;
};
// 蕴含陌生人版本
const getLocalConversationlist = () => {const myName = uni.getStorageSync('myUsername');
  uni.getStorageInfo({success: (res) => {
      let storageKeys = res.keys;
      let newChatMsgKeys = [];
      let historyChatMsgKeys = [];
      let len = myName.length;
      storageKeys.forEach((item) => {if (item.slice(-len) == myName && item.indexOf('rendered_') == -1) {newChatMsgKeys.push(item);
        } else if (item.slice(-len) == myName &&
          item.indexOf('rendered_') > -1
        ) {historyChatMsgKeys.push(item);
        } else if (item === 'INFORM') {newChatMsgKeys.push(item);
        }
      });
      packageConversation(newChatMsgKeys, historyChatMsgKeys);
    },
  });
};
// 组件会话列表办法
const packageConversation = (newChatMsgKeys, historyChatMsgKeys) => {const myName = uni.getStorageSync('myUsername');
  let conversationList = [];
  let lastChatMsg; // 最初一条音讯
  for (let i = historyChatMsgKeys.length; i > 0, i--;) {let index = newChatMsgKeys.indexOf(historyChatMsgKeys[i].slice(9));
    if (index > -1) {let newChatMsgs = uni.getStorageSync(newChatMsgKeys[index]) || [];
      if (newChatMsgs.length) {lastChatMsg = newChatMsgs[newChatMsgs.length - 1];
        lastChatMsg.unReadCount = newChatMsgs.length;
        newChatMsgKeys.splice(index, 1);
      } else {let historyChatMsgs = uni.getStorageSync(historyChatMsgKeys[i]);
        if (historyChatMsgs.length) {lastChatMsg = historyChatMsgs[historyChatMsgs.length - 1];
        }
      }
    } else {let historyChatMsgs = uni.getStorageSync(historyChatMsgKeys[i]);
      if (historyChatMsgs.length) {lastChatMsg = historyChatMsgs[historyChatMsgs.length - 1];
      }
    }
    if (
      lastChatMsg.chatType == msgtype.chatType.GROUP_CHAT ||
      lastChatMsg.chatType == msgtype.chatType.CHAT_ROOM ||
      lastChatMsg.chatType == 'groupchat'
    ) {lastChatMsg.groupName = conversationState.groupName[lastChatMsg.info.to];
    }
    lastChatMsg &&
      lastChatMsg.username != myName &&
      conversationList.push(lastChatMsg);
  }
  for (let i = newChatMsgKeys.length; i > 0, i--;) {let newChatMsgs = uni.getStorageSync(newChatMsgKeys[i]) || [];
    if (newChatMsgs.length) {lastChatMsg = newChatMsgs[newChatMsgs.length - 1];
      lastChatMsg.unReadCount = newChatMsgs.length;
      if (
        lastChatMsg.chatType == msgtype.chatType.GROUP_CHAT ||
        lastChatMsg.chatType == msgtype.chatType.CHAT_ROOM ||
        lastChatMsg.chatType == 'groupchat'
      ) {
        lastChatMsg.groupName =
          conversationState.groupName[lastChatMsg.info.to];
      }
      lastChatMsg.username != myName && conversationList.push(lastChatMsg);
    }
  }
  conversationList.sort((a, b) => {return b.time - a.time;});
  console.log('>>>>>>conversationList', conversationList);
  conversationState.conversationList = conversationList;
};
const openSearch = () => {
  conversationState.search_btn = false;
  conversationState.search_chats = true;
  conversationState.gotop = true;
};
const onSearch = (val) => {
  let searchValue = val.detail.value;
  var myName = uni.getStorageSync('myUsername');
  let serchList = [];
  let conversationList = [];
  uni.getStorageInfo({success: function (res) {
      let storageKeys = res.keys;
      let chatKeys = [];
      let len = myName.length;
      storageKeys.forEach((item) => {if (item.slice(-len) == myName) {chatKeys.push(item);
        }
      });
      chatKeys.forEach((item, index) => {if (item.indexOf(searchValue) != -1) {serchList.push(item);
        }
      });
      let lastChatMsg = '';
      serchList.forEach((item, index) => {let chatMsgs = uni.getStorageSync(item) || [];
        if (chatMsgs.length) {lastChatMsg = chatMsgs[chatMsgs.length - 1];
          conversationList.push(lastChatMsg);
        }
      });
      conversationState.conversationList = conversationList;
    },
  });
};
const cancel = () => {getLocalConversationlist();
  conversationState.search_btn = true;
  conversationState.search_chats = false;
  conversationState.unReadSpotNum =
    getApp().globalData.unReadMessageNum > 99
      ? '99+'
      : getApp().globalData.unReadMessageNum;
  conversationState.gotop = false;
};
const clearInput = () => {
  conversationState.input_code = '';
  conversationState.show_clear = false;
};
const onInput = (e) => {
  let inputValue = e.detail.value;
  if (inputValue) {conversationState.show_clear = true;} else {conversationState.show_clear = false;}
};
const tab_contacts = () => {
  uni.redirectTo({url: '../main/main?myName=' + uni.getStorageSync('myUsername'),
  });
};
const close_mask = () => {
  conversationState.search_btn = true;
  conversationState.search_chats = false;
  conversationState.show_mask = false;
};
const tab_setting = () => {
  uni.redirectTo({url: '../setting/setting',});
};
const tab_notification = () => {
  uni.redirectTo({url: '../notification/notification',});
};
const into_chatRoom = (event) => {let detail = JSON.parse(event.currentTarget.dataset.item);
  if (
    detail.chatType == msgtype.chatType.GROUP_CHAT ||
    detail.chatType == msgtype.chatType.CHAT_ROOM ||
    detail.groupName
  ) {into_groupChatRoom(detail);
  } else {into_singleChatRoom(detail);
  }
};
// 单聊
const into_singleChatRoom = (detail) => {var myName = uni.getStorageSync('myUsername');
  var nameList = {
    myName: myName,
    your: detail.username,
  };
  const friendUserInfoMap = getApp().globalData.friendUserInfoMap;
  if (friendUserInfoMap.has(nameList.your) &&
    friendUserInfoMap.get(nameList.your)?.nickname
  ) {nameList.yourNickName = friendUserInfoMap.get(nameList.your).nickname;
  }
  uni.navigateTo({
    url:
      '../singleChatEntry/singleChatEntry?username=' + JSON.stringify(nameList),
  });
};
// 群聊 和 聊天室(两个概念)const into_groupChatRoom = (detail) => {var myName = uni.getStorageSync('myUsername');
  var nameList = {
    myName: myName,
    your: detail.groupName,
    groupId: detail.info.to,
  };
  uni.navigateTo({
    url:
      '../groupChatEntry/groupChatEntry?username=' + JSON.stringify(nameList),
  });
};
const into_inform = () => {
  uni.redirectTo({url: '../notification/notification',});
};

const removeAndRefresh = (event) => {
  let removeId = event.currentTarget.dataset.item.info.from;
  let ary = getApp().globalData.saveFriendList;
  let idx;
  if (ary.length > 0) {ary.forEach((v, k) => {if (v.from == removeId) {idx = k;}
    });
    getApp().globalData.saveFriendList.splice(idx, 1);
  }
  uni.removeStorageSync('INFORM');
};

const del_chat = (event) => {
  let detail = event.currentTarget.dataset.item;
  let nameList = {};
  // 删除以后选中群组聊天列表
  if (detail.chatType == 'groupchat' || detail.chatType == 'chatRoom') {
    nameList = {your: detail.info.to,};
    // 删除以后选中告诉列表
  } else if (detail.chatType === 'INFORM') {
    nameList = {your: 'INFORM',};
  }
  // 删除以后选中好友聊天列表
  else {
    nameList = {your: detail.username,};
  }
  var myName = uni.getStorageSync('myUsername');
  var currentPage = getCurrentPages();

  uni.showModal({
    title: '确认删除?',
    confirmText: '删除',
    success: function (res) {if (res.confirm) {uni.removeStorageSync(nameList.your + myName);
        uni.removeStorageSync('rendered_' + nameList.your + myName);
        nameList.your === 'INFORM' && removeAndRefresh(event);
        // if (Object.keys(currentPage[0]).length>0) {//   currentPage[0].onShow();
        // }
        disp.fire('em.chat.session.remove');
        getLocalConversationlist();}
    },
    fail: function (err) {console.log('删除列表', err);
    },
  });
};
const removeLocalStorage = (yourname) => {var myName = uni.getStorageSync('myUsername');
  uni.removeStorageSync(yourname + myName);
  uni.removeStorageSync('rendered_' + yourname + myName);
};

/* 获取窗口尺寸 */
const getWindowSize = () => {
  uni.getSystemInfo({success: (res) => {
      conversationState.winSize = {
        witdh: res.windowWidth,
        height: res.windowHeight,
      };
    },
  });
};
const hidePop = () => {conversationState.showPop = false;};
const pickerMenuChange = () => {del_chat(conversationState.currentVal);
};

/*  disp event callback function */
const onChatPageSubscribe = () => {getLocalConversationlist();
  conversationState.messageNum = getApp().globalData.saveFriendList.length;
  conversationState.unReadTotalNotNum =
    getApp().globalData.saveFriendList.length +
    getApp().globalData.saveGroupInvitedList.length;};
const onChatPageDeleteGroup = (infos) => {listGroups();
  getRoster();
  getLocalConversationlist();
  conversationState.messageNum = getApp().globalData.saveFriendList.length;
  // 如果会话存在则执行删除会话
  removeLocalStorage(infos.gid);
};
const onChatPageUnreadspot = (message) => {getLocalConversationlist();
  let currentLoginUser = WebIM.conn.context.userId;
  let id =
    message && message.chatType === 'groupchat' ? message?.to : message?.from;
  let pushObj = uni.getStorageSync('pushStorageData');
  let pushAry = pushObj[currentLoginUser] || [];
  conversationState.pushConfigData = pushAry;

  // if (message && pushValue.includes(id)) return
  conversationState.unReadSpotNum =
    getApp().globalData.unReadMessageNum > 99
      ? '99+'
      : getApp().globalData.unReadMessageNum;};
const onChatPageJoingroup = () => {
  conversationState.unReadMessageNum =
    getApp().globalData.saveGroupInvitedList.length;
  conversationState.unReadTotalNotNum =
    getApp().globalData.saveFriendList.length +
    getApp().globalData.saveGroupInvitedList.length;
  getLocalConversationlist();};
const onChatPageRemoveContacts = () => {getLocalConversationlist();
  getRoster();};
const onChatPageUnsubscribed = (message) => {
  uni.showToast({title: ` 与 ${message.from}好友关系解除 `,
    icon: 'none',
  });
};
onUnload(() => {
  // 页面卸载同步勾销 onload 中的订阅,避免反复订阅事件。disp.off('em.subscribe', conversationState.onChatPageSubscribe);
  disp.off('em.invite.deleteGroup', conversationState.onChatPageDeleteGroup);
  disp.off('em.unreadspot', conversationState.onChatPageUnreadspot);
  disp.off('em.invite.joingroup', conversationState.onChatPageJoingroup);
  disp.off('em.contacts.remove', conversationState.onChatPageRemoveContacts);
  disp.off('em.unsubscribed', conversationState.onChatPageUnsubscribed);
});
</script>
<style>
@import './conversation.css';
</style>

重构后会话列表代码片段展现

<script setup>
import {reactive, computed, watch, watchEffect} from 'vue';
import {onLoad, onShow} from '@dcloudio/uni-app';
import swipeDelete from '@/components/swipedelete/swipedelete';
import {emConversation} from '@/EaseIM/imApis';
import {CHAT_TYPE, MESSAGE_TYPE} from '@/EaseIM/constant';
import {useConversationStore} from '@/stores/conversation';
import {useContactsStore} from '@/stores/contacts';
import {useGroupStore} from '@/stores/group';
import dateFormater from '@/utils/dateFormater';
/* store */
import {useInformStore} from '@/stores/inform';
const conversationState = reactive({
  search_btn: true,
  search_chats: false,
  search_keyword: '',
  show_mask: false,
  yourname: '',
  unReadSpotNum: 0,
  unReadNoticeNum: 0,
  messageNum: 0,
  unReadTotalNotNum: 0,
  conversationList: [], // 搜寻后返回的会话数据,
  show_clear: false,
  member: '',
  isIPX: false,
  gotop: false,
  groupName: {},
  winSize: {},
  popButton: ['删除该聊天'],
  showPop: false,
  currentVal: '',
  pushConfigData: [],
  defaultAvatar: '/static/images/theme2x.png',
  defaultGroupAvatar: '/static/images/groupTheme.png',
});
// 群组名称
const groupStore = useGroupStore();
const getGroupName = (groupid) => {
  const joinedGroupList = groupStore.joinedGroupList;
  let groupName = '';
  if (joinedGroupList.length) {joinedGroupList.forEach((item) => {if (item.groupid === groupid) {console.log(item.groupname);
        return (groupName = item.groupname);
      }
    });
    return groupName;
  } else {return groupid;}
};
/* 零碎告诉 */
const informStore = useInformStore();
// 最近一条零碎告诉
const lastInformData = computed(() => {
  return (informStore.getAllInformsList[informStore.getAllInformsList.length - 1] ||
    null
  );
});
// 未解决零碎告诉总数
const unReadNoticeNum = computed(() => {return informStore.getAllInformsList.filter((inform) => !inform.isHandled)
    .length;
});
/* 会话列表 */
const conversationStore = useConversationStore();
const {
  fetchConversationFromServer,
  removeConversationFromServer,
  sendChannelAck,
} = emConversation();
const fetchConversationList = async () => {const res = await fetchConversationFromServer();
  if (res?.data?.channel_infos) {
    conversationStore.setConversationList(Object.assign([], res.data.channel_infos)
    );
  }
};
// 会话列表数据
const conversationList = computed(() => {return conversationStore.sortedConversationList;});
watchEffect(() => {console.log('>>>>> 执行更新会话列表数据');
  conversationState.conversationList = Object.assign([],
    conversationList.value
  );
});
// 会话列表 name& 头像展现解决
const contactsStore = useContactsStore();
// 好友属性
const friendUserInfoMap = computed(() => {return contactsStore.friendUserInfoMap;});
// 会话列表头像
const showConversationAvatar = computed(() => {return (item) => {switch (item.chatType) {
      case CHAT_TYPE.SINGLE_CHAT:
        const friendInfo = friendUserInfoMap.value.get(item.channel_id) || {};
        return friendInfo.avatarurl ?? conversationState.defaultAvatar;
      case CHAT_TYPE.GROUP_CHAT:
        return conversationState.defaultGroupAvatar;
      default:
        return null;
    }
  };
});
// 会话列表名称
const showConversationName = computed(() => {return (item) => {switch (item.chatType) {
      case CHAT_TYPE.SINGLE_CHAT:
        const friendInfo = friendUserInfoMap.value.get(item.channel_id);
        return friendInfo?.nickname || item.channel_id;
      case CHAT_TYPE.GROUP_CHAT:
        return getGroupName(item.channel_id);
      default:
        return null;
    }
  };
});
// 工夫展现
const handleTime = computed(() => {return (item) => {return dateFormater('MM/DD/HH:mm', item.time);
  };
});
// 删除会话
const deleteConversation = async (eventItem) => {const { channel_id, chatType} = eventItem;
  try {
    const res = await uni.showModal({
      title: '确认删除?',
      confirmText: '删除',
    });
    if (res.confirm) {await removeConversationFromServer(channel_id, chatType);
      conversationStore.deleteConversation(channel_id);
    }
  } catch (error) {
    uni.showToast({
      title: '删除失败',
      icon: 'none',
      duration: 2000,
    });
    console.log('删除失败', error);
  }
};

/* 搜寻会话相干逻辑 */
// 开启搜寻模式
const openSearch = () => {
  conversationState.search_btn = false;
  conversationState.search_chats = true;
  conversationState.gotop = true;
};
// 执行搜寻办法
const actionSearch = () => {
  const keyWord = conversationState.search_keyword;
  let resConversationList = [];
  if (keyWord) {resConversationList = conversationStore.conversationList.filter((item) => {if (item.chatType === CHAT_TYPE.SINGLE_CHAT || item.chatType === 'chat') {
        if (friendUserInfoMap.value.has(item.channel_id) &&
          friendUserInfoMap.value.get(item.channel_id)?.nickname
        ) {
          return (item.lastMessage.msg?.indexOf(keyWord) > -1 ||
            item.channel_id?.indexOf(keyWord) > -1 ||
            friendUserInfoMap.value
              .get(item.channel_id)
              .nickname?.indexOf(keyWord) > -1
          );
        } else {
          return (item.lastMessage.msg?.indexOf(keyWord) > -1 ||
            item.channel_id?.indexOf(keyWord) > -1
          );
        }
      }
      if (
        item.chatType === CHAT_TYPE.GROUP_CHAT ||
        item.chatType === 'groupchat'
      ) {
        return (item.channel_id.indexOf(keyWord) > -1 ||
          getGroupName(item.channel_id).indexOf(keyWord) > -1 ||
          item.lastMessage.msg.indexOf(keyWord) > -1
        );
      }
    });
  }
  console.log('>>>>> 执行搜寻', resConversationList);
  conversationState.conversationList = resConversationList;
};
// 勾销搜寻
const cancelSearch = () => {
  conversationState.search_btn = true;
  conversationState.search_chats = false;
  conversationState.gotop = false;
  conversationState.conversationList = conversationList.value;
};
// 清空搜索框
const clearSearchInput = () => {
  conversationState.search_keyword = '';
  conversationState.show_clear = false;
};
// 输入框事件触发
const onInput = (e) => {
  let inputValue = e.detail.value;
  if (inputValue) {conversationState.show_clear = true;} else {cancelSearch();
  }
};
const close_mask = () => {
  conversationState.search_btn = true;
  conversationState.search_chats = false;
  conversationState.show_mask = false;
};

/* 获取窗口尺寸 */
const getWindowSize = () => {
  uni.getSystemInfo({success: (res) => {
      conversationState.winSize = {
        witdh: res.windowWidth,
        height: res.windowHeight,
      };
    },
  });
};
const hidePop = () => {conversationState.showPop = false;};
const entryInform = () => {
  uni.navigateTo({url: '../notification/notification',});
};
const entryemChat = (params) => {console.log('params', params);
  // 发送 channelack 革除服务端该会话未读数,并且革除本地未读红点
  sendChannelAck(params.channel_id, params.chatType);
  conversationStore.clearConversationUnReadNum(params.channel_id);
  uni.navigateTo({url: `../emChatContainer/index?targetId=${params.channel_id}&chatType=${params.chatType}`,
  });
};
onLoad(() => {if (!conversationList.value.length) {fetchConversationList();
  }
});
onShow(() => {uni.hideHomeButton && uni.hideHomeButton();
});
</script>
<style>
@import './conversation.css';
</style>

五、重构后新增 Tabbar 组件代码片段展现

<template>
  <view :class="isIPX ?'chatRoom_tab_X':'chatRoom_tab'">
    <view class="tableBar" @click="changeTab('conversation')">
      <view
        v-if="unReadSpotNum > 0 || unReadSpotNum =='99+'":class="
          'em-unread-spot' +
          (unReadSpotNum == '99+' ? 'em-unread-spot-litleFont' : '')"
        >{{unReadSpotNum + unReadTotalNotNum}}</view
      >
      <image
        :class="unReadSpotNum > 0 || unReadSpotNum =='99+'?'haveSpot':''"
        :src="tabType ==='conversation'
            ? highlightConversationImg
            : conversationImg
        "
      ></image>
      <text :class="tabType ==='conversation'&&'activeText'"> 会话 </text>
    </view>

    <view class="tableBar" @click="changeTab('contacts')">
      <image
        :src="tabType ==='contacts'? highlightContactsImg : contactsImg"
      ></image>
      <text :class="tabType ==='contacts'&&'activeText'"> 联系人 </text>
    </view>

    <view class="tableBar" @click="changeTab('me')">
      <image :src="tabType ==='me'? highlightSettingImg : settingImg"></image>
      <text :class="tabType ==='me'&&'activeText'"> 我的 </text>
    </view>
  </view>
</template>

<script setup>
import {ref, toRefs} from 'vue';
/* images */
const conversationImg = '/static/images/session2x.png';
const highlightConversationImg = '/static/images/sessionhighlight2x.png';
const contactsImg = '/static/images/comtacts2x.png';
const highlightContactsImg = '/static/images/comtactshighlight2x.png';
const settingImg = '/static/images/setting2x.png';
const highlightSettingImg = '/static/images/settinghighlight2x.png';
/* props */
const props = defineProps({
  tabType: {
    type: String,
    default: 'conversation',
    required: true,
  },
});
/* emits */
const emits = defineEmits(['switchHomeComponent']);
const {tabType} = toRefs(props);
const isIPX = ref(false);
const unReadSpotNum = ref(0);
const unReadTotalNotNum = ref(0);

const changeTab = (type) => {emits('switchHomeComponent', type);
};
</script>

<style scoped>
@import './index.css';
</style>

六、重构后新增 home 组件代码片段展现

没有应用 Vue 中的动静组件(component)实现是因为 uni-app 打包到某些平台不反对。

<template>
  <view>
    <template v-if="isActiveComps ==='conversation'">
      <Conversation />
    </template>
    <template v-if="isActiveComps ==='contacts'">
      <Contacts />
    </template>
    <template v-if="isActiveComps ==='me'">
      <Me />
    </template>
    <Tabbar
      :tab-type="isActiveComps"
      @switchHomeComponent="switchHomeComponent"
    />
  </view>
</template>

<script setup>
import {ref, watchEffect} from 'vue';
import {onLoad} from '@dcloudio/uni-app';
/* components */
import Tabbar from '@/layout/tabbar';
import Conversation from '@/pages/conversation/conversation.vue';
import Contacts from '@/pages/contacts/contacts.vue';
import Me from '@/pages/me/me.vue';
const isActiveComps = ref('conversation');

const switchHomeComponent = (type) => {isActiveComps.value = type;};
/* 设置以后题目 */
const titleMap = {
  conversation: '会话列表',
  contacts: '联系人',
  me: '我的',
};
watchEffect(() => {
  uni.setNavigationBarTitle({title: titleMap[isActiveComps.value],
  });
});
onLoad((options) => {
  // 通过路由传参的模式可指定该页面展现某个指定组件
  if (options.page) {switchHomeComponent(options.page);
  }
});
</script>

七、重构后新增 emChatContainer 组件代码展现

<template>
  <div>
    <em-chat />
  </div>
</template>

<script setup>
import {toRefs, reactive, provide, readonly, computed} from 'vue';
import EmChat from '@/components/emChat';
import {onNavigationBarButtonTap} from '@dcloudio/uni-app';
import {useContactsStore} from '@/stores/contacts';
import {useGroupStore} from '@/stores/group';
import {CHAT_TYPE} from '@/EaseIM/constant';
const props = defineProps({
  targetId: {
    type: String,
    value: '',
    required: true,
  },
  chatType: {
    type: String,
    value: '',
    required: true,
  },
});

const {targetId, chatType} = toRefs(reactive(props));
console.log(targetId, chatType);
provide('targetId', readonly(targetId));
provide('chatType', readonly(chatType));

/* 解决 NavigationBarTitle 展现 */
// 群组名称
const groupStore = useGroupStore();
const getGroupName = (groupid) => {
  const joinedGroupList = groupStore.joinedGroupList;
  let groupName = '';
  if (joinedGroupList.length) {joinedGroupList.forEach((item) => {if (item.groupid === groupid) {console.log(item.groupname);
        return (groupName = item.groupname);
      }
    });
    return groupName;
  } else {return groupid;}
};
const contactsStore = useContactsStore();
// 好友属性
const friendUserInfoMap = computed(() => {return contactsStore.friendUserInfoMap;});
// 会话列表名称
const getTheIdName = (chatType, targetId) => {switch (chatType) {
    case CHAT_TYPE.SINGLE_CHAT:
      const friendInfo = friendUserInfoMap.value.get(targetId);
      return friendInfo?.nickname || targetId;
    case CHAT_TYPE.GROUP_CHAT:
      return getGroupName(targetId);
    default:
      return null;
  }
};
uni.setNavigationBarTitle({title: getTheIdName(chatType.value, targetId.value),
});

onNavigationBarButtonTap(() => {
  uni.navigateTo({url: `/pages/moreMenu/moreMenu?username=${targetId.value}&type=${chatType.value}`,
  });
});
</script>

<style scoped>
@import './index.css';
</style>

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

九、重构前后发送音讯代码片段展现

重构前发送文本音讯组件

<template>
  <!-- <chat-suit-emoji id="chat-suit-emoji" bind:newEmojiStr="emojiAction"></chat-suit-emoji> -->
  <form class="text-input">
    <view :class="mainState.isIPX ?'f-row-x':'f-row'">
      <!-- 发送语音 -->
      <view>
        <image
          class="icon-mic"
          src="/static/images/voice.png"
          @tap="openRecordModal"
        ></image>
      </view>
      <!-- 输入框 -->
      <textarea
        class="f news"
        type="text"
        cursor-spacing="65"
        confirm-type="done"
        v-model.trim="mainState.inputMessage"
        @confirm="sendMessage"
        @input="bindMessage"
        @tap="focus"
        @focus="focus"
        @blur="blur"
        :confirm-hold="mainState.isIPX ? true : false"
        auto-height
        :show-confirm-bar="false"
        maxlength="300"
      />
      <view>
        <image
          class="icon-mic"
          src="/static/images/Emoji.png"
          @tap="openEmoji"
        ></image>
      </view>
      <view v-show="!mainState.inputMessage" @tap="openFunModal">
        <image class="icon-mic" src="/static/images/ad.png"></image>
      </view>
      <button
        class="send-btn-style"
        hover-class="hover"
        @tap="sendMessage"
        v-show="mainState.inputMessage"
      >
        发送
      </button>
    </view>
  </form>
</template>

<script setup>
import {reactive, toRefs} from 'vue';
import msgType from '@/components/chat/msgtype';
import msgStorage from '@/components/chat/msgstorage';
import disp from '@/utils/broadcast';
const WebIM = uni.WebIM;
/* props */
const props = defineProps({
  chatParams: {
    type: Object,
    default: () => ({}),
  },
  chatType: {
    type: String,
    default: msgType.chatType.SINGLE_CHAT,
  },
});
const {chatParams, chatType} = toRefs(props);
/* emits */
const $emits = defineEmits([
  'inputFocused',
  'inputBlured',
  'closeFunModal',
  'closeFunModal',
  'openEmoji',
  'openRecordModal',
  'openFunModal',
]);
const mainState = reactive({
  inputMessage: '',
  // render input 的值
  userMessage: '', // input 的实时值
  isIPX: false,
});

mainState.isIPX = getApp().globalData.isIPX;
const focus = () => {
  $emits('inputFocused', null, {bubbles: true,});
};
const blur = () => {
  $emits('inputBlured', null, {bubbles: true,});
};
const isGroupChat = () => {return chatType.value == msgType.chatType.CHAT_ROOM;};

const getSendToParam = () => {console.log('chatParmas', chatParams);
  return isGroupChat() ? chatParams.value.groupId : chatParams.value.your;};

const bindMessage = (e) => {mainState.userMessage = e.detail.value;};
const emojiAction = (emoji) => {
  let str;
  let msglen = mainState.userMessage.length - 1;

  if (emoji && emoji != '[del]') {str = mainState.userMessage + emoji;} else if (emoji == '[del]') {let start = mainState.userMessage.lastIndexOf('[');
    let end = mainState.userMessage.lastIndexOf(']');
    let len = end - start;

    if (end != -1 && end == msglen && len >= 3 && len <= 4) {str = mainState.userMessage.slice(0, start);
    } else {str = mainState.userMessage.slice(0, msglen);
    }
  }
  mainState.userMessage = str;
  mainState.inputMessage = str;
};
const sendMessage = () => {if (mainState.userMessage.match(/^\s*$/)) return;
  let id = WebIM.conn.getUniqueId();
  let msg = new WebIM.message(msgType.TEXT, id);
  msg.set({
    msg: mainState.userMessage,
    from: WebIM.conn.user,
    to: getSendToParam(),
    // roomType: false,
    chatType: isGroupChat()
      ? msgType.chatType.GROUP_CHAT
      : msgType.chatType.SINGLE_CHAT,
    success(id, serverMsgId) {console.log('胜利了');
      // 敞开表情弹窗
      $emits.cancelEmoji && $emits.cancelEmoji();
      $emits.closeFunModal && $emits.closeFunModal();
      disp.fire('em.chat.sendSuccess', id, mainState.userMessage);
    },
    fail(id, serverMsgId) {console.log('失败了');
    },
  });

  WebIM.conn.send(msg.body);
  let obj = {
    msg: msg,
    type: msgType.TEXT,
  };
  saveSendMsg(obj);
  mainState.userMessage = '';
  mainState.inputMessage = '';
  uni.hideKeyboard();};
const saveSendMsg = (evt) => {msgStorage.saveMsg(evt.msg, evt.type);
};
const openEmoji = () => {$emits('openEmoji');
};
const openRecordModal = () => {$emits('openRecordModal');
};
const openFunModal = () => {$emits('openFunModal');
};
defineExpose({emojiAction,});
</script>
<style>
@import './main.css';
</style>

重构后发送文本音讯组件

<template>
  <form class="text-input">
    <view class="f-row">
      <!-- 发送语音 -->
      <view @click="emits('toggleRecordModal')">
        <image class="icon-mic" src="/static/images/voice.png"></image>
      </view>
      <!-- 输入框 -->
      <textarea
        class="f news"
        type="text"
        cursor-spacing="65"
        confirm-type="send"
        v-model.trim="inputContent"
        @focus="inputFocus"
        @confirm="sendTextMessage"
        :confirm-hold="true"
        auto-height
        :show-confirm-bar="false"
        maxlength="300"
      />
      <view @click="emits('openEmojiModal')">
        <image class="icon-mic" src="/static/images/Emoji.png"></image>
      </view>
      <view v-show="!inputContent" @click="emits('openFunModal')">
        <image class="icon-mic" src="/static/images/ad.png"></image>
      </view>
      <button
        class="send-btn-style"
        hover-class="hover"
        @tap="sendTextMessage"
        v-show="inputContent"
      >
        发送
      </button>
    </view>
  </form>
</template>

<script setup>
import {ref, inject} from 'vue';
import {emMessages} from '@/EaseIM/imApis';
/* emits */
const emits = defineEmits([
  'toggleRecordModal',
  'openEmojiModal',
  'openFunModal',
  'closeAllModal',
]);
const inputContent = ref('');
// 删除输出内容中的 emojiMapStr
const delEmojiMapString = () => {if (!inputContent.value) return;
  let newInputContent = '';
  let inputContentlength = inputContent.value.length - 1;

  let start = inputContent.value.lastIndexOf('[');
  let end = inputContent.value.lastIndexOf(']');
  let len = end - start;

  if (end != -1 && end == inputContentlength && len >= 3 && len <= 4) {newInputContent = inputContent.value.slice(0, start);
  } else {newInputContent = inputContent.value.slice(0, inputContentlength);
  }
  inputContent.value = newInputContent;
};
// 发送文本音讯
const {sendDisplayMessages} = emMessages();
const injectTargetId = inject('targetId');
const injeactChatType = inject('chatType');
const sendTextMessage = async () => {
  const params = {
    // 音讯类型。type: 'txt',
    // 音讯内容。msg: inputContent.value,
    // 音讯接管方:单聊为对方用户 ID,群聊和聊天室别离为群组 ID 和聊天室 ID。to: injectTargetId.value,
    // 会话类型:单聊、群聊和聊天室别离为 `singleChat`、`groupChat` 和 `chatRoom`。chatType: injeactChatType.value,
  };
  try {const res = await sendDisplayMessages({ ...params});
    emits('closeAllModal');
    console.log('>>>>> 文本音讯发送胜利', res);
  } catch (error) {console.log('>>>>> 文本音讯发送失败', error);
    uni.showToast({
      title: '音讯发送失败',
      icon: 'none',
    });
  } finally {
    inputContent.value = '';
    uni.hideKeyboard();}
};
const inputFocus = () => {console.log('>>>> 输入框聚焦');
  emits('closeAllModal');
};
defineExpose({
  inputContent,
  delEmojiMapString,
});
</script>
<style>
@import './index.css';
</style>

十、重构前后音讯列表(messageList)代码展现

重构前音讯列表代码

<template>
  <view
    scroll-y="true"
    :class="msglistState.view +' wrap '+ (msglistState.isIPX ?'scroll_view_X':'')
    "@tap="onTap"upper-threshold="-50":scroll-into-view="msglistState.toView"
  >
    <view>
      <!-- 弹出举报入口 -->
      <uni-popup ref="alertReport">
        <button @click="showSelectReportType"> 举报 </button>
        <button @click="cannelReport"> 勾销 </button>
      </uni-popup>
      <!-- 展现举报选项 -->
      <uni-popup ref="selectReportType">
        <button
          v-for="(item, index) in msglistState.typeList"
          :key="index"
          @click="pickReportType(item)"
        >
          {{item.text}}
        </button>
        <button type="warn" @click="hideSelectReportType"> 勾销 </button>
      </uni-popup>
      <!-- 填写举报起因 -->
      <uni-popup ref="inputReportReason" type="dialog">
        <uni-popup-dialog
          mode="input"
          title="举报起因"
          placeholder="请输出举报起因"
          @confirm="reportMsg"
          @cancel="msglistState.reason =''"
        >
          <uni-easyinput
            type="textarea"
            v-model="msglistState.reason"
            placeholder="请填写举报内容"
            :maxlength="300"
          ></uni-easyinput>
        </uni-popup-dialog>
      </uni-popup>
    </view>
    <view class="tips"
      > 本利用仅用于环信产品性能开发测试,请勿用于非法用处。任何波及转账、汇款、裸聊、网恋、网购退款、投资理财等通通都是欺骗,请勿置信!</view
    >
    <view
      @longtap="actionAleartReportMsg(item)"
      class="message"
      v-for="item in msglistState.chatMsg"
      :key="item.mid"
      :id="item.mid"
    >
      <!-- <view class="time">
                <text class="time-text">{{item.time}}</text>
      </view>-->
      <view class="main" :class="item.style">
        <view class="user">
          <!-- yourname:就是音讯的 from -->
          <text v-if="!item.style" class="user-text">{{showMessageListNickname(item.yourname) + ' ' + handleTime(item)
          }}</text>
        </view>
        <image class="avatar" :src="showMessageListAvatar(item)" />
        <view class="msg">
          <image
            class="err"
            :class="item.style =='self'&& item.isFail ?'show':'hide'"src="/static/images/msgerr.png"
          />

          <image
            v-if="item.style =='self'"src="/static/images/poprightarrow2x.png"class="msg_poprightarrow"
          />
          <image
            v-if="item.style ==''"
            src="/static/images/popleftarrow2x.png"
            class="msg_popleftarrow"
          />
          <view
            v-if="item.msg.type == msgtype.IMAGE || item.msg.type == msgtype.VIDEO"
          >
            <image
              v-if="item.msg.type == msgtype.IMAGE"
              class="avatar"
              :src="item.msg.data"
              style="width: 90px; height: 120px; margin: 2px auto"
              mode="aspectFit"
              @tap="previewImage"
              :data-url="item.msg.data"
            />
            <video
              v-if="item.msg.type == msgtype.VIDEO"
              :src="item.msg.data"
              controls
              style="width: 300rpx"
            />
          </view>
          <audio-msg
            v-if="item.msg.type == msgtype.AUDIO"
            :msg="item"
          ></audio-msg>
          <file-msg v-if="item.msg.type == msgtype.FILE" :msg="item"></file-msg>
          <view
            v-else-if="item.msg.type == msgtype.TEXT || item.msg.type == msgtype.EMOJI"
          >
            <view
              class="template"
              v-for="(d_item, d_index) in item.msg.data"
              :key="d_index"
            >
              <text
                :data-msg="item"
                v-if="d_item.type == msgtype.TEXT"
                class="msg-text"
                style="float: left"
                selectable="true"
                >{{d_item.data}}</text
              >

              <image
                v-if="d_item.type == msgtype.EMOJI"
                class="avatar"
                :src="'/static/images/faces/' + d_item.data"style="
                  width: 25px;
                  height: 25px;
                  margin: 0 0 2px 0;
                  float: left;
                "
              />
            </view>
          </view>
          <!-- 集体名片 -->
          <view
            v-else-if="item.msg.type == msgtype.CUSTOM && item.customEvent ==='userCard' "
            @click="to_profile_page(item.msg.data)"
          >
            <view class="usercard_mian">
              <image
                :src="
                  item.msg.data.avatarurl ||
                  item.msg.data.avatar ||
                  defaultAvatar
                "
              />
              <text class="name">{{item.msg.data.nickname || item.msg.data.uid}}</text>
            </view>
            <!-- <u-divider :use-slot="false" /> -->
            <text>[集体名片]</text>
          </view>
        </view>
      </view>
    </view>
  </view>
  <!-- <view style="height: 1px;"></view> -->
</template>

<script setup>
import {reactive, ref, computed, onMounted, onUnmounted} from 'vue';
import msgStorage from '../msgstorage';
// let msgStorage = require("../msgstorage");
import disp from '@/utils/broadcast';
import dateFormater from '@/utils/dateFormater';
// let disp = require('../../../utils/broadcast');
import msgtype from '@/components/chat/msgtype';
import audioMsg from './type/audio/audio';
import fileMsg from './type/file';
let LIST_STATUS = {
  SHORT: 'scroll_view_change',
  NORMAL: 'scroll_view',
};
let page = 0;
let Index = 0;
let curMsgMid = '';
let isFail = false;

const WebIM = uni.WebIM;
/* props */
const props = defineProps({
  chatParams: {
    type: Object,
    default: () => ({}),
    required: true,
  },
});
const {chatParams} = props;
console.log('msglist', chatParams);
/* emits */
const $emit = defineEmits(['msglistTap']);
const msglistState = reactive({
  view: LIST_STATUS.NORMAL,
  toView: '',
  chatMsg: [],
  __visibility__: false,
  isIPX: false,
  title: '音讯举报',
  list: [
    {text: '举报',},
  ],
  rptMsgId: '', // 举报音讯 id
  rptType: '', // 举报类型
  reason: '',
  typeList: [
    {text: '涉政',},
    {text: '涉黄',},
    {text: '广告',},
    {text: '唾骂',},
    {text: '暴恐',},
    {text: '违禁',},
  ],
  defaultAvatar: '/static/images/theme2x.png',
  defaultGroupAvatar: '/static/images/groupTheme.png',
  usernameObj: null,
});
// 做初始参数设置
msglistState.__visibility__ = true;
page = 0;
Index = 0;

onUnmounted(() => {
  msglistState.__visibility__ = false;
  msgStorage.off('newChatMsg', dispMsg);
});

onMounted(() => {if (getApp().globalData.isIPX) {msglistState.isIPX = true;}
  // 依据原有 uni demo 解决仿佛支付宝小程序有参数传递问题,因而针对该平台独自取传递的参数
  if (uni.getSystemInfoSync().uniPlatform === 'mp-alipay') {msglistState.usernameObj = Object.assign({}, uni.username);
  } else {msglistState.usernameObj = Object.assign({}, chatParams);
  }
  const usernameObj = msglistState.usernameObj;
  console.log('usernameObj', usernameObj);
  let myUsername = uni.getStorageSync('myUsername');
  let sessionKey = usernameObj.groupId
    ? usernameObj.groupId + myUsername
    : usernameObj.your + myUsername;
  let chatMsg = uni.getStorageSync(sessionKey) || [];
  renderMsg(null, null, chatMsg, sessionKey);
  uni.setStorageSync(sessionKey, null);
  disp.on('em.error.sendMsgErr', function (err) {
    // curMsgMid = err.data.mid;
    isFail = true;
    // return;
    console.log('发送失败了');
    return;
    let msgList = me.chatMsg;
    msgList.map((item) => {
      if (item.mid.substring(item.mid.length - 10) ==
        curMsgMid.substring(curMsgMid.length - 10)
      ) {item.msg.data[0].isFail = true;
        item.isFail = true;
        me.setData({chatMsg: msgList,});
      }
    });
    uni.setStorageSync('rendered_' + sessionKey, msgList);
  });
  msgStorage.on('newChatMsg', dispMsg);
});
/* computed */
// 音讯列表头像展现
const showMessageListAvatar = computed(() => {const friendUserInfoMap = getApp().globalData.friendUserInfoMap;
  const myUserInfos = getApp().globalData.userInfoFromServer;
  return (item) => {if (!item.style) {
      if (friendUserInfoMap.has(item.username) &&
        friendUserInfoMap.get(item.username)?.avatarurl
      ) {return friendUserInfoMap.get(item.username).avatarurl;
      } else {return msglistState.defaultAvatar;}
    } else {if (myUserInfos?.avatarurl) {return myUserInfos.avatarurl;} else {return msglistState.defaultAvatar;}
    }
  };
});
// 音讯列表昵称显示
const showMessageListNickname = computed(() => {const friendUserInfoMap = getApp().globalData.friendUserInfoMap;
  return (hxId) => {if (friendUserInfoMap.has(hxId) && friendUserInfoMap.get(hxId)?.nickname) {return friendUserInfoMap.get(hxId).nickname;
    } else {return hxId;}
  };
});
// 解决工夫显示
const handleTime = computed(() => {return (item) => {return dateFormater('MM/DD/HH:mm', item.time);
  };
});

const normalScroll = () => {msglistState.view = LIST_STATUS.NORMAL;};
//TODO 待优化
// 此处用到了公布订阅默认去订阅,msgstorage 文件中 公布的 newChatMsg 事件从而取到了存储后的音讯 list
let curChatMsgList = null;
const dispMsg = (renderableMsg, type, curChatMsg, sesskey) => {
  const usernameObj = msglistState.usernameObj;
  let myUsername = uni.getStorageSync('myUsername');
  let sessionKey = usernameObj.groupId
    ? usernameObj.groupId + myUsername
    : usernameObj.your + myUsername;
  curChatMsgList = curChatMsg;

  if (!msglistState.__visibility__) return; // 判断是否属于以后会话

  if (usernameObj.groupId) {
    // 群音讯的 to 是 id,from 是 name
    if (
      renderableMsg.info.from == usernameObj.groupId ||
      renderableMsg.info.to == usernameObj.groupId
    ) {if (sesskey == sessionKey) {renderMsg(renderableMsg, type, curChatMsg, sessionKey, 'newMsg');
      }
    }
  } else if (
    renderableMsg.info.from == usernameObj.your ||
    renderableMsg.info.to == usernameObj.your
  ) {if (sesskey == sessionKey) {renderMsg(renderableMsg, type, curChatMsg, sessionKey, 'newMsg');
    }
  }
};
// 音讯渲染办法
const renderMsg = (renderableMsg, type, curChatMsg, sessionKey, isnew) => {console.log('curChatMsg, sessionKey, isnew', curChatMsg, sessionKey, isnew);
  let historyChatMsgs = uni.getStorageSync('rendered_' + sessionKey) || [];
  historyChatMsgs = historyChatMsgs.concat(curChatMsg);
  if (!historyChatMsgs.length) return;
  if (isnew == 'newMsg') {msglistState.chatMsg = msglistState.chatMsg.concat(curChatMsg);
    msglistState.toView = historyChatMsgs[historyChatMsgs.length - 1].mid;
  } else {msglistState.chatMsg = historyChatMsgs.slice(-10);
    msglistState.toView = historyChatMsgs[historyChatMsgs.length - 1].mid;
  }

  uni.setStorageSync('rendered_' + sessionKey, historyChatMsgs);
  let chatMsg = uni.getStorageSync(sessionKey) || [];
  chatMsg.map(function (item, index) {curChatMsg.map(function (item2, index2) {if (item2.mid == item.mid) {chatMsg.splice(index, 1);
      }
    });
  });
  uni.setStorageSync(sessionKey, chatMsg);
  Index = historyChatMsgs.slice(-10).length;
  // setTimeout 兼容支付宝小程序
  setTimeout(() => {
    uni.pageScrollTo({
      scrollTop: 5000,
      duration: 100,
      fail: (e) => {//console.log('滚失败了', e)
      },
    });
  }, 100);

  if (isFail) {renderFail(sessionKey);
  }
};
const renderFail = (sessionKey) => {
  let msgList = msglistState.chatMsg;
  msgList.map((item) => {
    if (item.mid.substring(item.mid.length - 10) ==
      curMsgMid.substring(curMsgMid.length - 10)
    ) {item.msg.data[0].isFail = true;
      item.isFail = true;
      msglistState.chatMsg = msgList;
    }
  });

  if (curChatMsgList[0].mid == curMsgMid) {curChatMsgList[0].msg.data[0].isShow = false;
    curChatMsgList[0].isShow = false;
  }

  uni.setStorageSync('rendered_' + sessionKey, msgList);
  isFail = false;
};
const onTap = () => {
  $emit('msglistTap', null, {bubbles: true,});
};

const shortScroll = () => {msglistState.view = LIST_STATUS.SHORT;};

const previewImage = (event) => {
  var url = event.target.dataset.url;
  uni.previewImage({urls: [url], // 须要预览的图片 http 链接列表
  });
};
const getHistoryMsg = () => {
  let usernameObj = msglistState.usernameObj;
  let myUsername = uni.getStorageSync('myUsername');
  let sessionKey = usernameObj.groupId
    ? usernameObj.groupId + myUsername
    : usernameObj.your + myUsername;
  let historyChatMsgs = uni.getStorageSync('rendered_' + sessionKey) || [];
  if (Index < historyChatMsgs.length) {let timesMsgList = historyChatMsgs.slice(-Index - 10, -Index);
    msglistState.chatMsg = timesMsgList.concat(msglistState.chatMsg);
    msglistState.toView = timesMsgList[timesMsgList.length - 1].mid;
    Index += timesMsgList.length;
    if (timesMsgList.length == 10) {page++;}
    uni.stopPullDownRefresh();}
};
const to_profile_page = (userInfo) => {if (userInfo) {
    uni.navigateTo({url: `../profile/profile?otherProfile=${JSON.stringify(userInfo)}`,
    });
  }
};

/* 举报音讯 */
// 弹出举报
const alertReport = ref(null);
const actionAleartReportMsg = (item) => {if (item.style !== 'self') {alertReport.value.open('bottom');
    msglistState.showRpt = true;
    msglistState.rptMsgId = item.mid;
  }
};
// 勾销举报
const cannelReport = () => {alertReport.value.close();
};

// 抉择举报类型
const selectReportType = ref(null);
// 展现举报类型面板
const showSelectReportType = () => {alertReport.value.close();
  selectReportType.value.open('bottom');
};
const pickReportType = (item) => {
  msglistState.rptType = item.text;
  hideSelectReportType();
  actionAleartReportReason(item);
};
const hideSelectReportType = () => {selectReportType.value.close();
};
// 填写举报起因
const inputReportReason = ref(null);
const actionAleartReportReason = (item) => {console.log('>>>>>> 输出举报内容', item);
  inputReportReason.value.open();};
const reportMsg = () => {if (msglistState.reason === '') {uni.showToast({ title: '请填写举报起因', icon: 'none'});
    return;
  }
  WebIM.conn
    .reportMessage({
      reportType: msglistState.rptType, // 举报类型
      reportReason: msglistState.reason, // 举报起因。messageId: msglistState.rptMsgId, // 上报音讯 id
    })
    .then(() => {uni.showToast({ title: '举报胜利', icon: 'none'});
    })
    .catch((e) => {console.log('>>>> 举报失败', e);
      uni.showToast({title: '举报失败', icon: 'none'});
    })
    .finally(() => {
      msglistState.reason = '';
      msglistState.rptType = '';
      msglistState.rptMsgId = '';
    });
};
defineExpose({
  normalScroll,
  getHistoryMsg,
  shortScroll,
});
</script>
<style>
@import './msglist.css';
</style>

重构前音讯列表代码

<template>
  <view
    scroll-y="true"
    :class="msglistState.view +' wrap '+ (msglistState.isIPX ?'scroll_view_X':'')
    "upper-threshold="-50":scroll-into-view="msglistState.toView"
  >
    <view>
      <!-- 弹出举报入口 -->
      <uni-popup ref="alertReport">
        <button @click="showSelectReportType"> 举报 </button>
        <button @click="cannelReport"> 勾销 </button>
      </uni-popup>
      <!-- 展现举报选项 -->
      <uni-popup ref="selectReportType">
        <button
          v-for="(item, index) in msglistState.typeList"
          :key="index"
          @click="pickReportType(item)"
        >
          {{item.text}}
        </button>
        <button type="warn" @click="hideSelectReportType"> 勾销 </button>
      </uni-popup>
      <!-- 填写举报起因 -->
      <uni-popup ref="inputReportReason" type="dialog">
        <uni-popup-dialog
          mode="input"
          title="举报起因"
          placeholder="请输出举报起因"
          @confirm="reportMsg"
          @cancel="msglistState.reason =''"
        >
          <uni-easyinput
            type="textarea"
            v-model="msglistState.reason"
            placeholder="请填写举报内容"
            :maxlength="300"
          ></uni-easyinput>
        </uni-popup-dialog>
      </uni-popup>
    </view>
    <view class="tips"
      > 本利用仅用于环信产品性能开发测试,请勿用于非法用处。任何波及转账、汇款、裸聊、网恋、网购退款、投资理财等通通都是欺骗,请勿置信!</view
    >
    <view
      @longtap="actionAleartReportMsg(msgBody)"
      class="message"
      v-for="(msgBody, index) in messageList"
      :key="msgBody.id + index +''"
      :id="msgBody.id"
    >
      <!-- 音讯体 -->
      <view class="main" :class="isSelf(msgBody) ?'self':''">
        <view class="user">
          <!-- yourname:就是音讯的 from -->
          <text v-if="!isSelf(msgBody)" class="user-text">{{showMessageListNickname(msgBody.from) + ' ' + handleTime(msgBody)
          }}</text>
        </view>
        <image class="avatar" :src="showMessageListAvatar(msgBody)" />
        <view class="msg">
          <image
            v-if="isSelf(msgBody)"
            src="/static/images/poprightarrow2x.png"
            class="msg_poprightarrow"
          />
          <image
            v-if="!isSelf(msgBody)"
            src="/static/images/popleftarrow2x.png"
            class="msg_popleftarrow"
          />
          <!-- 文本类型音讯 -->
          <view v-if="msgBody.type === MESSAGE_TYPE.TEXT">
            <view
              class="template"
              v-for="(d_item, d_index) in parseMsgEmoji(msgBody.msg)"
              :key="d_index"
            >
              <text
                :data-msg="msgBody"
                v-if="d_item.type == MESSAGE_TYPE.TEXT"
                class="msg-text"
                style="float: left"
                selectable="true"
                >{{d_item.data}}</text
              >

              <image
                v-if="d_item.type == MESSAGE_TYPE.EMOJI"
                class="avatar"
                :src="'/static/images/faces/' + d_item.data"style="
                  width: 25px;
                  height: 25px;
                  margin: 0 0 2px 0;
                  float: left;
                "
              />
            </view>
          </view>
          <!-- 文件类型音讯 -->
          <file-msg
            v-if="msgBody.type === MESSAGE_TYPE.FILE"
            :msg="msgBody"
          ></file-msg>
          <!-- 语音片段类型音讯 -->
          <audio-msg
            v-if="msgBody.type === MESSAGE_TYPE.AUDIO"
            :msg="msgBody"
          ></audio-msg>
          <!-- 图片以及视频类型音讯 -->
          <view
            v-if="
              msgBody.type == MESSAGE_TYPE.IMAGE ||
              msgBody.type == MESSAGE_TYPE.VIDEO
            "
          >
            <image
              v-if="msgBody.type == MESSAGE_TYPE.IMAGE"
              class="avatar"
              :src="msgBody.url"
              style="width: 90px; height: 120px; margin: 2px auto"
              mode="aspectFit"
              @tap="previewImage(msgBody.url)"
            />
            <video
              v-if="msgBody.type == MESSAGE_TYPE.VIDEO"
              :src="msgBody.url"
              controls
              style="width: 300rpx"
            />
          </view>
          <!-- 自定义类型音讯 -->
          <view
            v-if="
              msgBody.type == MESSAGE_TYPE.CUSTOM &&
              msgBody.customEvent === 'userCard'
            "@click="entryProfilePage(msgBody.customExts)"
          >
            <view class="usercard_mian">
              <image
                :src="
                  msgBody.customExts.avatarurl ||
                  msgBody.customExts.avatar ||
                  msglistState.defaultAvatar
                "
              />
              <text class="name">{{msgBody.customExts.nickname || msgBody.customExts.uid}}</text>
            </view>
            <!-- <u-divider :use-slot="false" /> -->
            <text>[集体名片]</text>
          </view>
        </view>
      </view>
    </view>
  </view>
</template>

<script setup>
import {
  ref,
  reactive,
  computed,
  watch,
  onMounted,
  inject,
  nextTick,
} from 'vue';
import {onPullDownRefresh, onNavigationBarButtonTap} from '@dcloudio/uni-app';
/* EaseIM */
import parseEmoji from '@/EaseIM/utils/paseEmoji';
import {CHAT_TYPE, MESSAGE_TYPE} from '@/EaseIM/constant';
/* stores */
import {useLoginStore} from '@/stores/login';
import {useMessageStore} from '@/stores/message';
import {useContactsStore} from '@/stores/contacts';
/* utils */
import dateFormater from '@/utils/dateFormater';
/* im apis */
import {emMessages} from '@/EaseIM/imApis';
/* components */
import FileMsg from './type/file';
import AudioMsg from './type/audio/audio';
const msglistState = reactive({
  isIPX: false,
  toView: 0,
  // 漫游以后游标
  view: 'wrap',
  title: '音讯举报',
  list: [
    {text: '举报',},
  ],
  rptMsgId: '', // 举报音讯 id
  rptType: '', // 举报类型
  reason: '',
  typeList: [
    {text: '涉政',},
    {text: '涉黄',},
    {text: '广告',},
    {text: '唾骂',},
    {text: '暴恐',},
    {text: '违禁',},
  ],
  defaultAvatar: '/static/images/theme2x.png',
  defaultGroupAvatar: '/static/images/groupTheme.png',
});
const injectTargetId = inject('targetId');
const injectChatType = inject('chatType');
/* 音讯相干逻辑解决 */
const {reportMessages, fetchHistoryMessagesFromServer} = emMessages();
// 该用户以后的聊天记录
const messageStore = useMessageStore();
const messageList = computed(() => {
  return (messageStore.messageCollection[injectTargetId.value] ||
    getMoreHistoryMessages() ||
    []);
});
// 获取更多历史音讯
const getMoreHistoryMessages = async () => {
  const sourceMessage =
    messageStore.messageCollection[injectTargetId.value] || [];
  const cursorMsgId = (sourceMessage.length && sourceMessage[0]?.id) || -1;
  const params = {
    targetId: injectTargetId.value,
    chatType: injectChatType.value,
    cursor: cursorMsgId,
  };
  try {let res = await fetchHistoryMessagesFromServer(params);
    if (res.messages.length) {
      messageStore.fetchHistoryPushToMsgCollection(
        injectTargetId.value,
        res.messages.reverse());
    } else {uni.showToast({ title: '暂无更多历史记录', icon: 'none'});
    }
    uni.stopPullDownRefresh();} catch (error) {uni.stopPullDownRefresh();
    uni.showToast('历史音讯获取失败...');
    console.log('>>>>> 返回失败', error);
  }
};
onMounted(() => {nextTick(() => {
    uni.pageScrollTo({
      scrollTop: 100000,
      duration: 50,
    });
  });
});
// 监听音讯内容扭转,滚动列表
watch(
  messageList,
  () => {nextTick(() => {
      uni.pageScrollTo({
        scrollTop: 100000,
        duration: 100,
      });
    });
  },
  {deep: true,}
);
// 音讯列表头像展现
const loginStore = useLoginStore();
const contactsStore = useContactsStore();
// 登录用户属性
const myUserInfos = computed(() => {return loginStore.loginUserProfiles;});
// 好友属性
const friendUserInfoMap = computed(() => {return contactsStore.friendUserInfoMap;});
// 判消息来源是否为本人
const isSelf = computed(() => {return (item) => {return item.from === loginStore.loginUserBaseInfos.loginUserId;};
});

const showMessageListAvatar = computed(() => {
  const friendMap = friendUserInfoMap.value;
  return (item) => {if (item.from !== loginStore.loginUserBaseInfos.loginUserId) {return friendMap.get(item.from)?.avatarurl || msglistState.defaultAvatar;
    } else {return myUserInfos.value?.avatarurl || msglistState.defaultAvatar;}
  };
});
// 音讯列表昵称显示
const showMessageListNickname = computed(() => {
  const friendMap = friendUserInfoMap.value;
  return (hxId) => {return friendMap.get(hxId)?.nickname || hxId;
  };
});
// 解决工夫显示
const handleTime = computed(() => {return (item) => {return dateFormater('MM/DD/HH:mm', item.time);
  };
});
// 解析表情图片
const parseMsgEmoji = computed(() => {return (content) => {return parseEmoji(content);
  };
});

// 预览图片办法
const previewImage = (url) => {
  uni.previewImage({urls: [url], // 须要预览的图片 http 链接列表
  });
};
// 点击查看集体名片
const entryProfilePage = (userInfo) => {if (userInfo) {
    uni.navigateTo({url: `../profile/profile?otherProfile=${JSON.stringify(userInfo)}`,
    });
  }
};

/* 举报音讯 */
// 弹出举报
const alertReport = ref(null);
const actionAleartReportMsg = (item) => {if (item.style !== 'self') {alertReport.value.open('bottom');
    msglistState.showRpt = true;
    msglistState.rptMsgId = item.id;
  }
};
// 勾销举报
const cannelReport = () => {alertReport.value.close();
};

// 抉择举报类型
const selectReportType = ref(null);
// 展现举报类型面板
const showSelectReportType = () => {alertReport.value.close();
  selectReportType.value.open('bottom');
};
const pickReportType = (item) => {
  msglistState.rptType = item.text;
  hideSelectReportType();
  actionAleartReportReason(item);
};
const hideSelectReportType = () => {selectReportType.value.close();
};
// 填写举报起因
const inputReportReason = ref(null);
const actionAleartReportReason = (item) => {inputReportReason.value.open();
};

const reportMsg = async () => {if (msglistState.reason === '') {uni.showToast({ title: '请填写举报起因', icon: 'none'});
    return;
  }
  const reportParams = {
    reportType: msglistState.rptType,
    reportReason: msglistState.reason,
    messageId: msglistState.rptMsgId,
  };
  try {await reportMessages({ ...reportParams});
    uni.showToast({title: '举报胜利', icon: 'none'});
  } catch (error) {console.log('>>>> 举报失败', error);
    uni.showToast({title: '举报失败', icon: 'none'});
  } finally {
    msglistState.reason = '';
    msglistState.rptType = '';
    msglistState.rptMsgId = '';
  }
};
onPullDownRefresh(() => {getMoreHistoryMessages();
  console.log('>>>>> 开始了下拉页面');
});
</script>

<style scoped>
@import './index.css';
</style>

还有更多重构代码篇幅无限不便一一展现,感兴趣请至片尾点击 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 文档地址

重构前 uni-app-demo-Vue2 版本源码地址

重构前 uni-app-demo-Vue3 版本源码地址

重构后 uni-app-demo 源码地址

环信 uni-app Demo 降级革新打算——Vue2 迁徙到 Vue3(一)

最初多说一句,如果感觉有帮忙请点赞反对一下!本 demo 还有三期打算(减少音视频性能),敬请期待!

退出移动版