rollup 快速入门与上手

前言

之前一直用 webpack 来打包前端项目,虽说配置复杂,但用多了也沉淀出了自己的配置,能够应对多数的打包和构建场景。然而最近在折腾一些自己的小项目时,需要打包一些类库发布到 npm 。虽说 webpack 也可以构建 library ,但是打包出的产物体积过大,而且代码也不是那么的“干净”,并且当想通过 webpack 一次打包出不同的版本,比如 esm 、 CommonJS 、 umd 等,webpack 就显得更加难用,于是就把目光锁定到了 rollup 。

什么是 rollup

rollup 和 webpack 一样,都是 JavaScript 模块打包器,用于打包和构建 JavaScript 应用程序和 library 。而 rollup 则更适合打包 library 且自身更为小巧和简单,所以有些开发应用时需要的功能,rollup 反而不支持,比如模块热更新(HMR)。我们熟知的 Vue 、 React 都是通过 rollup 打包。并且 rollup 进入大多是开发者的视野也要得益于 React 。2017年4月初,Facebook 将一个巨大的 pull 请求 合并到了 React 主分支中,将其现有的构建流程替换为基于 rollup ,这一举动让 rollup 得到了更多开发者的关注。

快速上手

首先创建项目,我们将会实现一个计算器类库,用于解决 js 在计算加减乘除时产生的精度丢失问题。项目目录如下:

仓库地址: https://github.com/onlymisaky/calculator

calculator
├── src
│ ├── index.js
| └── utils.js
└── package.json

index.js 文件中包含 加减乘除 四个方法,并通过匿名导出向外部暴露这些方法:

export default {
addition, subtraction, multiplication, division
}

utils.js 中则是一些辅助方法,无需多言。

由于只是一个简单的计算功能,并不设计和平台有关的功能或 API ,所以我们要实现的这个 library 可以在任意的 js 环境下使用,比如浏览器、 node 、 Electron 等等。所以我们也需要构建出不同版本的包。

接下来就开始正式进入 rollup 的正式使用。首先就是安装 rollup :

npm i rollup -D

和 webpack 一样, rollup 可以通过命令的方式直接使用(需要将 rollup 全局安装):

rollup src/index.js -f umd -o dist/index.js

上面的命令表示将 src/index.js 以 umd 形式打包,输出到 dist/index.js 。

很显然这种方式及其不灵活,所以只提一下不做详细的参数介绍。

最常用的方法还是使用配置文件的方式,不必担心, rollup 的配置文件比 webpack 简单多了,甚至比 gulp 的还简单。

核心概念

在介绍如何编写 rollup 的配置文件前,我么需要先了解几个它的核心概念,这有助于我们更好使用。

  • input : 入口文件,对标 webpack 的 entry ,指明了库文件入口位置
  • output : 输出文件,对标 webpack 的 output ,指明了打包后输出文件的位置、包名、格式等等
  • plugins : 插件,在构建过程中,需要一些辅助功能,都通过插件实现,比如语法转换、别名解析
  • external : 当我们的库是基于另一个库开发时,就需要用到它,比如开发一个基于 Vue 的指令,为了不将 Vue 打包到我们的库中,就需要将 Vue 写在 external 中。

以上就是 rollup 的一些核心概念,相较于 webpack 确实简化了许多,在了解核心概念后,编写 rollup 配置文件就简单多了。

编写和使用配置文件

在项目根目录创建 rollup.config.js 文件,代码内容如下:

import pkg from './package.json';

const banner = `
/**
* @license
* author: ${pkg.author}
* ${pkg.name} v${pkg.version}
* (c) 2022-${new Date().getFullYear()}
* Released under the ${pkg.license} license.
*/
`;

/** @type {import('rollup').RollupOptions} */
const rollupConfig = {
input: 'src/index.js',
output: [
{
file: pkg.main,
format: 'umd',
name: 'Calculator',
exports: 'default',
banner,
},
],
};

export default rollupConfig;

上面的配置代码中,只涉及到了 input 和 output 两个概念,需要额外解释的是 output 这个选项。

output 允许传入一个对象会数组,当传入数组时,会依次输出多个文件。

  • output.file : 表示输出的文件路径
  • output.format : 表示输出文件的格式,可选项有 umd 、 commonjs 、 esm 等。
  • output.name : 当 format 值为 umd 时,需要设置 output.name ,在浏览器环境下就可通过 name 使用。
  • output.name : 导出方式,可选值有 default 、 named 、 none 、 auto ,我们是匿名导出,就填写 default 。
  • output.banner : 文件头部添加的内容,当然也有对应的 output.footer 选项用于文件末尾添加的内容 。

接下来使用 rollup -c 命令便可以打包,就可以在 dist 文件夹输出 index.js 文件。为了方便,将 rollup -c 加入 npm scripts 中,运行 npm run build,得到的输出文件内容大致如下:


/** banner */

(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
typeof define === 'function' && define.amd ? define(factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.Calculator = factory());
})(this, (function () { 'use strict';

/** some code */

var index = {
addition, subtraction, multiplication, division
};

return index;

}));

这是一个标准的 umd 格式的包,我们可以在 浏览器环境下通过 Calculator 直接使用,也可以在 node 环境下通过 requrie 调用。

babel 插件

大致浏览打包出的代码后,可以发现有些较新的语法被没有转换,比如 模板字符串 、 箭头函数 、 rest参数 等等,这些代码在 es5 环境下是无法运行的。为什么会这样呢?因为 rollup 是不会转换这些的,那么就需要用到 babel 插件,来转换这些新的语法。

npm i @babel/core @babel/preset-env @rollup/plugin-babel -D

rollup.config.js 文件中添加 plugin 配置项:

import babel from '@rollup/plugin-babel';

export default {
input: '...',
output: [{...}],
plugins: [
babel(),
]
}

创建 babel.config.js 配置文件:

/** @type {import('@babel/core').TransformOptions} */
const transformOptions = {
presets: [
[
'@babel/preset-env',
]
],
};

module.exports = transformOptions;

配置好 babel 之后,再次打包,我们以发现一些高级的语法和 API (这里笔者偷懒了,实际上在没有安装和配置 @babel/runtime 等相关插件前,babel 只能转换语法,不能转化 API ,比如代码中的 includes 就是新的 API 但是并没有被转换,等笔者写完 babel 相关文章后再来更新) 已经被转换可以在低版本环境中运行了。

resolve 插件

在上述场景中,我们并没有引用其他的包,但在实际开发中引用第三方包辅助快速开发是非常常见的场景。

在默认情况下,如果我们直接导入 node_modules 中的包,打包完成之后,node_modules 中的包并不会和我们编写的库合并。为了举例说明,我们将安装一个用于迭代字符串的包 repeat-string ,然后在 index.js 导入并使用:

import repeat from 'repeat-string';
console.log(repeat(1, args.length));

在运行 npm run build 之后可以发现,虽然打包成功了,但是控制台确有一些警告提示:

(!) Unresolved dependencies
https://rollupjs.org/guide/en/#warning-treating-module-as-external-dependency
repeat-string (imported by src/index.js)

打包后的代码中也可以看到,repeat-string 默认是以参数的形式注入其中,而不是与我们的代码合并。

为了解决这个问题,就要用到 @rollup/plugin-node-resolve 插件,他可以帮助我们解析 node_modules 中的第三方包。

npm i @rollup/plugin-node-resolve -D

rollup.config.js :

import babel from '@rollup/plugin-babel';
import resolve from '@rollup/plugin-node-resolve';

export default {
input: '...',
output: [{...}],
plugins: [
resolve(),
babel(),
]
}

再次运行 npm run build 之后,你会惊讶的发现,直接报错了:

src/index.js → dist/index.js...

[!] Error: 'default' is not exported by node_modules/repeat-string/index.js, imported by src/index.js

https://rollupjs.org/guide/en/#error-name-is-not-exported-by-module

这是因为 repeat-string 是 commonjs 格式的包,而我们是以 ESModule 的方式引入的,所以就报错了。所以需要继续借助插件将 commonjs 转换为 ESM ,就是接下来要介绍的 commonjs 插件。

commonjs 插件

安装和配置:

npm i @rollup/plugin-commonjs -D
import babel from '@rollup/plugin-babel';
import resolve from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';

export default {
input: '...',
output: [{...}],
plugins: [
resolve(),
commonjs(),
babel(),
]
}

npm run build 之后,可以发现不仅正确的解析了 repeat-string ,而且也将代码和我们的库合并了。

其他常用插件

除了上述的三个插件之外,还有一些常见的插件:

  • @rollup/plugin-json : 解析编译源码中的 json 文件,并且配合 rollup 的 Tree Shaking 可只打包 .json 文件中我们真正用到的部分。
  • @rollup/plugin-typescript : 解析和转换 typescript
  • @rollup/plugin-eslint : eslint 插件
  • rollup-plugin-terser : 压缩代码

其它更多插件可以到官方仓库中查找: https://github.com/rollup/plugins

external 属性

当配置了 @rollup/plugin-node-resolve 和 @rollup/plugin-commonjs 之后, 所有从 node_modules 中导入的包都会合并到我们的库中,有时候我们并不希望如此,比如 Vuex 和 VueRouter 都基于 Vue 开发,但打包出的代码中若包含了 Vue 的源码,那显然不合适,所以需要将 Vue 设置为外部项,也就是通过 external 属性来设置。如果回到我们的案例中,我们想将 repeat-string 也设置为外部项,只需做如下修改:

import resolve from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import babel from '@rollup/plugin-babel';

export default {
input: '...',
output: [{...}],
plugins: [
resolve(),
commonjs(),
babel(),
],
external: [
'repeat-string', // 外部依赖的名称
path.resolve( './src/some-local-file-that-should-not-be-bundled.js') // 一个已被找到路径的ID(文件的绝对路径)
]
}
请我喝杯咖啡
请我喝杯咖啡