rollup 快速入门与上手

前言

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

什么是 rollup

rollupwebpack 一样,都是 JavaScript 模块打包器,用于打包和构建 JavaScript 应用程序和 library 。而 rollup 则更适合打包 library 且自身更为小巧和简单,所以有些开发应用时需要的功能,rollup 反而不支持,比如模块热更新(HMR)。我们熟知的 VueReact 都是通过 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 环境下使用,比如浏览器nodeElectron 等等。所以我们也需要构建出不同版本的包。

接下来就开始正式进入 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 : 入口文件,对标 webpackentry ,指明了库文件入口位置
  • output : 输出文件,对标 webpackoutput ,指明了打包后输出文件的位置、包名、格式等等
  • 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;

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

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

  • output.file : 表示输出的文件路径
  • output.format : 表示输出文件的格式,可选项有 umdcommonjsesm 等。
  • output.name : 当 format 值为 umd 时,需要设置 output.name ,在浏览器环境下就可通过 name 使用。
  • output.name : 导出方式,可选值有 defaultnamednoneauto ,我们是匿名导出,就填写 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-stringcommonjs 格式的包,而我们是以 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 文件,并且配合 rollupTree 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 中导入的包都会合并到我们的库中,有时候我们并不希望如此,比如 VuexVueRouter 都基于 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(文件的绝对路径)
]
}
请我喝杯咖啡
请我喝杯咖啡