关于uniapp:环信-uniapp-Demo升级改造计划Vue2迁移到Vue3一

57次阅读

共计 13941 个字符,预计需要花费 35 分钟才能阅读完成。

前言

因为环信 uni-app Demo 为晚期通过工具从微信小程序转换为的 uni-app 我的项目,通过理论的应用以及复用反馈,目前曾经不适用于以后的开发应用,因而开启了整体降级革新打算,目前一期打算将 vue2 代码进行手动转换为 vue3+vite,并剔除原我的项目中曾经无用的我的项目代码,上面记录一下降级操作,如果降级过程,对大家有所帮忙,深感荣幸~

后期筹备

  • 【重要】浏览 uni-app 官网文档 Vue2 降级 Vue3 指南 文档地址
  • 调研迁徙到 Vue3 中原有的 Demo 中哪些三方库或者办法将不可用(_次要 uview UI 库不反对 Vue3_)。
  • 下载并运行环信官网 uni-app 我的项目(原我的项目 master 分支)。Demo 下载地址
  • 在 HubilderX 中创立容器我的项目(_所谓容器我的项目即为创立一个空白的 Vue3 模板,用以逐渐将 Vue2 的我的项目代码逐渐挪到此我的项目中。_)
  • 在空白我的项目中引入 uni-ui 组件,次要为了应用其组件替换原我的项目 uviewUI 组件
  • 确认降级流程以及形式(_本次降级采纳渐进式语法批改模式_),次要形式为迁徙一个组件则将批改一个组件的语法为 vue3,如该组件依赖多个组件则先切断相组件的连贯(_正文大法_),后续逐渐放开并配套批改。

外围迁徙步骤

第一步、导入环信 uni-app SDK

原有 Vue2 版本 uni-app-demo 我的项目为本地引入 SDK 包,对于有些习惯 npm 装置导入的同学不太敌对,目前 uniSDK 曾经反对 npm 装置并导入,因而将原有本地引入 js 文件改为通过 npm 装置 SDK 并 import 导入 SDK。

// 第一步 关上终端执行 npm install easemob-websdk
// 第二步 复制原 demo 中的 utils 文件夹至空白我的项目中
// 第三步 找到 utils 文件夹中的 WebIM.js 文件中的导入 SDK 形式改写为 impot 导入 easemob-websdk/uniApp 包,具体代码如下。/* 原我的项目引入 SDK 代码 */
import websdk from '../newSDK/uniapp-sdk-4.1.2';
/* 改写后的代码 */
import websdk from 'easemob-websdk/uniApp/Easemob-chat';

第二步、CommonJS 导入导出改写为 ESM

这种改写起因两点:

1、CommonJS 标准在 Vite 中应用自身并不反对,如果反对则须要进行独自配置。

2、原始我的项目中既有 CommonJS 导入形式,也有 ESM 导入,借此机会进行对立。

进行到此次要是先将原始我的项目中的 CommonJS 导出 WebIM 实例改为 ESM 导出,后续会在语法革新过程中将所有 CommonJS 标准改写为 ESM 导出,后续将不在本文中提及,实例代码如下

/* 原始我的项目 utils/WebIM.js 的导入导出 WebIM 实例代码段 */
// 导入形式
let WebIM = (wx.WebIM = require('./utils/WebIM')['default']);
// 导出形式
module.exports = {default: WebIM,};

/* 改写后导入导出 */
// 导入形式
import WebIM from '@/utils/WebIM.js';
// 导出形式
export default WebIM;

第三步、迁入 App.vue 组件

残缺的复制原始我的项目中的 App.vue 组件(uni 的 Vue3 模板中也反对 Vue2 代码,因而能够释怀进行 CV)

App.vue 组件波及到的改变为正文掉临时没有引入的 js 文件,后续进行引入,去除 scss 中的 uview 款式代码,引入后续将要齐全剔除 uview 组件。

App.vue 中代码较多此示例做了大量的缩减,大抵调整之后的构造如下。

<script>
import WebIM from '@/utils/WebIM.js';
// 这些导入临时正文,后续再进行引入
//let msgStorage = require("./components/chat/msgstorage");
//let msgType = require("./components/chat/msgtype");
//let disp = require("./utils/broadcast");
//let logout = false;

//import {onGetSilentConfig} from './components/chat/pushStorage'
export default {
//export default 的代码块一成不变,此处先进行了删除,理论迁入不必动。data (){return {}
    }
}
</script>
<style lang="scss">
@import './app.css';
 /* 留神这行代码删除 @import "uview-ui/index.scss"; */
</style>

第四步 牛刀小试~ 迁入 Login 组件

先迁入一个 Login 组件热热身,毕竟从登录开始,原始我的项目中有注册、Token 登录、等等但目前暂不须要所以只需迁入 Login 组件。

在迁入前咱们先理解并思考一下,Vue2 的 Options API 与 Vue3 Composition API 一些特点,次要目标是用较小的代价进行 Vue3 语法革新。
Vue3 模版反对 setup 语法糖,因而能够间接应用应用 setup 语法糖形式进行语法革新。

<script setup>
    /* 原始代码片段 */
    let WebIM = require("../../utils/WebIM")["default"];
    let __test_account__, __test_psword__;
    let disp = require("../../utils/broadcast");
    data() {
        return {
          usePwdLogin:false, // 是否用户名 + 手机号形式登录
          name: "",
          psd: "",
          grant_type: "password",
          psdFocus: "",
          nameFocus: "",
          showPassword:false,
          type:'text',
              btnText: '获取验证码'
        };
      },
      /* 革新后的代码 */
    // 应用 reactive 替换并包裹原有 data 中的参数
    import {reactive} from 'vue'
    import disp from '@/utils/broadcast.js'; // 批改为 ESM 导入
    const WebIM = uni.WebIM; // 从挂载到 uni 下的 WebIM 中取出 WebIM 并赋值用以替换原有独自 require 导入的 WebIM
    const loginState = reactive({
      usePwdLogin: true, // 是否用户名 + 手机号形式登录
      name: '',
      psd: '',
      grant_type: 'password',
      psdFocus: '',
      nameFocus: '',
      showPassword: false,
      type: 'text',
      btnText: '获取验证码',
    });

    //methods 中的办法提取到外层中,例如将 login 登录 IM 进行调整
    // 登录 IM
const loginIM = () => {
  runAnimation = !runAnimation;
  if (!loginState.usePwdLogin) {if (!__test_account__ && loginState.name == '') {
      uni.showToast({
        title: '请输出手机号!',
        icon: 'none',
      });
      return;
    } else if (!__test_account__ && loginState.psd == '') {
      uni.showToast({
        title: '请输出验证码!',
        icon: 'none',
      });
      return;
    }
    const that = loginState;
    uni.request({
      url: 'https://a1.easemob.com/inside/app/user/login/V2',
      header: {'content-type': 'application/json',},
      method: 'POST',
      data: {
        phoneNumber: that.name,
        smsCode: that.psd,
      },
      success(res) {if (res.statusCode == 200) {const { phoneNumber, token, chatUserName} = res.data;
          getApp().globalData.conn.open({
            user: chatUserName,
            accessToken: token,
          });
          getApp().globalData.phoneNumber = phoneNumber;
          uni.setStorage({
            key: 'myUsername',
            data: chatUserName,
          });
        } else if (res.statusCode == 400) {if (res.data.errorInfo) {switch (res.data.errorInfo) {
              case 'UserId password error.':
                uni.showToast({
                  title: '用户名或明码谬误!',
                  icon: 'none',
                });
                break;
              case 'phone number illegal':
                uni.showToast({
                  title: '请输出正确的手机号',
                  icon: 'none',
                });
                break;
              case 'SMS verification code error.':
                uni.showToast({
                  title: '验证码谬误',
                  icon: 'none',
                });
                break;
              case 'Sms code cannot be empty':
                uni.showToast({
                  title: '验证码不能为空',
                  icon: 'none',
                });
                break;
              case 'Please send SMS to get mobile phone verification code.':
                uni.showToast({
                  title: '请应用短信验证码登录',
                  icon: 'none',
                });
                break;
              default:
                uni.showToast({
                  title: res.data.errorInfo,
                  icon: 'none',
                });
                break;
            }
          }
        } else {
          uni.showToast({
            title: '登录失败!',
            icon: 'none',
          });
        }
      },
      fail(error) {
        uni.showToast({
          title: '登录失败!',
          icon: 'none',
        });
      },
    });
  } else {if (!__test_account__ && loginState.name == '') {
      uni.showToast({
        title: '请输出用户名!',
        icon: 'none',
      });
      return;
    } else if (!__test_account__ && loginState.psd == '') {
      uni.showToast({
        title: '请输出明码!',
        icon: 'none',
      });
      return;
    }
    uni.setStorage({
      key: 'myUsername',
      data: __test_account__ || loginState.name.toLowerCase(),});
    console.log(111, {
      apiUrl: WebIM.config.apiURL,
      user: __test_account__ || loginState.name.toLowerCase(),
      pwd: __test_psword__ || loginState.psd,
      grant_type: loginState.grant_type,
      appKey: WebIM.config.appkey,
    });
    getApp().globalData.conn.open({
      apiUrl: WebIM.config.apiURL,
      user: __test_account__ || loginState.name.toLowerCase(),
      pwd: __test_psword__ || loginState.psd,
      grant_type: loginState.grant_type,
      appKey: WebIM.config.appkey,
    });
  }
};
</script>

革新中会遇到了原 Vue2 中原 data 局部参数通过应用 reactive 包裹并重命名,须要留神把语法中的 this.、me.、this.setData 进行替换为包裹后的 state 命名,另外 template 中也要同步进行替换,这一点在后续所有组件革新中都会遇到。

Login 组件须要 page.json 中进行路由的配置,只有配置胜利之后咱们方可运行我的项目并展现页面!

此时就能够启动我的项目运行察看一下看看页面是否能够失常的进行展现,当然是运行到小程序还是 H5 以及 App 上自行抉择。

第五步、迁入“Home 页中的”三个 Tab 页面【conversation 会话列表,mian 联系人页、Setting 我的页面】

迁徙各组件,此处应用 conversation 组件作为示例,其余两个组件完全相同的步骤,全副示例代码将在文章开端给出地址。

在原我的项目中包含已迁徙进来的 App.vue 组件中有上面这样一个办法,其作用即为环信 IM 连贯胜利之后触发 onOpened 该监听回调,进行路由跳转进入到会话页面,因而不难理解,open 之后首个跳转的页面即为 conversation。

    onLoginSuccess: function (myName) {uni.hideLoading();
      uni.redirectTo({url: "../conversation/conversation?myName=" + myName,});
    },
  • 在原始我的项目中 copy conversation(会话)组件至容器我的项目雷同目录下,另外不要遗记棘手在 page.json 下配置路由。
  • 开始改写会话组件中的代码
//script 标签减少 setup 使其反对 setup 语法糖
<script setup>
    /* 引入所需组合式 API */
    //computed 用以替换 options API 中的计算属性,Vue3 中计算属性应用略有差别。import {reactive,computed} from 'vue'
    /* 引入所需申明周期钩子函数替换原有钩子函数,该写法 uni-appvue2 降级 vue3 指南有提及 */
    import {onLoad, onShow, onUnload} from '@dcloudio/uni-app';
    /* 调整 disp 为 import 导入 */
    // let disp = require("../../utils/broadcast");
    import disp from '@/utils/broadcast';
    /* 调整 WebIM 引入间接从 uni 下取 */
    // var WebIM = require("../../utils/WebIM")["default"];
    const WebIM = uni.WebIM
    let isfirstTime = true;
    /* components 中的组件临时正文,template 中的组件引入也临时正文,* 另 options API 中的 components 中的组件注册也临时正文
    */
    // import swipeDelete from "../../components/swipedelete/swipedelete";
    // import longPressModal from "../../components/longPressModal/index";

    /* data 提出用 reactive 包裹并命名 */
    const conversationState = reactive({// 内容省略...});

    /* onLoad 替换 */
    onLoad(() => {
      // 所有通过 this. 进行办法办法调用全副删除
      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 替换 */
    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 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
            ) {return item.groupName;}
          };
        });
        const handleTime = computed(() => {return (item) => {return dateFormater('MM/DD/HH:mm', item.time);
          };
        });
  /* 将 methods 中办法全量提取到外层与 onLoad onShow 等 API 平级 */
      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;
    };

    // 还有很多办法就不一一展现,临时进行了省略...
    /* onUnload */
    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

在做这三个组件迁徙的时候次要的注意事项为,this 的替换,template 中的默认从 vue2 中 data 取的参数也要替换为被 reactive 包裹后的变量名。

启动运行调整

倡议迁徙一个组件调试一个组件,运行到 H5 端,从登录页面登录进去,并点击三个页面进行切换,察看是否有相应的报错,发现即进行批改并从新运行测试。

第六步、迁入复杂度最高的聊天相干组件。

以单聊作为阐明示例:

1)迁入单聊入口组件[pages/chatroom]

chatroom 组件(groupChatroom 作用雷同)为单聊性能聊天的入口组件,pages 中其余组件发动单聊聊天时均会跳转至该组件, 而该组件同时又承载 components 下的 chat 组件作为容器造成聊天性能。

将 chatroom 组件 copy 至容器我的项目 pages 下并配置路由映射,为了语义化将 chatroom 更名为 singleChatEntry,并进行语法革新,此时 singleChatEntry 如下:

不要忘了,路由门路配套也要从 chatroom 更名为 singleChatEntry

<template>
  <chat
    id="chat"
    ref="chatComp"
    :chatParams="chatParams"
    chatType="singleChat"
  ></chat>
</template>

<script setup>
import {ref, reactive} from 'vue';
import {
  onLoad,
  onUnload,
  onPullDownRefresh,
  onNavigationBarButtonTap,
} from '@dcloudio/uni-app';
import disp from '@/utils/broadcast';
import chat from '@/components/chat/chat.vue';

const chatComp = ref(null);
let chatParams = reactive({});
onNavigationBarButtonTap(() => {
  uni.navigateTo({url: `/pages/moreMenu/moreMenu?username=${chatParams.your}&type=singleChat`,
  });
});
onLoad((options) => {let params = JSON.parse(options.username);
  chatParams = Object.assign(chatParams, params);
  // 生成的支付宝小程序在 onLoad 里获取不到,这里放到全局变量下
  uni.username = params;
  uni.setNavigationBarTitle({title: params?.yourNickName || params?.your,});
});
onPullDownRefresh(() => {uni.showNavigationBarLoading();
  chatComp.value.getMore();
  // 进行下拉动作
  uni.hideNavigationBarLoading();
  uni.stopPullDownRefresh();});

onUnload(() => {disp.fire('em.chatroom.leave');
});
</script>
<style>
    @import './singleChatEntry.css';
</style>
2)残缺迁入 components 组件

components 组件构造如上图,因为音视频性能曾经废除本次迁徙决定剔除,但目前迁徙计划采取“抓大放小,后续清理”的策略先一起迁入,后续剔除。

引入之后运行起来之后会发现有很多 require not a function 字眼的谬误,同样咱们要将所有 CommonJS 的导出批改为 ESM 导出,剩下的则是一点一点的去进行语法革新,整个 chat 下其实波及组件十分多,因为 IM 所有音讯的收发,以及渲染均囊括在此组件。

这里提一下 msgpackager.js、msgstorage.js、msgtype.js、pushStorage.js 几个 js 文件的作用。

msgpackager.js 次要为将收发的 IM 音讯进行构造重组

msgstorage.js 将收发音讯进行本地缓存

msgtype.js 音讯类型以及聊天类型的常量文件

pushStorage.js 推送解决相干

迁入进去之后将开始针对大大小小十几个文件进行语法以及引入革新,另外其中个别文件还牵扯到应用的 uviewUI 那么则须要进行重写,最终通过革新以及剔除不再应用的组件以及音视频相干代码之后,构造如图:

有一点较为根底然而还是要强调留神的事项要提一下,在 components/chat 下的组件革新中经常出现父子组件的调用,那么父组件在应用子组件的办法的时候,因为 Vue3 中不能再通过相似 $ref 间接去调用子组件中的办法或者值,子组件须要通过 defineExpose 被动进行裸露方可应用,这个须要进行留神。

迁徙中发现 H5 的录音采纳的 recorder-core.js 库,js 按需导入中有用到 require,那么须要改写为 import 导入,然而发现实例化时发现仍然不是一个构造函数,通过改写从 window 下拜访即失常应用,相干代码如下:

    /* 原代码片段 */
    handleRecording(e) {const sysInfo = uni.getSystemInfoSync();
      console.log("getSystemInfoSync", sysInfo);
      if (sysInfo.app === "alipay") {
        // https://forum.alipay.com/mini-app/post/7301031?ant_source=opendoc_recommend
        uni.showModal({content: "支付宝小程序不反对语音音讯,请查看支付宝相干 api 理解详情"});
        return;
      }
      let me = this;
      me.recordClicked = true;
      // h5 不反对 uni.getRecorderManager, 须要独自解决
      if (sysInfo.uniPlatform === "web") {import("../../../../../recorderCore/src/recorder-core").then((Recorder) => {require("../../../../../recorderCore/src/engine/mp3");
          require("../../../../../recorderCore/src/engine/mp3-engine");
          if (me.recordClicked == true) {clearInterval(recordTimeInterval);
            me.initStartRecord(e);
            me.rec = new Recorder.default({type: "mp3"});
            me.rec.open(() => {me.saveRecordTime();
                me.rec.start();},
              (msg, isUserNotAllow) => {if (isUserNotAllow) {
                  uni.showToast({
                    title: "鉴权失败,请重试",
                    icon: "none"
                  });
                } else {
                  uni.showToast({
                    title: ` 开启失败,请重试 `,
                    icon: "none"
                  });
                }
              }
            );
          }
        });
      } else {setTimeout(() => {if (me.recordClicked == true) {me.executeRecord(e);
          }
        }, 350);
      }
    }
    /* 调整后代码片段 */
    const handleRecording = async (e) => {const sysInfo = uni.getSystemInfoSync();
      console.log('getSystemInfoSync', sysInfo);
      if (sysInfo.app === 'alipay') {
        // https://forum.alipay.com/mini-app/post/7301031?ant_source=opendoc_recommend
        uni.showModal({content: '支付宝小程序不反对语音音讯,请查看支付宝相干 api 理解详情',});
        return;
      }
      audioState.recordClicked = true;
      // h5 不反对 uni.getRecorderManager, 须要独自解决
      if (sysInfo.uniPlatform === 'web') {// console.log('>>>>>> 进入了 web 层面注册页面');
        // #ifdef H5
        await import('@/recorderCore/src/recorder-core');
        await import('@/recorderCore/src/engine/mp3');
        await import('@/recorderCore/src/engine/mp3-engine');
        if (audioState.recordClicked == true) {clearInterval(recordTimeInterval);
          initStartRecord(e);
          audioState.rec = new window.Recorder({type: 'mp3',});
          audioState.rec.open(() => {saveRecordTime();
              audioState.rec.start();},
            (msg, isUserNotAllow) => {if (isUserNotAllow) {
                uni.showToast({
                  title: '鉴权失败,请重试',
                  icon: 'none',
                });
              } else {
                uni.showToast({
                  title: ` 开启失败,请重试 `,
                  icon: 'none',
                });
              }
            }
          );
        }
        // #endif
      } else {setTimeout(() => {if (audioState.recordClicked == true) {executeRecord(e);
          }
        }, 350);
      }
};
3)启动进行后续调整测试

启动之后验证发现更多的是一些细节问题,同样边改边验证。

后续总结

在首期迁徙 vue2 降级 vue3 的工作中其实难度并没有很大,次要的工作量集中在语法的批改变更上,好在 uni-app 中能够同步去写 vue2 与 vue3 两种语法代码,这样有助于在引入之后陆续进行语法变更,另外迁徙之后开发体验启动速度的确快了很多,接下来就能够腾出手针对 uni-app-demo 源码代码进行整体品质晋升,敬请期待 …

此次降级后的源码地址:https://github.com/easemob/webim-uniapp-demo/tree/vue3

正文完
 0