嗨~那个少年,快来做一个跨vue2和vue3的组件库
前端时间,由于公司业务广度深度的拓展以及想要对之前网站的布局进行革新,洋洋洒洒的进行了多次的UI改版,致使很多前端同学是苦不堪言,但是总体来说最后的结果是好的,也得到了相应的好评。但是随着改版的范围越来越广,涉及的项目越来越多,面临的挑战也是越来越大。这个时候我们迫切的需要一个组件库来承接所有的设计规范和UI物料库和支撑公司各种形形色色项目的后续的改版(ps: 其实就是手痒,想做会死,搞出点动静来,颇有心计)。 于是罗列了公司目前项目的现状,还好技术栈还是比较统一用的vue,但是问题在于版本幅度有点大,我一听心中窃喜,活不就来了吗?不跨版本的活我还不干呢。小伙子,有点狂啊,好了,铺(废)垫(话)就先到这里了,直接开干。 想到跨版本,一定就会想起antfu大神的vue-demi,没错后续就是基于这个包来开展一系列的动作,这个时候有朋友就会问道为什么不用vue-component去实现,且听我徐徐道来。
基于vue-demi的几种方案
- 直接上最原始的组件,依赖宿主项目的编译能力。(pass,这也太耍无赖了)
- 判断当前版本,利用jsx以及render等去动态替换逻辑。(我就是只想用个vue3而已,你给我捆绑vue2)
- 构建时走多份配置(vue 2、vue2.7、vue3),构建多版本产物。
显而易见,我采取了第三种,当然其实也是都可以,(ps:又不是不能用)。 确定了后续的方向,那么我们就需要去分析选择哪个版本做主版本的问题,由于我是革新派毫无疑问选择了vue3作为主版本,以及再次借助了antfu大神写的unplugin-vue2-script-setup,(ps:感谢祖师爷赏饭吃),直接上了setup语法,小孩子才做选择,而我全都要。
依赖冲突
这个时候就会有朋友就会问了,这么版本这么多依赖杂糅在一个项目不会有冲突吗?不得不说这个朋友你真聪明,还真让你说对了,不过庆幸的是我们目前只有vue2跟vue3的项目,而2和3里面并没有太多的依赖交集,所以哈哈哈,完美避免。想知道vue2,vue2.7,vue3如何共存的同学请听后续我娓娓道来。 说是没有依赖交集,但还是会存在一个冲突,那就是执着的我是以vue3作为主版本,那么vue2专用的vue-template-compiler在读取版本的时候可能会出错,我们来打开它的源码看下它如何来引入vue的。
try {
var vueVersion = require('vue').version
} catch (e) {}
var packageName = require('./package.json').name
var packageVersion = require('./package.json').version
if (vueVersion && vueVersion !== packageVersion) {
var vuePath = require.resolve('vue')
var packagePath = require.resolve('./package.json')
throw new Error(
'\n\nVue packages version mismatch:\n\n' +
'- vue@' + vueVersion + ' (' + vuePath + ')\n' +
'- ' + packageName + '@' + packageVersion + ' (' + packagePath + ')\n\n' +
'This may cause things to work incorrectly. Make sure to use the same version for both.\n' +
'If you are using vue-loader@>=10.0, simply update vue-template-compiler.\n' +
'If you are using vue-loader@<10.0 or vueify, re-installing vue-loader/vueify should bump ' + packageName + ' to the latest.\n'
)
}
module.exports = require('./build')
喔嚯,一看源码那么事情就大了肯定是会读错的,难道我们就这样放弃了吗?不,点上一首孤勇者,谁说污泥满身的不算英雄,就在情绪的在高点时,脑海中涌现了一个库patch-package,救星来了,我们可以在安装依赖的时候利用postinstall钩子,覆盖它的源码强制指定vue的版本。顿时一顿操作猛如虎,却发现库太老了,已经不兼容pnpm安装的依赖结构了(不过听小道消息最近作者又‘活’过来了,准备更新下去适配pnpm这些新兴势力),有兴趣的同学可以持续关注下。 其实原理我们是知道的,那就自己写个脚本动态替换下吧,虽然做不到patch-package那么高级,好歹能用(ps:又不是不能用)。直接上代码,多的就不提了。
import { getPackageInfo, resolveModule } from "local-pkg";
import fs from "node:fs/promises";
const patch = async () => {
const { name, version, rootPath } = await getPackageInfo("vue-template-compiler");
console.log(`检测到当前${name}版本为${version}`);
const packagePath = await resolveModule("vue-template-compiler");
const content = await fs.readFile(packagePath, "utf8");
try {
await fs.writeFile(
packagePath,
content.replace(
"require('vue').version",
`require('vue@${version}').version`
)
);
console.log(`vue@${version}版本写入成功`);
console.log('写入路径:', rootPath);
} catch (e) {
console.log(`vue@${version}版本写入失败,请检测包脚本是否需要更新`);
}
};
patch();
完活,前期的冲突问题到这里就完结了。
构建工具
接下来就是选构建工具了,鉴于小本生意,没啥研发经费,那就选vite吧,开箱即用。整体的思路就是借助vue-demi提供的vue-demi-switch命令,在构建的时候动态的切换vue的版本,并根据当前vue的版本按需去注册相关构建依赖。核心代码大概就如下部分:
// package.json
scripts": {
"switch:v3": "vue-demi-switch 3 vue3",
"switch:v2": "vue-demi-switch 2 vue2",
}
// build.ts
import { build } from "vite";
import glob from "glob";
import { isVue3, version } from "vue-demi";
import dts from "vite-plugin-dts";
import { resolve, workRoot, getVuePlugins } from "./utils";
export const start = async () => {
console.log("当前vue版本", version);
const name = isVue3 ? "vue3" : "vue2";
const vuePlugins: any[] = await getVuePlugins();
glob("src/components/**/**.{vue,ts,js}", {
cwd: process.cwd(),
absolute: true,
onlyFiles: true,
}, async (err, files) => {
if (err) return;
files.forEach(async (file) => {
const plugins = [...vuePlugins,];
plugins.push(
dts({
entryRoot: `${workRoot}/src/components`,
outputDir: [resolve(`./dist/${name}/es`), resolve(`./dist/${name}/cjs`)],
exclude: ['src/vite-env.d.ts'],
cleanVueFileName: true,
staticImport: true,
compilerOptions: isVue3
? {}
: {
baseUrl: ".",
paths: {
vue: ["node_modules/vue2"],
"vue/*": ["node_modules/vue2/*"],
"@vue/composition-api": ["node_modules/@vue/composition-api"],
"@vue/runtime-dom": ["node_modules/@vue/runtime-dom"],
},
},
})
);
await build({
plugins,
resolve: {
alias: isVue3
? {}
: {
vue: resolve("./node_modules/vue2"),
"@vue/composition-api": resolve(
"./node_modules/@vue/composition-api"
),
},
},
build: {
assetsDir: resolve(`./dist/${name}/es/`),
emptyOutDir: false,
minify: 'esbuild',
sourcemap: true,
lib: {
entry: resolve(file),
name: "vue-ui"
},
rollupOptions: {
external: ["vue", "vue-demi"],
output: [
{
format: "es",
dir: resolve(`./dist/${name}/es`),
preserveModules: true,
preserveModulesRoot: `${workRoot}/src/components`,
entryFileNames: `[name].mjs`,
},
{
format: "cjs",
dir: resolve(`./dist/${name}/cjs`),
preserveModules: true,
preserveModulesRoot: `${workRoot}/src/components`,
exports: "named",
entryFileNames: `[name].js`
},
],
},
},
});
});
});
};
start();
到这里我们的组件构建过程应该是没啥太大的问题了,这个时候又有聪明的同学会问:你这组件生成了,能不能再生成相关的ts类型文件。好的,老板没问题,这个时候得请出vite-plugin-dts,重新构建一遍发现还好,能用。想更加细致的去定制化ts类型文件生成的同学可以去了解下ts-morph。
样式构建
上面只能构建了组件,但是样式应该咋办呢?脑海中模拟了无数场景,最终我们还是借鉴下element-plus,直接用gulp基于文件流去简单的构建一下。大致思路如下:
// gulpfile.ts
import path from 'path'
import chalk from 'chalk'
import { dest, parallel, series, src } from 'gulp'
import gulpSass from 'gulp-sass'
import dartSass from 'sass'
import autoprefixer from 'gulp-autoprefixer'
import cleanCSS from 'gulp-clean-css'
import rename from 'gulp-rename'
import consola from 'consola'
const distFolder = path.resolve(__dirname, 'style')
function buildThemeChalk() {
const sass = gulpSass(dartSass)
const noElPrefixFile = /(index|base|display)/
return src(path.resolve(__dirname, 'src/components/**/*.scss'))
.pipe(sass.sync())
.pipe(autoprefixer({ cascade: false }))
.pipe(
cleanCSS({}, (details) => {
consola.success(
`${chalk.cyan(details.name)}: ${chalk.yellow(
details.stats.originalSize / 1000
)} KB -> ${chalk.green(details.stats.minifiedSize / 1000)} KB`
)
})
)
.pipe(
rename((path) => {
if (!noElPrefixFile.test(path.basename)) {
path.basename = `vue-${path.basename}`
}
})
)
.pipe(dest(distFolder))
}
export const build: any = parallel(buildThemeChalk)
export default build
本地开发
由于基于vite,那么本地开就很舒服了,拉一个vite模板文件,配置一下vite.config.ts就行了。不得不说这个经费一下子省到位了,舒服。
// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { resolve } from './build/utils';
import { isVue3 } from 'vue-demi';
import { createVuePlugin } from 'vite-plugin-vue2';
import setUp from 'unplugin-vue2-script-setup/vite';
// https://vitejs.dev/config/
export default defineConfig(process.env.NODE_ENV === 'development' ? {
plugins: isVue3 ? [vue()] : [createVuePlugin(), setUp()],
resolve: {
alias: {
'@': resolve("./src"),
'@component': resolve("./src/components"),
vue: resolve(`./node_modules/vue${ isVue3 ? 3 : 2}`)
}
},
} : {});
我们在写组件的时候可以先切vue3的跑起来,再切vue2的看下兼容性问题,具体main.ts可以这样写:
import { createApp, version, Vue2, isVue3 } from 'vue-demi'
import App from './App.vue'
/** 自动搜索全局样式进行引入 **/
const styles = import.meta.glob('./**/*.scss');
Object.keys(styles).forEach((k) => {
console.log('注入css:', k);
styles[k]()
})
console.log('当前vue版本:', version);
if (isVue3) {
createApp(App).mount('#app')
} else {
new Vue2({
render: h => h(App as any)
}).$mount("#app")
}
自动引入
有不少同学会说,人家都已经开上宝马了,你搁这骑自行车。好的,收到,问题不大,那就让我们来扒一下unplugin-vue-components的源码,多的地方不用细看,只需要找到相关的resolver就行,照着写就行。当然你可以向这个库提交你的resolver,不过一般情况下demo是不会通过的,你可以自己重新构建一个自用的npm包就行,列如:unplugin-component-resolvers。 另外一些pnpm,postinstall,还有用到一些包就不在这里赘述了。
搞定完事,这个季度的kpi有了。
最后献上demo地址,没错就是它,给我狠狠的点它。
哦哦,忘了填上面挖的两个坑了。对于web-component可以阅读下: web.dev/declarative… css-tricks.com/using-web-c… 目前在ssr方面还比较薄弱,可以去国外的论坛或者推特上面看看后续对这块的规划,很多优化的案例还在起草中,就先不作死,让大佬们先走。
对于想要做到兼容vue2,vue2.7和vue3,就需要做相应的依赖隔离,因为vue2和vue2.7,vue2.7和vue3都有共同相交的依赖只是版本不同,比如可以分两个仓库去构建等去做隔离。
好了,最后让我们一起加油,去做一个天选打工人吧!!!
转载自:https://juejin.cn/post/7178118710387802168