How Vue Demi allows your library to support both Vue2 and Vue3

What is Vue Demi

If you want to develop a library that supports both Vue2and may think of the following two ways:Vue3

1. Create two branches to support Vue2andVue3

2. Only use Vue2and Vue3support bothAPI

These two methods have disadvantages. The first one is very troublesome, and the second one cannot use the Vue3newly added composition API. In fact, the current Vue2.7+version has built-in support for composition API, Vue2.6and the previous version can also use the @vue/composition-api plug-in to support it. , so it is completely possible to write only one set of code to support Vue2and at the same time 3. Even so, in actual development, the same APIsource may be imported in different versions. For example ref, methods can be imported Vue2.7+directly vuefrom , but Vue2.6-can only be imported @vue/composition-apifrom , which will inevitably involve version judgment. Vue Demi is used to solve this problem. It is very simple to use, just Vue Demiexport what you need from it:

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

Vue-demiIt will judge which version to use according to your project Vue. Specifically, its strategy is as follows:

  • <=2.6: derived from Vueand@vue/composition-api
  • 2.7: derived Vuefrom (composition APIbuilt in Vue 2.7)
  • >=3.0: derived Vuefrom and returned the sum of the polyfilltwo versionsVue 2setdel API

Next, let's take a look at how it is implemented from the perspective of source code.

Fundamental

When we install npm i vue-demiit in our project, it will automatically execute a script:

{
    
    
    "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.`)
}

Import the ones installed in our project vue, and then call the methods according to different versions switchVersion.

Let's take a look at loadModulethe method first:

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

It's very simple, it's just packaged requireto prevent errors from blocking the code.

Then look at switchVersionthe method:

function switchVersion(version, vue) {
    
    
  copy('index.cjs', version, vue)
  copy('index.mjs', version, vue)
  copy('index.d.ts', version, vue)

  if (version === 2)
    updateVue2API()
}

After executing copythe method, we can roughly know that it is a copy file from the function name, and the types of the three files are also very clear, which are commonjsversion file, ESMversion file, and TStype definition file.

In addition, the method Vue2.6is executed for the following version updateVue2API.

updateVue2APILet's look at the method later, let's take a look at copythe method first:

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')
}

In fact, it is to copy the above three files from the directory of different versions to the outer directory, which also supports the replacement vuename, which vueneeds to be used when you set an alias for it.

At this point, Vue Demithe automatic execution after the installation is completed. In fact, according to Vuethe version installed in the user project, the files are copied from the three corresponding directories as Vue Demithe entry file of the package, and Vue Demithree module syntaxes are supported:

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

The default entry is a commonjsmodule cjsfile, supported files ESMcan be used , and files of the type that mjscan be used directly on the browser are also provided .iife

VueNext, let's take a look at what has been done for the three versions .

v2 version

Vue2.6Versions have only one default export:

We only look at mjsthe document, and cjsthose who are interested can read it by themselves:

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)
// ...

Imports Vueand VueCompositionAPIplugins, and automatically calls Vue.usethe method install plugin.

continue:

// ...
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**/

isVue2First, two variables and are exported isVue3to facilitate our library code to judge the environment.

Then, at Vuethe same time of exporting, it is also Vue2exported again through the name. Why is this? In fact, it is because all the objects Vue2are APImounted on Vuethe object. For example, if I want to perform some global configurations, then I can only do this:

import {
    
     Vue, isVue2 } from 'vue-demi'

if (isVue2) {
    
    
  Vue.config.xxx
}

In this way, Vue2there is no problem in the environment, but when our library is in Vue3the environment, there is actually no need to import Vueobjects, because it is not needed, but the build tool does not know, so it will Vue3package all the code in it, But Vue3a lot of content that we don't use is unnecessary, but because we imported all APIthe Vueobjects, we can't remove them, so we can Vue2export an object separately for the version Vue2, we can do this:

import {
    
     Vue2 } from 'vue-demi'

if (Vue2) {
    
    
  Vue2.config.xxx
}

Then you will see that it is Vue3in the export of , so this problem can be solved.Vue2undefined

Then exported Vuethe version and installmethod, which means you can manually install VueCompositionAPIthe plugin.

Then it is VueCompositionAPIprovided by the export plug-in API, that is, combined , but you can see that there are two lines of comments before and after. Remember that the method APImentioned above also executes the method for the version. Now let’s take a look at what it does:switchVersionVue2updateVue2API

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\*\*\/[\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')
}

The main thing to do is to check whether it is installed @vue/composition-api, and then filter out all the exported content @vue/composition-apiexcept versionand default, finally:

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

is rewritten as:

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

Why filter out versionand default? versionBecause it has already been exported Vue, versionit will conflict. It is not necessary at all, defaultthat is, the default export. @vue/composition-apiThe default export is actually an object containing its installmethods. As you have seen before, you can import it by default @vue/composition-api. Then Vue.useinstall it through, this actually does not need to be Vue Demiexported, otherwise it will look very strange like the following:

import VueCompositionAPI from 'vue-demi'

At this point, all the content is exported, and then we can import various required content vue-demifrom it , such as:

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

v2.7 version

Next, let's take a look at how to deal with Vue2.7the export of the version. Compared with Vue2.6the previous version, Vue2.7it is directly built-in @vue/composition-api, so in addition to the default export Vueobject, the combined formula is also exported 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 }
// ...

Compared v2with , the exported content is similar, because it does not need to be installed @vue/composition-api, so installit is an empty function, the difference is that a method is also exported warn, which is not mentioned in this document, but the reason can be found from past issues , which is roughly Vue3exported A warnmethod, and Vue2the warnmethod is on Vue.utilthe object, so in order to unify the manual export, why V2doesn't the version manually export one? The reason is very simple, because this method is @vue/composition-apiincluded in the export.

continue:

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

Export Vueall the exports in the above figure, including version, combined API, but note that this way of writing will not export the default Vue, so if you use the default import like the following, you will not get Vuethe object:

import Vue from 'vue-demi'

continue:

// ...
// 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
}

It is different from Vue2creating an instance, it is through the method, new Vuethis method is plugged in, so it is done manually .VueVue3createApp@vue/composition-apipolyfillVue2.7Vue Demipolyfill

At this point, Vue2.7what has been done against is over.

v3 version

Vue3Compared with the previous version, the biggest difference is that there is no longer a separate Vueexport:

import * as Vue from 'vue'

var isVue2 = false
var isVue3 = true
var Vue2 = undefined

function install() {
    
    }

export {
    
    
  Vue,
  Vue2,
  isVue2,
  isVue3,
  install,
}
// ...

Because the object is not exported by default Vue, import * as Vueall the exports are loaded on Vuethe object through the overall import, and then you can also see that the Vue2export is also an empty function.undefinedinstall

continue:

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

There is nothing to say, Vueall the exported content is exported directly.

continue:

// ...
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]
}

The last polyfilltwo methods are actually @vue/composition-apiprovided by the plug-in, because @vue/composition-apithe provided responsive APIimplementation does not use Proxya proxy, and is still Vue2implemented based on the responsive system, so Vue2the limitations of the responsive system still exist. So you need to provide two similar Vue.setand Vue.deletemethods to add or remove attributes to responsive data.

Guess you like

Origin blog.csdn.net/sinat_33488770/article/details/128294084