Rollup是一款基于ESModule模块标准实现的JavaScript打包工具,在前端社区中赫赫有名,同时也在Vite的架构体系中施展着重要作用。不仅是Vite生产环境下的打包工具,其插件机制也被Vite所兼容,能够说是Vite的构建基石。

接下来,咱们将围绕Rollup的基本概念和外围个性开展,学习完本大节内容,你不仅能晓得Rollup是如何打包我的项目的,还能学会Rollup更高阶的应用形式,甚至可能通过JavaScriptAPI二次开发Rollup。

一、疾速上手

首先,创立一个空的文件夹,而后应用npm init -y新建一个我的项目,此时,关上我的项目会发现多了一个package.json文件。

{  "name": "demo",  "version": "1.0.0",  "description": "",  "main": "index.js",  "scripts": {    "test": "echo "Error: no test specified" && exit 1"  },  "keywords": [],  "author": "",  "license": "ISC"}

接着,持续装置 rollup 依赖,命令如下。

npm i rollup

新增 src/index.js 和 src/util.js 和rollup.config.js 三个文件,目录构造如下所示。

.├── package.json├── pnpm-lock.yaml├── rollup.config.js└── src    ├── index.js    └── util.js

其中,index.js 和 util.js 和rollup.config.js 文件的内容别离如下。

// src/index.jsimport { add } from "./util";console.log(add(1, 2));// src/util.jsexport const add = (a, b) => a + b;export const multi = (a, b) => a * b;// rollup.config.js// 以下正文是为了能应用 VSCode 的类型提醒/** * @type { import('rollup').RollupOptions } */const buildOptions = {  input: ["src/index.js"],  output: {    // 产物输入目录    dir: "dist/es",    // 产物格局    format: "esm",  },};export default buildOptions;

而后,在package.json中退出如下的构建脚本。

{  // rollup 打包命令,`-c` 示意应用配置文件中的配置  "build": "rollup -c"}

接着,在终端执行一下npm run build,能够看到如下的命令行信息。

接下来,咱们关上dist/es 目录查看一下产物的内容。

const add = (a, b) => a + b;console.log(add(1, 2));

能够发现,util.js中的multi办法并没有被打包到产物中,这是因为 Rollup 具备人造的 Tree Shaking 性能,能够剖析出未应用到的模块并主动擦除。

所谓 Tree Shaking(摇树),也是计算机编译原理中DCE(Dead Code Elimination,即打消无用代码) 技术的一种实现。因为 ES 模块依赖关系是确定的,和运行时状态无关。因而 Rollup 能够在编译阶段剖析出依赖关系,对 AST 语法树中没有应用到的节点进行删除,从而实现 Tree Shaking。
 

二、罕用配置解读

2.1 多产物配置

在打包 JavaScript 类库的场景中,咱们通常须要对外暴露出不同格局的产物供别人应用,不仅包含 ESM,也须要包含诸如CommonJS、UMD等格局,保障良好的兼容性。那么,同一份入口文件,如何让 Rollup 给咱们打包出不一样格局的产物呢?为了实现这一需要,咱们基于上述的配置文件来进行如下批改。

// rollup.config.js/** * @type { import('rollup').RollupOptions } */const buildOptions = {  input: ["src/index.js"],  // 将 output 革新成一个数组  output: [    {      dir: "dist/es",      format: "esm",    },    {      dir: "dist/cjs",      format: "cjs",    },  ],};export default buildOptions;

从代码中能够看到,咱们将output属性配置成一个数组,数组中每个元素都是一个形容对象,决定了不同产物的输入行为。

 

2.2 多入口配置

除了多产物配置,Rollup 中也反对多入口配置,而且通常状况下两者会被联合起来应用。接下来,就让咱们持续革新之前的配置文件,将 input 设置为一个数组或者一个对象,如下所示。

{  input: ["src/index.js", "src/util.js"]}// 或者{  input: {    index: "src/index.js",    util: "src/util.js",  },}

而后,再次执行npm run build。能够发现,所有入口的不同格局产物曾经胜利输入。

如果不同入口对应的打包配置不一样,咱们也能够应用默认的配置来导出一个配置数组,如下所示。

// rollup.config.js/** * @type { import('rollup').RollupOptions } */const buildIndexOptions = {  input: ["src/index.js"],  output: [    // 省略 output 配置  ],};/** * @type { import('rollup').RollupOptions } */const buildUtilOptions = {  input: ["src/util.js"],  output: [    // 省略 output 配置  ],};export default [buildIndexOptions, buildUtilOptions];

如果是比较复杂的打包场景(如 Vite 源码自身的打包),咱们须要将我的项目的代码分成几个局部,用不同的 Rollup 配置别离打包。
 

2.3 自定义output配置

后面咱们提到了input的应用,次要用来申明入口,能够配置成字符串、数组或者对象,应用比较简单。而output与之绝对,用来配置输入的相干信息,罕用的配置项如下。

output: {  // 产物输入目录  dir: path.resolve(__dirname, 'dist'),  // 以下三个配置项都能够应用这些占位符:  // 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: '$'  }}

2.4 依赖 external

对于某些第三方包,有时候咱们不想让 Rollup 进行打包,也能够通过 external 进行内部化,配置如下。

{  external: ['react', 'react-dom']}

在 SSR 构建或者应用 ESM CDN 的场景中,这个配置将十分有用

2.5 接入插件

在Rollup的日常应用中,咱们难免会遇到一些Rollup自身不反对的场景,比方兼容CommonJS打包、注入环境变量、配置门路别名、压缩产物代码等等。这个时候就须要咱们引入相应的Rollup插件了。接下来以一个具体的场景为例带大家相熟一下Rollup插件的应用。

尽管Rollup可能打包输入出CommonJS格局的产物,但对于输出给Rollup的代码并不反对CommonJS,仅仅反对ESM。你可能会说,那咱们间接在我的项目中对立应用ESM标准就能够了啊,这有什么问题呢?须要留神的是,咱们不光要思考我的项目自身的代码,还要思考第三方依赖。目前为止,还是有不少第三方依赖只有CommonJS格局产物而并未提供ESM产物,比方我的项目中用到lodash时,打包我的项目会呈现这样的报错:

所以,咱们须要引入额定的插件去解决这个问题,咱们须要装置两个外围的插件包。

pnpm i @rollup/plugin-node-resolve @rollup/plugin-commonjs 

对于这两个插件包的阐明如下:

  • @rollup/plugin-node-resolve是为了容许咱们加载第三方依赖,否则像import React from 'react' 的依赖导入语句将不会被 Rollup 辨认。
  • @rollup/plugin-commonjs 的作用是将 CommonJS 格局的代码转换为 ESM 格局
     

而后,咱们在配置文件中导入这些插件,相干的配置如下:

// rollup.config.jsimport resolve from "@rollup/plugin-node-resolve";import commonjs from "@rollup/plugin-commonjs";/** * @type { import('rollup').RollupOptions } */export default {  input: ["src/index.js"],  output: [    {      dir: "dist/es",      format: "esm",    },    {      dir: "dist/cjs",      format: "cjs",    },  ],  // 通过 plugins 参数增加插件  plugins: [resolve(), commonjs()],};

当初,咱们以lodash这个只有 CommonJS 产物的第三方包为例测试一下。

npm i lodash

而后,在src/index.js 退出如下的代码。

import { merge } from "lodash";console.log(merge);

而后,执行 npm run build命令,就能够发现产物曾经失常生成了,如下图所示。

在Rollup配置文件中,plugins除了能够与output配置在同一级,也能够配置在output参数外面。

// rollup.config.jsimport { 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()]}

须要留神的是,output.plugins中配置的插件是有肯定限度的,只有应用Output 阶段相干钩子的插件才可能放到这个配置中,大家能够去这个站点查看 Rollup 的 Output 插件列表。这里也给大家分享其它一些比拟罕用的 Rollup 插件库:

  • @rollup/plugin-json: 反对.json的加载,并配合rollup的Tree Shaking机制去掉未应用的局部,进行按需打包。
  • @rollup/plugin-babel:在 Rollup 中应用 Babel 进行 JS 代码的语法转译。
  • @rollup/plugin-typescript: 反对应用 TypeScript 开发。
  • @rollup/plugin-alias:反对别名配置。
  • @rollup/plugin-replace:在 Rollup 进行变量字符串的替换。
  • rollup-plugin-visualizer: 对 Rollup 打包产物进行剖析,主动生成产物体积可视化剖析图。

三、JavaScript API

咱们通过Rollup的配置文件联合rollup -c实现了 Rollup 的打包过程,但有些场景下咱们须要基于 Rollup 定制一些打包过程,配置文件就不够灵便了,这时候咱们须要用到对应 JavaScript API 来调用 Rollup,次要分为rollup.rollup和rollup.watch两个 API,接下来咱们以具体的例子来学习一下。

首先,是 rollup.rollup,用来一次性地进行 Rollup 打包,能够新建一个build.js文件,内容如下。

// build.jsconst rollup = require("rollup");// 罕用 inputOptions 配置const inputOptions = {  input: "./src/index.js",  external: [],  plugins:[]};const outputOptionsList = [  // 罕用 outputOptions 配置  {    dir: 'dist/es',    entryFileNames: `[name].[hash].js`,    chunkFileNames: 'chunk-[hash].js',    assetFileNames: 'assets/[name]-[hash][extname]',    format: 'es',    sourcemap: true,    globals: {      lodash: '_'    }  }  // 省略其它的输入配置];async function build() {  let bundle;  let buildFailed = false;  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) {    // 最初调用 bundle.close 办法完结打包    await bundle.close();  }  process.exit(buildFailed ? 1 : 0);}build();

让咱们来解释一下下面的代码:

  • 通过 rollup.rollup办法,传入 inputOptions,生成 bundle 对象;
  • 调用 bundle 对象的 generate 和 write 办法,传入outputOptions,别离实现产物和生成和磁盘写入。
  • 调用 bundle 对象的 close 办法来完结打包。

接着,执行node build.js命令。这样,咱们就能够实现了以编程的形式来调用 Rollup 打包的过程。除了通过rollup.rollup实现一次性打包,咱们也能够通过rollup.watch来实现watch模式下的打包,即每次源文件变动后主动进行从新打包。你能够新建watch.js文件,配置如下。

// watch.jsconst rollup = require("rollup");const watcher = rollup.watch({  // 和 rollup 配置文件中的属性基本一致,只不过多了`watch`配置  input: "./src/index.js",  output: [    {      dir: "dist/es",      format: "esm",    },    {      dir: "dist/cjs",      format: "cjs",    },  ],  watch: {    exclude: ["node_modules/**"],    include: ["src/**"],  },});// 监听 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);  }});

当初,咱们能够通过执行node watch.js开启 Rollup 的 watch 打包模式,当你改变一个文件后能够看到如下的日志,阐明 Rollup 主动进行了从新打包,并触发相应的事件回调函数。

产生生变动的模块id: /xxx/src/index.js从新构建...打包信息: {  code: 'BUNDLE_END',  duration: 10,  input: './src/index.js',  output: [    // 输入产物门路  ],  result: {    cache: { /* 产物具体信息 */ },    close: [AsyncFunction: close],    closed: false,    generate: [AsyncFunction: generate],    watchFiles: [      // 监听文件列表    ],    write: [AsyncFunction: write]  }}

基于如上的两个 JavaScript API 咱们能够很不便地在代码中调用 Rollup 的打包流程,相比于配置文件有了更多的操作空间,你能够在代码中通过这些 API 对 Rollup 打包过程进行定制,甚至是二次开发。