10-Rollup 打包基本概念及使用
# 0.前言
本节课程代码的仓库 (opens new window):
13-vite-rollup
本篇博客为《深入浅出 Vite
》掘金手册第十章总结。
# 问题1:为什么深入学习 Vite 需要掌握 rollup?
在开发阶段,我们主要使用 ESbuild
对第三方依赖进行预构建,但是 ESbuild
存在种种缺陷,如不支持降级到 es5
代码,不支持新式语法,不具备 ts
类型系统机制,完全不提供处理 chunk
产物的接口等。因此在生产阶段,推荐使用功能更丰富且稳定的 rollup
作为打包工具。当然 esbuild
同样也可以以 minify
器的身份 "混入其中"。
除此以外,整个 Vite
架构体系中就是基于 rollup
搭建的,因此对 rollup
的插件机制进行完全性的兼容,编写 vite
插件某种程度上等同于编写 rollup
插件,两者主要区别在于 vite
实现了些特有 hooks
钩子函数,扩展了 rollup
在开发阶段的能力。
# 1.概念理解
# 问题2:什么是Tree Shaking, 为什么rollup可以具有天然的Tree Shaking功能?
参考仓库 (opens new window)案例,执行脚本指令:
npm run build:nobundle
。
tree-shaking
的定义计算机编译原理中
DCE
(Dead Code Elimination
,即消除无用代码) 技术。具体表现:
在
bundle
模式(当rollup
配置文件设定 单入口 )下,基于esm
模块机制的代码并不会被全量打入一个chunk
中。tree-shaking
的实现机制:esm
模块在当初设计时就支持静态分析,即依赖关系在运行前编译时就能确定下来。通过AST
语法树对没有用到的节点打标记,统一进行删除操作。
通过下述案例直观感受下 tree shaking
(树摇) 功能。
准备文件:
// src/index.js
import { add } from "./util";
console.log(add(1, 2));
// src/util.js
export const add = (a, b) => a + b;
export const multi = (a, b) => a * b;
2
3
4
5
6
7
采用如下 rollup.config.js
配置:
// rollup.config.js
// 以下注释是为了能使用 VSCode 的类型提示
/**
* @type { import('rollup').RollupOptions }
*/
const buildOptions = {
input: ["src/index.js"],
output: {
// 产物输出目录
dir: "dist/es",
// 产物格式
format: "esm",
},
};
export default buildOptions;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
执行指令:rollup -c
后,示例案例中执行:npm run build:nobundle
到产物输出目录查看产物内容:
// dist/es/index.js
// 代码已经打包到一起
const add = (a, b) => a + b;
console.log(add(1, 2));
2
3
4
5
# 问题3:可以哪儿两种方式来使用 rollup?
有两种方式:命令行(推荐)和 代码调用。
命令行:
rollup -c
其中-c
不可省略,默认读取rollup.config.js
配置文件,当然也可如下指定配置文件。"scripts": "build:nobundle": "npx rollup -c ./rollups/basic.rollup.js", "build:umd": "npx rollup -c ./rollups/umd.rollup.js", "build:lodash": "npx rollup -c ./rollups/lodash.rollup.js" }
1
2
3
4
5代码编程
"scripts": { "command:build": "npx tsx ./scripts/command/build.ts", "command:watch": "npx tsx ./scripts/command/watch.ts", }
1
2
3
4上面是使用
tsx
进行node
脚本的执行,本质和ts-node
没太大区别,基于esbuild
实现。具体完整脚本就不展开了,可以到代码仓库中看,大致就两步。
import rollup from "rollup"; let bundle; try { /*1. rollup.rollup 获取 bundle 对象*/ bundle = await rollup.rollup({})// rollup.config.js 中处 output 属性外的参数,如 input 或者 plugins:[] /* 2. 使用 bundle.generate 或者 write (2选1) 产出 */ const {result} = bundle.generate({}) await bundle.write({}) // 直接配置 output 配置文件 }catch(err){}
1
2
3
4
5
6
7
8
9详细的使用方式见后续问题。
# 2. Rollup 基本配置
# 问题4:rollup 如何进行多入口+多产物配置?
本案例中的实践代码为:
"scripts": { "build:nobundle": "npx rollup -c ./rollups/basic.rollup.js", "command:build": "npx tsx ./scripts/command/build.ts", },
1
2
3
4
多入口的配置非常简单,当 rollup.config.js
中 input
为一数组即可,当数组大小超出1时,则为多入口。
多出口的概念同上,但区别在于命令行和代码调用的方式差别还是非常大的。
对于 命令行 形式 多产物 的配置还是很容易的,直接指定 output
为数组格式即可。
- 举例:打包为
cjs
和esm
格式。
const typescript = require("rollup-plugin-typescript2");
const path = require("path");
const srcPath = path.join(__dirname, "..", "src");
/**
* @type { import('rollup').RollupOptions }
*/
const buildOptions = {
input: [
path.join(srcPath, "basic/index.ts"),
path.join(srcPath, "basic/util.ts"),
],
output: [
{
/* 产物输出文件 */
dir: "dist/basic/es",
/* 产物格式 */
format: "esm",
},
{
/* 产物输出文件 */
dir: "dist/basic/cjs",
/* 产物格式 */
format: "cjs",
},
],
plugins: [typescript()],
};
module.exports = buildOptions;
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.rollup({})
这种代码调用格式时,bundle.write({})
无法传入数组,必须指定为对象格式,因此多产物的输出只能靠手动循环实现,代码示例如下:
import * as rollup from "rollup";
import typescript from "rollup-plugin-typescript2";
import * as path from "path";
import { SRC_PATH, DIST_PATH } from "const";
// 常用 inputOptions 配置
const inputOptions = {
input: [
path.join(SRC_PATH, "./basic/index.ts"),
path.join(SRC_PATH, "./basic/util.ts"),
],
external: [],
plugins: [typescript()],
};
const outputOptionsList = [
// 常用 outputOptions 配置
{
dir: path.join(DIST_PATH, "basic-node", "es"),
entryFileNames: `[name].[hash].js`,
chunkFileNames: "chunk-[hash].js",
assetFileNames: "assets/[name]-[hash][extname]",
format: "esm",
sourcemap: true,
},
{
dir: path.join(DIST_PATH, "basic-node", "cjs"),
entryFileNames: `[name].[hash].js`,
chunkFileNames: "chunk-[hash].js",
assetFileNames: "assets/[name]-[hash][extname]",
format: "cjs",
sourcemap: true,
},
];
async function build() {
let bundle;
let buildFailed = false;
debugger;
try {
/* 1. 调用 rollup.rollup 生成 bundle 对象 */
bundle = await rollup.rollup(inputOptions);
for (const outputOptions of outputOptionsList) {
/* 2. bundle 暴露两个函数:generate 和 write */
/* 此两个函数使用上没有差别,只是前者不会输出到磁盘,后者会输出到磁盘 */
const { output } = await bundle.generate(outputOptions);
await bundle.write(outputOptions);
}
} catch (error) {
buildFailed = true;
console.error(error);
}
if (bundle) {
/* 调用 close 方法结束打包工作 */
await bundle.close();
}
process.exit(buildFailed ? 1 : 0);
}
build();
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
# 问题5:rollup 中如何打包 umd
格式?
umd
格式的打包,需要单独区分,因为此类格式要求单入口+单出口。与前者不同支出在于,需要将 output
中的 dir
修改为 file
属性。
PS:这种配置方案,查了半天都没有配置出来,最终还是问
chatgpt
才解决的。本节问题可使用仓库代码中
npm run build:umd
指令测试。
rollup.config.js
配置文件如下:
const typescript = require("rollup-plugin-typescript2");
const path = require("path");
const srcPath = path.join(__dirname, "..", "src");
/**
* @type { import('rollup').RollupOptions }
*/
const buildOptions = {
input: [path.join(srcPath, "/basic/index.ts")],
output: [
{
/* 产物输出文件 */
file: "dist/basic/umd/index.js",
/* 产物格式 */
format: "umd",
},
],
plugins: [typescript()],
};
module.exports = buildOptions;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 问题6:rollup 除了 input
、output
外还能配置哪儿些?
对于打包工具所具备能力都很类似,如 format
、sourcemap
等,当用到时再去 rollup
官网搜索就好了。
详细配置清单见下:
import typescript from "rollup-plugin-typescript2";
import * as path from "path";
import { fileURLToPath } from "url";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
/**
* @type { import('rollup').RollupOptions }
*/
const buildOptions = {
input: ["src/index.ts", "src/util.ts"],
output: {
// 产物输出目录
dir: path.resolve(__dirname, "../", "dist/output"),
// 以下三个配置项都可以使用这些占位符:
// 1. [name]: 去除文件后缀后的文件名
// 2. [hash]: 根据文件名和文件内容生成的 hash 值
// 3. [format]: 产物模块格式,如 es、cjs
// 4. [extname]: 产物后缀名(带`.`)
// 入口模块的输出文件名
entryFileNames: `[name].js`,
// 非入口模块(如动态 import)的输出文件名
chunkFileNames: "chunk-[hash].js",
// 静态资源文件输出文件名
assetFileNames: "assets/[name]-[hash][extname]",
// 产物输出格式,包括`amd`、`cjs`、`es`、`iife`、`umd`、`system`
format: "cjs",
// 是否生成 sourcemap 文件
sourcemap: true,
// 如果是打包出 iife/umd 格式,需要对外暴露出一个全局变量,通过 name 配置变量名
name: "MyBundle",
// 全局变量声明
globals: {
// 项目中可以直接用`$`代替`jquery`
jquery: "$",
},
},
plugins: [typescript()],
};
export default buildOptions;
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
# 问题7:rollup 如何开启 watch
模式?
响应的代码见仓库 (opens new window)
import * as rollup from "rollup";
import * as path from "path";
import typescript from "rollup-plugin-typescript2";
import { SRC_PATH } from "const";
const watcher = rollup.watch({
// 和 rollup 配置文件中的属性基本一致,只不过多了`watch`配置
input: [
path.join(SRC_PATH, "/basic/index.ts"),
path.join(SRC_PATH, "basic/util.ts"),
],
output: [
{
dir: "dist/watch/es",
format: "esm",
},
{
dir: "dist/watch/cjs",
format: "cjs",
},
],
watch: {
exclude: ["node_modules/**"],
include: ["src/**"],
},
plugins: [typescript()],
});
// 监听 watch 各种事件
watcher.on("restart", () => {
console.log("重新构建...");
});
watcher.on("change", (id) => {
console.log("发生变动的模块id: ", id);
});
watcher.on("event", (e) => {
if (e.code === "BUNDLE_END") {
console.log("打包信息:", e);
}
if (e.code === "ERROR") {
console.log("ERROR:", e.error);
}
});
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
# 3.Rollup
插件相关
# 问题8:rollup 如何接入 plugin
插件?
配置 plugin
插件时首先要区分插件的运行时机是在 开发阶段 还是生产阶段?
如果分不清的话,推荐直接将所有插件配置到最外层的 plugins
,当明显为生产插件时,可配置到 output
中。
// rollup.config.js
import { terser } from 'rollup-plugin-terser'
import resolve from "@rollup/plugin-node-resolve";
import commonjs from "@rollup/plugin-commonjs";
export default {
output: {
// 加入 terser 插件,用来压缩代码
plugins: [terser()]
},
plugins: [resolve(), commonjs()]
}
2
3
4
5
6
7
8
9
10
11
12
# 问题9:如何打包 loadsh
这类 cjs
格式包,能说说在开发实践中都使用过哪些常用的Rollup库吗?
对于 rollup
来说,虽然提供支持多种输出格式的产物,但是对于输入代码仅仅支持 esm
格式。
ps:这里的意思是指,当入口文件为
cjs
时,rollup
不会对这个文件有任何处理,也不会处理第三方依赖。
这就要求我们统一使用 ESM
规范,并且要求导入的第三方依赖均为 esm
模块。
对于第一个要求还可以控制,但是第二条就很难满足,因为有的第三方依赖仅提供 cjs
,这类典型的包有 react
以及 lodash
。
为了解决这个问题,需要引入两个核心的插件包:
pnpm i @rollup/plugin-node-resolve @rollup/plugin-commonjs
其中:
@rollup/plugin-node-resolve
是为了允许我们加载第三方依赖。@rollup/plugin-commonjs
的作用是将CommonJS
格式的代码转换为ESM
格式。
rollup
配置如下:
const resolve = require("@rollup/plugin-node-resolve");
const commonjs = require("@rollup/plugin-commonjs");
const rollupOptions = {
plugins: [resolve(),commonjs()]
}
2
3
4
5
详细的执行结果:可执行 npm run build:lodash
查看构建结果。
除了上述的插件外,还推荐使用如下插件,后续章节会对其中几个插件进行源码分析:
@rollup/plugin-node-resolve
:允许解析第三方依赖。@rollup/plugin-commonjs
:将cjs
第三方依赖转化为esm
依赖。@rollup/plugin-json
:支持.json
的加载,甚至可配置rollup
的tree-shaking
机制进行按需打包。@rollup/plugin-babel
:接入Babel
rollup-plugin-terser
:接入terser
进行代码压缩,不过在vite
中默认采用esbuild
完成这部分工作。@rollup/plugin-typescript
:官方的ts
插件,但更推荐使用"rollup-plugin-typescript2"
插件,官方插件的增强版。@rolllup/plugin-alias
:支持别名配置。@rollup/plugin-replace
:全局字符串替换,内部核心使用margic string
工具包实现。rollup-plugin-visualizer
:构建产物分析,体积可视化分析图。
完整的官方插件清单:https://github.com/rollup/plugins/tree/master/packages
每个 packages
源码都在 100~200
行,非常值得借鉴学习。