vue?demi支持sfc方式的vue2vue3通用庫(kù)開(kāi)發(fā)詳解
背景
隨著vue3的逐漸成熟,公司項(xiàng)目逐漸會(huì)存在vue2和vue3項(xiàng)目共存的情況,兼容vue2和vue3的公共組件開(kāi)發(fā)能讓老項(xiàng)目較好地過(guò)渡到vue3。研究了vue-demi的源碼和demo,發(fā)現(xiàn)vue-demi只是簡(jiǎn)單地根據(jù)vue版本生成對(duì)應(yīng)的類似中間件的東西,而且render函數(shù)也只是做了簡(jiǎn)單的中轉(zhuǎn)處理;
國(guó)外大佬寫(xiě)了一個(gè)h-demi解決了vue2/vue3的render函數(shù)attrs屬性的問(wèn)題,這里我就直接貼issue鏈接,不做過(guò)多說(shuō)明了: github.com/vueuse/vue-…
雖然vue-demi沒(méi)有提供sfc的兼容方案,但是其實(shí)仔細(xì)想一下,sfc的解析處理也不應(yīng)該是由vue-demi來(lái)解決,應(yīng)該是交給打包工具將template轉(zhuǎn)成render,而vue-demi只需要關(guān)注composition-api就行;于是往著這個(gè)思路,花了幾天時(shí)間研究一下vue2.6、vue2.7和vue3的sfc-compiler,得到以下開(kāi)發(fā)方案。
技術(shù)要點(diǎn)
vue-demi
查看源碼可以發(fā)現(xiàn),vue-demi的工作是通過(guò)postinstall和 npx vue-demi-fix指令,判斷當(dāng)前項(xiàng)目安裝的vue版本,然后將對(duì)應(yīng)版本的插件復(fù)制到lib的根目錄,其插件的功能就是抹平vue2和vue3版本使用composition-api時(shí)的差異;
<=2.6: exports from vue + @vue/composition-api with plugin auto installing.
2.7: exports from vue (Composition API is built-in in Vue 2.7).
>=3.0: exports from vue, with polyfill of Vue 2's set and del API.

sfc compiler
在日常開(kāi)發(fā)中寫(xiě)的vue template,實(shí)際上最后是通過(guò)sfc-compiler轉(zhuǎn)成render函數(shù)輸出的,而vue2和vue3的sfc-compiler是互不兼容的。尤大大已經(jīng)提供了vue2.6.x,vue2.7和vue3的compiler,其實(shí)我們只需要在打包工具寫(xiě)判斷不同的vue版本使用不同的compiler邏輯即可,本文是基于vite開(kāi)發(fā),以下對(duì)應(yīng)的打包插件:
- vue2.6: vite-plugin-vue2@2.6.14 + vue-template-compiler@2.6.14
- vue2.7: vite-plugin-vue2@2.7.9 + vue-template-compiler@2.7.9; 或者@vitejs/plugin-vue2 + @vue/compiler-sfc
- vue3: @vitejs/plugin-vue + @vue/compiler-sfc
實(shí)現(xiàn)方式
以下實(shí)現(xiàn)方式均是基于vite開(kāi)發(fā),換成webpack和rollup原理上也是替換對(duì)應(yīng)的插件即可。
vue2.6 + vue3 + vite + vue-demi
以vue2.6為主包,開(kāi)發(fā)vue2/vue3組件,該方式能做到通過(guò)一個(gè)package.json的scripts同時(shí)調(diào)試和打包vue2、vue3環(huán)境,以下講一下重點(diǎn);
package.json
package.json中的vue包是固定了2.6.14版本,這里要注意vue-template-compiler要和vue的版本對(duì)齊;
scripts中的switch:2 指令沒(méi)有按照文檔說(shuō)的使用npx vue-demi-switch,是因?yàn)樵趯?shí)際調(diào)試過(guò)程中,由于vite是會(huì)緩存依賴的,dev調(diào)試時(shí)vue-demi-switch會(huì)出現(xiàn)一些莫名其妙的問(wèn)題,具體原因我還沒(méi)搞明白,所以就改成用npx vue-demi-fix。
//package.json部分片段
"main": "./lib/vue-demi-sfc-component.umd.cjs",
"exports": {
".": {
"import": "./lib/vue-demi-sfc-component.js",
"require": "./lib/vue-demi-sfc-component.umd.cjs"
}
},
"scripts": {
"postinstall": "node ./scripts/postinstall.mjs",
"dev": "vite",
"dev:3": "npm run switch:3 && vite --force",
"dev:2": "npm run switch:2 && vite",
"switch:2": "npx vue-demi-fix",
"switch:3": "npx vue-demi-switch 3 vue3",
"build:3": "npm run switch:3 && vue-tsc --noEmit && vite build",
"build:2": "npm run switch:2 && vue-tsc --noEmit && vite build",
"build": "rimraf lib && npm run build:2 && npm run build:3",
"preview": "vite preview",
"lint:fix": "eslint . --ext .js,.ts,.vue --fix",
"prepare": "husky install",
"pub": "npm publish --access=public"
},
"dependencies": {
"@vue/composition-api": "^1.7.0",
"vue-demi": "^0.13.8"
},
"peerDependencies": {
"@vue/composition-api": "^1.7.0",
"vue": "^2.0.0 || >=3.0.0"
},
"peerDependenciesMeta": {
"@vue/composition-api": {
"optional": true
}
},
"peerDependencies": {
"@vue/composition-api": "^1.7.0",
"vue": "^2.0.0 || >=3.0.0"
},
"peerDependenciesMeta": {
"@vue/composition-api": {
"optional": true
}
},
"devDependencies": {
// ...其他依賴,這里就不復(fù)制了
"@vitejs/plugin-vue": "^3.0.3",
"vite": "^3.0.7",
"vite-plugin-vue2": "^2.0.2",
"vue": "2.6.14",
"vue-eslint-parser": "^9.0.3",
"vue-template-compiler": "2.6.14",
"vue-tsc": "^0.39.5",
"vue2": "npm:vue@2.6.14",
"vue3": "npm:vue@^3.2.36"
}
vite.config.ts
import { defineConfig } from 'vite'
import { createVuePlugin } from 'vite-plugin-vue2'
import * as compiler from '@vue/compiler-sfc'
import vue3 from '@vitejs/plugin-vue'
import path from 'path'
import { getLibDir } from './scripts/utils.mjs'
import { isVue2, version } from 'vue-demi'
console.log({ version })
const resolve = (str: string) => {
return path.resolve(__dirname, str)
}
// https://vitejs.dev/config/
export default defineConfig({
resolve: {
alias: {
'@': resolve('src'),
vue: isVue2 ? resolve('/node_modules/vue2') : resolve('/node_modules/vue3')
}
},
build: {
lib: {
entry: resolve('./src/components/index.ts'),
name: 'vueDemiSfcComponent',
fileName: 'vue-demi-sfc-component'
},
cssTarget: 'chrome61',
rollupOptions: {
external: ['vue-demi', 'vue'],
output: {
dir: getLibDir(version),
globals: {
vue: 'Vue',
'vue-demi': 'VueDemi'
}
}
}
},
optimizeDeps: {
exclude: ['vue-demi']
},
plugins: [
isVue2
? createVuePlugin()
: vue3({
compiler: compiler
})
]
})
這個(gè)文件有幾個(gè)關(guān)鍵邏輯:
1、使用vue-demi的isVue2來(lái)判斷當(dāng)前打包環(huán)境
import { isVue2, version } from 'vue-demi'
2、alias要根據(jù)環(huán)境切換地址
alias: {
'@': resolve('src'),
vue: isVue2 ? resolve('/node_modules/vue2') : resolve('/node_modules/vue3')
}
3、在以vue2.6為主包的時(shí)候,如果直接使用@vitejs/plugin-vue, 打包時(shí)會(huì)報(bào)錯(cuò)
error when starting dev server:
Error: Failed to resolve vue/compiler-sfc.
@vitejs/plugin-vue requires vue (>=3.2.25) to be present in the dependency tree.
這是因?yàn)锧vitejs/plugin-vue源碼中是直接找vue/compiler-sfc目錄的,如果以vue2為主包,這個(gè)時(shí)候nod_modules/vue是vue2的目錄結(jié)構(gòu),并沒(méi)有vue/compiler-sfc;
function resolveCompiler(root) {
const compiler = tryRequire("vue/compiler-sfc", root) || tryRequire("vue/compiler-sfc");
if (!compiler) {
throw new Error(
`Failed to resolve vue/compiler-sfc.
@vitejs/plugin-vue requires vue (>=3.2.25) to be present in the dependency tree.`
);
}
return compiler;
}
所以就去尋找一下@vitejs/plugin-vue的options
interface Options {
include?: string | RegExp | (string | RegExp)[];
exclude?: string | RegExp | (string | RegExp)[];
isProduction?: boolean;
script?: Partial<Pick<SFCScriptCompileOptions, 'babelParserPlugins'>>;
template?: Partial<Pick<SFCTemplateCompileOptions, 'compiler' | 'compilerOptions' | 'preprocessOptions' | 'preprocessCustomRequire' | 'transformAssetUrls'>>;
style?: Partial<Pick<SFCStyleCompileOptions, 'trim'>>;
/**
* Transform Vue SFCs into custom elements.
* - `true`: all `*.vue` imports are converted into custom elements
* - `string | RegExp`: matched files are converted into custom elements
*
* @default /\.ce\.vue$/
*/
customElement?: boolean | string | RegExp | (string | RegExp)[];
/**
* Enable Vue reactivity transform (experimental).
* https://github.com/vuejs/core/tree/master/packages/reactivity-transform
* - `true`: transform will be enabled for all vue,js(x),ts(x) files except
* those inside node_modules
* - `string | RegExp`: apply to vue + only matched files (will include
* node_modules, so specify directories in necessary)
* - `false`: disable in all cases
*
* @default false
*/
reactivityTransform?: boolean | string | RegExp | (string | RegExp)[];
/**
* Use custom compiler-sfc instance. Can be used to force a specific version.
*/
compiler?: typeof _compiler;
}
發(fā)現(xiàn)option中是有自定義compiler-sfc的參數(shù),于是就得到以下方案:
// vite.config.ts
import * as compiler from '@vue/compiler-sfc'
export default defineConfig({
// ...
plugins: [
isVue2
? createVuePlugin()
: vue3({
compiler: compiler
})
]
})
main.ts
main.ts需要判斷isVue2后,區(qū)分vue2和vue3的依賴
import { isVue2 } from 'vue-demi'
import { createApp } from 'vue3'
import Vue2 from 'vue2'
import './style.css'
import App from './App.vue'
if (isVue2) {
const app = new Vue2({
render: (h) => h(App)
})
app.$mount('#app')
} else {
const app = createApp(App)
app.mount('#app')
}
postinstall
這里是模仿vue-demi的原理,在安裝時(shí)利用postinstall鉤子執(zhí)行node腳本,復(fù)制lib中的v2/v3目錄,具體可直接看文章最后的項(xiàng)目鏈接;這里有一個(gè)地方要注意,由于我是使用vite + ts 構(gòu)建的項(xiàng)目,package.json中的"type": "module"需要我把所有js改成mjs文件,這個(gè)時(shí)候,其他項(xiàng)目安裝這個(gè)項(xiàng)目時(shí),會(huì)找不到 __dirname,因此utils.mjs加了以下邏輯。
import { fileURLToPath } from 'url'
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
vue2.7 + vue3 + vite + vue-demi + yarn workspaces
以vue2.7為主包開(kāi)發(fā)時(shí),沒(méi)辦法像vue2.6可以在一個(gè)package.json項(xiàng)目下調(diào)試和打包,主要是因?yàn)関ue2.7的代碼方式已經(jīng)是monorepo項(xiàng)目,因此在安裝vue2.7的時(shí)候,會(huì)重新下載@vue/compuler-sfc的2.7.x版本。

所以沒(méi)辦法直接使用@vue/compiler-sfc 包作為vue3的compiler;
那么我們就要換一個(gè)思路,做node_modules隔離,而node_modules隔離的方案現(xiàn)在主流的就是yarn workspaces、lerna和pnpm,這里我就以yarn workspaces來(lái)簡(jiǎn)單講一下思路;
(ps: 該方式我并沒(méi)有上傳到github)
開(kāi)啟yarn workspaces之后,新建packages文件夾

然后再packages下分別新建v2和v3目錄,這兩個(gè)目錄存放對(duì)應(yīng)vue2和vue3的package.json和vite.config.ts
// v2/package.json
"scripts": {
"dev": "vite",
"build": "rimraf lib/v2 && vue-tsc --noEmit && vite build",
"preview": "vite preview",
"lint:fix": "eslint . --ext .js,.ts,.vue --fix",
"prepare": "husky install",
"pub": "npm publish --access=public"
},
"devDependencies": {
"@vitejs/plugin-vue2": "^2.7.9",
"vite": "^3.0.7",
"vite-plugin-vue2": "^2.0.2",
"vue": "2.7.9",
"vue-eslint-parser": "^9.0.3",
"vue-template-compiler": "2.7.9",
"vue-tsc": "^0.39.5",
"vue2": "npm:vue@2.7.9",
"vue3": "npm:vue@^3.2.36"
}
// v3/package.json
"scripts": {
"dev": "vite",
"build": "rimraf lib/v3 && vue-tsc --noEmit && vite build",
"preview": "vite preview",
"lint:fix": "eslint . --ext .js,.ts,.vue --fix",
"prepare": "husky install",
"pub": "npm publish --access=public"
},
"devDependencies": {
"@vitejs/plugin-vue": "^3.0.3",
"vite": "^3.0.7",
"vite-plugin-vue2": "^2.0.2",
"vue": "3.2.26",
"vue-eslint-parser": "^9.0.3",
"vue-template-compiler": "2.6.14",
"vue-tsc": "^0.39.5",
"vue2": "npm:vue@2.6.14",
"vue3": "npm:vue@^3.2.26"
}
vite.config.ts的區(qū)別主要是 rollupOptions.output.dir,和對(duì)應(yīng)的plugin,然后alias不需要再指定vue路徑,main.ts也不需要區(qū)分vue2和vue3的依賴;
// v2/vite.config.ts
import { defineConfig } from 'vite'
import { createVuePlugin } from 'vite-plugin-vue2'
// or import vue2 from '@vitejs/plugin-vue2'
import path from 'path'
const resolve = (str: string) => {
return path.resolve(__dirname, str)
}
// https://vitejs.dev/config/
export default defineConfig({
// ...
resolve: {
alias: {
'@': resolve('src'),
}
},
build: {
// ...
rollupOptions: {
external: ['vue-demi', 'vue'],
output: {
dir: resolve('../../lib/v2'), // 區(qū)別在這
globals: {
vue: 'Vue',
'vue-demi': 'VueDemi'
}
}
}
},
optimizeDeps: {
exclude: ['vue-demi']
},
plugins: [createVuePlugin()] // or vue2()
})
// v3/vite.config.ts
import { defineConfig } from 'vite'
import vue3 from '@vitejs/plugin-vue'
import path from 'path'
const resolve = (str: string) => {
return path.resolve(__dirname, str)
}
// https://vitejs.dev/config/
export default defineConfig({
// ...
resolve: {
alias: {
'@': resolve('src'),
}
},
build: {
rollupOptions: {
external: ['vue-demi', 'vue'],
output: {
dir: resolve('../../lib/v3'), // 區(qū)別在這
globals: {
vue: 'Vue',
'vue-demi': 'VueDemi'
}
}
}
},
optimizeDeps: {
exclude: ['vue-demi']
},
plugins: [vue3()]
})
main.ts
// main.ts
import { createApp } from 'vue-demi'
import './style.css'
const app = createApp(App)
app.mount('#app')
整體目錄結(jié)構(gòu)如下,最后通過(guò)node腳本去同時(shí)構(gòu)建v2和v3即可。

目前沒(méi)找到vue3為主包的開(kāi)發(fā)方式
文章看到這里,大概能知道整個(gè)方案其實(shí)是基于vue-demi處理composition-api和使用vue3的自定義compiler處理分別打包vue2、vue3;而vite-plugin-vue2是沒(méi)有對(duì)應(yīng)自定義compiler的options,并且在vue3為主包的情況下,會(huì)報(bào)vue-template-compiler與vue版本不一致的錯(cuò)誤;而@vitejs/plugin-vue2存在跟vue3沖突的情況;
目前如果要基于vue3為主包的方式開(kāi)發(fā),我想到如下2個(gè)思路,待后續(xù)有時(shí)間再去驗(yàn)證:
- vite-plugin-vue2增加自定義compiler選項(xiàng)
- 開(kāi)發(fā)rollup插件,支持修改vue-template-compiler在讀取require(vue)時(shí),重定向到"vue2": "npm:vue@2.6.14"對(duì)應(yīng)的路徑
注意點(diǎn)
1、@vue/composition-api重復(fù)引用問(wèn)題
由于vue-demi在v2.6的場(chǎng)景下,會(huì)自動(dòng)install @vue/composition-api,,如果項(xiàng)目自身也在需要在入口時(shí)注冊(cè)@vue/composition-api,會(huì)出現(xiàn)多次注冊(cè)@vue/composition-api實(shí)例的情況,導(dǎo)致出setup相關(guān)的報(bào)錯(cuò),這時(shí)需要在項(xiàng)目的alias加上以下代碼:
alias: {
'@vue/compostion-api': resolve('./node_modules/@vue/composition-api')
},
2、由于要兼容vue2,vue3的 setup sfc語(yǔ)法糖不兼容
這一點(diǎn)無(wú)法解決,寫(xiě)組件template的時(shí)候,還是只能用vue2的template寫(xiě)法,包括template還是需要有唯一的跟節(jié)點(diǎn);
最后
寫(xiě)到最后,其實(shí)我發(fā)現(xiàn)去寫(xiě)兼容vue2和vue3的template代碼,并不能完全解決vue2到vue3過(guò)渡的問(wèn)題。希望vue3社區(qū)以后越來(lái)越完善~
貼上項(xiàng)目地址(vue2.6 + vue3 + vite + vue-demi):vue-demi-sfc-component
以上就是vue demi支持sfc方式的vue2vue3通用庫(kù)開(kāi)發(fā)詳解的詳細(xì)內(nèi)容,更多關(guān)于vue demi支持sfc通用庫(kù)的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
vue基于Teleport實(shí)現(xiàn)Modal組件
Teleport 提供了一種干凈的方法,允許我們控制在 DOM 中哪個(gè)父節(jié)點(diǎn)下渲染了 HTML,而不必求助于全局狀態(tài)或?qū)⑵洳鸱譃閮蓚€(gè)組件。2021-05-05
vuex vue簡(jiǎn)單使用知識(shí)點(diǎn)總結(jié)
在本篇文章里小編給大家整理了關(guān)于vuex vue簡(jiǎn)單使用知識(shí)點(diǎn)總結(jié),有需要的朋友們可以參考下。2019-08-08
vue2.0實(shí)現(xiàn)導(dǎo)航菜單切換效果
這篇文章主要為大家詳細(xì)介紹了vue2.0實(shí)現(xiàn)導(dǎo)航菜單切換效果,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-05-05
解決Vue3報(bào)錯(cuò):Property?“xxx“?was?accessed?during?render?but
這篇文章主要給大家介紹了關(guān)于解決Vue3報(bào)錯(cuò):Property?“xxx“?was?accessed?during?render?but?is?not?defined?on?instance.的相關(guān)資料,文中通過(guò)圖文介紹的非常詳細(xì),需要的朋友可以參考下2023-01-01
vue?+elementui?項(xiàng)目登錄通過(guò)不同賬號(hào)切換側(cè)邊欄菜單的顏色
這篇文章主要介紹了vue?+elementui?項(xiàng)目登錄通過(guò)不同賬號(hào)切換側(cè)邊欄菜單的顏色,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友參考下吧2024-01-01
vueJS簡(jiǎn)單的點(diǎn)擊顯示與隱藏的效果【實(shí)現(xiàn)代碼】
下面小編就為大家?guī)?lái)一篇vueJS簡(jiǎn)單的點(diǎn)擊顯示與隱藏的效果【實(shí)現(xiàn)代碼】。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,一起跟隨小編過(guò)來(lái)看看吧2016-05-05
vue路由緩存的幾種實(shí)現(xiàn)方式小結(jié)
這篇文章主要介紹了vue路由緩存的幾種實(shí)現(xiàn)方式,結(jié)合實(shí)例形式詳細(xì)分析了vue.js路由緩存常見(jiàn)實(shí)現(xiàn)方式、使用技巧與操作注意事項(xiàng),需要的朋友可以參考下2020-02-02
vue移動(dòng)端自適應(yīng)適配問(wèn)題詳解
這篇文章主要介紹了vue移動(dòng)端自適應(yīng)適配問(wèn)題,本文通過(guò)實(shí)例代碼詳解給大家介紹的非常詳細(xì),需要的朋友可以參考下2021-04-04
Vue router-view和router-link的實(shí)現(xiàn)原理
這篇文章主要介紹了Vue router-view和router-link的實(shí)現(xiàn)原理,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2021-03-03

