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

相关推荐
PleaSure乐事1 小时前
【React.js】AntDesignPro左侧菜单栏栏目名称不显示的解决方案
前端·javascript·react.js·前端框架·webstorm·antdesignpro
getaxiosluo1 小时前
react jsx基本语法,脚手架,父子传参,refs等详解
前端·vue.js·react.js·前端框架·hook·jsx
理想不理想v1 小时前
vue种ref跟reactive的区别?
前端·javascript·vue.js·webpack·前端框架·node.js·ecmascript
58沈剑5 小时前
80后聊架构:架构设计中两个重要指标,延时与吞吐量(Latency vs Throughput) | 架构师之路...
架构
想进大厂的小王7 小时前
项目架构介绍以及Spring cloud、redis、mq 等组件的基本认识
redis·分布式·后端·spring cloud·微服务·架构
阿伟*rui8 小时前
认识微服务,微服务的拆分,服务治理(nacos注册中心,远程调用)
微服务·架构·firefox
ZHOU西口9 小时前
微服务实战系列之玩转Docker(十八)
分布式·docker·云原生·架构·数据安全·etcd·rbac
September_ning9 小时前
React.lazy() 懒加载
前端·react.js·前端框架
晴天飛 雪10 小时前
React 守卫路由
前端框架·reactjs
web行路人10 小时前
React中类组件和函数组件的理解和区别
前端·javascript·react.js·前端框架