Rollup 实践案例【1】:从入门到组件类库的实践过程

一、前言


在学习了解和使用webpack、vite构建工具后,我好像理解了什么叫:"事物本同源,万物本根生,区别于特点" 。出于好奇我打算在看看Rollup是不是也是如此。

还是老样子,本文将主要通过实践的形式,来了解Rollup的具体使用。


二、关于Rollup

设计目的 :高效的ESM打包器
官方Docs

Rollup 是一款 ES Modules打包器,从作用上来看,Rollup 与 Webpack 非常类似。不过相比于 Webpack,Rollup要小巧的多。与其他打包工具(如Webpack和Parcel)相比,Rollup更专注于构建库和工具,它的设计目标是产生更精简、高效的输出。

2.1.Rollup主要特点

通过优缺点的分析,选择更适应的场景

优点:

  1. Tree Shaking:Rollup可以通过静态分析代码来确定未使用的模块和代码段,然后将其从最终的输出中删除,这有助于减小文件大小并提升性能。
  2. 默认支持ES模块:Rollup对ES模块的支持非常好,可以直接处理ES模块导入和导出语法,并生成符合ES规范的代码。
  3. 增量构建: Rollup支持增量构建,在重新构建项目时,它只会重新编译发生更改的文件,而不需要重新编译整个项目,这可以提高开发者的构建速度。
  4. 插件系统:Rollup唯一的功能扩展方式,通过插件扩展其功能,例如处理CSS、压缩代码、转换框架特定的代码等。

缺点:

  1. 加载非ESM的第三方模块比较复杂,需要配置相关插件
  2. 模块最终被打倒一个函数中,无法实现HRM(热更新)
  3. 代码拆分功能,依赖AMD模式

综上优缺点:

  1. 如果用于应用程序开发 ,我们需要大量引用第三库(html、css等),同时又需要 HMR 这样的功能来提升我们的开发体验。此外,如果项目较大,需要进行文件分包以便按需加载,但rollup在分包方面的功能相对较弱。而这些需求对于rollup来说都是它不擅长的。

  2. 相反如果用于框架或类库开发,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中打包输入支持六种格式,分别是:escjsamdumdiifesystem 更多在线详情

恭喜!你已经使用 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 将哪些模块拆分成单独的块,主要用到以下几个属性:

  1. 用于设置输出文件的命名规则
  • output.entryFileNames:入口文件(即主要模块)的文件名规则
  • output.chunkFileNames:其他(拆分文件)异步加载的模块的文件名规则
  1. 自定义拆分模块
  • 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 实践的脚手架

参考附录

rollup Docs官方文档
rollup code
vite code

相关推荐
Lee川8 小时前
深度拆解:基于面向对象思维的“就地编辑”组件全模块解析
javascript·架构
勤劳打代码8 小时前
Flutter 架构日记 — 状态管理
flutter·架构·前端框架
子兮曰13 小时前
后端字段又改了?我撸了一个 BFF 数据适配器,从此再也不怕接口“屎山”!
前端·javascript·架构
卓卓不是桌桌16 小时前
如何优雅地处理 iframe 跨域通信?这是我的开源方案
javascript·架构
Qlly16 小时前
DDD 架构为什么适合 MCP Server 开发?
人工智能·后端·架构
优秀稳妥的JiaJi1 天前
基于腾讯地图实现电子围栏绘制与校验
前端·vue.js·前端框架
用户881586910911 天前
AI Agent 协作系统架构设计与实践
架构
鹏北海1 天前
Qiankun 微前端实战踩坑历程
前端·架构
货拉拉技术1 天前
货拉拉海豚平台-大模型推理加速工程化实践
人工智能·后端·架构
RoyLin2 天前
libkrun 深度解析:架构设计、模块实现与 Windows WHPX 后端
架构