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:接入Babelrollup-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 行,非常值得借鉴学习。