vue?demi支持sfc方式的vue2vue3通用庫開發(fā)詳解
背景
隨著vue3的逐漸成熟,公司項目逐漸會存在vue2和vue3項目共存的情況,兼容vue2和vue3的公共組件開發(fā)能讓老項目較好地過渡到vue3。研究了vue-demi的源碼和demo,發(fā)現(xiàn)vue-demi只是簡單地根據(jù)vue版本生成對應(yīng)的類似中間件的東西,而且render函數(shù)也只是做了簡單的中轉(zhuǎn)處理;
國外大佬寫了一個h-demi解決了vue2/vue3的render函數(shù)attrs屬性的問題,這里我就直接貼issue鏈接,不做過多說明了: github.com/vueuse/vue-…
雖然vue-demi沒有提供sfc的兼容方案,但是其實仔細(xì)想一下,sfc的解析處理也不應(yīng)該是由vue-demi來解決,應(yīng)該是交給打包工具將template轉(zhuǎn)成render,而vue-demi只需要關(guān)注composition-api就行;于是往著這個思路,花了幾天時間研究一下vue2.6、vue2.7和vue3的sfc-compiler,得到以下開發(fā)方案。
技術(shù)要點(diǎn)
vue-demi
查看源碼可以發(fā)現(xiàn),vue-demi的工作是通過postinstall和 npx vue-demi-fix指令,判斷當(dāng)前項目安裝的vue版本,然后將對應(yīng)版本的插件復(fù)制到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ā),以下對應(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
實現(xiàn)方式
以下實現(xiàn)方式均是基于vite開發(fā),換成webpack和rollup原理上也是替換對應(yīng)的插件即可。
vue2.6 + vue3 + vite + vue-demi
以vue2.6為主包,開發(fā)vue2/vue3組件,該方式能做到通過一個package.json的scripts同時調(diào)試和打包vue2、vue3環(huán)境,以下講一下重點(diǎ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": { // ...其他依賴,這里就不復(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 }) ] })
這個文件有幾個關(guān)鍵邏輯:
1、使用vue-demi的isVue2來判斷當(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為主包的時候,如果直接使用@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腳本,復(fù)制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目錄,這兩個目錄存放對應(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,和對應(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)如下,最后通過node腳本去同時構(gòu)建v2和v3即可。
目前沒找到vue3為主包的開發(fā)方式
文章看到這里,大概能知道整個方案其實是基于vue-demi處理composition-api和使用vue3的自定義compiler處理分別打包vue2、vue3;而vite-plugin-vue2是沒有對應(yīng)自定義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"對應(yīng)的路徑
注意點(diǎn)
1、@vue/composition-api重復(fù)引用問題
由于vue-demi在v2.6的場景下,會自動install @vue/composition-api,,如果項目自身也在需要在入口時注冊@vue/composition-api,會出現(xiàn)多次注冊@vue/composition-api實例的情況,導(dǎo)致出setup相關(guān)的報錯,這時需要在項目的alias加上以下代碼:
alias: { '@vue/compostion-api': resolve('./node_modules/@vue/composition-api') },
2、由于要兼容vue2,vue3的 setup sfc語法糖不兼容
這一點(diǎn)無法解決,寫組件template的時候,還是只能用vue2的template寫法,包括template還是需要有唯一的跟節(jié)點(diǎn);
最后
寫到最后,其實我發(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ā)詳解的詳細(xì)內(nèi)容,更多關(guān)于vue demi支持sfc通用庫的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
解決Vue3報錯:Property?“xxx“?was?accessed?during?render?but
這篇文章主要給大家介紹了關(guān)于解決Vue3報錯:Property?“xxx“?was?accessed?during?render?but?is?not?defined?on?instance.的相關(guān)資料,文中通過圖文介紹的非常詳細(xì),需要的朋友可以參考下2023-01-01vue?+elementui?項目登錄通過不同賬號切換側(cè)邊欄菜單的顏色
這篇文章主要介紹了vue?+elementui?項目登錄通過不同賬號切換側(cè)邊欄菜單的顏色,本文給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友參考下吧2024-01-01vueJS簡單的點(diǎn)擊顯示與隱藏的效果【實現(xiàn)代碼】
下面小編就為大家?guī)硪黄獀ueJS簡單的點(diǎn)擊顯示與隱藏的效果【實現(xiàn)代碼】。小編覺得挺不錯的,現(xiàn)在分享給大家,一起跟隨小編過來看看吧2016-05-05Vue router-view和router-link的實現(xiàn)原理
這篇文章主要介紹了Vue router-view和router-link的實現(xiàn)原理,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2021-03-03