Rollup 是一个 JavaScript 模块打包器,可以将小块代码编译成大块复杂的代码,例如 library 或应用程序。Rollup 对代码模块使用新的标准化格式,这些标准都包含在 JavaScript 的 ES6 版本中,而不是以前的特殊解决方案,如 CommonJS 和 AMD。ES6 模块可以使你自由、无缝地使用你最喜爱的 library 中那些最有用独立函数,而你的项目不必携带其他未使用的代码。ES6 模块最终还是要由浏览器原生实现,但当前 Rollup 可以使你提前体验。

为什么使用rollup

如果你将项目拆分成小的单独文件中,这样开发软件通常会很简单,因为这通常会消除无法预知的相互影响(remove unexpected interaction),以及显著降低了所要解决的问题的复杂度(complexity of the problem),并且可以在项目最初时,就简洁地编写小的项目。不幸的是,JavaScript 以往并没有将此功能作为语言的核心功能。

而rollup基于ES2015模块的,相比于webpack或Browserify所使用的CommonJS模块更加有效率,因为Rollup使用一种叫做
tree-shaking的特性来移除模块中未使用的代码,这也就是说当我们引用一个库的时候,我们只用到一个库的某一段的代码的时候,它不会把所有的代码打包进来,而仅仅打包使用到的代码。

它的优点有如下

  1. 能组合我们的脚本文件。
  2. 移除未使用的代码(仅仅使用ES6语法中)。
  3. 在浏览器中支持使用 Node modules。
  4. 压缩文件代码使文件大小尽可能最小化。

tree-shaking

除了使用 ES6 模块之外,Rollup 还静态分析代码中的 import,并将排除任何未实际使用的代码。这允许您架构于现有工具和模块之上,而不会增加额外的依赖或使项目的大小膨胀。

例如,在使用 CommonJS 时,必须导入(import)完整的工具(tool)或库(library)对象

1
2
3
4
5
6
// 使用 CommonJS 导入(import)完整的 utils 对象

var utils = require( 'utils' );
var query = 'Rollup';
// 使用 utils 对象的 ajax 方法
utils.ajax( 'https://api.example.com?search=' + query ).then( handleResponse );

但是在使用 ES6 模块时,无需导入整个 utils 对象,我们可以只导入(import)我们所需的 ajax 函数:

1
2
3
4
5
// 使用 ES6 import 语句导入(import) ajax 函数
import { ajax } from 'utils';
var query = 'Rollup';
// 调用 ajax 函数
ajax( 'https://api.example.com?search=' + query ).then( handleResponse );

因为 Rollup 只引入最基本最精简代码,所以可以生成轻量、快速,以及低复杂度的 library 和应用程序。因为这种基于显式的 importexport 语句的方式,它远比「在编译后的输出代码中,简单地运行自动 minifier 检测未使用的变量」更有效。

注意:Rollup只会在ES6模块中支持tree-shaking特性。目前按照CommonJS模块编写的jquery不能被支持tree-shaking.

兼容性

导入 CommonJS

Rollup 可以通过@rollup/plugin-commonjs插件 导入现有的 CommonJS 模块。

发布 ES 模块

为了确保你的 ES6 模块可以直接与「运行在 CommonJS(例如 Node.js 和 webpack)中的工具(tool)」使用,你可以使用 Rollup 编译为 UMD 或 CommonJS 格式,然后在 package.json 文件的 main 属性中指向当前编译的版本。如果你的 package.json 也具有 module 字段,像 Rollup 和 webpack 2 这样的 ES6 感知工具(ES6-aware tools)将会直接 导入ES6 模块版本

使用Rollup

安装

rollup一般有两种安装模式,全局与本地,建议本地安装,版本及协作时好维护。

全局安装

1
npm install --global rollup

这将使 Rollup 可用作全局命令行工具。

本地安装

在团队或分布式环境中工作时,将 Rollup 添加为本地依赖项是明智的。在本地安装 Rollup 可以防止要求多个贡献者单独安装 Rollup 作为额外的步骤,并确保所有贡献者都使用相同版本的 Rollup。

使用 NPM 在本地安装 Rollup:

1
npm install rollup --save-dev

或使用yarn:

1
yarn -D add rollup

rollup一般我们有两种使用方式,命令行和配置文件方式。

命令行使用

这些命令假定您的应用程序的入口点名为main.js,并且您希望所有导入都编译到一个名为bundle.js.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
-i, --input <filename>      要打包的文件(必须)
-o, --file <output> 输出的文件 (如果没有这个参数,则直接输出到控制台)
-f, --format <format> 输出的文件类型 (amd, cjs, esm, iife, umd)
-e, --external <ids> 将模块ID的逗号分隔列表排除
-g, --globals <pairs> 以`module ID:Global` 键值对的形式,用逗号分隔开
任何定义在这里模块ID定义添加到外部依赖
-n, --name <name> 生成UMD模块的名字
-h, --help 输出 help 信息
-m, --sourcemap 生成 sourcemap (`-m inline` for inline map)
--amd.id AMD模块的ID,默认是个匿名函数
--amd.define 使用Function来代替`define`
--no-strict 在生成的包中省略`"use strict";`
--no-conflict 对于UMD模块来说,给全局变量生成一个无冲突的方法
--intro 在打包好的文件的块的内部(wrapper内部)的最顶部插入一段内容
--outro 在打包好的文件的块的内部(wrapper内部)的最底部插入一段内容
--banner 在打包好的文件的块的外部(wrapper外部)的最顶部插入一段内容
--footer 在打包好的文件的块的外部(wrapper外部)的最底部插入一段内容
--interop 包含公共的模块(这个选项是默认添加的)

例如:

对于浏览器:

1
2
# compile to a <script> containing a self-executing function ('iife')
rollup main.js --file bundle.js --format iife

对于 Node.js:

1
2
3
# compile to a CommonJS module ('cjs')
rollup main.js --file bundle.js --format cjs

对于浏览器和 Node.js:

1
2
# UMD format requires a bundle name
rollup main.js --file bundle.js --format umd --name "myBundle"

配置文件(推荐)

我们一般在命令行中使用Rollup。也可以提供一份配置文件来简化命令行操作,同时还能启用Rollup的高级特性;

Rollup的配置文件是可选的,但是使用配置文件的作用很强大,而且很方便,因此我们推荐你使用

配置文件是一个ES6模块,它对外暴露一个对象,这个对象包含了一些Rollup需要的一些选项。通常,我们把这个配置文件叫做rollup.config.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
37
38
39
40
// rollup.config.js
export default {
// 核心选项
input, // 必须
external,
plugins,

// 额外选项
onwarn,

// danger zone
acorn,
context,
moduleContext,
legacy

output: { // 必须 (如果要输出多个,可以是一个数组)
// 核心选项
file, // 必须
format, // 必须
name,
globals,

// 额外选项
paths,
banner,
footer,
intro,
outro,
sourcemap,
sourcemapFile,
interop,

// 高危选项
exports,
amd,
indent
strict
},
};

你必须使用配置文件才能执行以下操作:

如果你想使用Rollup的配置文件,记得在命令行里加上--config或者-c

1
2
# 默认使用rollup.config.js$ rollup --config
# 或者, 使用自定义的配置文件,这里使用my.config.js作为配置文件$ rollup --config my.config.js

例如可以在项目的根目录中运行:

1
npx rollup --config或yarn rollup --config

但是安装后,通常的做法是向package.json添加单个构建脚本,为所有贡献者提供方便的命令。例如

1
2
3
4
5
{
"scripts": {
"build": "rollup --config"
}
}

基本使用

在项目的根目录下新建一个配置文件为 rollup.config.js,进行输入和输出配置:

1
2
3
4
5
6
7
8
9
export default {
input: './src/main.js', //rollup先执行的入口文件。
output: {
file: 'bundle.js', // 要写入的文件,也可以用于生成的sourcemaps。
format: 'iife', // rollup支持的多种输出格式(有amd,cjs, es, iife 和 umd)
name: 'MyBundle1', // 变量名,代表你的 `iife`/`umd` 包,同一页上的其他脚本可以访问它
sourcemap: true // 调试代码
}
}

下面再来了解一下format配置的含义:

  • format: rollup支持的多种输出格式(有amd,cjs, es, iife 和 umd)
  • sourcemap 可以进行 sourcemap 的配置,在调试代码时会提供很大的帮助,这个选项会在生成文件中添加 sourcemap,来让事情变得更加简单。

我们在package.json代码下配置运行命令,方便操作。

1
2
3
"scripts": {
"build": "rollup -c"
}

因此我们只要在命令行中 输入命令:npm run build 即可完成打包;

示例:

src/js/es_1.js 代码如下:

1
2
3
4
5
6
7
export function a(x) {
return 'Hello' + x;
}

export function b(y) {
return y+ 'rollup';
}

src/js/es_2.js代码如下:

1
2
3
4
5
function c(x,y) {
return x+y;
}

export default c;

src/main.js代码如下:

1
2
3
4
5
6
import { a } from './js/es_1.js';
import c from './js/es_2.js';


console.log(a('rollup'));
console.log(c(1,2));

最终会在项目的根目录下生成文件 bundle.js, 代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
(function () {
'use strict';

function a(x) {
return 'Hello' + x;
}

function c(x,y) {
return x+y;
}

console.log(a('rollup'));
console.log(c(1,2));

})();

我们可以观察到我们没有使用的方法打包时没有打包进最终代码内。这就是rollup的有点之一。

除了基本功能,我们根据需求的扩展,还有很多的功能需要组合与配置,也有很多插件或者开源依赖帮我们完成更多的需求。

例如以上代码都是ES6的模块化规范(esm),然而现今,很多工具类及模块依旧使用的其它模块化规范,主要以node使用的CommonJS(csj)为主。而rollup默认只支持esm规范,如果要正常引入csj规范,需要借助@rollup/plugin-node-resolve和 @rollup/plugin-commonjs插件。并且以上代码打包后,可能一些语法,只有支持ES6的现代浏览器支持,所以为了兼容旧浏览器,我们经常还用到了babel来让旧浏览器也能正常使用。

JavaScript API使用

Rollup 提供 JavaScript 接口那样可以通过 Node.js 来使用。你可以很少使用,而且很可能使用命令行接口,除非你想扩展 Rollup 本身,或者用于一些难懂的任务,例如用代码把文件束生成出来。

rollup.rollup

The rollup.rollup 函数返回一个 Promise,它解析了一个 bundle 对象,此对象带有不同的属性及方法,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const rollup = require('rollup');

// see below for details on the options
const inputOptions = {...};
const outputOptions = {...};

async function build() {
// create a bundle
const bundle = await rollup.rollup(inputOptions);

console.log(bundle.imports); // an array of external dependencies
console.log(bundle.exports); // an array of names exported by the entry point
console.log(bundle.modules); // an array of module objects

// generate code and a sourcemap
const { code, map } = await bundle.generate(outputOptions);

// or write the bundle to disk
await bundle.write(outputOptions);
}

build();
inputOptions

inputOptions 对象包含下列属性 (查看big list of options 以获得这些参数更详细的资料):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const inputOptions = {
// 核心参数
input, // 唯一必填参数
external,
plugins,

// 高级参数
onwarn,
cache,

// 危险参数
acorn,
context,
moduleContext,
legacy
};

复制

outputOptions

outputOptions 对象包括下列属性 (查看 big list of options 以获得这些参数更详细的资料):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const outputOptions = {
// 核心参数
file, // 若有bundle.write,必填
format, // 必填
name,
globals,

// 高级参数
paths,
banner,
footer,
intro,
outro,
sourcemap,
sourcemapFile,
interop,

// 危险区域
exports,
amd,
indent
strict
};

rollup.watch

我们在使用gulp或者webpack中,都经常需要针对文件修改进行监听,以便实时编译或者执行某些操作,rollup虽然偏向于打包sdk,但是也可以用于项目开发的,所以也可以借助插件做到文件监听。

老版本rollup需要导入rollup-watch插件来实现监听。0.46版本后默认提供了 rollup.watch 函数,当它检测到磁盘上单个模块已经改变,它会重新构建你的文件束。 当你通过命令行运行 Rollup,并带上 --watch 标记时,此函数会被内部使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const rollup = require('rollup');

const watchOptions = {...};
const watcher = rollup.watch(watchOptions);

watcher.on('event', event => {
// event.code 会是下面其中一个:
// START — 监听器正在启动(重启)
// BUNDLE_START — 构建单个文件束
// BUNDLE_END — 完成文件束构建
// END — 完成所有文件束构建
// ERROR — 构建时遇到错误
// FATAL — 遇到无可修复的错误
});

// 停止监听
watcher.close();
watchOptions

watchOptions 参数是一个你会从一个配置文件中导出的配置 (或一个配置数据)。

1
2
3
4
5
6
7
8
9
const watchOptions = {
...inputOptions,
output: [outputOptions],
watch: {
chokidar,
include,
exclude
}
};

查看以上文档知道更多 inputOptionsoutputOptions 的细节, 或查询 big list of optionschokidar, includeexclude 的资料。

扩展插件

CommonJS规范兼容

一般会用到下面两个插件。

@rollup/plugin-commonjs 插件用于将CommonJS模块转换为ES6。

@rollup/plugin-node-resolve 用于告诉 Rollup 如何查找外部模块。

首先安装依赖:

1
npm install @rollup/plugin-node-resolve @rollup/plugin-commonjs -D

配置(引入csj规范导出esm规范):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import resolve from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';

export default {
input: './src/main.js', //rollup先执行的入口文件。
output: {
file: 'bundle.js', // 要写入的文件,也可以用于生成的sourcemaps。
format: 'es', // rollup支持的多种输出格式(有amd,cjs, es, iife 和 umd)
name: 'rt', // 变量名,代表你的 `iife`/`umd` 包,同一页上的其他脚本可以访问它
sourceMap: true // 调试代码
}
plugins: [
resolve(),
commonjs()
]
};

示例:

src/js/csj.js 代码如下:

1
2
3
4
5
6
function add(data1, data2) {
return data1 + data2;
}


exports.default = add;

src/main.js代码如下:

1
2
3
4
5
const add = require('./js/csj');

const addP = add(1,2);

exports.default = addP;

最终会在项目的根目录下生成文件 bundle.js, 代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var main = {};

var csj = {};

function add$1(data1, data2) {
return data1 + data2;
}


csj.default = add$1;

const add = csj;

const addP = add(1,2);

main.default = addP;

export { main as default };

Babel转换(适配ES5)

Babel 是一个工具链,主要用于将采用 ECMAScript 2015+ 语法编写的代码转换为向后兼容的 JavaScript 语法,以便能够运行在当前和旧版本的浏览器或其他环境中。webpack,rollup等等都有对应的babel插件封装。具体Babel官方网站或者我的Babel学习笔记

我们首先需要安装一些依赖项如下命令(目前最新版本babel7):

1
npm install @rollup/plugin-babel @babel/core @babel/preset-env -D

然后配置rollup,注意当commonjs与babel同时使用时,commonjs必须在前面。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import resolve from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import { babel } from '@rollup/plugin-babel';

export default {
input: './src/main.js', //rollup先执行的入口文件。
output: {
file: 'bundle.js', // 要写入的文件,也可以用于生成的sourcemaps。
format: 'es', // rollup支持的多种输出格式(有amd,cjs, es, iife 和 umd)
name: 'rt', // 变量名,代表你的 `iife`/`umd` 包,同一页上的其他脚本可以访问它
sourceMap: true // 调试代码
},
plugins: [
resolve(),
commonjs(),
babel()
]
};


接下来需要在项目的根目录下创建 babel.config.json( v7.8.0+,v7.8.0以下版本babel.config.js)配置文件,并进行配置:

1
2
3
4
5
6
7
8
9
10
11
// v7.8.0+   babel.config.json
{
"presets": [
["@babel/preset-env", {
"targets": {
"browsers": ["> 1%", "last 2 versions", "not ie <= 8"]
}
}]
],
"plugins": []
}

为了避免转译第三方脚本,我们需要设置一个 exclude 的配置选项来忽略掉 node_modules 目录下的所有文件。安装完成后,我们重新运行命令;

1
2
3
4
5
6
7
8
9
10
11
// Rollup plugins
import { babel } from '@rollup/plugin-babel';

export default {
...
plugins: [
babel({
exclude: 'node_modules/**' // 排除node_module下的所有文件
})
]
}

示例:

src/js/babel.js 代码如下:

1
2
3
4
5
6
const add = (data1, data2)=> {
return data1 + data2;
}


exports.default = add;

src/main.js 代码如下:

1
2
3
4
5
const add = require('./js/babel');

const addP = add(1,2);

exports.default = addP;

然后打包后代码变成如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var main = {};

var babel = {};

var add$1 = function add(data1, data2) {
return data1 + data2;
};

babel.default = add$1;

var add = babel;
var addP = add(1, 2);

main.default = addP;

export { main as default };

我们对比下代码,可以看到箭头函数以及const解析成function与var了。

注意: Babel也提供了 babel-polyfill, 也可以让IE8之前的浏览器能够顺利执行。

Replace插件

我们通常有打包时全局替换目标字符串的需求,我们通常使用@rollup/plugin-replace插件就可以实现。可以用来替换全局模块的引入路径以及简化部分写法。

安装:

1
npm install @rollup/plugin-replace --save-dev
1
2
3
4
5
6
7
8
9
10
11
12
13
import replace from '@rollup/plugin-replace';

export default {
...
plugins: [
...
replace({
ENV: JSON.stringify(process.env.NODE_ENV || 'development'),
__buildDate__: () => JSON.stringify(new Date()),
__buildVersion: 15
})
]
};

我们node环境(例如rollup编译操作)中,可以根据执行的NODE变量定义不同的编译流程,但是我们需要编译的目标源码中不是node环境,拿不到我们的变量,如果我们需要根据不同的编译环境(比如生产和测试)执行一些不同的流程,那我们可以提前借助replace插件将环境提前写入到编译配置里,然后让源码根据定义的环境走不同的编译流程.详见下节。

替换环境变量与日志

因为只需要生产环境时进行代码压缩或者一些其它操作,开发环境部署不压缩等等。通常我们在node环境下使用process.env.NODE_ENV便可以获取到当前环境变量,但是我们可能需要在我们的打包代码中也使用这个环境变量,例如全局开关log信息等等。

NODE_ENV

NODE_ENV这个变量通常用来区分开发与生产环境,加载不同的配置。

node中有全局变量process表示当前node进程,process.env包含着关于系统环境的信息。但是process.env中并不存在NODE_ENV这个东西,NODE_ENV只是一个用户自定义的变量,当我们在服务启动时配置NODE_ENV,或在代码中给process.env.NODE_ENV赋值,js便能通过process.env.NODE_ENV获取信息。

  • 在类unix系统下,运行 NODE_ENV=production xxx 就可以将变量带入进去

  • 在winodw环境下,运行 SET NODE_ENV=production xxx,就可以将变量带入进去

  • 或者借助cross-env兼容两个系统环境,cross-env NODE_ENV=production xxx

定义环境变量

配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import resolve from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import { babel } from '@rollup/plugin-babel';
import replace from '@rollup/plugin-replace';

export default {
input: './src/main.js', //rollup先执行的入口文件。
output: {
file: 'bundle.js', // 要写入的文件,也可以用于生成的sourcemaps。
format: 'es', // rollup支持的多种输出格式(有amd,cjs, es, iife 和 umd)
name: 'rt', // 变量名,代表你的 `iife`/`umd` 包,同一页上的其他脚本可以访问它
sourceMap: true // 调试代码
},
plugins: [
resolve(),
commonjs(),
babel(),
replace({
ENV: JSON.stringify(process.env.NODE_ENV || 'development')
})
]
};

Debug日志

安装:

1
npm install --save debug

使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
const add = require('./js/babel');

const addP = add(1,2);

import debug from 'debug';

const log = debug('app:log');


debug.enable('*');
log('启动日志!');

exports.default = addP;

根据环境变量决定是否打开日志:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const add = require('./js/babel');

const addP = add(1,2);

import debug from 'debug';

const log = debug('app:log');

// 如果是正式环境的话,不输出日志信息
if (ENV !== 'production') {
// Enable the logger.
debug.enable('*');
log('Logging is enabled!');
} else {
debug.disable();
}

压缩与混淆

我们经常需要对我们最终编译好的代码就行压缩和混淆,我们经常使用uglifyjs或者terser,rollup也有对应的插件rollup-plugin-uglifyrollup-plugin-terser,使我们更方便的在rollup中集成使用。

使用uglifyjs

安装插件:

1
npm install --save-dev rollup-plugin-uglify

配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { uglify } from "rollup-plugin-uglify";

export default {
input: './src/main.js', //rollup先执行的入口文件。
output: {
file: 'bundle.js', // 要写入的文件,也可以用于生成的sourcemaps。
format: 'es', // rollup支持的多种输出格式(有amd,cjs, es, iife 和 umd)
name: 'rt', // 变量名,代表你的 `iife`/`umd` 包,同一页上的其他脚本可以访问它
sourceMap: true // 调试代码
},
plugins: [
...
uglify()// 根据环境变量决定是否压缩(process.env.NODE_ENV === 'production' && uglify())
]
};


编译结果:

1
2
var main={},babel={};const add$1=(a,d)=>a+d;babel.default=add$1;const add=babel,addP=add(1,2);main.default=addP;export{main as default};

使用terser(推荐)

安装插件:

1
npm install rollup-plugin-terser --save-dev

配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { terser } from "rollup-plugin-terser";

export default {
input: './src/main.js', //rollup先执行的入口文件。
output: {
file: 'bundle.js', // 要写入的文件,也可以用于生成的sourcemaps。
format: 'es', // rollup支持的多种输出格式(有amd,cjs, es, iife 和 umd)
name: 'rt', // 变量名,代表你的 `iife`/`umd` 包,同一页上的其他脚本可以访问它
sourcemap: true // 调试代码
},
plugins: [
...
terser() // 根据环境变量决定是否压缩(process.env.NODE_ENV === 'production' && terser())
]
};


编译结果:

1
var a={},t={};t.default=function(a,t){return a+t};var e=t(1,2);a.default=e;export{a as default};

为什么使用terser

1.uglify-js不支持ES6 +。而针对es6+的库uglify-es不再维护

2.terser是uglify-es的分支,主要保留了uglify-es和uglify-js@3的API以及CLI的兼容性。

参考资料

rollup中文网

rollup官网