vue demi支持sfc方式的vue2vue3通用库开发详解

背景

随着vue3的逐渐成熟,公司项目逐渐会存在vue2和vue3项目共存的情况,兼容vue2和vue3的公共组件开发能让老项目较好地过渡到vue3。研究了vue-demi的源码和demo,发现vue-demi只是简单地根据vue版本生成对应的类似中间件的东西,而且render函数也只是做了简单的中转处理;

国外大佬写了一个vue-demi解决了vue2/vue3的render函数attrs属性的问题,这里我就直接贴issue链接,不做过多说明了: github.com/vueuse/vue-…

虽然vue-demi没有提供sfc的兼容方案,但是其实仔细想一下,sfc的解析处理也不应该是由vue-demi来解决,应该是交给打包工具将template转成render,而vue-demi只需要关注composition-api就行;于是往着这个思路,花了几天时间研究一下vue2.6、vue2.7和vue3的sfc-compiler,得到以下开发方案。

技术要点
vue-demi

查看源码可以发现,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

在日常开发中写的vue template,实际上最后是通过sfc-compiler转成render函数输出的,而vue2和vue3的sfc-compiler是互不兼容的。尤大大已经提供了vue2.6.x,vue2.7和vue3的compiler,其实我们只需要在打包工具写判断不同的vue版本使用不同的compiler逻辑即可,本文是基于vite开发,以下对应的打包插件:

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

实现方式

以下实现方式均是基于vite开发,换成webpack和rollup原理上也是替换对应的插件即可。

vue2.6 + vue3 + vite + vue-demi

以vue2.6为主包,开发vue2/vue3组件,该方式能做到通过一个package.json的scripts同时调试和打包vue2、vue3环境,以下讲一下重点;

package.json

package.json中的vue包是固定了2.6.14版本,这里要注意vue-template-compiler要和vue的版本对齐;

scripts中的switch:2 指令没有按照文档说的使用npx vue-demi-switch,是因为在实际调试过程中,由于vite是会缓存依赖的,dev调试时vue-demi-switch会出现一些莫名其妙的问题,具体原因我还没搞明白,所以就改成用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.这个文件有几个关键逻辑:

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

2、alias要根据环境切换地址

  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的目录结构,并没有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;
}

发现option中是有自定义compiler-sfc的参数,于是就得到以下方案:

 // vite.config.ts
  import * as compiler from '@vue/compiler-sfc'
  export default defineConfig({
   // ... 
   plugins: [
     isVue2
    ? createVuePlugin()
    : vue3({
        compiler: compiler
     })
    ]
  })
main.ts

main.ts需要判断isVue2后,区分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钩子执行node脚本,复制lib中的v2/v3目录,具体可直接看文章最后的项目链接;这里有一个地方要注意,由于我是使用vite + ts 构建的项目,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为主包开发时,没办法像vue2.6可以在一个package.json项目下调试和打包,主要是因为vue2.7的代码方式已经是monorepo项目,因此在安装vue2.7的时候,会重新下载@vue/compuler-sfc的2.7.x版本。

所以没办法直接使用@vue/compiler-sfc 包作为vue3的compiler;

那么我们就要换一个思路,做node_modules隔离,而node_modules隔离的方案现在主流的就是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的区别主要是 rollupOptions.output.dir,和对应的plugin,然后alias不需要再指定vue路径,main.ts也不需要区分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'), // 区别在这
    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'), // 区别在这
    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')

整体目录结构如下,最后通过node脚本去同时构建v2和v3即可。

目前没找到vue3为主包的开发方式

文章看到这里,大概能知道整个方案其实是基于vue-demi处理composition-api和使用vue3的自定义compiler处理分别打包vue2、vue3;而vite-plugin-vue2是没有对应自定义compiler的options,并且在vue3为主包的情况下,会报vue-template-compiler与vue版本不一致的错误;而@vitejs/plugin-vue2存在跟vue3冲突的情况;

目前如果要基于vue3为主包的方式开发,我想到如下2个思路,待后续有时间再去验证:

vite-plugin-vue2增加自定义compiler选项
开发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,会出现多次注册@vue/composition-api实例的情况,导致出setup相关的报错,这时需要在项目的alias加上以下代码:

  alias: {
      '@vue/compostion-api': resolve('./node_modules/@vue/composition-api')
   },

2、由于要兼容vue2,vue3的 setup sfc语法糖不兼容

这一点无法解决,写组件template的时候,还是只能用vue2的template写法,包括template还是需要有唯一的跟节点;

最后

写到最后,其实我发现去写兼容vue2和vue3的template代码,并不能完全解决vue2到vue3过渡的问题。希望vue3社区以后越来越完善~

贴上项目地址(vue2.6 + vue3 + vite + vue-demi):vue-demi-sfc-component

https://github.com/sakibcc/vue-demi-sfc-component

以上就是vue demi支持sfc方式的vue2vue3通用库开发详解的详细内容,更多关于vue demi支持sfc通用库的资料请关注易盾网络其它相关文章!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,313评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,369评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,916评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,333评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,425评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,481评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,491评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,268评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,719评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,004评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,179评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,832评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,510评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,153评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,402评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,045评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,071评论 2 352

推荐阅读更多精彩内容