Rollup

Rollup 是一个非常纯粹的 JavaScript 模块打包器。它最擅长的场景,不是那种高度复杂、历史包袱沉重的大型前端应用,而是:

  • 组件库
  • 工具库
  • SDK
  • ESM 为核心的包输出

它的设计哲学和 Webpack 很不一样。Webpack 强调“把一切都纳入模块系统”;Rollup 更强调“围绕 ES Module 生成尽可能干净、可优化的产物”。

提示

Rollup 的简洁性和优秀的 Tree Shaking,让它天然适合类库打包。

一、Rollup 解决的核心问题

如果你在写一个库,通常会有这些诉求:

  • 产物要干净
  • 未使用代码要尽量被消除
  • 输出格式要兼容不同消费环境
  • 最好既能给现代打包器用,也能给 Node 或浏览器直接用

这正是 Rollup 最擅长的方向。

二、为什么 Rollup 特别适合库打包

1. 它天然偏向 ESM

RollupES Module 的静态结构利用得非常充分:

  • import / export 在编译期可分析
  • 依赖关系清晰
  • 更容易做细粒度 Tree Shaking

2. 输出结果通常更“干净”

对于工具库或组件库来说,很多时候你希望产物:

  • 包装代码更少
  • 冗余运行时代码更少
  • 更容易被下游继续优化

Rollup 在这方面表现通常很好。

3. 多格式输出很自然

一个库往往需要同时输出:

  • ESM
  • CommonJS
  • UMDIIFE

Rollup 对这类输出模型支持非常直接。

三、先理解 Rollup 的构建思路

Rollup 的基本过程可以概括为:

  1. 从入口模块开始分析
  2. 基于 import/export 构建依赖关系
  3. 静态分析哪些导出真正被用到
  4. 尽可能裁剪无用代码
  5. 输出为指定模块格式

这个流程决定了它和 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 开发阶段不等于 Rollup
  • Vite 的生产构建能力大量建立在 Rollup 之上

所以学会 Rollup,会帮助你更深入理解 Vite build 的很多行为。

十三、核心源码解读:Rollup 为什么能把产物做得这么干净

Rollup 的源码阅读体验,通常比 Webpack 更聚焦,因为它的核心目标更单一:

  • 建图
  • 标记引用
  • 做包含性分析
  • 输出最终 chunk

换句话说,Rollup 更像一个围绕 ESM 静态分析构建起来的编译器,而不是一个“什么都包”的超级工程平台。

1. 入口主线:rollup(inputOptions)

从阅读路径上看,你可以把顶层入口理解成:

  1. 标准化输入配置
  2. 初始化插件驱动器
  3. 创建模块图 Graph
  4. 让图去加载入口并递归扩张
  5. 完成分析后返回 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 会按大致这样的顺序工作:

  1. 调用插件 resolveId
  2. 得到真实 id
  3. 调用插件 load
  4. 读取原始代码
  5. 调用插件 transform
  6. 解析成 AST
  7. 收集静态依赖
  8. 继续递归加载依赖模块

也就是说,它虽然不像 Webpack 那样强调资源类型全兼容,但插件链依然是核心扩展点。

4. 为什么 AST 和作用域分析这么重要

Rollup 之所以能做细粒度 Tree Shaking,关键不只是“看到了 import/export”,而是它会继续做:

  • 标识符绑定
  • 变量引用追踪
  • 作用域分析
  • 语句级别的包含性判断

也就是说,它不是简单地说“这个模块被引用了就全留着”,而是尽量精确到:

  • 哪个导出真的被用到
  • 哪些语句必须保留
  • 哪些副作用不能删

5. include 机制很值得重点看

如果用一句话概括 RollupTree Shaking 思路,可以说:

从最终需要的导出开始,反向追踪必须保留的语句与依赖。

阅读源码时,你会反复看到一类思想:

  • 某个变量被引用了
  • 它对应的声明需要被包含
  • 声明里的其他依赖也要继续标记

最终形成一种“按需向上追溯”的保留链。

6. 插件驱动器为什么重要

Rollup 插件系统的核心不是“提供很多钩子”,而是它让整个构建主线保持了高度统一。

比如这些钩子会反复出现在主链路上:

  • options
  • buildStart
  • resolveId
  • load
  • transform
  • renderChunk
  • generateBundle

这说明 Rollup 的插件并不是外围附加能力,而是几乎嵌在构建主流程内部。

十四、输出阶段源码脉络

Rollup 真正让人觉得“产物很干净”的关键,还在输出阶段。

1. 从模块图到 chunk

在完成依赖分析和 tree-shaking 之后,Rollup 会进入 chunk 组织阶段。这里要解决的问题包括:

  • 哪些模块放进同一个 chunk
  • 哪些模块作为动态导入边界拆出去
  • 如何生成导入导出包装代码

2. 渲染阶段关注的是什么

渲染阶段主要做这些事:

  • 按格式输出导入导出语句
  • 处理命名冲突
  • 生成 chunk 包装结构
  • 调用 renderChunk
  • 最终 generateBundle

这也是为什么 Rollup 输出通常显得很“克制”:它对包装层非常节制,尽量保留模块结构本来的样子。

3. external 在源码层意味着什么

当一个依赖被标记为 external 时,Rollup 不会把它继续并入图内做同等打包处理,而是:

  • 在分析阶段识别为外部引用
  • 在输出阶段保留对应模块格式下的引用形式

这正是类库打包时非常关键的一点。

十五、源码阅读顺序建议

如果你准备读 Rollup 源码,推荐顺序是:

  1. 顶层 rollup(inputOptions)
  2. Graph.build() 主流程
  3. 模块加载的 resolve/load/transform
  4. AST、作用域和引用绑定逻辑
  5. 包含性分析与 tree-shaking
  6. chunk 生成与 generateBundle

这样读下来,你会更容易理解为什么 Rollup 的设计始终围绕“静态可分析模块”展开。

十六、学习 Rollup 最该关注什么

  • 理解 ESM 静态分析
  • 理解 Tree Shaking 的前提与限制
  • 理解多格式输出
  • 理解 external 的工程意义
  • 理解库打包和应用打包的目标差异

总结

Rollup 的价值,在于它把打包这件事做得很“克制”:

  • 关注模块结构本身
  • 追求更干净的产物
  • 对库打包非常友好
  • 对现代 ESM 生态很契合

如果你写的是类库、SDK、组件库,或者想更深入理解现代构建工具的产物优化思路,Rollup 是非常值得深入学习的一环。

上次更新:
贡献者: Joe, joe