继 Webpack、Vite 等前端工具链系列的了解之后,又碰到了 Rollup,我之前对 Rollup 的了解仅停留在 Vite 打包时使用、组里的大佬使用 Rollup 写过一个静态官网页面,为了更深入的学习,就通过这篇文章来学习 Rollup 以及 ta 的使用
概述
Rollup 是一个 JavaScript 模块打包工具,可以将多个小的代码片段编译为完整的库和应用。与传统的 CommonJS 和 AMD 这一类非标准化的解决方案不同,Rollup 使用的是 ES6 版本 Javascript 中的模块标准。新的 ES 模块可以让你自由、无缝地按需使用你最喜爱的库中那些有用的单个函数。这一特性在未来将随处可用,但 Rollup 让你现在就可以,想用就用。
Rollup 其实就是打包工具,和 Webpack 一样,毕竟我们现在都是用模块化的思路构建应用,而模块化则需要一个入口和出口的概念,那么打包工具就是用来干这个事的,将应用的模块集成到一个入口
与 Webpack 的区别
Webpack 作为打包工具链的大哥,出道早、地位高,采取的模块化方案也是基于早于 ES Module 的 CommonJS,而我们的 Rollup 就不一样了,主打的就是一个 ES Module,Rollup 会基于 ES Module 给你的工程提供全方面的解决方案,包括但不限于,兼容 CommonJS 库、提供不同模块标准的输出、Tree-Shaking...
使用
您可以在线查看源代码
命令行
按照 rollup.js 的中文文档先简单来上一段
npm install -D rollup
假设你有一个下面目录结构的项目
├── package-lock.json
├── package.json
├── public
│ ├── bundle.js
│ └── index.html
└── src
├── index.js
└── utils
└── math.js
// index.js
import { square } from "./utils/math";
console.log(square(2));
// math.js
export function square ( x ) {
return x * x;
}
index.js 是你的入口文件,math.js 是你的自定义模块,boudle.js 是你的输出文件(有没有这个不要紧,rollup 会帮你处理的!)
使用以下命令即可完成打包
npx rollup src/index.js --file public/bundle.js --format iife
你也可以在 package.json 里面配置 "script" 属性
{
...
"scripts": {
"build": "rollup src/index.js --file public/bundle.js --format iife"
},
...
}
让我们看看 bundle.js 的庐山真面目
(function () {
'use strict';
function square ( x ) {
return x * x;
}
console.log(square(2));
})();
这是一个 iife(Immediately Invoked Functions Expressions) 格式的输出文件,在浏览器解析至其对应代码时,会立即执行(所以叫立即调用函数表达式)
IIFE 就是最早的模块化,比如 JQuery,早期前端‘模块化’开发中通过将模块一股脑的引入来实现模块在浏览器的导入,通常如下
<script src="./jqeury1.js"></script>
<script src="./jqeury2.js"></script>
<script src="./jqeury3.js"></script>
...
<script src="./jqeuryn.js"></script>
除了 IIFE 这个最早的模块化标准之外,前端还有其它的模块化标准,如下(按照出现时间顺序排列)
IIFECJS(CommonJS)AMDCMDUMDSystem.jsESM(ES Module)
目前主流的还不是 rollup 主打的 ESM,也就是还没有完成大一统,仍是群雄割据的状态,所以 rollup 的 --format 选项就是为了兼容其它模块化准备的,即使项目使用的是 ESM 编写,但仍然能够输出其它格式(rollup 它真的,我哭死),其 --format 选项文档如下
rollup [option] [type]
# Options
# -f, --format <format> Type of output (amd, cjs, es, iife, umd, system)
# Example
# rollup src/index.js --file public/bundle.js --format iife
配置文件
实际上 rollup 给出的项目级别的例子是下面这个
rollup -f iife --globals jquery:jQuery,lodash:_ \
-i src/app.js -o build/app.js -m build/app.js.map
这实在是太长了,且难以读懂,所以 rollup 给出了自己的配置文件,用 JS 对象的方式去表示 rollup 的选项,通常它被命名为 rollup.config.js
在项目根目录下创建 rollup.config.js,并覆写如下
// rollup.config.js
module.exports = {
input: "src/index.js",
output: {
file: "public/bundle.js",
format: "iife",
},
};
运行命令
npx rollup -c
就可以得到同样的 bondle.js

注意:rollup.config.js 不能直接使用 ESM,比如 export default ... 语法(虽然官方文档的示例是这么写的),而是应该使用 CommonJS,具体详情可以参考文档 Using Configuration Files - rollup.js doc
其它更加详细的 API 就不多介绍了,不然就和文档没啥区别了(想要更深入了解 rollup 的可以去官方文档,说实话不是很长)
除了基本的 API 之外,其实 rollup 还有一些比较出色的亮点和使用技巧,如下
Tree-Sharking
像早期的 Webpack 和 browserify 是会全量打包的,无法做到只导入使用到的,这是因为 CommonJS 的缺陷,而 ESM 就很好的解决了这个问题,因为它是静态分析的(具体原因不细讲了,不然我下一篇写啥呢?)
那怎么做和怎么体现这个 Tree-Sharking 呢?看下面这个例子
先下载 browserify,早期 Webpack 打包(Webpack 4 以前,不含 Webpack 4)和 browserify 一致(但后来 Webpack 也加入了 Tree-Sharking)
npm install -D browserify
修改 math.js,并使用 CommonJS 标准,如下
// math.js
module.exports.square = function (x) {
return x * x;
};
module.exports.cube = function (x) {
return x * x * x;
};
修改 index.js,并使用 CommonJS 标准,如下
// math.js
const { square } = require("./utils/math.js");
console.log(square(2));
执行打包命令
npx browserify src/index.js -o public/bundle.js
得到的 bundle.js 如下
// 格式化之后的 bundle.js
(function () {
// browserify 加载模块代码
// ...
})()(
{
1: [
function (require, module, exports) {
const { square } = require("./utils/math.js");
console.log(square(2));
},
{ "./utils/math.js": 2 },
],
2: [
function (require, module, exports) {
module.exports.square = function (x) {
return x * x;
};
module.exports.cube = function (x) {
return x * x * x;
};
},
{},
],
},
{},
[1]
);
你会发现,没有使用到的 cube 也被打包进来了
但是基于 ESM 的 Rollup 就不会这样(前提是你的项目得使用 ESM)
修改 math.js 如下
// math.js
export function square ( x ) {
return x * x;
}
export function cube ( x ) {
return x * x * x;
}
重新打包,得到的 bundle.js 如下
(function () {
'use strict';
function square ( x ) {
return x * x;
}
console.log(square(2));
})();
只会将用到的 square 打包进来,非常的好用
导入 node_modules 模块
rollup 也有和 Webpack 同样的设计,即插件设计,用户可以利用 rollup 暴露出来的钩子函数调用 rollup 生命周期的各种 API,而 @rollup/plugin-node-resolve 就是基于这个设计的插件
@rollup/plugin-node-resolve 是作为导入 node_modules 里的模块设计,你可能疑惑为什么需要一个插件去完成这一步,因为伴随 IDE 的智能提示、智能补全,使用 ESM 和 CJS 去引用模块似乎是一件稀松平常的事情,但其实没有这么简单
要说引用模块,这就不得不提到我们平时是怎么从 require(...) 和 import...from... 里拿到 npm 包了,
假设上面两种模块引用如下
require('specifier')import...from 'specifier'
假设这个模块是在 node_modules 中的,会以下面这种方式拿到模块

在Node 环境执行的文件下,包含上述模块引用的,会先通过算法解析出模块在磁盘的路径(即 node_modules 目录下的路径),然后通过 Node.js 自己的 I/O 拿到模块的文件,解析其中的代码得到模块,并返回
上面这个只是解析算法的一部分,还有更加的复杂的情况需要考虑,require(...) 的解析参考,import...from... 的解析参考
想深入了解的同学,可以查看我这篇文章
回到 @rollup/plugin-node-resolve 这个插件,由于 Node.js 对 ESM(也就是上面说的 import...from...)的支持还在逐步跟进,并不是所有的 Node.js 版本都支持 ESM 的解析算法,但大多数是支持 CJS 的,所以对不同的版本兼容使用 Rollup 就需要 @rollup/plugin-node-resolve,以保证我们能够在所有版本下使用‘未来’的 ESM
避免产生错误
(!) Unresolved dependencies
https://rollupjs.org/guide/en/#warning-treating-module-as-external-dependency
使用
使用 @rollup/plugin-node-resolve 参考如下
# 安装
npm install -D @rollup/plugin-node-resolve
修改 rollup.cofig.js
const resolve = require("@rollup/plugin-node-resolve");
module.exports = {
// ...
plugins: [resolve()],
};
再安装一个具有 ESM 导出的 npm 包
# the-answer 会导出一个静态值 42
npm install the-answer
修改 index.js
// index.js
import answer from "the-answer";
console.log(answer);
执行打包
npx rollup -c
得到 bundle.js
(function () {
'use strict';
var index = 42;
console.log(index);
})();
如果你没有 @rollup/plugin-node-resolve 就会得到上面的错误
转换 CommonJS 模块
转换 CommonJS 模块也是一个兼容性的功能,因为现在大多数模块和 npm 包是用的 CommonJS 模块标准写的,而 Rollup 是基于 ESM 打包,因此正确的打包就需要将 CJS to ESM,而 Rollup 提供的插件就是 @rollup/plugin-commonjs,还是使用一个例子去教学
叛逆版(不使用 @rollup/plugin-commonjs)
使用模块 lodash
npm install lodash
修改 index.js
// index.js
import _ from "lodash";
console.log(_.concat([1, 2, 3], 4, [5]));
执行打包
npx rollup -c
然后不出意外的出意外了
src/index.js → public/bundle.js...
(!) "this" has been rewritten to "undefined"
https://rollupjs.org/guide/en/#error-this-is-undefined
node_modules/lodash/lodash.js
17207: root._ = _;
17208: }
17209: }.call(this));
^
[!] RollupError: "default" is not exported by "node_modules/lodash/lodash.js", imported by "src/index.js".
https://rollupjs.org/guide/en/#error-name-is-not-exported-by-module
一般都是报没有使用 ESM 语法的错误
虚心好学版(使用 @rollup/plugin-commonjs)
# 安装
npm install -D @rollup/plugin-commonjs
修改 rollup.cofig.js
const resolve = require("@rollup/plugin-node-resolve");
const commonjs = require("@rollup/plugin-commonjs");
module.exports = {
// ...
plugins: [resolve(), commonjs()],
};
再次执行打包
npx rollup -c
得到 bundle.js

你没看错是最终的 bundle.js 是 17216 行,全量打包 lodash,使用 CJS 的模块被 @rollup/plugin-commonjs 转换后是无法进行 Tree-Sharking 的
总结
我认为 Rollup 作用在于开发者能够使用未来的模块标准去写代码(ES Module 是未来的新标准,浏览器和 Node.js 都将逐步实现),同时也支持兼容导入旧的 CommonJS 库,也兼容不同模块标准的输出,就像 Babel 之于 ES6,开发者可以大胆的使用新的,设计良好的 API 而不用过分担忧新旧浏览器的兼容,不像 Android,代码总是有对于不同 Android 版本的兼容,同时采用新标准的代码也将提高代码的生命周期,不至于因为 JS 版本的迭代迅速过时并面临后期的重构的成本
不过国内的环境应该不需要考虑这些问题