Rollup配置实战:打造高效的前端打包流程
项目初期的技术选型
这个项目是个内部工具库,要打包成一个轻量级的 JS SDK,供其他团队通过 npm 引入。一开始我们用 Webpack,配置写了一堆,结果打包出来的文件太大,还带了一堆 runtime 和 polyfill,明明只是个简单的工具函数集合。
后来我寻思着,这种纯 JS 库其实不需要 HMR、Code Splitting 那些前端工程化功能,真正需要的是:干净的输出、支持 tree-shaking、模块格式灵活(cjs + esm)。Rollup 就是干这个的,于是决定切到 Rollup。
开始没想到,看着官网文档“几行配置搞定”,实际落地的时候一堆坑,尤其和 TypeScript 和 Babel 的联动上,折腾了快一周才稳定下来。
最开始的配置长这样
先搭了个基础架子,支持 TS 编译,输出 esm 和 cjs 两种格式:
// rollup.config.js
import typescript from '@rollup/plugin-typescript';
import { nodeResolve } from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
export default {
input: 'src/index.ts',
output: [
{
file: 'dist/index.esm.js',
format: 'esm'
},
{
file: 'dist/index.cjs.js',
format: 'cjs'
}
],
plugins: [
nodeResolve(),
commonjs(),
typescript({ tsconfig: './tsconfig.json' })
]
};
看着挺清爽对吧?跑起来发现——TypeScript 报错直接被吞了,编译失败也不中断构建,CI 上都过,本地一引入就炸。查了半天才发现,@rollup/plugin-typescript 默认不会把类型错误抛出来,得手动加个配置:
typescript({
tsconfig: './tsconfig.json',
declaration: true,
declarationDir: 'dist',
emitDeclarationOnly: false,
noEmitOnError: true // 关键!不然报错不终止
})
这里注意我踩过好几次坑,noEmitOnError 不加的话,TypeScript 类型报错只会打个 warning,Rollup 继续走后面的流程,最后输出一个残缺的 js 文件,非常隐蔽。
最大的坑:Babel 和 tslib 的纠缠
项目里用了 async/await,按理说经过 Babel 转义后应该没问题。但我加上 @rollup/plugin-babel 后,打包出来的代码里出现了大量的 regeneratorRuntime 引用,运行时报错找不到 regeneratorRuntime。
第一反应是没配好 preset-env,后来发现是 babelHelpers 没设对:
import babel from '@rollup/plugin-babel';
// 在 plugins 里
babel({
babelHelpers: 'runtime', // 必须设为 runtime,不然会内联 helpers
extensions: ['.ts', '.tsx']
})
但这样又引出了新问题:tslib。TypeScript 会生成 __extends、__awaiter 这种辅助函数,默认是 inline 的,导致每个文件都重复一份。虽然可以用 importHelpers: true 让它引用 tslib,但 Rollup 打包时如果处理不好,tslib 会被当成外部依赖拎出去。
我们的目标是“尽可能自包含”,不希望用户还得额外装 tslib。最终方案是:保留 importHelpers,但把 tslib 设为 internalDependencies:
import nodePolyfills from 'rollup-plugin-polyfill-node';
// 在 plugins 数组里加
nodePolyfills(), // 处理一些 Node 内置模块的兼容
{
resolveId(id) {
if (id === 'tslib') return 'tslib';
},
load(id) {
if (id === 'tslib') {
return export * from 'tslib'; export { default } from 'tslib';;
}
return null;
}
}
有点 hack,但有效。打包后 tslib 的内容会被正确内联进来,不增加外部依赖。
Tree-shaking 怎么就是不生效?
有个同事反馈说,他只引入了一个函数,结果整个包都被加载了。我一看,糟了,tree-shaking 没起作用。
排查一圈发现是我们导出方式的问题。我们原来是这样写的:
// src/index.ts
import { foo } from './foo';
import { bar } from './bar';
export default { foo, bar };
这种 default 导出对象的方式,Rollup 无法静态分析具体用了哪个属性,直接整个引入。改成 named export 就好了:
// 改成
export { foo } from './foo';
export { bar } from './bar';
同时确保 package.json 里有:
{
"module": "dist/index.esm.js",
"main": "dist/index.cjs.js",
"sideEffects": false
}
尤其是 sideEffects 设为 false,告诉打包工具这个包没有副作用,可以放心摇。
最终的解决方案
磨了一周,最终配置长这样:
// rollup.config.js
import typescript from '@rollup/plugin-typescript';
import { nodeResolve } from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import babel from '@rollup/plugin-babel';
import json from '@rollup/plugin-json';
export default {
input: 'src/index.ts',
output: [
{
file: 'dist/index.esm.js',
format: 'esm',
sourcemap: true
},
{
file: 'dist/index.cjs.js',
format: 'cjs',
sourcemap: true
}
],
external: [
'lodash', // 假设有外部依赖不想打包进去
/@babel/runtime/ // babel runtime 也作为外部依赖
],
plugins: [
json(),
nodeResolve({
browser: true,
preferBuiltins: false
}),
commonjs(),
typescript({
tsconfig: './tsconfig.json',
declaration: true,
declarationDir: 'dist',
emitDeclarationOnly: false,
noEmitOnError: true
}),
babel({
babelHelpers: 'runtime',
extensions: ['.ts', '.tsx'],
exclude: 'node_modules/**'
})
]
};
配合 .babelrc:
{
"presets": [
["@babel/preset-env", { "targets": { "ie": "11" } }],
"@babel/preset-typescript"
],
"plugins": [
["@babel/plugin-transform-runtime", { "regenerator": true }]
]
}
终于稳定了。打包体积从最初的 80KB(gzip 前)压到了 22KB,tree-shaking 也正常了。
还有点小毛病
不是所有问题都解决了。比如动态导入 dynamic import,目前直接报错,因为我们没开 code splitting。但这块暂时用不到,先放着。
另一个问题是 source map 路径偶尔对不上,怀疑是 TS 和 Babel 两层转换导致的映射丢失,但影响不大,调试时看原始文件就行。
总的来说,这方案不是最优的,但最简单,维护成本低,适合我们这种小团队。
回顾与反思
回头看看,最大的教训是:别看 Rollup 官网说“简单配置”,真到项目里,TS + Babel + 多格式输出,组合起来全是坑。
有几个关键点必须记住:
- typescript 插件一定要开 noEmitOnError,不然 CI 会骗你
- babelHelpers 设成 runtime,避免重复 helper 代码
- 导出方式优先用 named export,别用 default 对象包装
- external 列表要合理控制,该外链的就外链
另外,Rollup 真不适合复杂应用,但它做库是真的香。这次改完之后,其他团队接入顺利多了,没再反馈打包问题。
以上是我的项目经验
以上是我踩坑后的总结,希望对你有帮助。如果有更优的实现方式,比如怎么优雅地处理 tslib 或 dynamic import,欢迎评论区交流。这类配置问题,永远没有银弹,只有权衡。

暂无评论