关于前端:技术分享-如何编写同时兼容-Vue2-和-Vue3-的代码

1次阅读

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

LigaAI 的评论编辑器、附件展现以及富文本编辑器都反对在 Vue2(Web)与 Vue3(VSCode、lDEA)中应用。这样不仅能够在不同 Vue 版本的工程两头共享代码,还能为后续降级 Vue3 缩小肯定妨碍。

那么,同时兼容 Vue2 与 Vue3 的代码该如何实现?业务实际中又有哪些代码精简和优化的小技巧?让咱们先从兼容代码的工程化讲起。

1. 工程化:编写同时兼容 Vue2 与 Vue3 的代码

原理上,兼容工作由两局部实现:

  • 编译阶段:负责依据应用的我的项目环境,主动抉择应用 Vue2 或 Vue3 的 API。应用时,只须要从 Vue-Demi 外面 import 须要应用的 API,就会主动依据环境进行切换;能够分为在浏览器中运行(IIFE)和应用打包工具(cjs、umd、esm)两种状况。
  • 运行阶段:转换 createElement 函数的参数,使 Vue2 与 Vue3 的参数格局统一。Vue2 和 Vue3 Composition API 的区别十分小,运行时 API 最大的区别在于 createElement 函数的参数格局不统一,Vue3 换成了 React JSX 格局。

1.1 编译阶段——IIFE

window中定义一个 VueDemi 变量,而后查看 window 中的 Vue 变量的版本,依据版本 reexport 对应的 API。

    var VueDemi = (function (VueDemi, Vue, VueCompositionAPI) {  
      // Vue 2.7 有不同,这里只列出 2.0 ~ 2.6 的版本
      if (Vue.version.slice(0, 2) === '2.') {for (var key in VueCompositionAPI) {VueDemi[key] = VueCompositionAPI[key]    
        }    
        VueDemi.isVue2 = true
      } else if (Vue.version.slice(0, 2) === '3.') {for (var key in Vue) {VueDemi[key] = Vue[key]
        }    
        VueDemi.isVue3 = true
      }  
        return VueDemi
      })(this.VueDemi,this.Vue,this.VueCompositionAPI)

1.2 编译阶段——打包工具

利用 npm postinstall 的 hook,查看本地的 Vue 版本,而后依据版本 reexport 对应的 API。

    const Vue = loadModule('vue') // 这里是查看本地的 vue 版本
    if (Vue.version.startsWith('2.')) {switchVersion(2)
    }
    else if (Vue.version.startsWith('3.')) {switchVersion(3)
    }
    function switchVersion(version, vue) {copy('index.cjs', version, vue)
      copy('index.mjs', version, vue)
    }
    // VueDemi 本人的 lib 目录下有 v2 v3 v2.7 三个文件夹,别离对应不同的 Vue 版本,Copy 函数的性能就是把须要的版本复制到 lib 目录下
    // 而后在 package.json 外面指向 lib/index.cjs 和 lib/index.mjs
    function copy(name, version, vue) {const src = path.join(dir, `v${version}`, name)
      const dest = path.join(dir, name)
      fs.write(dest, fs.read(src))
    }

1.3 运行阶段 createElement 函数的区别

1.3.1 Vue 2

  • attrs 须要写在 attrs 属性中;
  • on: {click=> {}}
  • scopedSlots 写在 scopedSlots 属性中。
    h(LayoutComponent, {
        staticClass: 'button',
        class: {'is-outlined': isOutlined},
        staticStyle: {color: '#34495E'},
        style: {backgroundColor: buttonColor},
        attrs: {id: 'submit'},
        domProps: {innerHTML: ''},
        on: {click: submitForm},
        key: 'submit-button',
        // 这里只思考 scopedSlots 的状况了
        // 之前的 slots 没必要思考,全副用 scopedSlots 是一样的
        scopedSlots: {header: () => h('div', this.header),
          content: () => h('div', this.content),
        },
      }
    );

1.3.2 Vue 3

  • attrsprops 一样,只需写在最外层;
  • onClick: ()=> {}
  • slot 写在 createElement 函数的第三个参数中。
    h(LayoutComponent, {class: ['button', { 'is-outlined': isOutlined}],
        style: [{color: '#34495E'}, {backgroundColor: buttonColor}],
        id: 'submit',
        innerHTML: '',
        onClick: submitForm,
        key: 'submit-button',
      }, {header: () => h('div', this.header),
        content: () => h('div', this.content),
      }
    );

1.4 残缺代码

    import {h as hDemi, isVue2} from 'vue-demi';

    // 咱们应用的时候应用的 Vue2 的写法,然而 props 还是写在最外层,为了 ts 的智能提醒
    export const h = (
      type: String | Record<any, any>,
      options: Options & any = {},
      children?: any,
    ) => {if (isVue2) {
        const propOut = omit(options, [
          'props',
          // ... 省略了其余 Vue 2 的默认属性如 attrs、on、domProps、class、style
        ]);
        // 这里提取出了组件的 props
        const props = defaults(propOut, options.props || {}); 
        if ((type as Record<string, any>).props) {
          // 这里省略了一些过滤 attrs 和 props 的逻辑,不是很重要
          return hDemi(type, { ...options, props}, children);
        }
        return hDemi(type, { ...options, props}, children);
      }

      const {props, attrs, domProps, on, scopedSlots, ...extraOptions} = options;

      const ons = adaptOnsV3(on); // 处理事件
      const params = {...extraOptions, ...props, ...attrs, ...domProps, ...ons}; // 排除 scopedSlots

      const slots = adaptScopedSlotsV3(scopedSlots); // 解决 slots
      if (slots && Object.keys(slots).length) {
        return hDemi(type, params, {
          default: slots?.default || children,
          ...slots,
        });
      }
      return hDemi(type, params, children);
    };

    const adaptOnsV3 = (ons: Object) => {if (!ons) return null;
      return Object.entries(ons).reduce((ret, [key, handler]) => {
        // 修饰符的转换
        if (key[0] === '!') {key = key.slice(1) + 'Capture';
        } else if (key[0] === '&') {key = key.slice(1) + 'Passive';
        } else if (key[0] === '~') {key = key.slice(1) + 'Once';
        }
        key = key.charAt(0).toUpperCase() + key.slice(1);
        key = `on${key}`;

        return {...ret, [key]: handler };
      }, {});
    };

    const adaptScopedSlotsV3 = (scopedSlots: any) => {if (!scopedSlots) return null;
      return Object.entries(scopedSlots).reduce((ret, [key, slot]) => {if (isFunction(slot)) {return { ...ret, [key]: slot };
        }
        return ret;
      }, {} as Record<string, Function>);
    };

2. 编码技巧:利用代数数据类型精简代码

这里跟大家分享我本人总结的用于优化代码的实践工具。舒适提醒,可能和书本上的原有概念有些不同。

于我而言,掂量一段代码复杂度的办法是看状态数量。状态越少,逻辑、代码就越简略;状态数量越多,逻辑、代码越简单,越容易出错。因而,我认为「好代码」的特色之一就是,在实现业务需要的前提下,尽量减少状态的数量(即大小)。

那么,什么是状态?在 Vue 的场景下,能够这么了解:

  • data 外面的变量就是状态,props、计算属性都不是状态。
  • Composition API 中 refreactive 是状态,而 computed 不是状态。

2.1 什么是「状态」?

状态是能够由零碎外部行为更改的数据,而状态大小是状态所有可能的值的汇合的大小,记作 size(State)。而 代码复杂度 = States.reduce((acc, cur) => acc * size(cur),1)

2.1.1 常见数据类型的状态大小

一些常见的数据类型,比方 unit 的状态大小是 1,在前端里能够是 null、undefined;所有的常量、非状态的大小也是 1。而 Boolean 的状态大小是 2。

NumberString 一类有多个或有限个值的数据类型,在计算状态大小时需明确一点,咱们只关怀状态在业务逻辑中的意义,而不是其具体值,因而辨别会影响业务逻辑的状态值即可。

例如,一个接口返回的数据是一个数字,但咱们只关怀这个数字是负数还是正数,那么这个数字的状态大小就是 2。

2.1.2 复合类型的状态大小

复合类型分为和类型与积类型两种。

和类型状态大小的计算公式为 size(C) = size(A) + size(B),而积类型状态大小的计算公式为 size(C) = size(A) * size(B)

理解完代码优化规范后,咱们通过一个案例阐明如何利用代数数据类型,精简代码。

2.2 案例:评论编辑器的显示管制

在 LigaAI 中,每个评论都有两个编辑器,一个用来编辑评论,一个用来回复评论;且同一时间最多只容许存在一个流动的编辑器。

2.2.1 优化前的做法

为回复组件定义两个布尔变量 IsShowReplyIsShowEdit,通过 v-if 管制是否显示编辑器。点击「回复」按钮时,逻辑如下:

(1) 判断本人的 IsShowReply 是否为 true,如果是,间接返回;

(2) 判断本人的 IsshowEdit,如果为 true 则批改为 false,敞开编辑评论;

(3) 顺次设置所有其余评论组件的 IsShowReplyIsShowEdit 为 false;

(4) 批改本人的 IsShowReply 为 true。

当有 10 个评论组件时,代码复杂度是多少?

    size(CommentComponent) = size(Boolean) * size(Boolean) = 2 * 2 = 4
    size(total) = size(CommentComponent) ^ count(CommentComponent) = 4 ^ 10 = 1048576

只管逻辑上互斥,但这些组件在代码层面毫无关系,能够全副设置为 true。如果代码呈现问题(包含写错),没解决好互斥,这种状况齐全可能呈现。解决互斥还波及查找 dom 和组件,出问题的几率也会大大提高。

2.2.2 优化后的做法

store 中定义一个字符串变量 activeCommentEditor,示意以后流动的评论组件及其类型。

    type CommentId = number;
    type ActiveCommentStatus = `${'Edit' | 'Reply'}${CommentId}` | 'Close'; // TS 的模板字符串类型
    let activeCommentEditor: ActiveCommentStatus = 'Close';

'Close' 外,该变量还由两局部组成。第一局部阐明以后是「编辑评论」还是「回复评论」,第二局部阐明评论的 id。按钮的回调函数(如点击回复),只须要设置

    activeCommentEditor = `Reply${id}`

组件应用时,能够这样

    v-if="activeCommentEditor === `Edit${id}`"
    v-if="activeCommentEditor === `Reply${id}`"

就这么简略,没有判断,没有 dom,没有其余组件。尽管 id 是 number,但于前端而言只是一个常量,所以其大小为 1。那么当有 10 个评论组件时,这段代码的复杂度就是

    size(total) = size('Reply''Edit') * count(Comment) * 1 + size('close') = 2 * 10 * 1 +1 = 21

在理论应用中,咱们发现的确存在 21 种状态;在代码层面,咱们也精准管制了这个值只能在这 21 种正确的状态中,所以出错的几率也大大降低(简直不可能出错)。


以上就是明天想跟大家分享的 Vue2 和 Vue3 代码兼容的实现和优化计划。后续咱们也会分享或补充更多相干案例与残缺代码,请继续关注 LigaAI@SegmentFault。

LigaAI- 新一代智能研发合作平台 助力开发者扬帆远航,欢送申请试用咱们的产品,期待与你一路同行!

正文完
 0