vue?demi支持sfc方式的vue2vue3通用庫開發(fā)詳解
背景
隨著vue3的逐漸成熟,公司項目逐漸會存在vue2和vue3項目共存的情況,兼容vue2和vue3的公共組件開發(fā)能讓老項目較好地過渡到vue3。研究了vue-demi的源碼和demo,發(fā)現(xiàn)vue-demi只是簡單地根據(jù)vue版本生成對應的類似中間件的東西,而且render函數(shù)也只是做了簡單的中轉(zhuǎn)處理;
國外大佬寫了一個h-demi解決了vue2/vue3的render函數(shù)attrs屬性的問題,這里我就直接貼issue鏈接,不做過多說明了: github.com/vueuse/vue-…
雖然vue-demi沒有提供sfc的兼容方案,但是其實仔細想一下,sfc的解析處理也不應該是由vue-demi來解決,應該是交給打包工具將template轉(zhuǎn)成render,而vue-demi只需要關注composition-api就行;于是往著這個思路,花了幾天時間研究一下vue2.6、vue2.7和vue3的sfc-compiler,得到以下開發(fā)方案。
技術(shù)要點
vue-demi
查看源碼可以發(fā)現(xiàn),vue-demi的工作是通過postinstall和 npx vue-demi-fix指令,判斷當前項目安裝的vue版本,然后將對應版本的插件復制到lib的根目錄,其插件的功能就是抹平vue2和vue3版本使用composition-api時的差異;
<=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
在日常開發(fā)中寫的vue template,實際上最后是通過sfc-compiler轉(zhuǎn)成render函數(shù)輸出的,而vue2和vue3的sfc-compiler是互不兼容的。尤大大已經(jīng)提供了vue2.6.x,vue2.7和vue3的compiler,其實我們只需要在打包工具寫判斷不同的vue版本使用不同的compiler邏輯即可,本文是基于vite開發(fā),以下對應的打包插件:
- 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
實現(xiàn)方式
以下實現(xiàn)方式均是基于vite開發(fā),換成webpack和rollup原理上也是替換對應的插件即可。
vue2.6 + vue3 + vite + vue-demi
以vue2.6為主包,開發(fā)vue2/vue3組件,該方式能做到通過一個package.json的scripts同時調(diào)試和打包vue2、vue3環(huán)境,以下講一下重點;
package.json
package.json中的vue包是固定了2.6.14版本,這里要注意vue-template-compiler要和vue的版本對齊;
scripts中的switch:2 指令沒有按照文檔說的使用npx vue-demi-switch,是因為在實際調(diào)試過程中,由于vite是會緩存依賴的,dev調(diào)試時vue-demi-switch會出現(xiàn)一些莫名其妙的問題,具體原因我還沒搞明白,所以就改成用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": {
// ...其他依賴,這里就不復制了
"@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
})
]
})
這個文件有幾個關鍵邏輯:
1、使用vue-demi的isVue2來判斷當前打包環(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為主包的時候,如果直接使用@vitejs/plugin-vue, 打包時會報錯
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.
這是因為@vitejs/plugin-vue源碼中是直接找vue/compiler-sfc目錄的,如果以vue2為主包,這個時候nod_modules/vue是vue2的目錄結(jié)構(gòu),并沒有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的原理,在安裝時利用postinstall鉤子執(zhí)行node腳本,復制lib中的v2/v3目錄,具體可直接看文章最后的項目鏈接;這里有一個地方要注意,由于我是使用vite + ts 構(gòu)建的項目,package.json中的"type": "module"需要我把所有js改成mjs文件,這個時候,其他項目安裝這個項目時,會找不到 __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為主包開發(fā)時,沒辦法像vue2.6可以在一個package.json項目下調(diào)試和打包,主要是因為vue2.7的代碼方式已經(jīng)是monorepo項目,因此在安裝vue2.7的時候,會重新下載@vue/compuler-sfc的2.7.x版本。

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

然后再packages下分別新建v2和v3目錄,這兩個目錄存放對應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,和對應的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)如下,最后通過node腳本去同時構(gòu)建v2和v3即可。

目前沒找到vue3為主包的開發(fā)方式
文章看到這里,大概能知道整個方案其實是基于vue-demi處理composition-api和使用vue3的自定義compiler處理分別打包vue2、vue3;而vite-plugin-vue2是沒有對應自定義compiler的options,并且在vue3為主包的情況下,會報vue-template-compiler與vue版本不一致的錯誤;而@vitejs/plugin-vue2存在跟vue3沖突的情況;
目前如果要基于vue3為主包的方式開發(fā),我想到如下2個思路,待后續(xù)有時間再去驗證:
- vite-plugin-vue2增加自定義compiler選項
- 開發(fā)rollup插件,支持修改vue-template-compiler在讀取require(vue)時,重定向到"vue2": "npm:vue@2.6.14"對應的路徑
注意點
1、@vue/composition-api重復引用問題
由于vue-demi在v2.6的場景下,會自動install @vue/composition-api,,如果項目自身也在需要在入口時注冊@vue/composition-api,會出現(xiàn)多次注冊@vue/composition-api實例的情況,導致出setup相關的報錯,這時需要在項目的alias加上以下代碼:
alias: {
'@vue/compostion-api': resolve('./node_modules/@vue/composition-api')
},
2、由于要兼容vue2,vue3的 setup sfc語法糖不兼容
這一點無法解決,寫組件template的時候,還是只能用vue2的template寫法,包括template還是需要有唯一的跟節(jié)點;
最后
寫到最后,其實我發(fā)現(xiàn)去寫兼容vue2和vue3的template代碼,并不能完全解決vue2到vue3過渡的問題。希望vue3社區(qū)以后越來越完善~
貼上項目地址(vue2.6 + vue3 + vite + vue-demi):vue-demi-sfc-component
以上就是vue demi支持sfc方式的vue2vue3通用庫開發(fā)詳解的詳細內(nèi)容,更多關于vue demi支持sfc通用庫的資料請關注腳本之家其它相關文章!
相關文章
解決Vue3報錯:Property?“xxx“?was?accessed?during?render?but
這篇文章主要給大家介紹了關于解決Vue3報錯:Property?“xxx“?was?accessed?during?render?but?is?not?defined?on?instance.的相關資料,文中通過圖文介紹的非常詳細,需要的朋友可以參考下2023-01-01
vue?+elementui?項目登錄通過不同賬號切換側(cè)邊欄菜單的顏色
這篇文章主要介紹了vue?+elementui?項目登錄通過不同賬號切換側(cè)邊欄菜單的顏色,本文給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友參考下吧2024-01-01
vueJS簡單的點擊顯示與隱藏的效果【實現(xiàn)代碼】
下面小編就為大家?guī)硪黄獀ueJS簡單的點擊顯示與隱藏的效果【實現(xiàn)代碼】。小編覺得挺不錯的,現(xiàn)在分享給大家,一起跟隨小編過來看看吧2016-05-05
Vue router-view和router-link的實現(xiàn)原理
這篇文章主要介紹了Vue router-view和router-link的實現(xiàn)原理,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2021-03-03

