Rollup
Rollup 是一个非常纯粹的 JavaScript 模块打包器。它最擅长的场景,不是那种高度复杂、历史包袱沉重的大型前端应用,而是:
- 组件库
- 工具库
- SDK
- 以
ESM为核心的包输出
它的设计哲学和 Webpack 很不一样。Webpack 强调“把一切都纳入模块系统”;Rollup 更强调“围绕 ES Module 生成尽可能干净、可优化的产物”。
提示
Rollup 的简洁性和优秀的 Tree Shaking,让它天然适合类库打包。
一、Rollup 解决的核心问题
如果你在写一个库,通常会有这些诉求:
- 产物要干净
- 未使用代码要尽量被消除
- 输出格式要兼容不同消费环境
- 最好既能给现代打包器用,也能给 Node 或浏览器直接用
这正是 Rollup 最擅长的方向。
二、为什么 Rollup 特别适合库打包
1. 它天然偏向 ESM
Rollup 对 ES Module 的静态结构利用得非常充分:
import/export在编译期可分析- 依赖关系清晰
- 更容易做细粒度
Tree Shaking
2. 输出结果通常更“干净”
对于工具库或组件库来说,很多时候你希望产物:
- 包装代码更少
- 冗余运行时代码更少
- 更容易被下游继续优化
Rollup 在这方面表现通常很好。
3. 多格式输出很自然
一个库往往需要同时输出:
ESMCommonJSUMD或IIFE
Rollup 对这类输出模型支持非常直接。
三、先理解 Rollup 的构建思路
Rollup 的基本过程可以概括为:
- 从入口模块开始分析
- 基于
import/export构建依赖关系 - 静态分析哪些导出真正被用到
- 尽可能裁剪无用代码
- 输出为指定模块格式
这个流程决定了它和 Webpack 的关注重点不完全一样。
四、一个最小可用示例
1. 初始化项目
mkdir rollup-demo
cd rollup-demo
npm init -y
npm install -D rollup @rollup/plugin-node-resolve @rollup/plugin-commonjs typescript
2. 目录结构
rollup-demo/
├── package.json
├── rollup.config.mjs
└── src/
└── index.js
3. package.json
{
"name": "rollup-demo",
"version": "1.0.0",
"main": "dist/index.cjs.js",
"module": "dist/index.esm.js",
"browser": "dist/index.umd.js",
"scripts": {
"build": "rollup -c",
"dev": "rollup -c -w"
}
}
4. rollup.config.mjs
import resolve from '@rollup/plugin-node-resolve'
import commonjs from '@rollup/plugin-commonjs'
export default [
{
input: 'src/index.js',
output: [
{
file: 'dist/index.esm.js',
format: 'es',
},
{
file: 'dist/index.cjs.js',
format: 'cjs',
},
{
file: 'dist/index.umd.js',
format: 'umd',
name: 'MyLibrary',
},
],
plugins: [resolve(), commonjs()],
},
]
5. 入口文件
export function sum(a, b) {
return a + b
}
export function multiply(a, b) {
return a * b
}
这个例子展示了 Rollup 最典型的用法:从一个简单入口出发,输出多个格式的产物。
五、最重要的能力:Tree Shaking
Rollup 最被广泛提及的能力,就是 Tree Shaking。
1. 什么是 Tree Shaking
它指的是:在最终产物中,尽可能移除那些没有被实际使用的导出代码。
例如:
export function used() {
return 'used'
}
export function unused() {
return 'unused'
}
如果使用方只导入 used,理论上 unused 就可以被裁掉。
2. 为什么 ESM 更适合 Tree Shaking
因为 ESM 的依赖关系是静态的。编译器在构建期就能知道:
- 导出了什么
- 被谁导入
- 哪些导入没有被真正使用
3. Tree Shaking 也不是魔法
它会受到很多因素影响,例如:
- 副作用代码
CommonJS包装方式- 动态导入行为
- 不纯函数和全局修改
如果模块在顶层就做了副作用操作,构建工具通常不敢随便删。
六、输出格式怎么选
es
现代工具链最推荐的格式。适合:
- 现代前端项目
- 二次打包
- Tree Shaking 友好场景
cjs
适合 Node.js 或仍依赖 CommonJS 的环境。
umd
适合同时兼容浏览器直接引入和模块环境,但现在更多作为兼容产物存在。
iife
适合直接在浏览器执行的独立脚本。
七、插件系统是 Rollup 的扩展核心
虽然 Rollup 自身相对纯粹,但真实项目依然离不开插件:
- 解析第三方模块
- 兼容
CommonJS - 处理 TypeScript
- 处理 JSON
- 压缩代码
- 生成声明文件
常见插件包括:
@rollup/plugin-node-resolve@rollup/plugin-commonjs@rollup/plugin-typescript@rollup/plugin-json@rollup/plugin-replace
八、外部依赖为什么常常要标记为 external
库打包和应用打包有个重要差别:
- 应用通常希望把依赖一并打进去,方便部署
- 库通常不希望把所有依赖都打进产物
例如你写一个 React 组件库,通常会把 react 标记为外部依赖:
export default {
input: 'src/index.ts',
external: ['react', 'react-dom'],
}
这样做的目的是:
- 避免产物重复打包宿主依赖
- 避免出现多个 React 实例
- 减少包体积
九、Rollup 真正难的地方不在配置,而在边界判断
初学者常以为 Rollup 很简单,因为配置文件看起来比 Webpack 短。但真实难点在于:
- 哪些依赖应该 external
- 哪些格式必须输出
- 哪些代码会影响 Tree Shaking
- 哪些插件顺序会影响最终产物
- 是否需要拆分 chunk
这类问题更偏工程判断,而不只是语法记忆。
十、一个典型场景:打包组件库
如果你在做一个组件库,通常会希望:
- 输出
esm + cjs - 可能额外提供浏览器格式
- 生成
d.ts - 保留样式或把样式抽离
- 把
react/vue标记为 peer dependency
在这种场景下,Rollup 很合适,因为它的输出控制粒度很高。
十一、Rollup 的局限也要清楚
虽然 Rollup 很适合库,但它不是所有项目的最佳答案。
它不一定最适合
- 资源类型极其复杂的大型应用
- 依赖历史包袱很多的旧项目
- 特别依赖复杂开发服务器能力的系统
这也是为什么很多现代应用会选择:
- 开发阶段用
Vite - 生产构建由
Vite + Rollup完成
十二、Rollup 与 Webpack、Vite 的关系
Rollup 和 Webpack
Rollup更偏库输出与纯ESM优化Webpack更偏复杂应用工程系统
Rollup 和 Vite
Vite开发阶段不等于RollupVite的生产构建能力大量建立在Rollup之上
所以学会 Rollup,会帮助你更深入理解 Vite build 的很多行为。
十三、核心源码解读:Rollup 为什么能把产物做得这么干净
Rollup 的源码阅读体验,通常比 Webpack 更聚焦,因为它的核心目标更单一:
- 建图
- 标记引用
- 做包含性分析
- 输出最终 chunk
换句话说,Rollup 更像一个围绕 ESM 静态分析构建起来的编译器,而不是一个“什么都包”的超级工程平台。
1. 入口主线:rollup(inputOptions)
从阅读路径上看,你可以把顶层入口理解成:
- 标准化输入配置
- 初始化插件驱动器
- 创建模块图
Graph - 让图去加载入口并递归扩张
- 完成分析后返回 bundle 对象
一个简化伪代码大概像这样:
async function rollup(inputOptions) {
const options = normalizeInputOptions(inputOptions)
const pluginDriver = new PluginDriver(options.plugins)
const graph = new Graph(options, pluginDriver)
await graph.build()
return new Bundle(graph)
}
这段伪代码背后最重要的信息是:Rollup 的构建核心其实非常集中,重点就是 Graph。
2. Graph 是真正的核心
如果只允许记一个核心对象,那就是 Graph。
它负责:
- 管理模块集合
- 协调模块加载
- 记录依赖关系
- 触发 tree-shaking 分析
- 驱动 chunk 生成
你可以把它理解成:Rollup 把“构建理解”几乎都压缩到了图模型里。
3. 模块加载并不是简单读文件
从入口模块出发后,Rollup 会按大致这样的顺序工作:
- 调用插件
resolveId - 得到真实 id
- 调用插件
load - 读取原始代码
- 调用插件
transform - 解析成 AST
- 收集静态依赖
- 继续递归加载依赖模块
也就是说,它虽然不像 Webpack 那样强调资源类型全兼容,但插件链依然是核心扩展点。
4. 为什么 AST 和作用域分析这么重要
Rollup 之所以能做细粒度 Tree Shaking,关键不只是“看到了 import/export”,而是它会继续做:
- 标识符绑定
- 变量引用追踪
- 作用域分析
- 语句级别的包含性判断
也就是说,它不是简单地说“这个模块被引用了就全留着”,而是尽量精确到:
- 哪个导出真的被用到
- 哪些语句必须保留
- 哪些副作用不能删
5. include 机制很值得重点看
如果用一句话概括 Rollup 的 Tree Shaking 思路,可以说:
从最终需要的导出开始,反向追踪必须保留的语句与依赖。
阅读源码时,你会反复看到一类思想:
- 某个变量被引用了
- 它对应的声明需要被包含
- 声明里的其他依赖也要继续标记
最终形成一种“按需向上追溯”的保留链。
6. 插件驱动器为什么重要
Rollup 插件系统的核心不是“提供很多钩子”,而是它让整个构建主线保持了高度统一。
比如这些钩子会反复出现在主链路上:
optionsbuildStartresolveIdloadtransformrenderChunkgenerateBundle
这说明 Rollup 的插件并不是外围附加能力,而是几乎嵌在构建主流程内部。
十四、输出阶段源码脉络
Rollup 真正让人觉得“产物很干净”的关键,还在输出阶段。
1. 从模块图到 chunk
在完成依赖分析和 tree-shaking 之后,Rollup 会进入 chunk 组织阶段。这里要解决的问题包括:
- 哪些模块放进同一个 chunk
- 哪些模块作为动态导入边界拆出去
- 如何生成导入导出包装代码
2. 渲染阶段关注的是什么
渲染阶段主要做这些事:
- 按格式输出导入导出语句
- 处理命名冲突
- 生成 chunk 包装结构
- 调用
renderChunk - 最终
generateBundle
这也是为什么 Rollup 输出通常显得很“克制”:它对包装层非常节制,尽量保留模块结构本来的样子。
3. external 在源码层意味着什么
当一个依赖被标记为 external 时,Rollup 不会把它继续并入图内做同等打包处理,而是:
- 在分析阶段识别为外部引用
- 在输出阶段保留对应模块格式下的引用形式
这正是类库打包时非常关键的一点。
十五、源码阅读顺序建议
如果你准备读 Rollup 源码,推荐顺序是:
- 顶层
rollup(inputOptions) Graph.build()主流程- 模块加载的
resolve/load/transform - AST、作用域和引用绑定逻辑
- 包含性分析与 tree-shaking
- chunk 生成与
generateBundle
这样读下来,你会更容易理解为什么 Rollup 的设计始终围绕“静态可分析模块”展开。
十六、学习 Rollup 最该关注什么
- 理解
ESM静态分析 - 理解
Tree Shaking的前提与限制 - 理解多格式输出
- 理解
external的工程意义 - 理解库打包和应用打包的目标差异
总结
Rollup 的价值,在于它把打包这件事做得很“克制”:
- 关注模块结构本身
- 追求更干净的产物
- 对库打包非常友好
- 对现代
ESM生态很契合
如果你写的是类库、SDK、组件库,或者想更深入理解现代构建工具的产物优化思路,Rollup 是非常值得深入学习的一环。
