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 Demi是怎么让你的库一起支撑Vue2和Vue3的

其实便是从不同版别的目录里仿制上述三个文件到外层目录,其中还支撑替换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版别只要一个默许导出:

Vue Demi是怎么让你的库一起支撑Vue2和Vue3的

咱们只看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)
// ...

导入VueVueCompositionAPI插件,而且自动调用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**/

首先导出了两个变量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-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除了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

Vue Demi是怎么让你的库一起支撑Vue2和Vue3的

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办法,而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 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
}

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

到这儿,针对Vue2.7所做的工作就结束了。

v3版别

Vue3比较之前的版别,最大差异是不再供给一个单独的Vue导出:

Vue Demi是怎么让你的库一起支撑Vue2和Vue3的

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目标上,然后也能够看到导出的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] = 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.setVue.delete办法用来给呼应性数据增加或删去特点。