What is Vue Demi
If you want to develop a library that supports both Vue2
and may think of the following two ways:Vue3
1. Create two branches to support Vue2
andVue3
2. Only use Vue2
and Vue3
support bothAPI
These two methods have disadvantages. The first one is very troublesome, and the second one cannot use the Vue3
newly added composition API
. In fact, the current Vue2.7+
version has built-in support for composition API
, Vue2.6
and 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 Vue2
and at the same time 3
. Even so, in actual development, the same API
source may be imported in different versions. For example ref
, methods can be imported Vue2.7+
directly vue
from , but Vue2.6-
can only be imported @vue/composition-api
from , which will inevitably involve version judgment. Vue Demi is used to solve this problem. It is very simple to use, just Vue Demi
export what you need from it:
import {
ref, reactive, defineComponent } from 'vue-demi'
Vue-demi
It will judge which version to use according to your project Vue
. Specifically, its strategy is as follows:
<=2.6
: derived fromVue
and@vue/composition-api
2.7
: derivedVue
from (compositionAPI
built inVue 2.7
)>=3.0
: derivedVue
from and returned the sum of thepolyfill
two versionsVue 2
set
del
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-demi
it 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 loadModule
the method first:
function loadModule(name) {
try {
return require(name)
} catch (e) {
return undefined
}
}
It's very simple, it's just packaged require
to prevent errors from blocking the code.
Then look at switchVersion
the 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 copy
the 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 commonjs
version file, ESM
version file, and TS
type definition file.
In addition, the method Vue2.6
is executed for the following version updateVue2API
.
updateVue2API
Let's look at the method later, let's take a look at copy
the 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 vue
name, which vue
needs to be used when you set an alias for it.
At this point, Vue Demi
the automatic execution after the installation is completed. In fact, according to Vue
the version installed in the user project, the files are copied from the three corresponding directories as Vue Demi
the entry file of the package, and Vue Demi
three 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 commonjs
module cjs
file, supported files ESM
can be used , and files of the type that mjs
can be used directly on the browser are also provided .iife
Vue
Next, let's take a look at what has been done for the three versions .
v2 version
Vue2.6
Versions have only one default export:
We only look at mjs
the document, and cjs
those 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 Vue
and VueCompositionAPI
plugins, and automatically calls Vue.use
the 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**/
isVue2
First, two variables and are exported isVue3
to facilitate our library code to judge the environment.
Then, at Vue
the same time of exporting, it is also Vue2
exported again through the name. Why is this? In fact, it is because all the objects Vue2
are API
mounted on Vue
the 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, Vue2
there is no problem in the environment, but when our library is in Vue3
the environment, there is actually no need to import Vue
objects, because it is not needed, but the build tool does not know, so it will Vue3
package all the code in it, But Vue3
a lot of content that we don't use is unnecessary, but because we imported all API
the Vue
objects, we can't remove them, so we can Vue2
export 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 Vue3
in the export of , so this problem can be solved.Vue2
undefined
Then exported Vue
the version and install
method, which means you can manually install VueCompositionAPI
the plugin.
Then it is VueCompositionAPI
provided 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 API
mentioned above also executes the method for the version. Now let’s take a look at what it does: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\*\*\/[\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-api
except version
and 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 version
and default
? version
Because it has already been exported Vue
, version
it will conflict. It is not necessary at all, default
that is, the default export. @vue/composition-api
The default export is actually an object containing its install
methods. As you have seen before, you can import it by default @vue/composition-api
. Then Vue.use
install it through, this actually does not need to be Vue Demi
exported, 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-demi
from 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.7
the export of the version. Compared with Vue2.6
the previous version, Vue2.7
it is directly built-in @vue/composition-api
, so in addition to the default export Vue
object, 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 v2
with , the exported content is similar, because it does not need to be installed @vue/composition-api
, so install
it 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 Vue3
exported A warn
method, and Vue2
the warn
method is on Vue.util
the object, so in order to unify the manual export, why V2
doesn't the version manually export one? The reason is very simple, because this method is @vue/composition-api
included in the export.
continue:
// ...
export * from 'vue'
// ...
Export Vue
all 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 Vue
the 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 Vue2
creating an instance, it is through the method, new Vue
this method is plugged in, so it is done manually .Vue
Vue3
createApp
@vue/composition-api
polyfill
Vue2.7
Vue Demi
polyfill
At this point, Vue2.7
what has been done against is over.
v3 version
Vue3
Compared with the previous version, the biggest difference is that there is no longer a separate Vue
export:
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 Vue
all the exports are loaded on Vue
the object through the overall import, and then you can also see that the Vue2
export is also an empty function.undefined
install
continue:
// ...
export * from 'vue'
// ...
There is nothing to say, Vue
all 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 polyfill
two methods are actually @vue/composition-api
provided by the plug-in, because @vue/composition-api
the provided responsive API
implementation does not use Proxy
a proxy, and is still Vue2
implemented based on the responsive system, so Vue2
the limitations of the responsive system still exist. So you need to provide two similar Vue.set
and Vue.delete
methods to add or remove attributes to responsive data.