Rollup 简介与核心概念

Rollup 是一个现代化的 JavaScript 模块打包器,专门为构建库和应用程序而设计。它的核心理念是利用 ES6 模块的静态结构特性,实现高效的 Tree Shaking 和代码优化。
Rollup 的设计理念是”充分利用 ES6 模块的静态特性”,这不仅仅是支持语法,而是从根本上改变了代码分析和优化的方式。

ES6 模块的关键特性

  1. 静态结构 (Static Structure): ES6 模块的导入导出关系在编译时就确定,这是 Rollup 进行深度优化的基础:

    静态结构带来的优势

  • 编译时分析:无需执行代码就能理解模块关系
  • 精确依赖追踪:准确知道每个模块的使用情况
  • 安全优化:可以安全地移除未使用的代码
  1. 明确的导入导出 (Explicit Imports/Exports)

  2. 活绑定 (Live Bindings)

1
2
3
4
5
6
7
8
9
10
11
// counter.js
export let count = 0;
export function increment() {
count++;
}

// main.js
import { count, increment } from "./counter.js";
console.log(count); // 0
increment();
console.log(count); // 1 - 实时更新!

Rollup 如何利用这些特性

  1. 精确的 Tree Shaking

    Rollup Tree Shaking 的工作流程

  • 依赖图构建:分析所有模块的导入导出关系
  • 使用标记:从入口点开始标记所有被使用的导出
  • 递归分析:追踪每个被使用导出的内部依赖
  • 死代码消除:移除所有未被标记的代码
  1. 作用域提升 (Scope Hoisting)

传统打包工具的输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 传统工具 - 每个模块独立作用域
(function (modules) {
function require(id) {
/* ... */
}

// Module A
modules[0] = function (require, module, exports) {
exports.a = function () {
return "A";
};
};

// Module B
modules[1] = function (require, module, exports) {
const a = require(0).a;
exports.b = function () {
return a() + "B";
};
};

// 启动逻辑
require(1);
})([]);

Rollup 的输出:

1
2
3
4
5
6
7
8
9
10
// Rollup - 作用域提升后
function a() {
return "A";
}
function b() {
return a() + "B";
}

// 直接调用,没有模块包装器开销
b();

作用域提升的优势

  • 运行时性能:减少函数调用开销
  • 代码体积:消除模块包装器代码
  • 引擎优化:更容易被 JavaScript 引擎优化
  1. 循环依赖检测和处理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// a.js
import { b } from "./b.js";
export function a() {
return "a-" + b();
}

// b.js
import { a } from "./a.js"; // 循环依赖
export function b() {
return "b-" + a();
}

// Rollup 编译时错误提示:
// [!] Error: Circular dependency:
// a.js -> b.js -> a.js

循环依赖处理策略

  • 静态检测:编译时发现循环依赖
  • 详细报告:提供完整的依赖路径
  • 解决建议:帮助开发者重构代码结构
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// 相同源代码的输出对比

// 源代码
export function add(a, b) {
return a + b;
}
export function subtract(a, b) {
return a - b;
}

// 使用代码
import { add } from "./math.js";
console.log(add(1, 2));

// Rollup 输出 - 简洁直接
function add(a, b) {
return a + b;
}
console.log(add(1, 2));

// Webpack 输出 - 包含运行时代码
(function (modules) {
// webpack 运行时代码 (~200 行)
function __webpack_require__(moduleId) {
/* ... */
}

return __webpack_require__(0);
})([
function (module, exports) {
function add(a, b) {
return a + b;
}
console.log(add(1, 2));
},
]);

Rollup 核心原理

编译流程

Rollup 的编译过程是现代模块打包器的典型代表,它采用了一种基于静态分析的编译策略。与传统的打包工具不同,Rollup 从一开始就专注于 ES6 模块的特性,这使得它能够进行更深度的代码分析和优化。

Rollup 编译的核心特点

  • 静态分析优先:在编译时就确定模块关系,而不是运行时
  • Tree Shaking 原生支持:基于 ES6 模块的静态特性实现精确的死代码消除
  • 作用域提升:将多个模块的作用域合并,减少运行时开销
  • 格式无关:同一套代码可以输出多种模块格式

阶段一:解析

解析阶段的核心工作

  1. 词法分析和语法分析:将源代码转换为抽象语法树(AST)
  2. 模块结构识别:分析模块的导入导出关系
  3. 依赖关系建立:确定模块之间的引用关系
  4. 缓存机制:避免重复解析同一模块

为什么使用 AST?

AST(抽象语法树)是代码的结构化表示,它将代码的语法结构转换为树形数据结构。这样做的好处是:

  • 精确分析:可以准确识别代码的语义结构
  • 安全转换:保证代码转换的正确性
  • 高效处理:便于进行各种代码分析和转换操作

导入导出分析的重要性

ES6 模块的导入导出是静态的,这意味着在编译时就能确定模块的依赖关系。Rollup 正是利用这个特性来实现:

  • 精确的 Tree Shaking:只有被使用的导出才会被包含
  • 循环依赖检测:提前发现模块间的循环引用
  • 优化的代码生成:基于实际使用情况生成最优代码

下面是解析阶段的核心实现代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
// Rollup 解析阶段核心实现
class RollupParser {
constructor(config) {
this.config = config;
this.moduleCache = new Map();
this.astCache = new Map();
}

async parseModule(id, code) {
// 1. 检查缓存
if (this.astCache.has(id)) {
return this.astCache.get(id);
}

// 2. 使用 acorn 解析 AST
const ast = this.parseToAST(code, {
ecmaVersion: 2020,
sourceType: "module",
locations: true,
ranges: true,
});

// 3. 分析导入导出
const moduleInfo = this.analyzeModuleStructure(ast);

// 4. 缓存结果
this.astCache.set(id, { ast, moduleInfo });

return { ast, moduleInfo };
}

parseToAST(code, options) {
try {
return acorn.parse(code, options);
} catch (error) {
throw new RollupParseError(`解析失败: ${error.message}`, {
code,
location: error.location,
});
}
}

analyzeModuleStructure(ast) {
const imports = [];
const exports = [];
const declarations = [];

// 遍历 AST 节点
ast.body.forEach((node) => {
switch (node.type) {
case "ImportDeclaration":
imports.push(this.analyzeImport(node));
break;

case "ExportNamedDeclaration":
case "ExportDefaultDeclaration":
case "ExportAllDeclaration":
exports.push(this.analyzeExport(node));
break;

case "VariableDeclaration":
case "FunctionDeclaration":
case "ClassDeclaration":
declarations.push(this.analyzeDeclaration(node));
break;
}
});

return {
imports,
exports,
declarations,
hasDefaultExport: exports.some((exp) => exp.type === "default"),
hasNamedExports: exports.some((exp) => exp.type === "named"),
};
}

analyzeImport(node) {
return {
source: node.source.value,
specifiers: node.specifiers.map((spec) => ({
type: spec.type, // ImportDefaultSpecifier, ImportNamespaceSpecifier, ImportSpecifier
local: spec.local.name,
imported: spec.imported?.name || null,
})),
start: node.start,
end: node.end,
};
}

analyzeExport(node) {
const exportInfo = {
type: node.type === "ExportDefaultDeclaration" ? "default" : "named",
start: node.start,
end: node.end,
};

if (node.declaration) {
// export function foo() {} 或 export default function() {}
exportInfo.declaration = this.analyzeDeclaration(node.declaration);
} else if (node.specifiers) {
// export { foo, bar }
exportInfo.specifiers = node.specifiers.map((spec) => ({
local: spec.local.name,
exported: spec.exported.name,
}));
}

if (node.source) {
// export { foo } from './module'
exportInfo.source = node.source.value;
}

return exportInfo;
}
}

阶段二:依赖图构建

依赖图的作用和意义

依赖图不仅仅是模块之间的引用关系,它还包含了大量的元信息:

  • 模块的作用域信息:每个模块内部的变量声明和引用
  • 副作用分析结果:哪些代码可以安全移除,哪些不能
  • 导入导出映射:精确记录模块间的数据流动
  • 循环依赖检测:发现并处理潜在的依赖环路

为什么需要构建完整的依赖图?

  1. 全局视角:只有了解完整的依赖关系,才能进行全局优化
  2. 精确分析:避免误删有用的代码或保留无用的代码
  3. 性能优化:为代码分割和懒加载提供决策依据
  4. 错误检测:提前发现循环依赖等潜在问题

循环依赖的处理策略

循环依赖是模块系统中的一个经典问题。Rollup 采用深度优先遍历的方式来检测循环依赖:

  • 检测阶段:使用 “正在访问” 和 “已访问” 两个集合来追踪访问状态
  • 报告机制:提供详细的循环路径信息,帮助开发者定位问题
  • 容错处理:在某些情况下允许循环依赖存在,但会给出警告

作用域分析的深度

Rollup 进行的作用域分析比简单的变量收集更加深入:

  • 变量声明追踪:记录每个变量的声明位置和作用域
  • 引用关系分析:分析变量的读写关系
  • 提升可能性评估:判断哪些声明可以安全地提升到全局作用域

下面是依赖图构建的核心实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
// 模块依赖图构建器
class ModuleGraphBuilder {
constructor(resolver) {
this.resolver = resolver;
this.moduleMap = new Map(); // id -> ModuleNode
this.visited = new Set();
this.visiting = new Set(); // 用于循环依赖检测
}

async buildGraph(entryPoints) {
const graph = {
nodes: new Map(),
edges: new Map(),
entryPoints: [],
};

// 从入口点开始构建
for (const entry of entryPoints) {
const entryNode = await this.processModule(entry, null, graph);
graph.entryPoints.push(entryNode);
}

// 检测循环依赖
this.detectCycles(graph);

return graph;
}

async processModule(id, importer, graph) {
// 检查循环依赖
if (this.visiting.has(id)) {
throw new RollupError(`检测到循环依赖: ${this.getCircularPath(id)}`);
}

// 检查是否已处理
if (this.visited.has(id)) {
return graph.nodes.get(id);
}

this.visiting.add(id);

try {
// 解析模块
const resolved = await this.resolver.resolve(id, importer);
const code = await this.resolver.load(resolved.id);
const { ast, moduleInfo } = await this.parser.parseModule(
resolved.id,
code
);

// 创建模块节点
const moduleNode = {
id: resolved.id,
ast,
moduleInfo,
dependencies: new Set(),
dependents: new Set(),
exports: new Map(),
imports: new Map(),
scope: this.analyzeScope(ast),
sideEffects: this.analyzeSideEffects(ast, moduleInfo),
};

graph.nodes.set(resolved.id, moduleNode);

// 处理依赖
for (const importInfo of moduleInfo.imports) {
const depId = await this.resolver.resolve(
importInfo.source,
resolved.id
);

const depNode = await this.processModule(depId.id, resolved.id, graph);

// 建立依赖关系
moduleNode.dependencies.add(depNode);
depNode.dependents.add(moduleNode);

// 记录导入关系
moduleNode.imports.set(importInfo.source, {
module: depNode,
specifiers: importInfo.specifiers,
});
}

this.visited.add(id);
return moduleNode;
} finally {
this.visiting.delete(id);
}
}

analyzeScope(ast) {
const scope = {
declarations: new Map(),
references: new Map(),
hoisted: new Set(),
};

// 作用域分析器
class ScopeAnalyzer {
constructor() {
this.scopes = [scope]; // 作用域栈
}

visitNode(node) {
switch (node.type) {
case "VariableDeclarator":
this.handleDeclaration(node.id, node.init);
break;

case "FunctionDeclaration":
this.handleFunctionDeclaration(node);
break;

case "Identifier":
this.handleIdentifier(node);
break;

case "BlockStatement":
this.enterScope();
node.body.forEach((stmt) => this.visitNode(stmt));
this.exitScope();
break;

default:
// 递归访问子节点
for (const key in node) {
const child = node[key];
if (Array.isArray(child)) {
child.forEach((item) => {
if (item && typeof item === "object" && item.type) {
this.visitNode(item);
}
});
} else if (child && typeof child === "object" && child.type) {
this.visitNode(child);
}
}
}
}

handleDeclaration(id, init) {
const currentScope = this.scopes[this.scopes.length - 1];

if (id.type === "Identifier") {
currentScope.declarations.set(id.name, {
node: id,
init,
scope: this.scopes.length - 1,
});
}
}

handleIdentifier(node) {
const currentScope = this.scopes[this.scopes.length - 1];

if (!currentScope.references.has(node.name)) {
currentScope.references.set(node.name, []);
}

currentScope.references.get(node.name).push(node);
}

enterScope() {
this.scopes.push({
declarations: new Map(),
references: new Map(),
parent: this.scopes[this.scopes.length - 1],
});
}

exitScope() {
this.scopes.pop();
}
}

const analyzer = new ScopeAnalyzer();
analyzer.visitNode(ast);

return scope;
}

analyzeSideEffects(ast, moduleInfo) {
// 副作用分析
const sideEffects = {
hasTopLevelSideEffects: false,
sideEffectStatements: [],
pureExports: new Set(),
impureExports: new Set(),
};

// 检查顶层语句的副作用
ast.body.forEach((node) => {
if (this.hasSideEffect(node)) {
sideEffects.hasTopLevelSideEffects = true;
sideEffects.sideEffectStatements.push(node);
}
});

// 分析导出的纯度
moduleInfo.exports.forEach((exportInfo) => {
if (exportInfo.declaration) {
const isPure = this.isPureDeclaration(exportInfo.declaration);
const exportSet = isPure
? sideEffects.pureExports
: sideEffects.impureExports;

if (exportInfo.type === "default") {
exportSet.add("default");
} else if (exportInfo.specifiers) {
exportInfo.specifiers.forEach((spec) => {
exportSet.add(spec.exported);
});
}
}
});

return sideEffects;
}

hasSideEffect(node) {
switch (node.type) {
case "ExpressionStatement":
return this.expressionHasSideEffect(node.expression);

case "ImportDeclaration":
case "ExportNamedDeclaration":
case "ExportDefaultDeclaration":
case "ExportAllDeclaration":
return false; // 导入导出本身无副作用

case "VariableDeclaration":
return node.declarations.some(
(decl) => decl.init && this.expressionHasSideEffect(decl.init)
);

case "FunctionDeclaration":
case "ClassDeclaration":
return false; // 声明本身无副作用

default:
return true; // 默认认为有副作用
}
}

expressionHasSideEffect(expression) {
switch (expression.type) {
case "Literal":
case "Identifier":
return false;

case "CallExpression":
// 函数调用通常有副作用,除非标记为纯函数
return !this.isPureFunction(expression.callee);

case "AssignmentExpression":
return true; // 赋值有副作用

case "UpdateExpression":
return true; // ++, -- 有副作用

case "BinaryExpression":
case "LogicalExpression":
return (
this.expressionHasSideEffect(expression.left) ||
this.expressionHasSideEffect(expression.right)
);

default:
return true;
}
}

isPureFunction(callee) {
// 检查是否为已知的纯函数
const pureFunctions = new Set([
"Math.abs",
"Math.max",
"Math.min",
"Object.keys",
"Object.values",
"Array.isArray",
"JSON.stringify",
"JSON.parse",
]);

if (callee.type === "MemberExpression") {
const name = this.getMemberExpressionName(callee);
return pureFunctions.has(name);
}

return false;
}

getMemberExpressionName(node) {
if (node.type === "MemberExpression") {
const object = this.getMemberExpressionName(node.object);
const property = node.property.name;
return `${object}.${property}`;
} else if (node.type === "Identifier") {
return node.name;
}
return "";
}
}

阶段三:Tree Shaking

Tree Shaking 的工作原理

Tree Shaking 基于一个关键假设:ES6 模块的导入导出是静态的。这意味着:

  • 在编译时就能确定哪些导出被使用了
  • 可以安全地移除那些从未被引用的导出
  • 能够进行跨模块的死代码消除

可达性分析算法

Tree Shaking 的核心是可达性分析,这是一个经典的图论算法应用:

  1. 标记阶段:从入口点开始,标记所有可达的代码
  2. 传播阶段:递归地标记被可达代码引用的其他代码
  3. 清理阶段:移除所有未被标记的代码

副作用检测的挑战

副作用检测是 Tree Shaking 中最复杂的部分。Rollup 需要判断:

  • 函数调用:这个函数是否会产生副作用?
  • 变量赋值:这个赋值是否影响全局状态?
  • 模块初始化:模块的顶层代码是否有副作用?

常见的副作用类型

  • 全局变量修改window.x = 1
  • DOM 操作document.getElementById('app')
  • 网络请求fetch('/api/data')
  • 控制台输出console.log() (在生产环境可能被认为是副作用)

Tree Shaking 的局限性

虽然 Tree Shaking 很强大,但它也有一些局限:

  • 动态导入import() 语法的动态特性限制了静态分析
  • 第三方库:许多库没有考虑 Tree Shaking 优化
  • CommonJS 模块:动态特性使得精确分析变得困难

下面是 Tree Shaking 分析器的核心实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
// Tree Shaking 分析器
class TreeShakingAnalyzer {
constructor(moduleGraph) {
this.moduleGraph = moduleGraph;
this.usedExports = new Set();
this.usedModules = new Set();
this.reachableCode = new Map();
}

analyze() {
// 1. 从入口点开始标记使用的代码
this.moduleGraph.entryPoints.forEach((entry) => {
this.markModuleAsUsed(entry);
});

// 2. 递归标记依赖
this.propagateUsage();

// 3. 分析未使用的导出
const unusedExports = this.findUnusedExports();

// 4. 生成 Tree Shaking 报告
return {
usedModules: Array.from(this.usedModules),
unusedExports,
eliminatedCode: this.calculateEliminatedCode(),
shakingEffectiveness: this.calculateEffectiveness(),
};
}

markModuleAsUsed(moduleNode, importedNames = null) {
if (this.usedModules.has(moduleNode.id)) {
return; // 已经处理过
}

this.usedModules.add(moduleNode.id);

// 如果是入口模块,标记所有导出为使用
if (!importedNames) {
moduleNode.moduleInfo.exports.forEach((exportInfo) => {
if (exportInfo.type === "default") {
this.usedExports.add(`${moduleNode.id}:default`);
} else if (exportInfo.specifiers) {
exportInfo.specifiers.forEach((spec) => {
this.usedExports.add(`${moduleNode.id}:${spec.exported}`);
});
}
});
} else {
// 标记特定的导入为使用
importedNames.forEach((name) => {
this.usedExports.add(`${moduleNode.id}:${name}`);
});
}

// 分析代码可达性
this.analyzeReachability(moduleNode);
}

analyzeReachability(moduleNode) {
const reachableNodes = new Set();

// 从导出开始进行可达性分析
moduleNode.moduleInfo.exports.forEach((exportInfo) => {
const exportKey =
exportInfo.type === "default"
? `${moduleNode.id}:default`
: `${moduleNode.id}:${exportInfo.specifiers?.[0]?.exported}`;

if (this.usedExports.has(exportKey)) {
if (exportInfo.declaration) {
this.markNodeReachable(
exportInfo.declaration,
reachableNodes,
moduleNode
);
}
}
});

// 标记副作用代码为可达
if (moduleNode.sideEffects.hasTopLevelSideEffects) {
moduleNode.sideEffects.sideEffectStatements.forEach((stmt) => {
this.markNodeReachable(stmt, reachableNodes, moduleNode);
});
}

// 保存可达性信息
this.reachableCode.set(moduleNode.id, reachableNodes);
}
}

阶段四:代码生成与优化

代码生成的核心挑战

  1. 作用域扁平化:将多个模块的作用域合并到一个全局作用域
  2. 变量冲突解决:处理不同模块中同名变量的冲突
  3. 格式适配:生成符合目标模块格式(ES、CJS、UMD 等)的代码
  4. 性能优化:减少运行时开销,提高执行效率

作用域提升的原理

作用域提升(Scope Hoisting)是 Rollup 的一个重要优化技术:

  • 传统方式:每个模块保持独立的作用域(通过闭包实现)
  • 提升方式:将所有模块的作用域合并到全局作用域
  • 优势:减少闭包开销,提高运行时性能,便于 JavaScript 引擎优化

变量重命名策略

当多个模块存在同名变量时,Rollup 采用以下策略:

  1. 冲突检测:遍历所有模块,收集变量声明信息
  2. 重命名算法:为冲突变量生成唯一的新名称
  3. 引用更新:更新所有对重命名变量的引用

代码分割的考量

虽然 Rollup 主要用于库打包,但在处理大型项目时也需要考虑代码分割:

  • 入口点分割:为每个入口点生成独立的 chunk
  • 动态导入处理:为动态导入的模块创建独立的 chunk
  • 公共代码提取:避免重复打包相同的依赖

多格式输出的实现

Rollup 支持多种输出格式,每种格式都有其特定的包装代码:

  • ES 模块:保持原生的 import/export 语法
  • CommonJS:使用 module.exportsrequire()
  • UMD:同时支持 AMD、CommonJS 和全局变量
  • IIFE:立即执行函数表达式,适用于浏览器环境

下面是代码生成器的核心实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
// 代码生成器
class RollupCodeGenerator {
constructor(moduleGraph, options) {
this.moduleGraph = moduleGraph;
this.options = options;
this.generateContext = new Map();
}

async generateBundle() {
const chunks = this.createChunks();
const bundles = new Map();

for (const chunk of chunks) {
const bundle = await this.generateChunk(chunk);
bundles.set(chunk.id, bundle);
}

return bundles;
}

createChunks() {
// 代码分割决策
const chunks = [];
const entryChunks = new Map();

// 为每个入口点创建 chunk
this.moduleGraph.entryPoints.forEach((entry) => {
const chunk = {
id: entry.id,
isEntry: true,
modules: new Set([entry]),
dependencies: new Set(),
dynamicDependencies: new Set(),
};

entryChunks.set(entry.id, chunk);
chunks.push(chunk);
});

// 分析动态导入
this.moduleGraph.nodes.forEach((moduleNode) => {
const dynamicImports = this.findDynamicImports(moduleNode.ast);

dynamicImports.forEach((dynamicImport) => {
const targetModule = this.resolveModule(dynamicImport.source);

if (targetModule && !entryChunks.has(targetModule.id)) {
// 创建动态 chunk
const dynamicChunk = {
id: `dynamic-${targetModule.id}`,
isEntry: false,
isDynamic: true,
modules: new Set([targetModule]),
dependencies: new Set(),
dynamicDependencies: new Set(),
};

chunks.push(dynamicChunk);
}
});
});

// 分配模块到 chunk
this.assignModulesToChunks(chunks);

return chunks;
}

async generateChunk(chunk) {
const context = this.createGenerationContext(chunk);

// 1. 作用域提升分析
const hoistingPlan = this.analyzeScopeHoisting(chunk);

// 2. 生成模块代码
const moduleCode = await this.generateModuleCode(chunk, context);

// 3. 合并模块
const mergedCode = this.mergeModules(moduleCode, hoistingPlan);

// 4. 应用输出格式
const formattedCode = this.applyOutputFormat(mergedCode, context);

// 5. 生成 source map
const sourceMap = this.generateSourceMap(chunk, formattedCode);

return {
code: formattedCode,
map: sourceMap,
modules: Array.from(chunk.modules),
dependencies: Array.from(chunk.dependencies),
exports: this.getChunkExports(chunk),
};
}

analyzeScopeHoisting(chunk) {
// 作用域提升分析
const hoistingPlan = {
canHoist: new Set(),
conflicts: new Map(),
renamings: new Map(),
};

const globalScope = new Map(); // 全局作用域变量

// 分析每个模块的作用域
chunk.modules.forEach((moduleNode) => {
const moduleScope = moduleNode.scope;

// 检查变量名冲突
moduleScope.declarations.forEach((declaration, name) => {
if (globalScope.has(name)) {
// 发现冲突,需要重命名
const originalModule = globalScope.get(name);
const newName = this.generateUniqueName(name, globalScope);

hoistingPlan.conflicts.set(name, {
modules: [originalModule, moduleNode.id],
resolution: "rename",
});

hoistingPlan.renamings.set(`${moduleNode.id}:${name}`, newName);
globalScope.set(newName, moduleNode.id);
} else {
globalScope.set(name, moduleNode.id);
hoistingPlan.canHoist.add(`${moduleNode.id}:${name}`);
}
});
});

return hoistingPlan;
}

async generateModuleCode(chunk, context) {
const moduleCode = new Map();

for (const moduleNode of chunk.modules) {
const transformedCode = await this.transformModuleForOutput(
moduleNode,
context
);

moduleCode.set(moduleNode.id, transformedCode);
}

return moduleCode;
}

async transformModuleForOutput(moduleNode, context) {
let code = this.astToCode(moduleNode.ast);

// 1. 移除导入导出语句
code = this.removeImportExportStatements(code, moduleNode);

// 2. 重写变量引用
code = this.rewriteVariableReferences(code, moduleNode, context);

// 3. 应用插件转换
for (const plugin of context.plugins) {
if (plugin.renderChunk) {
code = await plugin.renderChunk(code, chunk, context.options);
}
}

return code;
}

removeImportExportStatements(code, moduleNode) {
// 移除 import 语句
let transformedCode = code.replace(
/import\s+.*?\s+from\s+['"][^'"]+['"];?\s*/g,
""
);

// 转换 export 语句
transformedCode = transformedCode.replace(
/export\s+default\s+/g,
"var __default__ = "
);

transformedCode = transformedCode.replace(
/export\s+\{([^}]+)\}/g,
(match, exports) => {
const exportList = exports.split(",").map((exp) => exp.trim());
return exportList
.map((exp) => {
const [local, exported] = exp.includes(" as ")
? exp.split(" as ").map((s) => s.trim())
: [exp, exp];
return `var __export_${exported}__ = ${local};`;
})
.join("\n");
}
);

return transformedCode;
}

rewriteVariableReferences(code, moduleNode, context) {
// 重写跨模块的变量引用
const renamings = context.hoistingPlan.renamings;

// 替换重命名的变量
let transformedCode = code;

renamings.forEach((newName, oldReference) => {
const [moduleId, varName] = oldReference.split(":");

if (moduleId === moduleNode.id) {
const regex = new RegExp(`\\b${varName}\\b`, "g");
transformedCode = transformedCode.replace(regex, newName);
}
});

return transformedCode;
}

mergeModules(moduleCode, hoistingPlan) {
const mergedParts = [];

// 添加提升的声明
hoistingPlan.canHoist.forEach((hoistedVar) => {
const [moduleId, varName] = hoistedVar.split(":");
const code = moduleCode.get(moduleId);

// 提取变量声明
const declarationRegex = new RegExp(
`(var|let|const|function|class)\\s+${varName}\\b[^;]*;?`
);

const match = code.match(declarationRegex);
if (match) {
mergedParts.push(match[0]);
}
});

// 添加模块代码(移除已提升的声明)
moduleCode.forEach((code, moduleId) => {
let processedCode = code;

// 移除已提升的声明
hoistingPlan.canHoist.forEach((hoistedVar) => {
const [varModuleId, varName] = hoistedVar.split(":");

if (varModuleId === moduleId) {
const declarationRegex = new RegExp(
`(var|let|const|function|class)\\s+${varName}\\b[^;]*;?`,
"g"
);
processedCode = processedCode.replace(declarationRegex, "");
}
});

if (processedCode.trim()) {
mergedParts.push(`// Module: ${moduleId}`);
mergedParts.push(processedCode);
}
});

return mergedParts.join("\n");
}

applyOutputFormat(code, context) {
const format = context.options.format;

switch (format) {
case "es":
return this.generateESModule(code, context);

case "cjs":
return this.generateCommonJS(code, context);

case "umd":
return this.generateUMD(code, context);

case "iife":
return this.generateIIFE(code, context);

default:
throw new Error(`不支持的输出格式: ${format}`);
}
}

generateESModule(code, context) {
const exports = context.chunk.exports || [];
const exportStatements = exports.map((exp) => {
if (exp.type === "default") {
return `export default __default__;`;
} else {
return `export { __export_${exp.name}__ as ${exp.name} };`;
}
});

return `${code}\n\n${exportStatements.join("\n")}`;
}

generateCommonJS(code, context) {
const exports = context.chunk.exports || [];
const exportStatements = exports.map((exp) => {
if (exp.type === "default") {
return `module.exports = __default__;`;
} else {
return `exports.${exp.name} = __export_${exp.name}__;`;
}
});

return `${code}\n\n${exportStatements.join("\n")}`;
}

generateUMD(code, context) {
const moduleName = context.options.name;
const globals = context.options.globals || {};

return `
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
typeof define === 'function' && define.amd ? define(['exports'], factory) :
(global = global || self, factory(global.${moduleName} = {}));
}(this, (function (exports) { 'use strict';

${code}

// 导出
${this.generateExportStatements(context.chunk.exports)}

})));`;
}

generateSourceMap(chunk, code) {
// 简化的 source map 生成
return {
version: 3,
sources: Array.from(chunk.modules).map((m) => m.id),
names: [],
mappings: "", // 实际实现中需要生成详细的映射
sourcesContent: Array.from(chunk.modules).map((m) => m.originalCode),
};
}
}

编译过程详细说明

编译流程的整体协调

Rollup 的四个编译阶段并不是独立工作的,它们之间存在紧密的协调关系:

数据流动

  1. 解析阶段产生 AST 和模块元信息
  2. 依赖图构建阶段使用这些信息建立完整的模块关系
  3. Tree Shaking 阶段基于依赖图进行死代码消除
  4. 代码生成阶段将优化后的结果转换为目标代码

性能优化策略

  • 缓存机制:避免重复解析相同的模块
  • 并行处理:在可能的情况下并行处理独立的模块
  • 增量更新:只重新处理发生变化的模块
  • 内存管理:及时释放不再需要的中间数据

实际应用中的最佳实践

模块组织建议

1
2
3
4
5
6
// ✅ 推荐:清晰的导入导出
export { specificFunction } from "./utils";
export const CONSTANT = "value";

// ❌ 避免:动态导出
export const dynamicExport = condition ? funcA : funcB;

Tree Shaking 优化技巧

1
2
3
4
5
6
// ✅ 推荐:具名导出
export const add = (a, b) => a + b;
export const subtract = (a, b) => a - b;

// ❌ 避免:默认导出对象
export default { add, subtract };

副作用标记

1
2
3
4
5
6
// package.json
{
"sideEffects": false, // 标记为无副作用
// 或者指定有副作用的文件
"sideEffects": ["*.css", "polyfill.js"]
}

插件系统与生态

核心插件介绍

  1. @rollup/plugin-node-resolve:解决了 Node.js 模块解析的问题,让 Rollup 能够找到和处理 node_modules 中的第三方包。
  2. @rollup/plugin-commonjs:CommonJS 模块转换
  3. @rollup/plugin-babel:Babel 转换

常用插件集合

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
// rollup.config.js - 常用插件配置
import resolve from "@rollup/plugin-node-resolve";
import commonjs from "@rollup/plugin-commonjs";
import babel from "@rollup/plugin-babel";
import terser from "@rollup/plugin-terser";
import typescript from "@rollup/plugin-typescript";
import json from "@rollup/plugin-json";
import replace from "@rollup/plugin-replace";
import alias from "@rollup/plugin-alias";
import copy from "rollup-plugin-copy";
import { visualizer } from "rollup-plugin-visualizer";
import serve from "rollup-plugin-serve";
import livereload from "rollup-plugin-livereload";

export default {
input: "src/index.ts",

plugins: [
// 路径别名
alias({
entries: [
{ find: "@", replacement: path.resolve(__dirname, "src") },
{ find: "@utils", replacement: path.resolve(__dirname, "src/utils") },
],
}),

// 环境变量替换
replace({
"process.env.NODE_ENV": JSON.stringify(process.env.NODE_ENV),
__VERSION__: JSON.stringify(process.env.npm_package_version),
preventAssignment: true,
}),

// JSON 文件支持
json(),

// TypeScript 支持
typescript({
tsconfig: "./tsconfig.json",
}),

// Node 模块解析
resolve({
browser: true,
extensions: [".js", ".jsx", ".ts", ".tsx"],
}),

// CommonJS 转换
commonjs(),

// Babel 转换
babel({
babelHelpers: "bundled",
exclude: "node_modules/**",
}),

// 文件复制
copy({
targets: [
{ src: "src/assets/*", dest: "dist/assets" },
{ src: "README.md", dest: "dist" },
],
}),

// 开发服务器
process.env.NODE_ENV === "development" &&
serve({
open: true,
contentBase: "dist",
port: 3000,
}),

// 热重载
process.env.NODE_ENV === "development" && livereload("dist"),

// 生产环境压缩
process.env.NODE_ENV === "production" && terser(),

// 构建分析
process.env.ANALYZE &&
visualizer({
filename: "dist/stats.html",
open: true,
}),
].filter(Boolean),
};

自定义插件开发

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
// plugins/custom-plugin.js - 自定义插件
export function customPlugin(options = {}) {
return {
name: "custom-plugin",

// 构建开始
buildStart(opts) {
console.log("Build started with options:", opts);
},

// 解析模块 ID
resolveId(id, importer) {
if (id === "virtual:my-module") {
return id;
}
return null;
},

// 加载模块
load(id) {
if (id === "virtual:my-module") {
return 'export const msg = "Hello from virtual module!";';
}
return null;
},

// 转换代码
transform(code, id) {
if (id.endsWith(".special")) {
// 自定义文件格式转换
return {
code: `export default ${JSON.stringify(code)};`,
map: null,
};
}
return null;
},

// 生成代码块
generateBundle(opts, bundle) {
// 添加额外的文件到输出
this.emitFile({
type: "asset",
fileName: "manifest.json",
source: JSON.stringify({
version: options.version,
buildTime: new Date().toISOString(),
}),
});
},

// 构建结束
buildEnd(error) {
if (error) {
console.error("Build failed:", error);
} else {
console.log("Build completed successfully");
}
},
};
}

// 使用自定义插件
import { customPlugin } from "./plugins/custom-plugin.js";

export default {
input: "src/index.js",
plugins: [
customPlugin({
version: "1.0.0",
}),
],
};

最佳实践与常见问题

最佳实践

项目结构组织

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
my-library/
├── src/
│ ├── index.js # 主入口
│ ├── components/ # 组件目录
│ │ ├── Button/
│ │ │ ├── index.js
│ │ │ ├── Button.css
│ │ │ └── Button.test.js
│ │ └── Modal/
│ ├── utils/ # 工具函数
│ │ ├── index.js
│ │ ├── helpers.js
│ │ └── constants.js
│ └── types/ # 类型定义
│ └── index.ts
├── dist/ # 构建输出
├── tests/ # 测试文件
├── docs/ # 文档
├── rollup.config.js # Rollup 配置
├── package.json
├── tsconfig.json
└── README.md

配置文件组织

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// rollup.base.js - 基础配置
export const createBaseConfig = (options = {}) => ({
plugins: [
resolve({
browser: options.browser || false,
preferBuiltins: !options.browser,
}),
commonjs(),
babel({
babelHelpers: "bundled",
exclude: "node_modules/**",
}),
],
});

// rollup.config.js - 主配置文件
import { createBaseConfig } from "./rollup.base.js";

const baseConfig = createBaseConfig();

export default [
// 库构建
{
...baseConfig,
input: "src/index.js",
output: [
{ file: "dist/index.esm.js", format: "es" },
{ file: "dist/index.cjs.js", format: "cjs" },
],
},

// 浏览器构建
{
...createBaseConfig({ browser: true }),
input: "src/index.js",
output: {
file: "dist/index.umd.js",
format: "umd",
name: "MyLibrary",
},
},
];

错误处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
// rollup.config.js - 错误处理
export default {
input: "src/index.js",

plugins: [
{
name: "error-handler",

buildStart() {
this.errors = [];
},

load(id) {
try {
// 加载逻辑
} catch (error) {
this.error(`Failed to load ${id}: ${error.message}`);
}
},

transform(code, id) {
try {
// 转换逻辑
} catch (error) {
this.warn(`Transform warning for ${id}: ${error.message}`);
return null;
}
},

generateBundle(opts, bundle) {
// 检查构建结果
Object.keys(bundle).forEach((fileName) => {
const chunk = bundle[fileName];
if (chunk.type === "chunk" && chunk.code.length === 0) {
this.warn(`Empty chunk generated: ${fileName}`);
}
});
},

buildEnd(error) {
if (error) {
console.error("Build failed:", error);
} else if (this.errors.length > 0) {
console.warn(`Build completed with ${this.errors.length} warnings`);
}
},
},
],
};

常见问题解决

1. 循环依赖问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 问题代码
// a.js
import { b } from "./b.js";
export const a = () => b();

// b.js
import { a } from "./a.js";
export const b = () => a();

// 解决方案1:重构代码结构
// shared.js
export const shared = () => console.log("shared");

// a.js
import { shared } from "./shared.js";
export const a = () => shared();

// b.js
import { shared } from "./shared.js";
export const b = () => shared();

// 解决方案2:使用动态导入
// a.js
export const a = async () => {
const { b } = await import("./b.js");
return b();
};

2. 外部依赖处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// rollup.config.js - 外部依赖配置
export default {
input: "src/index.js",

// 方式1:数组形式
external: ["react", "react-dom", "lodash"],

// 方式2:函数形式
external: (id) => {
// 排除所有 node_modules
if (id.includes("node_modules")) return true;

// 排除特定包
if (["react", "react-dom"].includes(id)) return true;

// 排除 Node.js 内置模块
if (require("module").builtinModules.includes(id)) return true;

return false;
},

output: {
format: "umd",
globals: {
react: "React",
"react-dom": "ReactDOM",
lodash: "_",
},
},
};