Vue Demi是什么
如果你想开发一个一起支撑Vue2
和Vue3
的库或许想到以下两种办法:
1.创立两个分支,别离支撑Vue2
和Vue3
2.只运用Vue2
和Vue3
都支撑的API
这两种办法都有缺点,第一种很费事,第二种无法运用Vue3
新增的组合式 API
,其完成在Vue2.7+
版别现已内置支撑组合式API
,Vue2.6
及之前的版别也能够运用@vue/composition-api插件来支撑,所以完全能够只写一套代码一起支撑Vue2
和3
。虽然如此,可是实践开发中,同一个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
版别的set
和del
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 || Vue
if (_vue && !_vue['__composition_api_installed__'])
_vue.use(VueCompositionAPI)
}
install(Vue)
// ...
导入Vue
和VueCompositionAPI
插件,而且自动调用Vue.use
办法装置插件。
持续:
// ...
var isVue2 = true
var isVue3 = false
var Vue2 = Vue
var version = Vue.version
export {
isVue2,
isVue3,
Vue,
Vue2,
version,
install,
}
/**VCA-EXPORTS**/
export * from '@vue/composition-api/dist/vue-composition-api.mjs'
/**VCA-EXPORTS**/
首先导出了两个变量isVue2
和isVue3
,方便咱们的库代码判别环境。
然后在导出Vue
的一起,还经过Vue2
的称号再导出了一遍,这是为啥呢,其实是由于Vue2
的API
都是挂载在Vue
目标上,比方我要进行一些全局装备,那么只能这么操作:
import { Vue, isVue2 } from 'vue-demi'
if (isVue2) {
Vue.config.xxx
}
这样在Vue2
的环境中没有啥问题,可是当咱们的库处于Vue3
的环境中时,其实是不需求导入Vue
目标的,由于用不上,可是构建东西不知道,所以它会把Vue3
的一切代码都打包进去,可是Vue3
中很多咱们没有用到的内容是不需求的,可是由于咱们导入了包括一切API
的Vue
目标,所以无法进行去除,所以针对Vue2
版别单独导出一个Vue2
目标,咱们就能够这么做:
import { Vue2 } from 'vue-demi'
if (Vue2) {
Vue2.config.xxx
}
然后后续你会看到在Vue3
的导出中Vue2
是undefined
,这样就能够处理这个问题了。
接着导出了Vue
的版别和install
办法,意味着你能够手动装置VueCompositionAPI
插件。
然后是导出VueCompositionAPI
插件供给的API
,也便是组合式API
,可是能够看到前后有两行注释,还记得前面说到的switchVersion
办法里针对Vue2
版别还履行了updateVue2API
办法,现在来看一看它做了什么工作:
function updateVue2API() {
const ignoreList = ['version', 'default']
// 检查是否装置了composition-api
const 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**/[sS]+/**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
除了version
和default
之外的一切导出内容,最后将:
export * from '@vue/composition-api/dist/vue-composition-api.mjs'
的办法改写成:
export { EffectScope, ... } from '@vue/composition-api/dist/vue-composition-api.mjs'
为什么要过滤掉version
和default
呢,version
是由于现已导出了Vue
的version
了,所以会抵触,本来也不需求,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.warn
function install() {}
export { Vue, Vue2, isVue2, isVue3, install, warn }
// ...
和v2
比较,导出的内容是差不多的,由于不需求装置@vue/composition-api
,所以install
是个空函数,差异在于还导出了一个warn
办法,这个文档里没有说到,可是能够从过往的issues中找到原因,大致便是Vue3
导出了一个warn
办法,而Vue2
的warn
办法在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 vm
var 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] = value
return 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
}
和Vue2
的new Vue
创立Vue
实例不一样,Vue3
是经过createApp
办法,@vue/composition-api
插件polyfill
了这个办法,所以针对Vue2.7
,Vue Demi
手动进行了polyfill
。
到这儿,针对Vue2.7
所做的工作就结束了。
v3版别
Vue3
比较之前的版别,最大差异是不再供给一个单独的Vue
导出:
import * as Vue from 'vue'
var isVue2 = false
var isVue3 = true
var Vue2 = undefined
function install() {}
export {
Vue,
Vue2,
isVue2,
isVue3,
install,
}
// ...
由于默许不导出Vue
目标了,所以经过整体导入import * as Vue
的办法把一切的导出都加载到Vue
目标上,然后也能够看到导出的Vue2
为undefined
,install
同样是个空函数。
持续:
// ...
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] = val
return 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.set
和Vue.delete
办法用来给呼应性数据增加或删去特点。