一、前言
在学习了解和使用webpack、vite构建工具后,我好像理解了什么叫:"事物本同源,万物本根生,区别于特点" 。出于好奇我打算在看看Rollup是不是也是如此。
还是老样子,本文将主要通过实践的形式,来了解Rollup的具体使用。
二、关于Rollup
设计目的 :高效的ESM打包器
官方 :Docs
Rollup
是一款 ES Modules
打包器,从作用上来看,Rollup 与 Webpack 非常类似。不过相比于 Webpack,Rollup要小巧的多。与其他打包工具(如Webpack和Parcel)相比,Rollup更专注于构建库和工具,它的设计目标是产生更精简、高效的输出。
2.1.Rollup主要特点
通过优缺点的分析,选择更适应的场景
优点:
- Tree Shaking:Rollup可以通过静态分析代码来确定未使用的模块和代码段,然后将其从最终的输出中删除,这有助于减小文件大小并提升性能。
- 默认支持ES模块:Rollup对ES模块的支持非常好,可以直接处理ES模块导入和导出语法,并生成符合ES规范的代码。
- 增量构建: Rollup支持增量构建,在重新构建项目时,它只会重新编译发生更改的文件,而不需要重新编译整个项目,这可以提高开发者的构建速度。
- 插件系统:Rollup唯一的功能扩展方式,通过插件扩展其功能,例如处理CSS、压缩代码、转换框架特定的代码等。
缺点:
- 加载非
ESM
的第三方模块比较复杂,需要配置相关插件 - 模块最终被打倒一个函数中,无法实现
HRM(热更新)
- 代码拆分功能,依赖
AMD
模式
综上优缺点:
-
如果用于应用程序开发 ,我们需要大量引用第三库(html、css等),同时又需要
HMR
这样的功能来提升我们的开发体验。此外,如果项目较大,需要进行文件分包以便按需加载,但rollup在分包方面的功能相对较弱。而这些需求对于rollup来说都是它不擅长的。 -
相反如果用于框架或类库开发,rollup的优点就体现出来了。框架或类库开发通常不需要引入大量的第三方库,因为它们本身就是供其他开发者使用的库。它可以将代码打包为更小、更精简的形式,减少库的体积,提高加载速度和用户体验。而且由于框架或类库的代码一般比较稳定,不需要频繁更新,因此对于HMR的需求也相对较低。
2.2.Rollup和其他工具对比
专注领域:
- Rollup:专注于构建库和工具,它的设计目标是产生精简、高效的输出,尤其擅长处理ES模块。Rollup更适合开发可复用的代码。
- Webpack: 通用的打包工具,可以处理各种类型的文件和依赖关系,适用于构建复杂的应用程序。
- Vite: 基于ES模块的新一代前端构建工具,旨在提供快速的开发体验,在开发环境下使用原生ES模块的引入方式,不需要打包。
- Parcel: 零配置的打包工具 "傻瓜式" 的使用体验,支持多种文件类型,并提供了自动化的资源解析和依赖管理。
三、实践案例
3.1.基础案例实践
该案例主要简单的,记录使用Rollup的基本过程。
3.1.1.项目初始化
📝新建文件夹
bash
mkdir my-rollup
cd my-rollup
📦初始化并安装依赖
bash
npm init
npm install rollup
📝在根目录新建src
文件夹,定义src/foo.js
方法导入,入口文件为main.js
javascript
// # src/foo.js
export default () => {
console.log("༼ つ ◕_◕ ༽つ Hi Rollup");
};
javascript
// # src/main.js
import foo from './foo.js';
export default function () {
foo()
console.log('main end...');
}
修改packages.json
入口文件与脚本与脚本配置
bash
# --format:定义文件格式 ,--dir:导出到文件夹
rollup --format cjs --dir dist
# or
rollup -f cjs -d dist
javascript
"main": "./src/main.js",
"scripts": {
"build": "rollup src/main.js --format cjs --dir dist"
},
♻️执行脚本测试:
生成打包文件dist/main.js
javascript
'use strict';
// # src/foo.js
var foo = () => {
console.log("༼ つ ◕_◕ ༽つ Hi Rollup");
};
// # src/main.js
function main () {
foo();
console.log("main end...");
}
module.exports = main;
恭喜!你已经使用 Rollup 完成了一次打包!✅
3.1.2.使用配置文件打包
上述过程中,直接在在脚本定义了文件输出格式,和最终输出的文件路径。
为了更完善更多内容的配置,接下来是通过配置文件来实现。
📝在根目录下新建打包配置文件 rollup.config.js
javascript
// rollup.config.js
export default {
input: 'src/main.js',
output: {
file: "dist/bundle.js",
format: 'cjs'
}
};
由于是默认支持ESM
配置文件需要用 .mjs
格式,如果使用.js
格式可以在配置文件添加"type": "module"
属性。
修改配置文件脚本:
javascript
// # package.json
"type": "module",
"scripts": {
"build": "rollup src/main.js --format cjs --dir dist",
"build:config": "rollup --config rollup.config.js"
},
为了方便对比,保留了build
脚本,新增一个区分的脚本build:config
♻️执行测试脚本:
bash
npm run build:config
查看打包输出文件dist/bundle.js
,发现打包格式跟脚本上配置的一样,为了看到不同格式的打包,你可以将配置文件中的format
切换为不同的模式。
当切换为es模式发现打包的内容,与写的内容几乎一致
javascript
// # src/foo.js
var foo = () => {
console.log("༼ つ ◕_◕ ༽つ Hi Rollup");
};
// # src/main.js
function main () {
foo();
console.log("main end...");
}
export { main as default };
在Rollup中打包输入支持六种格式,分别是:es
、cjs
、amd
、umd
、iife
、system
更多在线详情
恭喜!你已经使用 Rollup 完成了配置文件的打包!✅
3.1.3.配置插件
随着你需要打包更复杂的代码,通常需要更灵活的配置。以下是Rollup常见的几种插件:
@rollup/plugin-json :它允许从JSON 文件中直接导入数据
@rollup/plugin-node-resolve :将第三方模块,插入打包的文件中
@rollup/plugin-commonjs:兼容commonJS 语法模式
@rollup/plugin-terser :用于打包输出代码压缩
rollup-plugin-delete:每次打包时,删除之前的打包文件
3.1.3.1.JSON数据引入/@rollup/plugin-json
JSON文件格式在工程化中的使用是不可或缺的,如对package.json包配置信息的获取
📦安装依赖
npm install --save-dev @rollup/plugin-json
在 rollup.config.mjs 文件中引入JSON 插件
javascript
// # rollup.config.js
import json from "@rollup/plugin-json";
export default {
input: "src/main.js",
output: {
file: "dist/bundle.js",
format: "es",
},
plugins: [json()],
};
测试插件是否可用
📝更新 src/main.js 文件,直接引入 package.json文件的属性
javascript
import foo from "./esm";
import { version } from "../package.json";
export default function main() {
foo();
console.log("当前包的版本号是:", version);
console.log("main end...");
}
main();
📝修改配置文件:在package.json脚本中,添加dev
用于测试打包后JSON数据是否被使用
json
"scripts": {
"dev": "node dist/bundle.js",
"build": "rollup src/main.js --format cjs --dir dist",
"build:config": "rollup --config rollup.config.js"
},
♻️执行打包脚本:并运行打包结果
bash
npm run build:config
npm run dev
测试结果如下:
javascript
// # src/foo.js
var foo = () => {
console.log("༼ つ ◕_◕ ༽つ Hi Rollup");
};
// # src/main.js
var version = "1.0.0";
function main() {
foo();
console.log("当前包的版本号是:", version);
console.log("main end...");
console.log();
}
main();
export { main as default };
恭喜!你已经使用 Rollup 完成了@rollup/plugin-json
插件的配置的打包与使用!✅
3.1.3.2.模块引入/@rollup/plugin-node-resolve
该插件可以让Rollup在打包过程中解析和处理Node.js模块的导入(import)语句
由于不像webpack一样可以默认导入第三方插件的模块,需要安装 @rollup/plugin-node-resolve 插件来实现,通过Node解析算法定位模块,来获取node_modules
的第三方模块
1.未使用插件时的测试:
📦安装lodash库进测试
bash
npm i lodash-es
📝在src/main.js中引入lodash库的某个方法
javascript
import foo from "./esm";
import { version } from "../package.json";
import { compact } from "lodash-es";
export default function main() {
foo();
console.log("当前包的版本号是:", version);
console.log("lodash-es方法:", compact([0, 1, false, 2, "", 3]));
console.log("main end...");
}
main();
♻️执行打包脚本测试:结果如下
打包时会出现外部依赖提示: **(!) Unresolved dependencies ,**这是因为 bundle 文件中没有讲第三方库的模块放入其中。
javascript
import { compact } from 'lodash-es';
// # src/foo.js
var foo = () => {
console.log("༼ つ ◕_◕ ༽つ Hi Rollup");
};
var version = "1.0.0";
// # src/main.js
function main() {
foo();
console.log("当前包的版本号是:", version);
console.log("lodash-es方法:", compact([0, 1, false, 2, "", 3]));
console.log("main end...");
}
main();
export { main as default };
通过打包文件可以看出,引入的compact
并没有在打包时将完整的方法插入到 bundle.js
文件中。
为了解决这个问题,需要使用如下插件
2.使用@rollup/plugin-node-resolve 插件后的测试:
📦安装依赖
bash
npm install --save-dev @rollup/plugin-node-resolve
在rollup.config.js 中配置
javascript
// # rollup.config.js
import { nodeResolve } from '@rollup/plugin-node-resolve';
export default {
input: "src/main.js",
output: {
file: "dist/bundle.js",
format: "es",
},
plugins: [nodeResolve()]
};
♻️执行打包脚本:并运行查看结果
通过终端发现,提示已经没有,查看bundle文件如下:
javascript
// # src/foo.js
var foo = () => {
console.log("༼ つ ◕_◕ ༽つ Hi Rollup");
};
var version = "1.0.0";
/**
* Creates an array with all falsey values removed. The values `false`, `null`,
* `0`, `""`, `undefined`, and `NaN` are falsey.
*
* @static
* @memberOf _
* @since 0.1.0
* @category Array
* @param {Array} array The array to compact.
* @returns {Array} Returns the new array of filtered values.
* @example
*
* _.compact([0, 1, false, 2, '', 3]);
* // => [1, 2, 3]
*/
function compact(array) {
var index = -1,
length = array == null ? 0 : array.length,
resIndex = 0,
result = [];
while (++index < length) {
var value = array[index];
if (value) {
result[resIndex++] = value;
}
}
return result;
}
// # src/main.js
function main() {
foo();
console.log("当前包的版本号是:", version);
console.log("lodash-es方法:", compact([0, 1, false, 2, "", 3]));
console.log("main end...");
}
main();
export { main as default };
通过上述打包文件发现,compact 被直接插入bundle 中,说明 @rollup/plugin-node-resolve 插件已经生效。✅
3.1.3.3.模块兼容/@rollup/plugin-commonjs
该插件通过Ast方式,将原本的 CommonJS 模块转换为了 ES6 模块的语法
Rollup默认支持ESM模式,由于大多npm库使用了commonJS 模式,为了兼容我们需要通过 @rollup/plugin-commonjs 插件对这部分做兼容处理。
📦安装依赖
bash
npm i -D @rollup/plugin-commonjs
引用配置
javascript
// rollup.config.js
import commonjs from "@rollup/plugin-commonjs";
export default {
input: "src/main.js",
output: {
file: "dist/bundle.js",
format: "es",
},
plugins: [commonjs()],
};
这是为了防止其他插件对 CommonJS 检测产生影响,一般放在其他插件之前。如果有使用Bable解析的,可以将它放在 commonjs 插件之前。
测试配置是否成功!
📝新增一个src/common.js
并以commonJS
的格式导出一个方法
javascript
// # src/common.js
function greet(name) {
return `我是, ${name}!`;
}
module.exports = greet;
使用import
语法将该文件导入,入口文件
javascript
// # main.js
// 其他部分省略...
import greet from "./common";
export default function main() {
console.log("CommonJS方式引入:", greet("CommonJS"));
}
main();
♻️执行打包脚本:并运行查看结果
javascript
// # dist/bundle.js
// 其他部分省略...
function getDefaultExportFromCjs (x) {
return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x;
}
// # src/common.js
function greet(name) {
return `我是, ${name}!`;
}
var common = greet;
var greet$1 = /*@__PURE__*/getDefaultExportFromCjs(common);
通过上述打包结果,不难发现 commonJS 语法被转换成了ES的形式。
执行bundle.js 文件结果如下
恭喜!你已经使用 Rollup 完成了@rollup/plugin-commonjs
插件的配置的打包与使用!✅
3.1.3.4.输出代码压缩/@rollup/plugin-terser
该插件会将打包输出的js文件进行压缩,从而达到减小代码打包体积的目的。
📦安装
bash
npm install -D @rollup/plugin-commonjs
引用配置
javascript
// rollup.config.js
// 省略其他内容...
import terser from "@rollup/plugin-terser";
export default {
input: "src/main.js",
output: {
file: "dist/bundle.js",
format: "es",
},
plugins: [terser()],
};
♻️执行打包脚本测试:最终打包bundle.js文件压缩如下
如果你熟悉webapck,不难发现它与terser-webpack-plugin
的功能是差不多的。
恭喜!你已经使用 Rollup 完成了@rollup/plugin-terser
插件的配置的打包与使用!✅
3.1.3.5.删旧的打包文件/rollup-plugin-delete
在每次运行 Rollup 打包之前删除先前生成的打包内容
引入配置
javascript
// rollup.config.js
// 忽略其他。。。
import del from "rollup-plugin-delete";
export default {
input: "src/main.js",
output: {
file: "dist/bundle.js",
format: "es",
},
plugins: [
del({targets: "./dist"}),
],
};
另一种方式:
使用rimraf
库,结合脚本配置直接删除
安装rimraf库
bash
npm i -D rimraf
配置脚本
bash
"scripts": {
"build:config": "npm run rimraf dist && rollup --config rollup.config.js"
},
完成删除插件配置!✅
3.1.4.代码分割/code splitting
代码拆分通常也叫分包,它将JS应用程序拆分成多个较小的模块,每个模块只包含应用程序的一部分功能
在rollup 将哪些模块拆分成单独的块,主要用到以下几个属性:
- 用于设置输出文件的命名规则
output.entryFileNames
:入口文件(即主要模块)的文件名规则output.chunkFileNames
:其他(拆分文件)异步加载的模块的文件名规则
- 自定义拆分模块
output.manualChunks
:用于手动配置模块的代码分割
具体引入配置如下:
为了方便观看拆分后的打包代码,先将压缩插件删除terser
。
📝并使用manualChunks
属性,手动自定义需要拆分成独立模块的第三方库、或方法。
javascript
// rollup.config.js
import json from "@rollup/plugin-json";
import { nodeResolve } from "@rollup/plugin-node-resolve";
import commonjs from "@rollup/plugin-commonjs";
export default {
input: "src/main.js",
output: {
dir: "dist",
format: "es",
entryFileNames: "[name].[hash:6].js",
chunkFileNames: "chunks/chunk-[name]-[hash].js",
manualChunks: {
vendors: ["lodash-es"],
utils: ["src/utils/index.js"],
},
},
plugins: [commonjs(), json(), nodeResolve()],
};
拆分的代码模块命名chunkFileNames
通[hash]
规范过动态命名。拆分文件名将按命名规范导出。
manualChunks
中自定义的 utils、vendors 名称将注入到[name]
中。
✨注意:动态拆分成多个模块
Rollup本身不支持通配符 * ,所以需要动态拆分多个文件时,需要动态获取模块列表,你可结合工具库(例如glob)来帮助解析并返回模块列表
bash
npm install glob
javascript
// rollup.config.js
// ....
manualChunks: {
vendors: ["lodash-es"],
utils: getUtilsModules(),
},
// ....
function getUtilsModules() {
const files = glob.sync('src/utils/*.js');
return files.map(file => file.replace(/^src\//, ''));
}
如不需要动态拆分,可以用之前的拆分配置
📝新增src/utils/index.js 文件、并添加相关方法
javascript
// # utils/index.js
const add = (a, b) => {
return a + b;
};
const subtract = (a, b) => {
return a - b;
};
export { add, subtract };
将其导入,入口文件中使用
javascript
// # src/main.js
// 省略其他方法..
import { subtract, add } from "./utils";
export default function main() {
console.log("utils的add方法:", add(1, 1));
console.log("utils的subtract方法:", subtract(10, 5));
console.log("main end...");
console.log();
}
main();
♻️执行打包脚本测试:并运行查看效果
查看单独导出的 chunk-utils-xxx 文件结果如下:
javascript
// # utils/index.js
const add = (a, b) => {
return a + b;
};
const subtract = (a, b) => {
return a - b;
};
export { add as a, subtract as s };
通过上述代码发现,单独拆分的包相关方法与包的内容一致,这说明已经拆分成功!✅
3.1.5.多入口文件打包
在多包项目中,有可能需要一次对多个包进行打包。
rollup 实现多入口打包的方式是,通过input
属性实现,具体如下:
📝以对象的形式,配置不同的入口文件路径
javascript
// rollup.config.js
import json from "@rollup/plugin-json";
import { nodeResolve } from "@rollup/plugin-node-resolve";
import commonjs from "@rollup/plugin-commonjs";
export default {
input: {
main1: "src/main.js",
main2: "src/main.js",
},
output: {
dir: "dist",
format: "es",
entryFileNames: "[name].[hash:6].js",
},
plugins: [commonjs(), json(), nodeResolve()],
};
新增入口文件 src/main2.js
javascript
// # src/main2.js
import foo from "./esm";
export default function main() {
foo();
console.log("main end...");
console.log();
}
main();
♻️执行打包脚测试:
打包后,第二个入口文件使用的 foo 方法也被单独,分到chunk包中。具体如下:
javascript
import { f as foo } from './chunks/chunk-esm-9d2429b6.js';
// # src/main2.js
function main2() {
foo();
console.log("main2 end...");
console.log();
}
main2();
export { main2 as default };
更多的多入口包实现方式-->更多详细
到这里常见的使用方法以基本完成!✅
总结
结合webpack对比使用上的总体感受 :
-
webpack 通过入口文件将需要打包的内容,编译后统一输出到一个文件中(也称之为 bundle)。由于经过编译处理,其代码内容本质已发生改变,所以一般较难看懂打包后的产物。总体来说,webpack更像是一个干练的"揉面团"阿姨。
-
而相对于Rollup来说,打包后的产物去向以及生成格式,开发者需要自己定义,相对来说比较自由。相对自由的同时,我们需要考虑的壁垒也逐渐变多,就好比如你是一个伐木工,在处理一棵树时,我们需要考虑到不同位置的处理方式,树干怎么处理、树枝怎么处理,树叶应该怎么处理......
对于组件或类库来说:
组件库的开发,就像前文提到的伐木工处理树木一样,我们需要根据不同的位置和部分来处理不同的组件和功能。比如,树干可能需要进行加工和修整,树叶可能需要进行清理和整理。同样地,对于组件库的开发,我们需要考虑如何处理不同的组件和功能,以确保它们能够正常运行和被其他开发者使用。
相关案例
考虑一遍到底阅读起来比较疲惫,将组件库的实践案例独立出来。(未完! 以下案例待后续更新...)
Rollup案例实践【2】:基于Rollup + TS + React 实践的组件库 Rollup案例实践【3】:基于Rollup + TS + Node 实践的脚手架