Rollup打包
# 前言
项目中一直用的都是webpack
,前一段需要开发几个类库供其他平台使用,本来打算继续用webpack
的,但感觉webpack
用来开发js
库,不仅繁琐而且打包后的文件体积也比较大。正好之前看vue
源码,知道vue
也是通过rollup
打包的。这次又是开发类库的,于是就快速上手了rollup
。
本篇文章是我有了一定的项目实践后,回过来给大家分享一下如何从零快速上手rollup
。
# 什么是rollup
?
系统的了解rollup
之前,我们先来简单了解下What is rollup?
关于rollup
的介绍,官方文档已经写的很清楚了:
Rollup 是一个 JavaScript 模块打包器,可以将小块代码编译成大块复杂的代码,例如 library 或应用程序。
与Webpack
偏向于应用打包的定位不同,rollup.js
更专注于Javascript
类库打包。
我们熟知的Vue
、React
等诸多知名框架或类库都是通过rollup.js
进行打包的。
# 为什么是rollup
?
webpack
我相信做前端的同学大家都用过,那么为什么有些场景还要使用rollup
呢?这里我简单对webpack
和rollup
做一个比较:
总体来说webpack
和rollup
在不同场景下,都能发挥自身优势作用。webpack
对于代码分割和静态资源导入有着“先天优势”,并且支持热模块替换(HMR
),而rollup
并不支持。
所以当开发应用时可以优先选择webpack
,但是rollup
对于代码的Tree-shaking
和ES6
模块有着算法优势上的支持,若你项目只需要打包出一个简单的bundle
包,并是基于ES6
模块开发的,可以考虑使用rollup
。
其实webpack
从2.0
开始就已经支持Tree-shaking
,并在使用babel-loader
的情况下还可以支持es6 module
的打包。实际上,rollup
已经在渐渐地失去了当初的优势了。但是它并没有被抛弃,反而因其简单的API
、使用方式被许多库开发者青睐,如React
、Vue
等,都是使用rollup
作为构建工具的。
# 快速上手
我们先花大概十分钟左右的时间来了解下rollup
的基本使用以及完成一个hello world
。
# 安装
首先全局安装rollup
:
npm i rollup -g
# 目录准备(hello world)
接着,我们初始化一个如下所示的项目目录
├── dist # 编译结果
├── example # HTML引用例子
│ └── index.html
├── package.json
└── src # 源码
└── index.js
首先我们在src/index.js
中写入如下代码:
console.log("柯森");
然后在命令行执行以下命令:
rollup src/index.js -f umd -o dist/bundle.js
执行命令,我们即可在dist
目录下生成bundle.js
文件:
(function(factory) {
typeof define === "function" && define.amd ? define(factory) : factory();
})(function() {
"use strict";
console.log("柯森");
});
这时,我们再在example/index.html
中引入上面打包生成的bundle.js
文件,打开浏览器:
如我们所预料的,控制台输出了柯森
。
到这里,我们就用rollup
打包了一个最最简单的demo
。
可能很多同学看到这里对于上面命令行中的参数不是很明白,我依次说明下:
-f
。-f
参数是--format
的缩写,它表示生成代码的格式,amd
表示采用AMD
标准,cjs
为CommonJS
标准,esm
(或 es)为ES
模块标准。-f
的值可以为amd
、cjs
、system
、esm
('es’也可以)、iife
或umd
中的任何一个。-o
。-o
指定了输出的路径,这里我们将打包后的文件输出到dist
目录下的bundle.js
其实除了这两个,还有很多其他常用的命令(这里我暂且列举剩下两个也比较常用的,完整的rollup 命令行参数 (opens new window)):
-c
。指定rollup
的配置文件。-w
。监听源文件是否有改动,如果有改动,重新打包。
# 使用配置文件(rollup.config.js
)
使用命令行的方式,如果选项少没什么问题,但是如果添加更多的选项,这种命令行的方式就显得麻烦了。
为此,我们可以创建配置文件来囊括所需的选项
在项目中创建一个名为rollup.config.js
的文件,增加如下代码:
export default {
input: ["./src/index.js"],
output: {
file: "./dist/bundle.js",
format: "umd",
name: "experience",
},
};
然后命令行执行:
rollup - c;
打开dist/bundle.js
文件,我们会发现和上面采用命令行的方式打包出来的结果是一样的。
这里,我对配置文件的选项做下简单的说明:
input
表示入口文件的路径(老版本为 entry,已经废弃)output
表示输出文件的内容,它允许传入一个对象或一个数组,当为数组时,依次输出多个文件,它包含以下内容:output.file
:输出文件的路径(老版本为 dest,已经废弃)output.format
:输出文件的格式output.banner
:文件头部添加的内容output.footer
:文件末尾添加的内容
到这里,相信你已经差不多上手rollup
了。
# 进阶
但是,这对于真实的业务场景是远远不够的。
下面,我将介绍rollup
中的几种常用的插件以及external
属性、tree-shaking
机制。
# resolve
插件
# 为什么要使用resolve
插件
在上面的入门案例中,我们打包的对象是本地的js
代码和库,但实际开发中,不太可能所有的库都位于本地,我们大多会通过npm
下载远程的库。
与webpack
和browserify
这样的其他捆绑包不同,rollup
不知道如何打破常规去处理这些依赖。因此我们需要添加一些配置。
# resolve
插件使用
首先在我们的项目中添加一个依赖the-answer
,然后修改src/index.js
文件:
import answer from "the-answer";
export default function() {
console.log("the answer is " + answer);
}
执行npm run build
。
这里为了方便,我将原本的
rollup -c -w
添加到了package.json
的scripts
中:"build": "rollup -c -w"
会得到以下报错: 打包后的bundle.js
仍然会在Node.js
中工作,但是the-answer
不包含在包中。为了解决这个问题,将我们编写的源码与依赖的第三方库进行合并,rollup.js
为我们提供了resolve
插件。
首先,安装resolve
插件:
npm i -D @rollup/plugin-node-resolve
修改配置文件rollup.config.js
:
import resolve from "@rollup/plugin-node-resolve";
export default {
input: ["./src/index.js"],
output: {
file: "./dist/bundle.js",
format: "umd",
name: "experience",
},
plugins: [resolve()],
};
这时再次执行npm run build
,可以发现报错已经没有了:
打开dist/bundle.js
文件:
(function(global, factory) {
typeof exports === "object" && typeof module !== "undefined"
? (module.exports = factory())
: typeof define === "function" && define.amd
? define(factory)
: ((global =
typeof globalThis !== "undefined" ? globalThis : global || self),
(global.experience = factory()));
})(this, function() {
"use strict";
var index = 42;
function index$1() {
console.log("the answer is " + index);
}
return index$1;
});
打包文件bundle.js
中已经包含了引用的模块。
有些场景下,虽然我们使用了resolve
插件,但可能我们仍然想要某些库保持外部引用状态,这时我们就需要使用external
属性,来告诉rollup.js
哪些是外部的类库。
# external 属性
修改rollup.js
的配置文件:
import resolve from "@rollup/plugin-node-resolve";
export default {
input: ["./src/index.js"],
output: {
file: "./dist/bundle.js",
format: "umd",
name: "experience",
},
plugins: [resolve()],
external: ["the-answer"],
};
重新打包,打开dist/bundle.js
文件:
(function(global, factory) {
typeof exports === "object" && typeof module !== "undefined"
? (module.exports = factory(require("the-answer")))
: typeof define === "function" && define.amd
? define(["the-answer"], factory)
: ((global =
typeof globalThis !== "undefined" ? globalThis : global || self),
(global.experience = factory(global.answer)));
})(this, function(answer) {
"use strict";
function _interopDefaultLegacy(e) {
return e && typeof e === "object" && "default" in e ? e : { default: e };
}
var answer__default = /*#__PURE__*/ _interopDefaultLegacy(answer);
function index() {
console.log("the answer is " + answer__default["default"]);
}
return index;
});
这时我们看到the-answer
已经是做为外部库被引入了。
# commonjs
插件
# 为什么需要commonjs
插件
rollup.js
编译源码中的模块引用默认只支持 ES6+
的模块方式import/export
。然而大量的npm
模块是基于CommonJS
模块方式,这就导致了大量 npm
模块不能直接编译使用。
因此使得rollup.js
编译支持npm
模块和CommonJS
模块方式的插件就应运而生:@rollup/plugin-commonjs
。
# commonjs
插件使用
首先,安装该模块:
npm i -D @rollup/plugin-commonjs
然后修改rollup.config.js
文件:
import resolve from "@rollup/plugin-node-resolve";
import commonjs from "@rollup/plugin-commonjs";
export default {
input: ["./src/index.js"],
output: {
file: "./dist/bundle.js",
format: "umd",
name: "experience",
},
plugins: [resolve(), commonjs()],
external: ["the-answer"],
};
# babel
插件
# 为什么需要babel
插件?
我们在src
目录下添加es6.js
文件(⚠️ 这里我们使用了 es6 中的箭头函数):
const a = 1;
const b = 2;
console.log(a, b);
export default () => {
return a + b;
};
然后修改rollup.config.js
配置文件:
import resolve from "@rollup/plugin-node-resolve";
import commonjs from "@rollup/plugin-commonjs";
export default {
input: ["./src/es6.js"],
output: {
file: "./dist/esBundle.js",
format: "umd",
name: "experience",
},
plugins: [resolve(), commonjs()],
external: ["the-answer"],
};
执行打包,可以看到dist/esBundle.js
文件内容如下:
(function(global, factory) {
typeof exports === "object" && typeof module !== "undefined"
? (module.exports = factory())
: typeof define === "function" && define.amd
? define(factory)
: ((global =
typeof globalThis !== "undefined" ? globalThis : global || self),
(global.experience = factory()));
})(this, function() {
"use strict";
const a = 1;
const b = 2;
console.log(a, b);
var es6 = () => {
return a + b;
};
return es6;
});
可以看到箭头函数被保留下来,这样的代码在不支持ES6
的环境下将无法运行。我们期望在rollup.js
打包的过程中就能使用babel
完成代码转换,因此我们需要babel
插件。
# babel
插件的使用
首先,安装:
npm i -D @rollup/plugin-babel
同样修改配置文件rollup.config.js
:
import resolve from "@rollup/plugin-node-resolve";
import commonjs from "@rollup/plugin-commonjs";
import babel from "@rollup/plugin-babel";
export default {
input: ["./src/es6.js"],
output: {
file: "./dist/esBundle.js",
format: "umd",
name: "experience",
},
plugins: [resolve(), commonjs(), babel()],
external: ["the-answer"],
};
然后打包,发现会出现报错:
提示我们缺少@babel/core
,因为@babel/core
是babel
的核心。我们来进行安装:
npm i @babel/core
再次执行打包,发现这次没有报错了,但是我们尝试打开dist/esBundle.js
:
(function(global, factory) {
typeof exports === "object" && typeof module !== "undefined"
? (module.exports = factory())
: typeof define === "function" && define.amd
? define(factory)
: ((global =
typeof globalThis !== "undefined" ? globalThis : global || self),
(global.experience = factory()));
})(this, function() {
"use strict";
const a = 1;
const b = 2;
console.log(a, b);
var es6 = () => {
return a + b;
};
return es6;
});
可以发现箭头函数仍然存在,显然这是不正确的,说明我们的babel
插件没有起到作用。这是为什么呢?
原因是由于我们缺少.babelrc
文件,添加该文件:
{
"presets": [
[
"@babel/preset-env",
{
"modules": false,
// "useBuiltIns": "usage"
}
]
]
}
我们看.babelrc
配置了preset env
,所以先安装这个插件:
npm i @babel/preset-env
这次再次执行打包,我们打开dist/esBundle.js
文件:
(function(global, factory) {
typeof exports === "object" && typeof module !== "undefined"
? (module.exports = factory())
: typeof define === "function" && define.amd
? define(factory)
: ((global =
typeof globalThis !== "undefined" ? globalThis : global || self),
(global.experience = factory()));
})(this, function() {
"use strict";
var a = 1;
var b = 2;
console.log(a, b);
var es6 = function() {
return a + b;
};
return es6;
});
可以看到箭头函数被转换为了function
,说明babel
插件正常工作。
# json
插件
# 为什么要使用json
插件?
在src
目录下创建json.js
文件:
import json from "../package.json";
console.log(json.author);
内容很简单,就是引入package.json
,然后去打印author
字段。
修改rollup.config.js
配置文件:
import resolve from "@rollup/plugin-node-resolve";
import commonjs from "@rollup/plugin-commonjs";
import babel from "@rollup/plugin-babel";
export default {
input: ["./src/json.js"],
output: {
file: "./dist/jsonBundle.js",
format: "umd",
name: "experience",
},
plugins: [resolve(), commonjs(), babel()],
external: ["the-answer"],
};
执行打包,发现会发生如下报错: 提示我们缺少@rollup/plugin-json
插件来支持json
文件。
# json
插件的使用
来安装该插件:
npm i -D @rollup/plugin-json
同样修改下配置文件,将插件加入plugins
数组即可。
然后再次打包,发现打包成功了,我们打开生成的dist/jsonBundle
目录:
(function(factory) {
typeof define === "function" && define.amd ? define(factory) : factory();
})(function() {
"use strict";
var name = "rollup-experience";
var version = "1.0.0";
var description = "";
var main = "index.js";
var directories = {
example: "example",
};
var scripts = {
build: "rollup -c -w",
test: 'echo "Error: no test specified" && exit 1',
};
var author = "Cosen";
var license = "ISC";
var dependencies = {
"@babel/core": "^7.11.6",
"@babel/preset-env": "^7.11.5",
"the-answer": "^1.0.0",
};
var devDependencies = {
"@rollup/plugin-babel": "^5.2.0",
"@rollup/plugin-commonjs": "^15.0.0",
"@rollup/plugin-json": "^4.1.0",
"@rollup/plugin-node-resolve": "^9.0.0",
};
var json = {
name: name,
version: version,
description: description,
main: main,
directories: directories,
scripts: scripts,
author: author,
license: license,
dependencies: dependencies,
devDependencies: devDependencies,
};
console.log(json.author);
});
完美!!
# tree-shaking
机制
这里我们以最开始的src/index.js
为例进行说明:
import answer from "the-answer";
export default function() {
console.log("the answer is " + answer);
}
修改上述文件:
const a = 1;
const b = 2;
export default function() {
console.log(a + b);
}
执行打包。打开dist/bundle.js
文件:
(function(global, factory) {
typeof exports === "object" && typeof module !== "undefined"
? (module.exports = factory())
: typeof define === "function" && define.amd
? define(factory)
: ((global =
typeof globalThis !== "undefined" ? globalThis : global || self),
(global.experience = factory()));
})(this, function() {
"use strict";
var a = 1;
var b = 2;
function index() {
console.log(a + b);
}
return index;
});
再次修改src/index.js
文件:
const a = 1;
const b = 2;
export default function() {
console.log(a);
}
再次执行打包,打开打包文件:
(function(global, factory) {
typeof exports === "object" && typeof module !== "undefined"
? (module.exports = factory())
: typeof define === "function" && define.amd
? define(factory)
: ((global =
typeof globalThis !== "undefined" ? globalThis : global || self),
(global.experience = factory()));
})(this, function() {
"use strict";
var a = 1;
function index() {
console.log(a);
}
return index;
});
发现了什么?
我们发现关于变量b
的定义没有了,因为源码中并没有用到这个变量。这就是ES
模块著名的tree-shaking
机制,它动态地清除没有被使用过的代码,使得代码更加精简,从而可以使得我们的类库获得更快的加载速度。
# 总结
本文大致向大家介绍了什么是rollup
以及如何快速上手rollup
。文中提到的这些其实只是冰山一角,rollup
能玩的东西还有很多,关于更多可以去rollup 官网 (opens new window)查询