Huanxin uni-app Demo upgrade plan - Vue2 migrated to Vue3 (1)

foreword

Since Huanxin uni-app Demo is a uni-app project converted from a WeChat applet through tools in the early days, after actual use and reuse feedback, it is no longer suitable for current development and use. Therefore, the overall upgrade and transformation plan has been launched. The current phase I plan to manually convert the vue2 code to vue3+vite, and remove the useless project code in the original project. The following is a record of the upgrade operation. If the upgrade process is helpful to everyone, I am very honored~

Preparation

  • [Important] Read the uni-app official website document Vue2 upgrade Vue3 guide document address
  • Investigate which tripartite libraries or methods will not be available in the original Demo migrated to Vue3 ( the main uview UI library does not support Vue3 ).
  • Download and run the uni-app project on the official website of Huanxin (the master branch of the original project). Demo download address
  • Create a container project in HubilderX ( the so-called container project is to create a blank Vue3 template to gradually move the Vue2 project code to this project. )
  • Introduce the uni-ui component in the blank project, mainly to replace the uviewUI component of the original project with its component
  • Confirm the upgrade process and method ( this upgrade adopts the form of gradual syntax modification ) . The main method is to migrate a component, and modify the syntax of a component to vue3. If the component depends on multiple components, first cut off the connection of the relative components (comment method ) , and then gradually release and modify it accordingly.

Core Migration Steps

The first step is to import Huanxin uni-app SDK

The uni-app-demo project of the original Vue2 version imports the SDK package locally. It is not friendly to some students who are used to npm installation and import. Currently, uniSDK already supports npm installation and import, so the original local import js file is changed to install the SDK through npm and import the 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';

The second step, CommonJS import and export rewritten as ESM

There are two reasons for this rewriting:

1. The CommonJS specification is not supported in Vite itself. If it is supported, it needs to be configured separately.

2. There are both CommonJS import methods and ESM import methods in the original project, so take this opportunity to unify.

Up to this point, the main purpose is to change the CommonJS export WebIM instance in the original project to ESM export, and then rewrite all CommonJS specifications to ESM export in the process of grammar transformation, which will not be mentioned in this article, and the example code is as follows

/* 原始项目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;

The third step, move into the App.vue component

Completely copy the App.vue component in the original project (uni's Vue3 template also supports Vue2 code, so you can rest assured to carry out CV)

The changes involved in the App.vue component are to comment out the js files that have not been imported for the time being, and then import them, remove the uview style code in scss, and remove the uview components completely after the introduction.

There are many codes in App.vue. This example has been greatly reduced. The roughly adjusted structure is as follows.

<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>

The fourth step is a small test~ Move into the Login component

First move into a Login component to warm up. After all, starting from login, there are registration, Token login, etc. in the original project, but they are not needed at the moment, so you only need to move into the Login component.

Before moving in, let's understand and think about some features of Vue2's Options API and Vue3 Composition API. The main purpose is to modify the Vue3 syntax at a relatively low cost.
Vue3 templates support setup syntax sugar, so you can directly use setup syntax sugar for syntax transformation.

<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>

During the transformation, some parameters of the original data in the original Vue2 are packaged and renamed using reactive. It is necessary to replace the this., me., and this.setData in the syntax with the state name after the package. In addition, the template must also be replaced synchronously. This will be encountered in all subsequent component transformations.

The Login component requires routing configuration in page.json. Only after the configuration is successful can we run the project and display the page!

At this point, you can start the project and observe to see if the page can be displayed normally. Of course, you can choose whether to run it on the applet or H5 and App.

The fifth step, move to the three tab pages in the "Home page" [conversation session list, mian contact page, setting my page]

Migrate each component. The conversation component is used as an example here. The steps of the other two components are exactly the same. All sample codes will be given at the end of the article.

In the original project, including the migrated App.vue component, there is the following method. Its function is to trigger the onOpened monitoring callback after the Huanxin IM connection is successful, and perform a routing jump to enter the conversation page. Therefore, it is not difficult to understand that the first page to jump after opening is conversation.

    onLoginSuccess: function (myName) {
    
    
      uni.hideLoading();
      uni.redirectTo({
    
    
        url: "../conversation/conversation?myName=" + myName,
      });
    },
  • Copy the conversation (session) component in the original project to the same directory as the container project, and don't forget to configure routing under page.json.

  • Start rewriting the code in the session component

//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

The main thing to note when migrating these three components is to replace this, and the default parameters in the template taken from data in vue2 should also be replaced with variable names wrapped in reactive.

start running adjustments

It is recommended to migrate a component to debug a component, run it to the H5 side, log in from the login page, and click on the three pages to switch, observe whether there is a corresponding error report, modify it and re-run the test if found.

The sixth step is to move in the chat-related components with the highest complexity.

Take a single chat as an example:

1) Move into the single chat entry component [pages/chatroom]

The chatroom component (groupChatroom has the same function) is the entry component of the single-chat function chat. When other components in the pages initiate a single-chat chat, they will jump to this component, and this component also carries the chat component under components as a container to form a chat function.

Copy the chatroom component to the container project pages and configure the routing mapping. In order to semantically change the name of chatroom to singleChatEntry, and perform grammatical transformation, singleChatEntry is as follows:

Don't forget, the routing path matching should also be renamed from chatroom to 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) Completely move into the components component

image.png

The component structure of components is as shown in the figure above. Since the audio and video functions have been abandoned, this migration decided to remove them. However, the current migration plan adopts the strategy of "grasp the large and let go of the small, and follow-up liquidation".

After importing and running, you will find that there are many errors in the words require not a function. Similarly, we will modify all CommonJS exports to ESM exports, and the rest will be modified bit by bit. The whole chat actually involves a lot of components, because all IM message sending and receiving, and rendering are included in this component.

Let me mention the functions of several js files such as msgpackager.js, msgstorage.js, msgtype.js, and pushStorage.js.

msgpackager.js 主要为将收发的IM消息进行结构重组

msgstorage.js 将收发消息进行本地缓存

msgtype.js 消息类型以及聊天类型的常量文件

pushStorage.js 推送处理相关

After moving in, we will start to modify the syntax and import of more than a dozen files, large and small. In addition, some of the files also involve the use of uviewUI, so they need to be rewritten. Finally, after modification and removal of unused components and audio and video related codes, the structure is shown in the figure:
image.png

There is one point that is relatively basic but still needs to be emphasized. In the component transformation under components/chat, parent-child components are often called. When the parent component uses the method of the child component, since Vue3 can no longer directly call the method or value in the child component through something like $ref, the child component needs to be actively exposed through defineExpose before it can be used. This requires attention.

During the migration, it was found that the recorder-core.js library used by H5 recordings, and require is useful in js on-demand import, so it needs to be rewritten as import import, but it is found that it is still not a constructor when it is instantiated, and it can be used normally by rewriting it from the window. The relevant code is as follows:

    /* 原代码片段 */
    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) Start the follow-up adjustment test

After the startup, the verification found that there are more details, and it is also verified while changing.

follow-up summary

In the first phase of migrating vue2 to upgrade vue3, the difficulty is actually not very big. The main workload is concentrated on the modification and change of grammar. Fortunately, uni-app can write both vue2 and vue3 grammar codes simultaneously, which helps to make grammar changes after introduction. In addition, the development experience startup speed after migration is indeed much faster. Next, you can free up your hands to improve the overall quality of uni-app-demo source code, so stay tuned...

The source code address after this upgrade: https://github.com/easemob/webim-uniapp-demo/tree/vue3

Guess you like

Origin blog.csdn.net/huan132456765/article/details/130203629