前言
自动化工具?npm 发包?样式按需引入?组件及样式自动引入?组件文档?单元测试?听着有点熟悉又带点陌生😱。。。别急,阅读本系列文章:将从工程化角度带你从0到1实现一个组件库,一站到底!
上篇:组件库工程化环境设计(一):自动化工具,编译构建打包?一篇搞定
阅读完本篇,你的组件库将具有以下特点:
- 使用 rollup 作为打包工具 ✅
- 支持 babel 和 esbuild 两种构建方式☑️
- 支持 cjs、esm 和浏览器直接引入✅
- 支持组件样式按需引入❓🆕
- 自动引入☑️
- 接入eslint、commitlint 等静态检测工具✅
- 能够进行 npm 发包和产出 changelog☑️
- 提供组件文档和组件示例☑️
- 接入单元测试☑️
组件按需引入
前面我们进行了组件库整体打包,接下来我们来看如何构建来支持组件库按需引入。
我设想的每一个组件目录结构都应该如下:
D:.
├─dynamic-list
│ │ dynamic-list.css
│ │ dynamic-list.mjs
│ │ index.d.ts
│ │ index.mjs
│ │ useRO.d.ts
│ │ useRO.mjs
│ │
这样我们就可以单独引入某一个组件了,初步设想用 rollup 多 input entry 来分别 build 每一个组件:
{
...base,
input: getComponentEntries(),
output: [
{
format: 'es', // 产物 js 的格式
dir: 'dist/es', // 产物所在的目录
entryFileNames: '[name].mjs', // 产物 js 的名称,这里的 name 是 input entry 的 key
assetFileNames: '[name][extname]', // 产出资源的名称
}
],
}
这里的 input 通过 getComponentEntries 方法来获取:
const resolve = dir => {
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
return path.resolve(__dirname, '../', dir);
};
// 获取导出入口
export function getComponentEntries() {
return Object.fromEntries(
glob
.sync('src/**/*.ts')
.map(file => [
path.relative('src', file.slice(0, file.length - path.extname(file).length)),
resolve(file),
]),
);
}
获取的 input 会类似如下,src 下每个组件文件都会有一条 entry:
{
'fixed-size-list/index': '绝对路径/src/fixed-size-list/index.ts'
}
上述配置的作用下,组件按照以下输出:
src/fixed-size-list/index.ts -----> dist/es/fixed-size-list/index.mjs
这样就实现了每一个组件都产出一个单独的入口。
样式按需引入
rollup-plugin-postcss 开启 extract 后能导出外部样式文件,但不能定义样式文件路径。
使用 rollup-plugin-styles代替,他的mode: 'extract'会遵循 assetFileNames,最终会产出到正确的位置:
D:.
├─dynamic-list
│ │ dynamic-list.css
│ │ dynamic-list.mjs
│ │ index.d.ts
│ │ index.mjs
│ │ useRO.d.ts
│ │ useRO.mjs
│ │
问题
这貌似就实现了按需引入,我们引入组件测试一下,组件能够正常运行。
但思考一下,我们的组件库构建速度是否合格?按需引入实现有没有什么问题?该怎样优化?
打包速度慢
roullp + babel 冷启动需要近 21.6s,重复构建也要 4.5s,这还只是几个组件。能不能想办法提升构建速度,比如接入 esbuild / SWC 来提升构建速度?
组件库内部组件引用 chunk
组件库内部组件相互引用时,被引用的组件会被 rollup chunk。
如组件fixed-size-list引用了list-item,假设有如下配置:
export const esbuildOptions = {
... base,
input: {
'packages/fixed-size-list/fixed-size-list': 'src/packages/fixed-size-list/index.ts',
'packages/list-item/list-item': 'src/packages/list-item/index.ts',
},
output: [
{
format: 'es',
dir: 'dist',
entryFileNames: `[name].mjs`,
assetFileNames: '[name][extname]',
},
],
external: ['vue'],
plugins: [
... base.plugins,
del({ targets: 'dist/*' }), // 每次 build 之前删除 dist
vue(),
esbuild(),
styles({
// 遵从 assetFileNames 路径
mode: 'extract',
plugins: [
// 依据 browserlist 自动加浏览器私有前缀
autoprefixer(),
postcssPresetEnv(),
// // 压缩 css
cssnanoPlugin(),
],
}),
],
};
我所预期的 build 后目录结构如下:
├─packages
│ ├─fixed-size-list
│ │ │ fixed-size-list.css
│ │ │ fixed-size-list.mjs
│ └─list-item
│ │ list-item.css
│ │ list-item.mjs
实际上得到的目录结构:
│ index-18712f89.js
│ index.css
└─packages
├─fixed-size-list
│ fixed-size-list.css
│ fixed-size-list.mjs
└─list-item
list-item.mjs
rollup 会将 list-item组件作为公共 chunk 输出到项目根目录。
公共 css 重复打包
rollup-plugin-postcss或一些类似的 css 处理插件虽然可以配置 extract 将样式导出为外部文件,但是当我在多个组件中引入公共 css 时,会将公共的 css 分别打包进每一个组件样式,无法自动模块分割,造成代码体积增大。
优化
打包速度慢
方案:esbuild 作为 bundler 和 transformer ?
如果目标浏览器是现代浏览器支持 es6,esbuild 作为 bundler 和 transformer,效率是 babel 的几十倍,同时也能当作 minifier,组件库是支持打包成 esm 的,那么就可以使用 rollup-plugin-esbuild:
- 来代替 babel 进行语法转换。
- 同时它还兼具压缩代码的能力,可以代替
rollup-plugin-terser。 - 同时支持 ts 转换,可以代替
rollup-plugin-typescript2或@rollup/plugin-typescript。
import esbuild, { minify } from 'rollup-plugin-esbuild';
export const esbuildOptions = {
... base,
input: `${ES_DIR}/index.mjs`,
output: [
{
format: 'es',
dir: ES_DIR,
entryFileNames: `${PACKAGE_NAME}.esm.js`,
},
{
format: 'cjs',
dir: CJS_DIR,
entryFileNames: `${PACKAGE_NAME}.cjs.js`,
exports: 'named',
},
{
format: 'umd',
name: GLOBAL_NAME,
dir: CJS_DIR,
entryFileNames: `${PACKAGE_NAME}.js`,
exports: 'named',
globals: {
vue: 'Vue',
},
},
{
format: 'umd',
name: GLOBAL_NAME,
dir: CJS_DIR,
entryFileNames: `${PACKAGE_NAME}.min.js`,
exports: 'named',
globals: {
vue: 'Vue',
},
plugins: [minify()],
},
],
external: ['vue'],
plugins: [ ... base.plugins, esbuild()],
};
结果
如上面配置我一共输出 4 种产物:
- es module
- cjs module
- umd
- umd 压缩
实际测试打包效率,冷启动耗时 18.7s,后续打包是 1.7s。
| 场景/工具 | babel + rollup | esbuild + rollup |
|---|---|---|
| 初次启动 | 21.6s | 18.7s |
| 后续打包 | 4.5s | 1.7s |
冷启动速度较慢是因为 rollup 会在第一次构建时生成缓存文件,后续的构建过程会根据缓存文件进行增量构建,速度会更快。
在后续打包方面 esbuild 比 babel 快了 1.6 倍,还是快不少,当前项目组件较少,并不能体现出很大的差距,当项目足够大时更能体现差距。
组件库内部公共组件chunk
方案:output.preserveModules
事实上 rollup 有一个 output.preserveModules 配置,该选项将使用原始模块名作为文件名,为所有模块创建单独的 chunk,而不是创建尽可能少的 chunk。
如组件fixed-size-list dynamic-list引用了list-item, 开启preserveModules:
export const esbuildOptions = {
... base,
input: {
index: 'src/index.ts',
},
output: [
{
format: 'es',
dir: 'dist',
entryFileNames: `[name].mjs`,
assetFileNames: '[name][extname]',
preserveModules: true,
},
],
external: ['vue'],
plugins: [
... base.plugins,
del({ targets: 'dist/*' }), // 每次 build 之前删除 dist
vue(),
esbuild(),
styles({
// 遵从 assetFileNames 路径
mode: 'extract',
plugins: [
// 依据 browserlist 自动加浏览器私有前缀
autoprefixer(),
postcssPresetEnv(),
// // 压缩 css
cssnanoPlugin(),
],
}),
],
};
产出:
- 打包后多出了
_virtual目录,@vitejs/plugin-vue等一些 plugin 为实现某些结果而产生了额外的“虚拟”文件。 - 组件库按需引入功能实现,但目录结构上不理想。
- 公共样式依然被打包在组件里。
方案:output.manualChunks
使用 manualChunks 来自定义公共 chunk 名称路径:
output: [
{
format: 'es',
dir: 'dist',
entryFileNames: `[name].mjs`,
assetFileNames: '[name][extname]',
manualChunks(id) {
if (id.includes('packages\list-item')) {
return path.join('packages', 'list-item', 'list-item');
}
},
},
],
再次打包:
└─packages
├─fixed-size-list
│ fixed-size-list.css
│ fixed-size-list.mjs
└─list-item
list-item-3ac03190.js
list-item.css
list-item.mjs
chunk 的名称、位置、大小、分包规则都需要处理,并且组件库组件被内部其他组件引用很常见,很难为这些有可能被引用的组件去创建特殊的 manualChunks。
结果:目录结构依然不理想
光靠 rollup 配置和插件无法满足我们组件库的需求。
公共 css 重复打包
公共 css 会在需要的组件中单独引入,这样难免被打包在一起,我们如果可以为组件生成单独的样式样式入口,组件和样式分开打包,那么就可以避免这个问题,注意还需要考虑到组件样式隔离,可以从 css 引入方案做出改变。
方案一:私有前缀
组件和样式互不干涉,css 不写在 .vue文件中,组件中不引用公共 css 文件。
CSS Module、CSS In JS 方案需要引用样式文件可以排除,使用带私有前缀的 class 来进行样式隔离,样式文件单独打包可以满足要求。
方案二:动态提取 css 导入语句
使用 Vue Scoped来样式隔离,编写 babel plugin 或者自己写一个 compiler 来高度自定义打包流程。
可以在处理 js 前将 css 导入语句提取出,随后再写入,以此避免公共 css 随组件样式打包在一起。
结果
我选择的是方案二。
方案一比较适合 JSX 这种 HTML In JS,Vue SFC 自带 Scoped 样式隔离方案,选择方案二更合适。
结论:编写 compiler 自定义打包流程
打包速度慢还有优化空间,组件库内部公共组件chunk问题却无法解决,公共 css 重复打包方案还有待确认。
综上,现有的工具可能无法满足编译需求,下一篇章,我们将编写一个自己的组件库 compiler,在自定义打包流程中解决上述问题。