Vue Demi是如何让你的库同时支持Vue2和Vue3的
创始人
2024-04-22 10:15:37
0

Vue Demi是什么

如果你想开发一个同时支持Vue2Vue3的库可能想到以下两种方式:

1.创建两个分支,分别支持Vue2Vue3

2.只使用Vue2Vue3都支持的API

这两种方式都有缺点,第一种很麻烦,第二种无法使用Vue3新增的组合式 API,其实现在Vue2.7+版本已经内置支持组合式APIVue2.6及之前的版本也可以使用@vue/composition-api插件来支持,所以完全可以只写一套代码同时支持Vue23。虽然如此,但是实际开发中,同一个API在不同的版本中可能导入的来源不一样,比如ref方法,在Vue2.7+中直接从vue中导入,但是在Vue2.6-中只能从@vue/composition-api中导入,那么必然会涉及到版本判断,Vue Demi就是用来解决这个问题,使用很简单,只要从Vue Demi中导出你需要的内容即可:

import { ref, reactive, defineComponent } from 'vue-demi'

Vue-demi会根据你的项目判断到底使用哪个版本的Vue,具体来说,它的策略如下:

  • <=2.6: 从Vue@vue/composition-api中导出
  • 2.7: 从Vue中导出(组合式API内置于Vue 2.7中)
  • >=3.0: 从Vue中导出,并且还polyfill了两个Vue 2版本的setdel API

接下来从源码角度来看一下它具体是如何实现的。

基本原理

当我们使用npm i vue-demi在我们的项目里安装完以后,它会自动执行一个脚本:

{"scripts": {"postinstall": "node ./scripts/postinstall.js"}
}
// postinstall.js
const { switchVersion, loadModule } = require('./utils')const Vue = loadModule('vue')if (!Vue || typeof Vue.version !== 'string') {console.warn('[vue-demi] Vue is not found. Please run "npm install vue" to install.')
}
else if (Vue.version.startsWith('2.7.')) {switchVersion(2.7)
}
else if (Vue.version.startsWith('2.')) {switchVersion(2)
}
else if (Vue.version.startsWith('3.')) {switchVersion(3)
}
else {console.warn(`[vue-demi] Vue version v${Vue.version} is not suppported.`)
}

导入我们项目里安装的vue,然后根据不同的版本分别调用switchVersion方法。

先看一下loadModule方法:

function loadModule(name) {try {return require(name)} catch (e) {return undefined}
}

很简单,就是包装了一下require,防止报错阻塞代码。

然后看一下switchVersion方法:

function switchVersion(version, vue) {copy('index.cjs', version, vue)copy('index.mjs', version, vue)copy('index.d.ts', version, vue)if (version === 2)updateVue2API()
}

执行了copy方法,从函数名可以大概知道是复制文件,三个文件的类型也很清晰,分别是commonjs版本的文件、ESM版本的文件、TS类型定义文件。

另外还针对Vue2.6及一下版本执行了updateVue2API方法。

updateVue2API方法我们后面再看,先看一下copy方法:

const dir = path.resolve(__dirname, '..', 'lib')function copy(name, version, vue) {vue = vue || 'vue'const src = path.join(dir, `v${version}`, name)const dest = path.join(dir, name)let content = fs.readFileSync(src, 'utf-8')content = content.replace(/'vue'/g, `'${vue}'`)try {fs.unlinkSync(dest)} catch (error) { }fs.writeFileSync(dest, content, 'utf-8')
}

其实就是从不同版本的目录里复制上述三个文件到外层目录,其中还支持替换vue的名称,这当你给vue设置了别名时需要用到。

到这里,Vue Demi安装完后自动执行的事情就做完了,其实就是根据用户项目中安装的Vue版本,分别从三个对应的目录中复制文件作为Vue Demi包的入口文件,Vue Demi支持三种模块语法:

{"main": "lib/index.cjs","jsdelivr": "lib/index.iife.js","unpkg": "lib/index.iife.js","module": "lib/index.mjs","types": "lib/index.d.ts"
}

默认入口为commonjs模块cjs文件,支持ESM的可以使用mjs文件,同时还提供了可以直接在浏览器上使用的iife类型的文件。

接下来看一下分别针对三种版本的Vue具体都做了什么。

v2版本

Vue2.6版本只有一个默认导出:

我们只看mjs文件,cjs有兴趣的可以自行阅读:

import Vue from 'vue'
import VueCompositionAPI from '@vue/composition-api/dist/vue-composition-api.mjs'function install(_vue) {_vue = _vue || Vueif (_vue && !_vue['__composition_api_installed__'])_vue.use(VueCompositionAPI)
}install(Vue)
// ...

导入VueVueCompositionAPI插件,并且自动调用Vue.use方法安装插件。

继续:

// ...
var isVue2 = true
var isVue3 = false
var Vue2 = Vue
var version = Vue.versionexport {isVue2,isVue3,Vue,Vue2,version,install,
}/**VCA-EXPORTS**/
export * from '@vue/composition-api/dist/vue-composition-api.mjs'
/**VCA-EXPORTS**/

首先导出了两个变量isVue2isVue3,方便我们的库代码判断环境。

然后在导出Vue的同时,还通过Vue2的名称再导出了一遍,这是为啥呢,其实是因为Vue2API都是挂载在Vue对象上,比如我要进行一些全局配置,那么只能这么操作:

import { Vue, isVue2 } from 'vue-demi'if (isVue2) {Vue.config.xxx
}

这样在Vue2的环境中没有啥问题,但是当我们的库处于Vue3的环境中时,其实是不需要导入Vue对象的,因为用不上,但是构建工具不知道,所以它会把Vue3的所有代码都打包进去,但是Vue3中很多我们没有用到的内容是不需要的,但是因为我们导入了包含所有APIVue对象,所以无法进行去除,所以针对Vue2版本单独导出一个Vue2对象,我们就可以这么做:

import { Vue2 } from 'vue-demi'if (Vue2) {Vue2.config.xxx
}

然后后续你会看到在Vue3的导出中Vue2undefined,这样就可以解决这个问题了。

接着导出了Vue的版本和install方法,意味着你可以手动安装VueCompositionAPI插件。

然后是导出VueCompositionAPI插件提供的API,也就是组合式API,但是可以看到前后有两行注释,还记得前面提到的switchVersion方法里针对Vue2版本还执行了updateVue2API方法,现在来看一看它做了什么事情:

function updateVue2API() {const ignoreList = ['version', 'default']// 检查是否安装了composition-apiconst VCA = loadModule('@vue/composition-api')if (!VCA) {console.warn('[vue-demi] Composition API plugin is not found. Please run "npm install @vue/composition-api" to install.')return}// 获取除了version、default之外的其他所有导出const exports = Object.keys(VCA).filter(i => !ignoreList.includes(i))// 读取ESM语法的入口文件const esmPath = path.join(dir, 'index.mjs')let content = fs.readFileSync(esmPath, 'utf-8')// 将export * 替换成 export { xxx }的形式content = content.replace(/\/\*\*VCA-EXPORTS\*\*\/[\s\S]+\/\*\*VCA-EXPORTS\*\*\//m,
`/**VCA-EXPORTS**/
export { ${exports.join(', ')} } from '@vue/composition-api/dist/vue-composition-api.mjs'
/**VCA-EXPORTS**/`)// 重新写入文件fs.writeFileSync(esmPath, content, 'utf-8')
}

主要做的事情就是检查是否安装了@vue/composition-api,然后过滤出了@vue/composition-api除了versiondefault之外的所有导出内容,最后将:

export * from '@vue/composition-api/dist/vue-composition-api.mjs'

的形式改写成:

export { EffectScope, ... } from '@vue/composition-api/dist/vue-composition-api.mjs'

为什么要过滤掉versiondefault呢,version是因为已经导出了Vueversion了,所以会冲突,本来也不需要,default即默认导出,@vue/composition-api的默认导出其实是一个包含它的install方法的对象,前面也看到了,可以默认导入@vue/composition-api,然后通过Vue.use来安装,这个其实也不需要从Vue Demi导出,不然像下面这样就显得很奇怪:

import VueCompositionAPI from 'vue-demi'

到这里,就导出所有内容了,然后我们就可以从vue-demi中导入各种需要的内容了,比如:

import { isVue2, Vue, ref, reactive, defineComponent } from 'vue-demi'

v2.7版本

接下来看一下是如何处理Vue2.7版本的导出的,和Vue2.6之前的版本相比,Vue2.7直接内置了@vue/composition-api,所以除了默认导出Vue对象外还导出了组合式API

import Vue from 'vue'var isVue2 = true
var isVue3 = false
var Vue2 = Vue
var warn = Vue.util.warnfunction install() {}export { Vue, Vue2, isVue2, isVue3, install, warn }
// ...

v2相比,导出的内容是差不多的,因为不需要安装@vue/composition-api,所以install是个空函数,区别在于还导出了一个warn方法,这个文档里没有提到,但是可以从过往的issues中找到原因,大致就是Vue3导出了一个warn方法,而Vue2warn方法在Vue.util对象上,所以为了统一手动导出,为什么V2版本不手动导出一个呢,原因很简单,因为这个方法在@vue/composition-api的导出里有。

继续:

// ...
export * from 'vue'
// ...

导出上图中Vue所有的导出,包括version、组合式API,但是要注意这种写法不会导出默认的Vue,所以如果你像下面这样使用默认导入是获取不到Vue对象的:

import Vue from 'vue-demi'

继续:

// ...
// createApp polyfill
export function createApp(rootComponent, rootProps) {var vmvar provide = {}var app = {config: Vue.config,use: Vue.use.bind(Vue),mixin: Vue.mixin.bind(Vue),component: Vue.component.bind(Vue),provide: function (key, value) {provide[key] = valuereturn this},directive: function (name, dir) {if (dir) {Vue.directive(name, dir)return app} else {return Vue.directive(name)}},mount: function (el, hydrating) {if (!vm) {vm = new Vue(Object.assign({ propsData: rootProps }, rootComponent, { provide: Object.assign(provide, rootComponent.provide) }))vm.$mount(el, hydrating)return vm} else {return vm}},unmount: function () {if (vm) {vm.$destroy()vm = undefined}},}return app
}

Vue2new Vue创建Vue实例不一样,Vue3是通过createApp方法,@vue/composition-api插件polyfill了这个方法,所以针对Vue2.7Vue Demi手动进行了polyfill

到这里,针对Vue2.7所做的事情就结束了。

v3版本

Vue3相比之前的版本,最大区别是不再提供一个单独的Vue导出:

import * as Vue from 'vue'var isVue2 = false
var isVue3 = true
var Vue2 = undefinedfunction install() {}export {Vue,Vue2,isVue2,isVue3,install,
}
// ...

因为默认不导出Vue对象了,所以通过整体导入import * as Vue的方式把所有的导出都加载到Vue对象上,然后也可以看到导出的Vue2undefinedinstall同样是个空函数。

继续:

// ...
export * from 'vue'
// ...

没啥好说的,直接导出Vue的所有导出内容。

继续:

// ...
export function set(target, key, val) {if (Array.isArray(target)) {target.length = Math.max(target.length, key)target.splice(key, 1, val)return val}target[key] = valreturn val
}export function del(target, key) {if (Array.isArray(target)) {target.splice(key, 1)return}delete target[key]
}

最后polyfill了两个方法,这两个方法实际上是@vue/composition-api插件提供的,因为@vue/composition-api提供的响应性API实现上并没有使用Proxy代理,仍旧是基于Vue2的响应系统来实现的,所以Vue2中响应系统的限制仍旧还是存在的,所以需要提供两个类似Vue.setVue.delete方法用来给响应性数据添加或删除属性。

相关内容

热门资讯

银河麒麟V10SP1高级服务器... 银河麒麟高级服务器操作系统简介: 银河麒麟高级服务器操作系统V10是针对企业级关键业务...
【NI Multisim 14...   目录 序言 一、工具栏 🍊1.“标准”工具栏 🍊 2.视图工具...
不能访问光猫的的管理页面 光猫是现代家庭宽带网络的重要组成部分,它可以提供高速稳定的网络连接。但是,有时候我们会遇到不能访问光...
AWSECS:访问外部网络时出... 如果您在AWS ECS中部署了应用程序,并且该应用程序需要访问外部网络,但是无法正常访问,可能是因为...
Android|无法访问或保存... 这个问题可能是由于权限设置不正确导致的。您需要在应用程序清单文件中添加以下代码来请求适当的权限:此外...
AWSElasticBeans... 在Dockerfile中手动配置nginx反向代理。例如,在Dockerfile中添加以下代码:FR...
月入8000+的steam搬砖... 大家好,我是阿阳 今天要给大家介绍的是 steam 游戏搬砖项目,目前...
​ToDesk 远程工具安装及... 目录 前言 ToDesk 优势 ToDesk 下载安装 ToDesk 功能展示 文件传输 设备链接 ...
北信源内网安全管理卸载 北信源内网安全管理是一款网络安全管理软件,主要用于保护内网安全。在日常使用过程中,卸载该软件是一种常...
AWS管理控制台菜单和权限 要在AWS管理控制台中创建菜单和权限,您可以使用AWS Identity and Access Ma...