【Vuejs】1842- 一文揭秘Vue3组件库的优雅打包与细节
我认为,前端工程化更像是个命题作文,没有标准答案,能解决问题即可。所以,本文旨在以我解决问题的实战经过来分享一种组件库的打包方式,希望大家都会有所启发、有所收获!废话少说,直接开始吧。
现状&目标
看过我文章的同学应该知道很久之前我搞了个组件库,还写了些文章——快上车!从零开始搭建一个属于自己的组件库![1]...之类的分享给大家。但其实组件库一直处于一个发展阶段,还有很多东西都不成熟,就好比今天的主角——组件库打包。
这里先简单回顾一下之前的组件库打包:
-
直接配置 vite.config
文件。只配置了lib
模式打包 -
运行 pnpm run build
,其实就是执行vite build
简单粗暴完成组件库打包 -
输出 umd
、iife
、es
、cjs
模式的产物文件 -
输出一个 style.css
样式文件
这是最终打包完的 dist 包下的目录结构:
简单点开看一下 es 包的产物代码:
好了,以上就是之前的组件库打包的方案和结果,简单又好用,简直了。但是为什么现在的我要选择升级打包方式呢?是基于什么样的痛点?这一个我会在后文慢慢道来。这里,大家暂且先跟我一起看看本文的实战目标。
讲到实战目标,不得不以优秀的开源项目为标杆。这里我就直接按照 element-plus
的产物格式作为目标了。跟大家简单的看看它的打包产物的结构,如下图所示:
其中再点开它的 es 目录看看究竟:
非常工整的结构,也就是打包前的原项目结构。感兴趣的可以去 unpkg[2]或者自己装一个在node_modules
中查看。这里我们直接分析产物结构如下:
-
dist
。放整包的,简单理解为一个打包所有代码并压缩的.min.js
-
es
。es包,产物按项目的目录结构生成。简单理解为只将ts
、.vue
文件编译成js
-
lib
。跟上面的 es 的一样,只是这里是 cjs 模式的 -
theme-chalk
。样式代码,各组件的css
文件和一个整体的index
文件 -
package.json
。emmm...这个大家自己翻译吧 -
*.d.ts
。可配合 vscode 的 voloar 插件实现代码提示 -
*.json
。用于 webstorm 的代码提示
所以,上述就是本文的目标了,我也要通过对组件库打包的升级改造,让组件库的打包产物跟上述结构、功能相似。不过,本文只着重分享组件库的打包,代码提示相关的实践并不会涉及,并且我打算另外写一篇文章来分享组件库的代码提示实战。
分离CSS
这一小节,我将为大家解开上一小节我遗留下来的一个问题:我是基于什么样的痛点才要升级组件库的打包?毕竟这种大佬都不愿意投入资源的非kpi项目,我有那时间摸摸鱼不香吗是吧~
回到本小节的主题,为什么要分离css
?那就得看之前是怎么开发的了。
如上图所示,这是最基本的 vue 开发模式了:template
+ script
+ css
。这样写法的组件,通过 vite
的 lib
模式直接打包,可以得到产物:**.js
和一个 style.css
,这一点没问题吧,前文已经讲过了。
这样有什么样的问题?这样会导致所有组件的样式都被打包进了同一个 css
文件里,按需引用对于样式文件来说就不存在了。当然,这也只是其中一个问题,而且还是个小问题而已,我肯定不会因此而升级打包方式和改变项目组成结构的,所以我们接着往下看先。
如果之前有看过我另一篇组件库实战文章:组件库实战——按需加载工程化[3] 的同学应该知道,那时候的解决方案其实也是有缺陷的,这也是当前遇到痛点必须解决的地方。当然,没看过的也没关系,这里我简单的说明一下:
在使用element-plus按需导入[4]的项目中,如果直接使用自己组件库的组件(我是vc-
前缀的),会丢掉原本el-
组件的样式。比如说我直接在代码中使用 vc-button
,而他又依赖el-button
的样式,此时就会丢失el-button
的样式。原因就是按需引入的插件匹配 vc-button
的组件时并不知道需要引入 el-button
的样式,于是我的自己写一个插件工具来实现这一点。
如上图所示,这个插件工具——resolver当解析到 template
中有 vc-button
的时候,会去 import vcButton
的组件代码和 import el-button
的样式代码。(此时 vc-button
的样式文件是全局引入的。因为打包后只有一个 style.css
)
虽然这个自动导入的问题是解决了,但是又遗留了另一个问题就是组合组件的样式问题。比如说此时我的 vc-button
不仅用了 el-button
,还组合了 el-tag
、el-select
等组件使用,那上述插件工具就没用了,因为缺少import
另外两个组件的样式。
如需在使用组合组件的时候样式能正常引入,那你得告诉插件这个组件用了哪些组件的样式是吧?那这个要怎么做呢?其实我们可以参考老大哥——element-plus
的实现。因为他就有这样的案例,比如说他的 select
组件,就是多个 el-
组件组合成的。我们去看一下他的源码:
可以看到 select
目录下有一个 style
目录,点开里面的文件可以看到其做了一个样式的集成。引用了 input
、tag
、popper
等组件的样式。虽然之前参与这个项目的时候没怎么留意过这个 style
目录(也不知道是干嘛的),但是现在大概可以猜出他就是一个样式关系表,做整合的。结合上述说明,我们重新画个图来看看就很清晰了:
这里,我们可以看出 import
样式那一块上不再是直接用 el-button
的样式了,而是用了 vc-button
的样式索引文件(是个js文件),而这个索引文件呢,引用了各种它所需要的样式文件。如此一来,之前遗留下来的缺陷问题也就引刃而解了。
这也是将 css 拆分出来的好处,让他形成一个 原子css 的概念。将每一个组件的样式都单独抽出来写,每一个组件的样式就是一个原子css,这样组合使用的时候也就很方便了。
然后,这里再顺便提一下为什么这次的更新打包,要保留原本的目录结构来打包。或者说为什么 element-plus
产物的 es
、lib
目录下是保留了原项目结构的。我们来看看它官网的其中一个介绍:
我个人猜测,如果需要手动引入样式的话,还是要知道去哪里引用的对吧?总不能打包完代码就乱成一团,然后用户需要手动引入的时候不知道去哪里引入了...
好了,这一小节说得有点长,我简单总结一下:
-
解释了自建组件库使用 unplugin之类插件[5] 实现自动按需导入的样式引入问题。 -
解释了为什么要分离 css?核心解决组合组件的样式问题,顺便解决按需加载的体量问题。 -
顺便分析了为什么组件库打包完要保留原项目结构。
编写打包脚本
前文铺垫了这么多,终于轮到本文的重头戏了。那这一小节,我们主要实现几个目标点:
-
使用 gulp 串并联工作流完成打包任务 -
打包出 全局dist、es、lib 的产物。其中es、lib目录要保持原目录结构 -
抽离css,并且编译打包css(这里我用的是scss)
这里因为我们要对不同格式根本打包,再配置成 vite.config
并直接通过 vite build
来打包肯定是不够方便的了,所以我借助 gulp
来完成一个简单的打包脚本。正式进入打包环节前,先来解决工具选择的问题。目前我个人意向的是 vite
和 rollup
,我是如何选择的呢?在此,我先撸了个图:
从图中大概可以看出来,vite
虽然生产环境默认使用 rollup
来构建,但相比于 rollup
,它是更为上层的。它会有更多的集成,比如说集成了对 ts 的支持,对 scss、less 等支持,还有各种基础的插件集成(如支持直接 require
模块),当然,他还预置了一些通用的 rollup
配置。
讲这么多,简单来说就是 vite
更上层,使用方便,适合懒人;**rollup
更底层,使用灵活**,适合有激情爱折腾的大佬!我当然是选择了前者~如果说想使用 rollup
的话,建议大家直接参考 element-plus
的打包吧,它就是基于 rollup
写的打包脚本。
我们直接看基于 vite
写打包脚本的基本格式:
import { build } from 'vite'
function buildScript () {
build({
plugins: [],
build: {
outDir: 'xxx',
lib: {
entry: 'xxx'
}
},
})
}
其实也很简单,安装 vite
,然后 import
它暴露出来的 build
函数,并对其中做一些配置。这些配置就跟我们平时配置 vite.config
文件是一样的,一把梭哈,基本没什么难度。接下来我们看看其中每一步的一些核心点吧。
1. 打包 es、lib 包并保留原结构
关于 es、lib 包,我们依旧是使用 vite 的 lib 模式去构建[6](详细可点击链接去了解)。这里简单的列一下基础的配置:
{
plugins: [
vue(),
vueJsx()
],
build: {
outDir: join(vcElementPlusRoot, 'dist', 'es'),
lib: {
entry: files,
formats: ['es'],
},
rollupOptions: {
external: ['element-plus', 'vue', 'vue-router', '@element-plus/icons-vue']
}
}
}
其中的 plugins
配置中,因为我们打包的是 vue3 组件库,所以会用到 vitejs/plugin-vue[7] 等相关的 vue 插件。
build
配置中,我们主要看 lib.entry
即可。这里的入口是可以传数组(多个)的,如下图的说明:
这里可以通过一个工具——fast-glob[8]拿到所有的入口,包括 style
目录中的索引文件的入口(这一点后面会提到)。
如以下写法,就能拿到组件库的全部入口了:
const files = await glob('**/*.{js,ts,vue}', {
cwd: vcElementPlusComponentsRoot,
absolute: true,
onlyFiles: true,
})
获取的结果如下截图(大家可以自己试着玩玩):
剩下的就是 rollupOptions
配置,当我们想实现打包后保留原项目结构,必须配置:
-
preserveModules[9]。此模式将使用原始模块名称作为文件名为所有模块创建单独的块,而不是创建尽可能少的块。(感兴趣的点击进去看看吧,这里我随便找个翻译软件翻译的) -
preserveModulesRoot[10]。当output.preserveModules为true时,应该从output.dir路径中剥离的输入模块的目录路径(同上,感兴趣点链接了解吧)
ok,基本上有了这些之后,打包 es、lib 的任务就完成了。最终脚本代码如下:
await build({
resolve: {
alias: VcElementAlias()
},
plugins: [
vue(),
vueJsx()
],
build: {
outDir: join(vcElementPlusRoot, 'dist', 'es'),
lib: {
entry: files,
formats: ['es'],
},
rollupOptions: {
external: ['element-plus', 'vue', 'vue-router', '@element-plus/icons-vue'],
output: {
preserveModules: true,
preserveModulesRoot: vcElementPlusComponentsRoot,
exports: 'named'
}
}
}
})
接着,我运行一下打包看看打包后的效果:
为了方便大家看产物结构,我用不同颜色的框框划分了打包后的产物结构,可以看到基本符合我们的预期了。(cjs模式的打包跟es类似的,所以我就不展开lib目录的打包过程和产物了)
2. 打包 dist 代码
这一点相比上一点来说要更加的简单,其实就是我们改版前的那种。无脑配置个输出模式为 umd
和 iife
就完成了。简单看看相比前文的差异的配置:
build: {
outDir: join(vcElementPlusRoot, 'dist'),
lib: {
entry: file,
formats: ['umd', 'iife'],
name: VC_ELEMENT_PLUS_CAMELCASE_NAME,
fileName: format => `${VC_ELEMENT_PLUS}.${format}.js`
}
...
}
其余的基本没什么不同,不过注意这里要配置 name
和 fileName
。当然,这些你不配置的话,打包也不会成功,并且会有报错提示,只要根据报错提示完成对应的配置后,问题也不大。我打算把这两个文件放在 dist 的根目录中,跟 es
、lib
目录同级。
最终打包出来的结果如下:
也是符合预期的,我们接着往下走。
3. 编译&打包CSS
这一步,我借助了gulp-sass[11]插件。它的作用是:用于将 Sass 代码编译成 CSS 代码。在正式讲打包之前,我给大家看看我抽离的样式文件大概成什么样:
我将原本都各自写在组件内的样式抽离出来,放在一个 theme
的目录下的。每一个 scss
文件以组件名命名,他们就是一个个组件的原子css。然后我会在组件对应的 style
索引文件中这样引用样式(直接引用 scss
文件):
大家也可能注意到了,在 theme
目录下,也有一个 index.scss
文件,它的代码是这样的:
没错,它其实就是一个总的样式文件。紧接着,我们直接看看关于 scss
的编译、打包脚本如何实现吧:
import gulpSass from 'gulp-sass'
import gulp from 'gulp'
import dartSass from 'sass'
import { vcElementPlusRoot } from '../../utils/path';
export async function sassCompiler () {
const sass = gulpSass(dartSass)
return await gulp.src(`${vcElementPlusRoot}/theme/*.scss`) // 入口
.pipe(sass.sync()) // 编译
.pipe(gulp.dest(`${vcElementPlusRoot}/dist/theme`)) // 输出目录
}
对于上述代码,他的作用就是编译所有的 scss
文件,并且将他们都打包进 index
文件中。因为关于 gulp-sass
、 gulp
等这些工具,我也是要用的时候才去写,平时也了解不多,所以我就不多展开他们的一些用法、写法了,大家感兴趣可以自己去研究一下。
最后也是来看看打包后的效果:
可以看到,所有的 scss
文件都被编译成了 css
文件,此时,我们打开一下 index.css
文件大概看看成什么样:
我没有做代码压缩,所以大家可以一目了然,应该是所有组件的 css 都被打进来了,没问题!
4. 通过 gulp
编排任务
其实这就是一个工作流工具,有了解过 CI/CD 的同学应该很清楚它是干嘛的了。当然,还是那句话,我并不是常年使用 gulp 的,所以了解得也并不多,这里使用也就是为了解决问题,达成目的,所以我也不会过多的展开对 gulp 的讲解。大家感兴趣的可以去他的官网[12]详细看看。
因为前面我们已经把打包任务都分别实现了,最后通过 gulp
做一个串联而已。所以我还是直接上代码吧:
import { series, parallel } from 'gulp'
import { cleanDist, elpBuildModules, elpBuildBundle, sassCompiler } from './tasks'
export default series(
cleanDist, // 删除上次的dist
parallel(
elpBuildModules, // 并行执行 es、lib 打包
elpBuildBundle, // 并行执行 全局dist 打包
sassCompiler // 并行执行 scss 的编译打包
)
)
整个 gulpfile 就这么点,简单来说它做的事情就是删除上次的 dist,然后进行 es、lib、dist、scss 的编译打包工作。
彩蛋——rollup插件改动样式索引文件
不知道大家发现没,前文两个地方我都埋了点伏笔:
-
入口为什么要包含 style
目录中的索引文件 -
组件 style
目录中的索引文件直接引用scss
文件:
相信已经很明显了,因为直接引用 scss
文件作为样式文件在浏览器中无法直接使用!所以我们需要对其做一些改动。**开发环境中,因为 vite
天然支持 scss
**(只要安装了sass的包就行,不用任何插件配置),所以我们在开发环境使用样式时,直接 import
我们的索引文件(再次提醒索引文件是个js
)是没问题的,比如:
import {vcButton} from '@xxx'
import '@xxx/button/style/index.js'
但是如果此时到了浏览器环境直接使用的话就不行了,因为 index.js
中 import
的是一个 scss
的文件。所以我们还需要自己写一个 rollup
插件,在打包的时候将索引文件引用的路径做一点改动。
这个插件的目标就是**将 scss
替换成 css
**,如:
-
import '@lizhife/vc-element-plus/theme/back.scss'
-
import '@lizhife/vc-element-plus/theme/back.css'
当然,对应的import
路径配置那些也要配置好,不然可能在路径上也要有所改动。这里我就基于 rollup
的 resolveId[13] 钩子。当然,这一段在 vite 官网[14]也能看到。
简单说说 resolveId
钩子的作用,他能拿到你所有 import
的包名、路径,并在参数中提供给你。所以基于此,我们可以这样来写这个插件:
export function rollupPluginCompileStyleEntry (): Plugin {
const themeEntryPrefix = `${PREFIX}/${VC_ELEMENT_PLUS}/${THEME}`
return {
name: 'rollup-plugin-compile-style-entry',
resolveId (id) {
// 匹配是否满足 @xxx/vc-el.. 开头的字符
if (!id.startsWith(themeEntryPrefix)) return
return {
// 将 scss 字符替换成 css
id: id.replaceAll('.scss', '.css'),
external: 'absolute',
}
}
}
}
这个插件的核心就如注释那样了,匹配一个固定开头的字符(比如这里是 @lizhife/vc-element-plus
),**将这个字符串的 .scss
替换成 .css
**。我们直接看结果,看看使用了这个插件后的效果如何:
可以看到,import
的最终结果变成了 xx.css
,这也符合我们的期望,完美~当然啦,记得把插件配置上,不然就白搞了:
plugins: [
rollupPluginCompileStyleEntry(),
vue(),
vueJsx()
],
彩蛋——alias配置
当在项目中使用自身依赖时,需要注意配置alias。
Error: [vite]: Rollup failed to resolve import "@lizhife/vc-element-plus" from '...'
当遇到上述的一些因为包名而导致的无法解析的问题,可以通过配置 alias 来解决,特别是一些开发环境和打包完之后有所变动的。相关的我也在这里说太多了,之前的文章也有提到这一点。
涉及的插件简介
这里我会介绍本次实战中会用到的各种插件、工具和他们的作用简介,希望可以帮助大家更清晰地了解本文的内容。另外我会附上每个插件的gayhub地址,感兴趣的同学可以戳进去详细了解。
-
@esbuild-kit/cjs-loader[15]:
-
支持在 gulpfile 使用 esm(import、export)的模块化写法 -
支持在 gulpfile 使用 ts
-
fast-glob[16]
-
提供了一种快速、灵活的方式来匹配文件和目录。
-
@vitejs/plugin-vue[17]:
-
支持 vite 解析 .vue
后缀的单文件组件(SFC),类似 webpack 中我们用的vue-loader
-
@vitejs/plugin-vue-jsx[18]:
-
支持 vite 解析 jsx/tsx
。同第3
点,并且二者是放在同一个仓库中的
-
gulp-sass[19]
-
一个 gulp 插件,用于将 Sass 代码编译成 CSS 代码
写在最后
文章内容有点长,大家点赞关注慢慢看~关于组件库打包的内容输出,之前就有小伙伴催更了,但是因为之前没啥使用上的问题,并且这一块投入也麻烦,所以一直没搞。组件库慢慢发展到现在,组件数量慢慢上升,发展遇到瓶颈了所以需要升级一下组件库的架构和打包。当然,后续有相关的组件库实战我会持续的输出文章分享。
最后,如果本文有哪些写得不对的地方,大家尽管指出。希望这篇文章在你的工程化道路上有所启发。再重申一下,工程化是开放性作文,思路、方案有很多,能解决问题的就是可行的,并没有标准答案。
作者:
回复“加群”,一起学习进步