函数库Rollup构建优化

本节涉及的内容源码可在vue-pro-components c7 分支[1]找到,欢迎 star 支持!

前言

本文是基于Vite+AntDesignVue打造业务组件库[2] 专栏第 8 篇文章【函数库Rollup构建优化】,在上一篇文章的基础上,聊聊在使用 Rollup 构建函数库的过程中还可以做哪些优化。

terser 压缩

在上篇文章中,我们掌握了怎么打包 ESM, CJS, UMD,还掌握了怎么生成类型声明文件d.ts,但是我们可以发现,我们生成的 UMD 文件dist/index.js并不知道没有经过压缩,我们可以尝试给它再压缩一下,这可以用到 Rollup 官方的插件 rollup-plugin-terser。

c943cf74cedf0224349e93207a792055.png

由于压缩版通常是直接通过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 的压缩代码了。

3f897a6cc9a5bd46006b582959aee0b7.png

按需使用子模块时提供类型支持

我们已经支持了生成类型声明文件,所以正常使用@vue-pro-components/utils模块时,是有类型支持的。

9b1620692e3f731e4f042243bc420dc0.png

可以看到,上面的函数签名都是有的。

但是,当我们按需使用其中一个模块时,会发现 TypeScript 似乎找不到对应的类型声明。

adc634023dc30616d85fa02a95a5e3de.png

观察上图可以发现,当我们引用其中一个模块的完整路径时,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 目录抄过来即可)。

335529f7322089e904520794bed6993f.png

可以发现已经不报错了,那我们的思路就很清晰了,只要把 types 目录下生成的类型声明文件抄一份到 es 和 lib 目录,就可以保证按需使用模块时的类型支持了。

我们回忆一下整个流程,

5bac56439a81b0994db43b23522bfef6.png

不难想明白要抄一份类型声明文件到 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
)

效果如下:

fc4416aa58bd2975e5eb093af556ce2f.gif


基于此,我们按需使用任何一个子模块都能得到完备的类型支持了。

第三方依赖解析和打包问题

当函数库依赖第三方模块时,我们需要考虑打包问题。

比如:打包成 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 的默认行为。

362286c3a8ddbc6c1be5e5a9e69538bd.png

这个问题需要借助插件@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),
})

但我们继续打包还是会遇到一个问题:

9342c5bdfb8fdfcd5e0405e0037df5a8.png

关键信息是:

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

END

7a87a78851bd16b0fdbbc96a154e5c00.png

如果觉得这篇文章还不错

点击下面卡片关注我

来个【分享、点赞、在看】三连支持一下吧15739e3151a9e83b51ae20ff50a33b28.png

   “分享、点赞、在看” 支持一波 8625aa947a558e0c3f8fea34dcb1fe0f.png 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值