网络日志

互动接收端 UI 组件如何兼容 Vue2 和 Vue3

互动接收端 UI 组件如何兼容 Vue2 和 Vue3

互动接收端 SDK

保利威云直播产品可以在直播过程发起签到、抽奖、问卷等互动。对应地,在观看端也需要呈现这些互动。为了让保利威云直播观看页以及客户定制观看页都能方便地接入这些功能,我们把互动功能做成了一个 SDK,即互动接收端 SDK。

互动接收端 SDK 严格按照逻辑UI 分离的方式开发。基于逻辑层可以开发界面不同的 UI,甚至是不同技术栈的 UI。而默认的 UI 则是基于 Vue.js 2.6 开发的。

众所周知,Vue.js 3.x 已经正式发布。如果把基于 Vue.js 2.6 开发的这套 UI 组件用于 Vue.js 3.x 的项目,会出现报错;如果重新开发一套能适配 Vue.js 3.x 的 UI 组件,则需要维护两套代码,不利于后续功能迭代更新。

本文主要讲述如何在一份代码的基础上,构建出两个版本的 Vue.js 组件,从而适配 Vue.js 2.6 与 Vue.js 3.x(下文分别把 Vue.js 2.6 和 Vue.js 3.x 简称为 Vue2 和 Vue3)。

Vue2 和 Vue3 兼容架构介绍

在原有的代码基础上,分别编译构建出Vue2和Vue3的包。

  • 优点: 后续功能有新增或者优化,互动功能的源代码只需要改动一次。
  • 缺点: 无法引用不兼容Vue2和Vue3的UI 组件库。 不能使用 Vue2、Vue3 不兼容的语法,包括 Vue3 新特性。

为UI组件项目搭建Vue3构建环境

原来UI组件项目目录结构如下:

  • 目录 ui

    • 目录 vue2

      • 目录 build
      • 目录 src
      • 文件 package.json
      • ..

为了搭建 Vue3 的构建环境,在ui目录下中新建一个vue3目录,并将vue2目录下除src以外的所有配置文件拷贝过来。目录结构如下:

  • 目录 ui

    • 目录 vue2

      • 目录 build
      • 目录 src
      • 文件package.json
      • ...
    • 目录 vue3

      • 目录 build
      • 文件 package.json
      • ...

接下来修改vue3目录下的配置文件,在package.json将vue更新到 3.1, 安装相同版本的 @vue/compat@vue/compiler-sfc,并将vue-loader升级到16以上

"dependencies": {
-  "vue": "^2.6.12",
+  "vue": "^3.1.0",
+  "@vue/compat": "^3.1.0",
-  "vue-loader": "15.9.7"
+  "vue-loader": "16.0.0"
   ...
},
"devDependencies": {
+  "@vue/compiler-sfc": "^3.1.0"
}

在 Webpack 的构建配置中为 vue 设置别名 @vue/compat后,可以检测在Vue3中不兼容的代码片段。设置webpack入口文件路径指向src目录下的源代码。

const srcDirname = path.resolve(__dirname, '../../vue2/src');
module.exports = {
    entry: path.join(srcPath, 'main.js'),
    resolve: {
        alias: {
            vue: '@vue/compat',
            '@': srcDirname
        }
    },
    ...
}

至此环境搭建完毕。

处理代码兼容问题

运行项目后,打开demo页面仍有问题,并且控制台出现异常信息,需要修复这些问题,项目才能运行正常。

问题主要是因为以下原因导致的:(更多兼容问题参考官方文档

  • Vue2和Vue3分别通过 extend 和 createApp创建节点
  • 在Vue3中$options 属性不可修改
  • Vue3 不支持过滤器filter
  • Vue3 不支持2.6以下的插槽语法
  • Vue3 不支持全局方法$set
  • 生命周期destroy和beforeDestroy,在Vue3更名为unmounted和beforeUnmount

Vue2和Vue3分别通过 extend 和 createApp创建节点

对于无法同时在Vue2和Vue3运行的代码,可通过判断版本号分别写两套代码

import Vue from 'vue';
function isVue3() {
    return Vue.version.startsWith('3.');
}

if(isVue3()) {
    // Vue3 环境代码
    const TipApp = Vue.createApp(SubmitTip, propsData);
    TipApp.mount(wrapContain);
    // 业务代码...
} else {
    // Vue2 环境代码
    const Componet = Vue.extend(SubmitTip, propsData);
    const TipApp = new Componet({
        propsData
    });
    TipApp.$mount(wrapContain);
    // 业务代码...
}

在Vue3中$options 属性不可修改

原因:在Vue2中 $options可以动态修改,但在Vue3中$options属性只可读,不可修改。

解决方案: 将原本放在$options 的逻辑代码,迁移到data()下。

// 原来的写法
export default {
  $i18n: null,
  provide() {
    // 初始化多语言实例
    this.$options.$i18n = new I18n(this.$options.langs);
    // 设置响应式语言属性
    this.$options.$i18n.updateLocale(() => this.lang);
    return {
      getI18n: () => this.$options.$i18n
    };
  },
  ...
}

// 替代写法
export default {
  data() {
    return {
      _i18n: null
    }
  }
  provide() {
    // 初始化多语言实例
    this._i18n = new I18n(this.langs);
    // 设置响应式语言属性
    this._i18n.updateLocale(() => this.lang);
    return {
      getI18n: () => this._i18n,
    };
  },
  ...
}

Vue3 不支持过滤器filter

可以使用 methods 代替 filter。例如:

<!-- 过滤器filter写法 -->
 <div> {{ date  |  dateFilter }}</div>

<!-- methods写法 -->
 <div> {{ dateFilter(date) }}</div>

Vue3 不支持2.6以下的插槽语法

使用最新插槽语法可以在Vue2和Vue3同时运行。例如:

<!-- 废弃语法-->
<template>
    <template slot="header">
      <h1> title </h1>
    </template>
</template>
<!--2.6语法-->
<template>
    <template v-slot:header>
      <h1> title </h1>
    </template>
</template>

Vue3 不支持全局方法$set

原因:在Vue3 $set 中已经被移除。

解决方案:避免使用$set,可以在data()定义或者赋值对象和数组的时候,确保对象属性已经响应式处理。

<!--错误示例-->
<template>
    <div v-for="(item,index) in list">
        名字:{{ item.name }},
        性别:{{ item.sex }},
    </div>
</template>
<script>
export default {
    data() {
        return {
            list: []
        }
    },
    mounted() {
      this.list.push({name: '张三'})
      // 在vue3中不生效
      this.$set(this.list[0], 'sex', 'man');
    }
}
</script>
<!--正确示例-->
<template>
    <div v-for="(item,index) in list">
        名字:{{ item.name }},
        性别:{{ item.sex }},
    </div>
</template>
<script>
export default {
    data() {
        return {
            list: []
        }
    },
    mounted() {
      this.list.push({name: '张三', sex: 'man'})
    }
}
</script>

生命周期destroy和beforeDestroy,在Vue3更名为unmounted和beforeUnmount

通过版本判断使用不同的方法名。

export const BEFORE_DESTROY = isVue3() ? 'beforeUmount' : 'beforeDestory';
export const DESTORY = isVue3() ? 'unmounted' : 'destory';
import { BEFORE_DESTROY } from '@/assets/utils/compat';

export default {
  [BEFORE_DESTROY]() {
    // 业务代码...
  }
}