一文揭秘 Vue3 组件库的优雅打包与细节

作者:井柏然 

https://juejin.cn/post/7287524648808333323

千呼万唤始出来干货满满的组件库打包分享!这次,终于要给大家带来关于组件库的工程化打包升级与细节的文章啦。回顾组件库的搭建,已经是很久以前了,目前组件库也发展到一个瓶颈期需要升级架构和打包了...

我认为,前端工程化更像是个命题作文,没有标准答案,能解决问题即可。所以,本文旨在以我解决问题的实战经过来分享一种组件库的打包方式,希望大家都会有所启发、有所收获!废话少说,直接开始吧。

现状&目标

很久之前我搞了个组件库,还写了文章——从零开始搭建一个属于你自己的组件库!分享给大家。但其实组件库一直处于一个发展阶段,还有很多东西都不成熟,就好比今天的主角——组件库打包

这里先简单回顾一下之前的组件库打包:

  1. 直接配置 vite.config 文件。只配置了lib模式打包
  2. 运行 pnpm run build,其实就是执行 vite build 简单粗暴完成组件库打包
  3. 输出 umdiifeescjs 模式的产物文件
  4. 输出一个 style.css 样式文件

这是最终打包完的 dist 包下的目录结构:

简单点开看一下 es 包的产物代码:

好了,以上就是之前的组件库打包的方案和结果,简单又好用,简直了。但是为什么现在的我要选择升级打包方式呢?是基于什么样的痛点?这一个我会在后文慢慢道来。这里,大家暂且先跟我一起看看本文的实战目标

讲到实战目标,不得不以优秀的开源项目为标杆。这里我就直接按照 element-plus 的产物格式作为目标了。跟大家简单的看看它的打包产物的结构,如下图所示:

其中再点开它的 es 目录看看究竟:

非常工整的结构,也就是打包前的原项目结构。感兴趣的可以去 unpkg[1]或者自己装一个在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。这样写法的组件,通过 vitelib 模式直接打包,可以得到产物:**.js 和一个 style.css,这一点没问题吧,前文已经讲过了。

这样有什么样的问题?这样会导致所有组件的样式都被打包进了同一个 css 文件里,按需引用对于样式文件来说就不存在了。当然,这也只是其中一个问题,而且还是个小问题而已,我肯定不会因此而升级打包方式和改变项目组成结构的,所以我们接着往下看先。

如果之前有看过我另一篇组件库实战文章:组件库实战——按需加载工程化[2] 的同学应该知道,那时候的解决方案其实也是有缺陷的,这也是当前遇到痛点必须解决的地方。当然,没看过的也没关系,这里我简单的说明一下:

在使用element-plus按需导入[3]的项目中,如果直接使用自己组件库的组件(我是vc-前缀的),会丢掉原本el-组件的样式。比如说我直接在代码中使用 vc-button,而他又依赖el-button的样式,此时就会丢失el-button的样式。原因就是按需引入的插件匹配 vc-button 的组件时并不知道需要引入 el-button 的样式,于是我的自己写一个插件工具来实现这一点。

resolver说明.png

如上图所示,这个插件工具——resolver当解析到 template 中有 vc-button 的时候,会去 import vcButton 的组件代码import el-button 的样式代码。(此时 vc-button 的样式文件是全局引入的。因为打包后只有一个 style.css

虽然这个自动导入的问题是解决了,但是又遗留了另一个问题就是组合组件的样式问题。比如说此时我的 vc-button 不仅用了 el-button,还组合了 el-tagel-select 等组件使用,那上述插件工具就没用了,因为缺少import另外两个组件的样式

如需在使用组合组件的时候样式能正常引入,那你得告诉插件这个组件用了哪些组件的样式是吧?那这个要怎么做呢?其实我们可以参考老大哥——element-plus 的实现。因为他就有这样的案例,比如说他的 select 组件,就是多个 el- 组件组合成的。我们去看一下他的源码:

可以看到 select 目录下有一个 style 目录,点开里面的文件可以看到其做了一个样式的集成。引用了 inputtagpopper 等组件的样式。虽然之前参与这个项目的时候没怎么留意过这个 style 目录(也不知道是干嘛的),但是现在大概可以猜出他就是一个样式关系表,做整合的。结合上述说明,我们重新画个图来看看就很清晰了:

resolver说明2.png

这里,我们可以看出 import 样式那一块上不再是直接用 el-button 的样式了,而是用了 vc-button的样式索引文件(是个js文件),而这个索引文件呢,引用了各种它所需要的样式文件。如此一来,之前遗留下来的缺陷问题也就引刃而解了。

这也是将 css 拆分出来的好处,让他形成一个 原子css 的概念。将每一个组件的样式都单独抽出来写,每一个组件的样式就是一个原子css,这样组合使用的时候也就很方便了。

然后,这里再顺便提一下为什么这次的更新打包,要保留原本的目录结构来打包。或者说为什么 element-plus 产物的 eslib 目录下是保留了原项目结构的。我们来看看它官网的其中一个介绍:

我个人猜测,如果需要手动引入样式的话,还是要知道去哪里引用的对吧?总不能打包完代码就乱成一团,然后用户需要手动引入的时候不知道去哪里引入了...

好了,这一小节说得有点长,我简单总结一下:

  1. 解释了自建组件库使用 unplugin之类插件[4] 实现自动按需导入的样式引入问题。
  2. 解释了为什么要分离 css?核心解决组合组件的样式问题,顺便解决按需加载的体量问题。
  3. 顺便分析了为什么组件库打包完要保留原项目结构。

编写打包脚本

前文铺垫了这么多,终于轮到本文的重头戏了。那这一小节,我们主要实现几个目标点:

  1. 使用 gulp 串并联工作流完成打包任务
  2. 打包出 全局dist、es、lib 的产物。其中es、lib目录要保持原目录结构
  3. 抽离css,并且编译打包css(这里我用的是scss)

这里因为我们要对不同格式根本打包,再配置成 vite.config 并直接通过 vite build 来打包肯定是不够方便的了,所以我借助 gulp 来完成一个简单的打包脚本。正式进入打包环节前,先来解决工具选择的问题。目前我个人意向的是 viterollup,我是如何选择的呢?在此,我先撸了个图:

打包工具选择.png

从图中大概可以看出来,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 模式去构建[5](详细可点击链接去了解)。这里简单的列一下基础的配置:

 {
  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[6] 等相关的 vue 插件。

build 配置中,我们主要看 lib.entry 即可。这里的入口是可以传数组(多个)的,如下图的说明:

这里可以通过一个工具——fast-glob[7]拿到所有的入口,包括 style 目录中的索引文件的入口(这一点后面会提到)。

如以下写法,就能拿到组件库的全部入口了:

   const files = await glob('**/*.{js,ts,vue}', {
    cwd: vcElementPlusComponentsRoot,
    absolutetrue,
    onlyFilestrue,
  })

获取的结果如下截图(大家可以自己试着玩玩):

剩下的就是 rollupOptions 配置,当我们想实现打包后保留原项目结构,必须配置:

  1. preserveModules[8]。此模式将使用原始模块名称作为文件名为所有模块创建单独的块,而不是创建尽可能少的块。(感兴趣的点击进去看看吧,这里我随便找个翻译软件翻译的)
  2. preserveModulesRoot[9]。当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: {
          preserveModulestrue,
          preserveModulesRoot: vcElementPlusComponentsRoot,
          exports'named'
        }
      }
    }
  })

接着,我运行一下打包看看打包后的效果:

为了方便大家看产物结构,我用不同颜色的框框划分了打包后的产物结构,可以看到基本符合我们的预期了。(cjs模式的打包跟es类似的,所以我就不展开lib目录的打包过程和产物了)

2. 打包 dist 代码

这一点相比上一点来说要更加的简单,其实就是我们改版前的那种。无脑配置个输出模式为 umdiife 就完成了。简单看看相比前文的差异的配置:

   build: {
    outDir: join(vcElementPlusRoot, 'dist'),
    lib: {
      entry: file,
      formats: ['umd''iife'],
      name: VC_ELEMENT_PLUS_CAMELCASE_NAME,
      fileNameformat => `${VC_ELEMENT_PLUS}.${format}.js`
    }
    ...
  }  

其余的基本没什么不同,不过注意这里要配置 namefileName。当然,这些你不配置的话,打包也不会成功,并且会有报错提示,只要根据报错提示完成对应的配置后,问题也不大。我打算把这两个文件放在 dist 的根目录中,eslib 目录同级

最终打包出来的结果如下:

也是符合预期的,我们接着往下走。

3. 编译&打包CSS

这一步,我借助了gulp-sass[10]插件。它的作用是:用于将 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-sassgulp 等这些工具,我也是要用的时候才去写,平时也了解不多,所以我就不多展开他们的一些用法、写法了,大家感兴趣可以自己去研究一下。

最后也是来看看打包后的效果:

可以看到,所有的 scss 文件都被编译成了 css 文件,此时,我们打开一下 index.css 文件大概看看成什么样:

我没有做代码压缩,所以大家可以一目了然,应该是所有组件的 css 都被打进来了,没问题!

4. 通过 gulp 编排任务

其实这就是一个工作流工具,有了解过 CI/CD 的同学应该很清楚它是干嘛的了。当然,还是那句话,我并不是常年使用 gulp 的,所以了解得也并不多,这里使用也就是为了解决问题,达成目的,所以我也不会过多的展开对 gulp 的讲解。大家感兴趣的可以去他的官网[11]详细看看。

因为前面我们已经把打包任务都分别实现了,最后通过 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插件改动样式索引文件

不知道大家发现没,前文两个地方我都埋了点伏笔:

  1. 入口为什么要包含 style 目录中的索引文件
  2. 组件 style 目录中的索引文件直接引用 scss 文件:

相信已经很明显了,因为直接引用 scss 文件作为样式文件在浏览器中无法直接使用!所以我们需要对其做一些改动。**开发环境中,因为 vite 天然支持 scss**(只要安装了sass的包就行,不用任何插件配置),所以我们在开发环境使用样式时,直接 import 我们的索引文件(再次提醒索引文件是个js)是没问题的,比如:

 import {vcButton} from '@xxx'
import '@xxx/button/style/index.js'

但是如果此时到了浏览器环境直接使用的话就不行了,因为 index.jsimport 的是一个 scss 的文件。所以我们还需要自己写一个 rollup 插件,在打包的时候将索引文件引用的路径做一点改动。

这个插件的目标就是**将 scss 替换成 css**,如:

  • import '@lizhife/vc-element-plus/theme/back.scss'
  • import '@lizhife/vc-element-plus/theme/back.css'

当然,对应的import路径配置那些也要配置好,不然可能在路径上也要有所改动。这里我就基于 rollupresolveId[12] 钩子。当然,这一段在 vite 官网[13]也能看到。

简单说说 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地址,感兴趣的同学可以戳进去详细了解。

  1. @esbuild-kit/cjs-loader[14]
  • 支持在 gulpfile 使用 esm(import、export)的模块化写法
  • 支持在 gulpfile 使用 ts
  1. fast-glob[15]
  • 提供了一种快速、灵活的方式来匹配文件和目录。
  1. @vitejs/plugin-vue[16]
  • 支持 vite 解析.vue后缀的单文件组件(SFC),类似 webpack 中我们用的 vue-loader
  1. @vitejs/plugin-vue-jsx[17]
  • 支持 vite 解析 jsx/tsx。同第3点,并且二者是放在同一个仓库中的
  1. gulp-sass[18]
  • 一个 gulp 插件,用于将 Sass 代码编译成 CSS 代码

写在最后

文章内容有点长,大家点赞关注慢慢看~关于组件库打包的内容输出,之前就有小伙伴催更了,但是因为之前没啥使用上的问题,并且这一块投入也麻烦,所以一直没搞。组件库慢慢发展到现在,组件数量慢慢上升,发展遇到瓶颈了所以需要升级一下组件库的架构和打包。当然,后续有相关的组件库实战我会持续的输出文章分享。

最后,如果本文有哪些写得不对的地方,大家尽管指出。希望这篇文章在你的工程化道路上有所启发。再重申一下,工程化是开放性作文,思路、方案有很多,能解决问题的就是可行的,并没有标准答案。

参考资料

[1]

unpkg: https://link.juejin.cn?target=https%3A%2F%2Funpkg.com%2Fbrowse%2Felement-plus%402.3.14%2F

[2]

组件库实战——按需加载工程化: https://juejin.cn/post/7199591833522176058

[3]

element-plus按需导入: https://link.juejin.cn?target=https%3A%2F%2Felement-plus.org%2Fzh-CN%2Fguide%2Fquickstart.html%23%E6%8C%89%E9%9C%80%E5%AF%BC%E5%85%A5

[4]

unplugin之类插件: https://link.juejin.cn?target=https%3A%2F%2Fgithub.com%2Funplugin%2Funplugin-auto-import

[5]

vite 的 lib 模式去构建: https://link.juejin.cn?target=https%3A%2F%2Fcn.vitejs.dev%2Fguide%2Fbuild.html%23library-mode

[6]

vitejs/plugin-vue: https://link.juejin.cn?target=https%3A%2F%2Fgithub.com%2Fvitejs%2Fvite-plugin-vue

[7]

fast-glob: https://link.juejin.cn?target=https%3A%2F%2Fgithub.com%2Fmrmlnc%2Ffast-glob%23readme

[8]

preserveModules: https://link.juejin.cn?target=https%3A%2F%2Frollupjs.org%2Fconfiguration-options%2F%23output-preservemodules

[9]

preserveModulesRoot: https://link.juejin.cn?target=https%3A%2F%2Frollupjs.org%2Fconfiguration-options%2F%23output-preservemodulesroot

[10]

gulp-sass: https://link.juejin.cn?target=https%3A%2F%2Fgithub.com%2Fdlmanning%2Fgulp-sass

[11]

官网: https://link.juejin.cn?target=https%3A%2F%2Fwww.gulpjs.com.cn%2Fdocs%2Fgetting-started%2Fquick-start%2F

[12]

resolveId: https://link.juejin.cn?target=https%3A%2F%2Frollupjs.org%2Fplugin-development%2F%23resolveid

[13]

vite 官网: https://link.juejin.cn?target=https%3A%2F%2Fcn.vitejs.dev%2Fguide%2Fapi-plugin.html%23universal-hooks

[14]

@esbuild-kit/cjs-loader: https://link.juejin.cn?target=https%3A%2F%2Fgithub.com%2Fesbuild-kit%2Fcjs-loader

[15]

fast-glob: https://link.juejin.cn?target=https%3A%2F%2Fgithub.com%2Fmrmlnc%2Ffast-glob%23readme

[16]

@vitejs/plugin-vue: https://link.juejin.cn?target=https%3A%2F%2Fgithub.com%2Fvitejs%2Fvite-plugin-vue

[17]

@vitejs/plugin-vue-jsx: https://link.juejin.cn?target=https%3A%2F%2Fgithub.com%2Fvitejs%2Fvite-plugin-vue

[18]

gulp-sass: https://link.juejin.cn?target=https%3A%2F%2Fgithub.com%2Fdlmanning%2Fgulp-sass

向下滑动查看

推荐阅读  点击标题可跳转

1、React 和 Vue 全方位对比总结

2、如何写一个属于自己的vue3组件库

3、React 还是 Vue?我对 Web 前端现状的看法

相关推荐

  • 超详细讲解H5移动端适配
  • 300元一年,QQ邮箱准备收费了
  • 今年这行情......我看大家还是多留一手准备吧 !
  • TimeGPT:时间序列预测的第一个基础模型
  • 深入理解 Spring 注解驱动配置与 XML 配置的融合与区别
  • 独家专访@爱可可-爱生活:如何做好科学研究(干货满满)
  • 七张图解锁Mybatis整体脉络,让你轻松拿捏面试官
  • 一套万能通用的异步处理方案
  • 一年私吞260余万元?程序员利用漏洞篡改ETC余额,已被刑拘
  • 小米 14 系列手机起售价 3999 元;新神经网络在语言归纳能力上接近人类;亚马逊推出人工智能图像生成功能|极客头条
  • 如何防止网站信息泄露(复制/水印/控制台)
  • 一文揭秘Vue3组件库的优雅打包与细节
  • 技术栈Vue全家桶,面某大厂被吊打的惨痛教训
  • (待会删)付费搞来的AI资源,低调浏览!!!
  • 《HelloGitHub》第 91 期
  • 得益于 WeakMap,新发布的 Vue 3.3.6 更快了
  • React Router初学者入门指南(2023版)
  • Spring的BeanFactory与FactoryBean的区别
  • RLHF模型普遍存在「阿谀奉承」,从Claude到GPT-4无一幸免
  • 大年三十,我在公司过大年