Rollup打包配置实战与性能优化技巧分享

皇甫明昊 前端 阅读 2,706
赞 27 收藏
二维码
手机扫码查看
反馈

先别管原理,直接跑起来再说

我第一次用 Rollup 的时候,根本没搞懂什么 Tree Shaking、ESM、CJS,就是项目里有个小工具库要打包,Webpack 太重了,同事说“试试 Rollup”,我就硬着头皮上了。结果发现:配置其实比想象中简单得多。

Rollup打包配置实战与性能优化技巧分享

下面这个是最小可用配置,亲测有效,你复制过去就能跑:

// rollup.config.js
export default {
  input: 'src/index.js',
  output: {
    file: 'dist/bundle.js',
    format: 'es'
  }
}

然后在 package.json 里加个脚本:

{
  "scripts": {
    "build": "rollup -c"
  }
}

npm run build,搞定。是不是比 Webpack 那堆 loader 简单多了?

这个场景最好用:打包一个 npm 包

Rollup 真正大放异彩的地方,是打包发布到 npm 的库。比如你写了个 utils 工具集,想让别人通过 import { debounce } from 'my-utils' 引入,这时候 Rollup 是首选。

关键点来了:你要同时输出多种模块格式。为什么?因为用户可能用 ES 模块(Vite/现代浏览器)、CommonJS(Node.js 或老项目)、甚至 UMD(直接 script 标签引入)。所以 output 最好这么配:

// rollup.config.js
export default {
  input: 'src/index.js',
  output: [
    {
      file: 'dist/my-utils.es.js',
      format: 'es'
    },
    {
      file: 'dist/my-utils.cjs.js',
      format: 'cjs'
    },
    {
      file: 'dist/my-utils.umd.js',
      format: 'umd',
      name: 'MyUtils' // UMD 全局变量名
    }
  ]
}

这样一套打下来,基本覆盖所有使用场景。我在几个开源小工具里都这么干,社区反馈都没问题。

踩坑提醒:这三点一定注意

别看 Rollup 配置简单,但有几个坑我踩过不止一次,这里必须重点提醒:

  • 不处理 JSON 和 CSS:Rollup 默认只认 JS。如果你的代码里有 import data from './config.json',直接报错。解决办法是装插件:@rollup/plugin-json。同理,CSS 需要 @rollup/plugin-node-resolve + rollup-plugin-postcss 之类的组合。别偷懒,该装就装。
  • 第三方依赖默认不打包:Rollup 默认会把 node_modules 里的包 external 掉,也就是不打进 bundle。这在打包库的时候是对的(让用户自己装依赖),但如果你是要打一个独立应用(比如命令行工具),就得手动关掉 external。我曾经打包一个 CLI 工具,结果运行时报 “Cannot find module ‘lodash’”,折腾半天才发现是因为 lodash 被 external 了。解决方法是在配置里加:external: [],或者更精细地控制哪些要打包。
  • 路径别名容易翻车:很多人喜欢用 @/utils 这种别名。Rollup 本身不支持,得靠 @rollup/plugin-alias。但要注意,这个插件的配置顺序很重要,必须放在 @rollup/plugin-node-resolve 前面,否则 resolve 插件先跑,别名还没生效,直接找不到模块。我在这上面浪费过至少两小时。

实战配置:带 Babel 和压缩的完整例子

光能跑还不够,生产环境肯定要转译和压缩。下面是我现在项目里用的配置,已经稳定跑了半年多:

// rollup.config.js
import babel from '@rollup/plugin-babel';
import { nodeResolve } from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import json from '@rollup/plugin-json';
import terser from '@rollup/plugin-terser';

export default {
  input: 'src/index.js',
  output: [
    {
      file: 'dist/lib.es.js',
      format: 'es',
      sourcemap: true
    },
    {
      file: 'dist/lib.cjs.js',
      format: 'cjs',
      sourcemap: true
    }
  ],
  plugins: [
    json(),
    nodeResolve({
      browser: true
    }),
    commonjs(),
    babel({
      babelHelpers: 'bundled',
      presets: ['@babel/preset-env']
    }),
    terser() // 压缩
  ],
  external: ['react', 'react-dom'] // 如果是 React 组件库,这些通常 external
}

几点说明:

  • babelHelpers: 'bundled' 很关键,不然 Babel 生成的 helper 函数(比如 _classCallCheck)会重复出现在每个文件里,体积爆炸。
  • terser() 放最后,压缩前的所有步骤都走完再压。
  • 如果只是纯 JS 工具库,可以去掉 Babel,直接用现代语法,让使用者自己处理兼容性——现在很多库都这么干了。

高级技巧:动态生成多个入口

有时候你不是一个入口,而是多个组件要分别打包,比如一个 UI 库里有 Button、Modal、Toast 各自独立导出。这时候可以用 Rollup 的数组配置:

// rollup.config.js
import fs from 'fs';
import path from 'path';

const components = fs.readdirSync('src/components')
  .filter(f => fs.statSync(path.join('src/components', f)).isDirectory());

const configs = components.map(comp => ({
  input: src/components/${comp}/index.js,
  output: {
    file: dist/${comp}.js,
    format: 'es'
  },
  plugins: [/* 你的插件 */]
}));

export default configs;

这样每个组件单独打包,用户按需引入时不会把整个库都带进来。不过要注意,公共依赖(比如 utils)可能会被重复打包。如果组件间共享逻辑多,建议还是单入口 + Tree Shaking 更稳妥。

别迷信 Tree Shaking,它没那么智能

很多人以为用了 Rollup 就自动瘦身,其实 Tree Shaking 有很多限制。比如:

  • 必须用 ES 模块语法(import/export),CommonJS 不行
  • 不能有副作用的代码(比如顶层执行的函数调用),否则 Rollup 不敢删
  • 动态 import 或 require() 会导致整个模块被保留

我之前有个工具函数写了 console.log('init') 在顶层,结果整个文件都没被摇掉。后来加上 /*#__PURE__*/ 注释才解决:

const result = /*#__PURE__*/ heavyComputation();

所以别指望开箱即用,得配合代码写法。实在不确定,就看生成的 bundle,手动检查有没有不该有的代码。

结尾碎碎念

Rollup 不是银弹,但对于库作者来说,它依然是目前最顺手的打包工具。配置灵活、生态成熟、输出干净。我现在的几个 npm 包全靠它撑着,CI/CD 流水线跑得稳稳的。

以上是我踩坑后的总结,希望对你有帮助。这个技术的拓展用法还有很多(比如插件开发、与 TypeScript 深度集成),后续会继续分享这类博客。如果你有更好的实践,欢迎评论区交流——毕竟前端工具链这东西,三天不看就过时了。

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论

暂无评论