从一个简单的打包示例开始
新建 example
我们在本项目的根目录下新建一个 example 文件夹,然后分别新建如下文件:
- index.js - 作为我们的打包入口文件,导入 user.js 文件中的一些导出
- user.js - 作为一个依赖模块,导出一些变量
- rollup.js - 作为打包配置文件
example 目录结构:
example
├─ index.js
├─ user.js
├─ rollup.js
然后我们分别在这三个文件中写点东西来测试打包后的文件:
user.js 文件中我们分别导出了 name, age, foo:
const name = 'victor jiang';
const age = 17;
function foo() {
console.log(123);
function innerFunc() {
// tree-shaking
console.log(3);
}
return 'foo';
var bar = 'bar'; // 函数已经返回了,这里的赋值语句永远不会执行
}
export { name, age, foo };
index.js 文件中导入了 age, foo, name。并且还写了一个 if-else 语句,然后导出了 hello 函数:
import { age, foo, name } from './user';
const fname = foo();
if (0) {
console.log('这段代码不会被执行');
} else {
console.log('这段代码保留');
}
// 导出一个foo函数
export default function hello() {
console.log(fname);
console.log(`hello! ${name}`);
}
rollup.js 文件:
//导入rollup
const { rollup } = require('../dist/rollup'); //注意稍后我们需要运行在node环境中,所以使用了require,而不是 import 方式
//打包输入配置只给一个入口信息
const inputOption = {
input: 'example/index.js'
};
//打包输出配置信息就简单点,分别定义输出的js文件格式还有文件名称
const outputOption = {
format: 'es',
file: 'example/es.js'
};
async function build() {
try {
const { write } = await rollup(inputOption);
await write(outputOption);
} catch (error) {
console.log('error: ', error);
}
}
//开始打包
build();
如上,我们的示例代码就写好了。进入 example 文件目录,在控制台输入 node rollup.js。等待片刻即可看到该文件夹下生成了一个新的文件 /example/es.js。
//es.js
const name = 'victor jiang';
function foo() {
console.log(123);
return 'foo';
}
const fname = foo();
{
console.log('这段代码保留');
}
// 导出一个foo函数
function hello() {
console.log(fname);
console.log(`hello! ${name}`);
}
export { hello as default };
当然,我个人比较喜欢使用 vscode 的 Code Runner 插件。安装完成后在 rollup.js 文件中右键鼠标并选择“run code”即可在 example 打包生成一个 es.js 文件(注意这个方式生成的 es.js 文件的位置是和 rollup.js 文件同一级)
我们可以看到经过 rollup 打包后的生成的代码非常简洁。
比如说:
- rollup 会帮我们将 if 语句中的代码块给删除掉,保留 else 逻辑中的代码块。
- rollup 会帮我们将 foo 函数中的 innerFunc 也删除掉,并且 foo 函数中 var bar = 'bar'; 这段代码也不见了。
- index.js 文件中导入的 age 变量经过 rollup 发现没有使用到就被删除了,并且移除了整个 import { age, foo, name } from './user';语句
经过上面的例子我们发现 rollup 在打包的时候它会默认帮我们清除无用代码。其实这个技能就是 tree-shaking 了,通俗来讲就叫“摇树”。看到这里就有同学非常好奇了,这也太神奇了吧?居然还可以这么智能的将我们的代码进行“瘦身”!
总结
经过上面的示例分析我们学会了如何使用配置来让 rollup 打包我们想要的代码格式文件,并且它默认支持 tree-shaking (也可以指定它不执行 tree-shaking,后面章节会介绍)。而且 rollup 默认只支持 ES 模块。因为ES 模块是官方标准,也是 JavaScript 语言明确的发展方向。并且 ES 模块允许进行静态分析,从而实现像 tree-shaking 的优化,并提供诸如循环引用和动态绑定等高级功能。
既然 rollup 在打包 js 库的方面这么优秀,那么为了探究它的底层原理,接下来我们即将进入到源码的揭秘过程。
源码目录结构
此文档是基于 rollup-3.2.3 版本的源码进行分析。其核心源码目录如下:
src
├─ ast //node类型分析模块
├─ finalisers //打包输出格式定义
├─ rollup //rollup函数定义
├─ utils //工具函数
├─ watch //监听函数
├─ Graph.ts //rollup核心-图
├─ Module.ts //模块类
└─ ModuleLoader.ts //模块加载器
ast
ast 即 Abstract Syntax Tree 的简称,直接翻译过来就是抽象语法树。它包括了对我们程序中所有的 node 节点的类型扩展,作用域的定义,变量类型的定义等等
finalisers
finalisers 目录存放了对包格式的定义,如下所示:
- amd - 异步模块定义,用于像 RequireJS 这样的模块加载器
- cjs – CommonJS,适用于 Node 和 Browserify/Webpack
- esm – 将软件包保存为 ES 模块文件,在现代浏览器中可以通过 <script type=module> 标签引入
- iife – 一个自动执行的功能,适合作为<script>标签。
- umd – 通用模块定义,以 amd,cjs 和 iife 为一体
- system - SystemJS 加载器格式
rollup
这个文件夹是 rollup 打包的程序执行入口。主要定义 rollup 类型以及 rollup 函数等。
utils
存放公共函数的方法
watch
watch方法定义
Graph.ts
rollup 的核心模块。它保存了所有的模块信息,moduleLoader,pluginDriver,ast 解析器,以及缓存访问过的 module 来提升性能。
Module.ts
module 简单来理解就是我们所写的各个 js 文件。module 类保存了各模块的依赖关系,模块中的导入导出信息,模块变量等等。最重要的一点是它还保存了 js 文件中的源码和被语法解析器解析后的 ast 信息。
ModuleLoader.ts
模块加载器。根据文件的绝对路径读取 js 文件并提取源码。生成 module 实例等等。
从入口处分析
回过头来看我们例子中的 rollup.js 文件:
const { rollup } = require('../dist/rollup'); //注意稍后我们需要运行在node环境中,所以使用了require,而不是 import 方式
//打包输入配置只给一个入口信息
const inputOption = {
input: 'example/index.js'
};
//打包输出配置信息就简单点,分别定义输出的js文件格式还有文件名称
const outputOption = {
format: 'es',
file: 'example/es.js'
};
async function build() {
try {
const { write } = await rollup(inputOption);
await write(outputOption);
} catch (error) {
console.log('error: ', error);
}
}
//开始打包
build();
我们代码中导入了 /dist/rollup.js
/*
@license
Rollup.js v3.2.3
Wed, 09 Nov 2022 06:43:19 GMT - commit 8f0af3a4a91e10ba3fa982417193bab2cb00fc03
https://github.com/rollup/rollup
Released under the MIT License.
*/
'use strict';
/**
* Node.js 提供了 exports 和 require 两个对象,其中 exports 是模块公开的接口,require 用于从外部获取一个模块的接口,即所获取模块的 exports 对象。
* 此处预先定义了两个属性
*/
Object.defineProperties(exports, {
__esModule: { value: true },
[Symbol.toStringTag]: { value: 'Module' }
});
const rollup = require('./shared/rollup.js');
require('node:path');
require('path');
require('node:process');
require('node:perf_hooks');
require('node:crypto');
require('node:fs');
require('node:events');
require('tty');
exports.VERSION = rollup.version;
exports.defineConfig = rollup.defineConfig;
exports.rollup = rollup.rollup;
exports.watch = rollup.watch;
//# sourceMappingURL=rollup.js.map
通过分析代码得知我们在 rollup.js 文件中使用的 rollup 其实是从./shared/rollup.js 文件中引入的。由于这个./shared/rollup.js 中的文件经过 rollup 打包将所有代码输出到一个文件中,这样非常不利于我们分析源码。因此我们借助 rollup.js.map 文件可以去调试 src 文件夹中的源码。因此我们需要回到项目根目录的 src 文件夹中去查阅源码。
rollup 揭秘相关文章
- rollup 技术揭秘系列一 准备篇(可能是全网最系统性的 rollup 源码分析文章)
- rollup 技术揭秘系列二 源码目录结构及打包入口分析(可能是全网最系统性的 rollup 源码分析文章)
- rollup 技术揭秘系列三 rollup 函数(可能是全网最系统性的 rollup 源码分析文章)
- rollup 技术揭秘系列四 graph.build(可能是全网最系统性的 rollup 源码分析文章)
- rollup 技术揭秘系列五 构建依赖图谱(可能是全网最系统性的 rollup 源码分析文章)
- rollup 技术揭秘系列六 模块排序(可能是全网最系统性的 rollup 源码分析文章)
- rollup 技术揭秘系列七 includeStatements(可能是全网最系统性的 rollup 源码分析文章
- rollup 技术揭秘系列八 node.hasEffects(可能是全网最系统性的 rollup 源码分析文章)
- rollup 技术揭秘系列九 module.include()(可能是全网最系统性的 rollup 源码分析文章)
- rollup 技术揭秘系列十 includeStatements 总结(可能是全网最系统性的 rollup 源码分析文章)
- rollup 技术揭秘系列十一 rollup 打包配置选项整理(可能是全网最系统性的 rollup 源码分析文章)
- rollup 技术揭秘系列十二 handleGenerateWrite(可能是全网最系统性的 rollup 源码分析文章)
- rollup 技术揭秘系列十三 bundle.generate(可能是全网最系统性的 rollup 源码分析文章)
- rollup 技术揭秘系列十四 renderChunks(可能是全网最系统性的 rollup 源码分析文章)
- rollup 技术揭秘系列十五 renderModules(可能是全网最系统性的 rollup 源码分析文章)
- rollup 技术揭秘系列十六 Rollup 插件开发指南(可能是全网最系统性的 rollup 源码分析文章)
- rollup 技术揭秘系列十七 rollup-cli 的开发(可能是全网最系统性的 rollup 源码分析文章)
- rollup 技术揭秘系列十八 Rollup 打包流程示意图(可能是全网最系统性的 rollup 源码分析文章)