本节涉及的内容源码可在vue-pro-components c7 分支[1]找到,欢迎 支持!
本文是基于Vite+AntDesignVue打造业务组件库[2] 专栏第 8 篇文章【函数库Rollup构建优化】,在上一篇文章的基础上,聊聊在使用 Rollup 构建函数库的过程中还可以做哪些优化。
在上篇文章中,我们掌握了怎么打包 ESM, CJS, UMD,还掌握了怎么生成类型声明文件d.ts
,但是我们可以发现,我们生成的 UMD 文件dist/index.js
并不知道没有经过压缩,我们可以尝试给它再压缩一下,这可以用到 Rollup 官方的插件 rollup-plugin-terser。
由于压缩版通常是直接通过script
标签引入用在浏览器环境中,所以打包成 IIFE(立即执行函数表达式)格式就行。我们改造一下buildBundle
函数。
export const buildBundle = async () => {
const bundle = await rollup({
input: resolve(UTILS_PATH, 'src/index.ts'),
plugins: [rollupTypescript()],
})
await Promise.all([
bundle.write({
name: 'VpUtils',
format: 'umd',
file: resolve(UTILS_PATH, 'dist/index.js'),
sourcemap: true,
}),
bundle.write({
name: 'VpUtils',
// 考虑到使用场景,输出 iife 格式即可
format: 'iife',
// 生成一个 dist/index.min.js 作为压缩版本
file: resolve(UTILS_PATH, 'dist/index.min.js'),
// 使用了 rollup-plugin-terser 插件
plugins: [terser()]
})
])
}
再次打包就会生成这种 IIFE 的压缩代码了。
我们已经支持了生成类型声明文件,所以正常使用@vue-pro-components/utils
模块时,是有类型支持的。
可以看到,上面的函数签名都是有的。
但是,当我们按需使用其中一个模块时,会发现 TypeScript 似乎找不到对应的类型声明。
观察上图可以发现,当我们引用其中一个模块的完整路径时,TypeScript 报了错表示找不到类型声明文件。这是为什么呢?明明我们已经生成了d.ts
,也配置了 package.json 文件中的types
属性......
实际上,package.json 中的types
属性只是为简单的包名引用提供了类型声明文件的路径,也就是说types
只是让import { xxx } from '@vue-pro-components/utils'
有了类型支持。对其他的路径下的模块引用并没有什么帮助。
不慌,在导入.js
模块时,TypeScript 会自动加载与.js
同名的.d.ts
文件,以提供类型声明。我们可以在生产的es/fullscreen.js
文件的相同目录中放置一个fullscreen.d.ts
试试(从 types 目录抄过来即可)。
可以发现已经不报错了,那我们的思路就很清晰了,只要把 types 目录下生成的类型声明文件抄一份到 es 和 lib 目录,就可以保证按需使用模块时的类型支持了。
我们回忆一下整个流程,
不难想明白要抄一份类型声明文件到 es 和 lib 目录,最好的时机就是在并行任务结束之后,再补一个 copy dts 节点。copy 文件在 gulp 里是很容易实现的,不需要借助任何插件。通过 src 取得输入后,可以用两个 pipe + dest 分别 copy 到 es 和 lib 目录中。
export const copyDts = async () => {
return src("types/**/*.d.ts", {
cwd: UTILS_PATH,
})
.pipe(dest(resolve(UTILS_PATH, "es")))
.pipe(dest(resolve(UTILS_PATH, "lib")))
}
然后改造一下入口函数startBuildUtils
,在并行任务结束后,加一个 copyDts 节点。
export const startBuildUtils = series(
parallel(buildModules, buildBundle, buildTypes),
copyDts
)
效果如下:
基于此,我们按需使用任何一个子模块都能得到完备的类型支持了。
当函数库依赖第三方模块时,我们需要考虑打包问题。
比如:打包成 ESM / CJS / UMD / IIFE 模块时,第三方依赖是作为 external,还是将其代码直接打进产物里?
当依赖作为 external 处理时,就代表着函数库的构建产物中不包含对应依赖的代码,打包出来的大小也会相对小一点。
当依赖的代码直接打进产物中,很显然会增大构建产物的大小。
这就需要考虑第三方依赖的性质和大小。如果第三方依赖是某个运行时框架或者依赖的体积很大,那最好作为 external 处理,由调用方提供具体的依赖。反之可以酌情将依赖打进构建产物中,避免调用方在依赖问题花费太多的精力。
为了验证第三方依赖问题,我特意加了一个date-utils.ts
,这是一个基于dayjs
的日期函数集合。
针对 ESM / CJS 情况,最好将第三方依赖作为 external 处理,因为除了我的函数库会依赖dayjs
,项目中也可能会依赖dayjs
,在构建工具的帮助下,能在 Dependency Graph 中实现复用。
我们将buildModules
改一改,
const bundle = await rollup({
input,
plugins: [rollupTypescript()],
// 把依赖作为 external(dependencies 中包含 dayjs)
external: Object.keys(pkgJson.dependencies),
})
重新打包会发现报了一个错,
'dayjs' is imported by packages/utils/src/date-utils.ts, but could not be resolved – treating it as an external dependency
因为 Rollup 默认的模块解析策略符合 ESM 规范,只有从相对路径上找得到的模块,才能被成功解析。
我们可能已经习惯了import { ref } from "vue"
这种用法,就会想当然认为 Rollup 默认也能理解这种引用第三方依赖的行为,实际上并不能。我们熟悉的这种模块解析策略其实是遵从 Node Resolution Algorithm,它是 NodeJS 的默认行为,并不是 ESM 的默认行为。
这个问题需要借助插件@rollup/plugin-node-resolve[3]来解决。
首先安装一下依赖,
yarn add -DW @rollup/plugin-node-resolve
然后在插件中引用它,
const bundle = await rollup({
input,
plugins: [rollupTypescript(), nodeResolve()],
external: Object.keys(pkgJson.dependencies),
})
但我们继续打包还是会遇到一个问题:
关键信息是:
Error: 'default' is not exported by node_modules/dayjs/dayjs.min.js, imported by packages/utils/src/date-utils.ts
其实这是因为 dayjs 的 package.json 中只给出了main
入口,而没有配置module
入口,而main
入口指定的不是符合 ESM 规范的文件,从而导致这个问题。我当时还给 dayjs 提了一个PR[4]说明了这个问题,希望增加module
入口优化这个问题,不过 dayjs 团队似乎不太在意这个问题,关闭了这个 PR,建议我改用 v2 alpha 版本,实际上 v1 版本后面也一直在更新和发版。
不过没关系,即便有一些模块不符合 ESM 规范也是合情合理,毕竟 npm 生态中还有很多不支持 ESM 的包,Rollup 自然也考虑到了这一点,给出了插件@rollup/plugin-commonjs[5],那我们直接用上它就好了。
export const buildBundle = async () => {
const bundle = await rollup({
input: resolve(UTILS_PATH, 'src/index.ts'),
plugins: [rollupTypescript(), nodeResolve(), commonjs()],
// 如果你觉得第三方依赖体积很大,也可以用 external 拆出来,让调用方提供对应依赖,此时要配合 globals 一起用
// external: Object.keys(pkgJson.dependencies),
})
// const globals = {
// dayjs: "dayjs",
// }
await Promise.all([
bundle.write({
name: 'VpUtils',
format: 'umd',
file: resolve(UTILS_PATH, 'dist/index.js'),
sourcemap: true,
// globals
}),
bundle.write({
name: 'VpUtils',
format: 'iife',
file: resolve(UTILS_PATH, 'dist/index.min.js'),
sourcemap: false,
plugins: [terser()],
// globals,
})
])
}
如上面代码中注释所述,你可以根据实际情况选择是否将 dayjs 等依赖打进 bundle。
如果使用了 external,最好通过文档告知用户应该预先引入哪些依赖,降低用户的心智负担。
本文主要介绍了函数库的构建过程中的一些优化方案和注意事项,希望对读者们有所帮助。如果您对我的专栏感兴趣,欢迎您订阅关注本专栏[6],接下来可以一同探讨和交流组件库开发过程中遇到的问题。
[1]
vue-pro-components c7 分支: https://github.com/cumt-robin/vue-pro-components/tree/c7
[2]
基于Vite+AntDesignVue打造业务组件库: https://juejin.cn/column/7140103979697963045
[3]
@rollup/plugin-node-resolve: https://www.npmjs.com/package/@rollup/plugin-node-resolve
[4]
给 dayjs 提了一个PR: https://github.com/iamkun/dayjs/pull/2002
[5]
@rollup/plugin-commonjs: https://www.npmjs.com/package/@rollup/plugin-commonjs
[6]
订阅关注本专栏: https://juejin.cn/column/7140103979697963045