Skip to content

Rollup - 专注于库开发的打包工具

Rollup 的定位很清晰:打包 JavaScript 库。如果你在写一个 npm 包,想让它体积小、tree shaking 效果好,Rollup 基本是首选。

Vue、React、Three.js、D3 这些知名库都用 Rollup 打包。Vite 的生产构建也是基于 Rollup。

与 Webpack 的区别#

Webpack 的设计目标是打包应用,要处理各种资源(图片、CSS、字体),还要考虑代码分割、懒加载、HMR。功能很全,但也复杂。

Rollup 只关心 JavaScript 模块。它假设你的代码都是标准的 ES Module,然后做最极致的优化。

举个例子,假设有这样的代码:

utils.js
export function add(a, b) {
return a + b
}
export function multiply(a, b) {
return a * b
}
// index.js
import { add } from './utils.js'
console.log(add(1, 2))

Webpack 打包后,会保留模块的边界,每个模块是一个函数。Rollup 会把这些模块”展平”成一个文件:

// Rollup 输出
function add(a, b) {
return a + b
}
console.log(add(1, 2))
// multiply 被完全移除了

这就是 Rollup 的 Tree Shaking——未使用的代码不仅被标记为无用,而且根本不会出现在输出里。

安装与基本使用#

Terminal window
npm install rollup --save-dev

命令行使用#

Terminal window
# 打包,输出到 stdout
rollup src/index.js
# 输出到文件
rollup src/index.js --file dist/bundle.js
# 指定输出格式
rollup src/index.js --file dist/bundle.js --format esm
# 生成 sourcemap
rollup src/index.js --file dist/bundle.js --format esm --sourcemap

配置文件#

实际项目都用配置文件。创建 rollup.config.js

export default {
input: 'src/index.js',
output: {
file: 'dist/bundle.js',
format: 'esm',
sourcemap: true,
},
}

运行:

Terminal window
rollup -c
# 或者
rollup --config rollup.config.js

多入口配置#

export default {
input: {
main: 'src/index.js',
utils: 'src/utils.js',
},
output: {
dir: 'dist',
format: 'esm',
},
}

多输出格式#

库通常需要同时输出 ESM、CommonJS、UMD 格式:

export default {
input: 'src/index.js',
output: [
{
file: 'dist/bundle.esm.js',
format: 'esm',
},
{
file: 'dist/bundle.cjs.js',
format: 'cjs',
},
{
file: 'dist/bundle.umd.js',
format: 'umd',
name: 'MyLibrary', // UMD 格式需要指定全局变量名
},
],
}

输出格式详解#

esm(ES Module)#

// 输出示例
export function add(a, b) {
return a + b
}

现代浏览器和打包工具都支持,tree shaking 效果最好。

cjs(CommonJS)#

// 输出示例
'use strict'
function add(a, b) {
return a + b
}
exports.add = add

Node.js 传统模块格式,兼容性最好。

umd(Universal Module Definition)#

// 输出示例
;(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined'
? factory(exports)
: typeof define === 'function' && define.amd
? define(['exports'], factory)
: ((global = global || self), factory((global.MyLibrary = {})))
})(this, function (exports) {
'use strict'
function add(a, b) {
return a + b
}
exports.add = add
})

兼容 CommonJS、AMD 和浏览器全局变量,适合需要支持各种环境的库。

iife(立即执行函数)#

// 输出示例
var MyLibrary = (function () {
'use strict'
function add(a, b) {
return a + b
}
return { add }
})()

直接在浏览器里用 <script> 标签引入。

常用插件#

Rollup 的核心很小,功能靠插件扩展。

@rollup/plugin-node-resolve#

让 Rollup 能解析 node_modules 里的模块:

Terminal window
npm install @rollup/plugin-node-resolve --save-dev
import resolve from '@rollup/plugin-node-resolve'
export default {
input: 'src/index.js',
output: {
file: 'dist/bundle.js',
format: 'esm',
},
plugins: [resolve()],
}

@rollup/plugin-commonjs#

把 CommonJS 模块转换成 ES Module:

Terminal window
npm install @rollup/plugin-commonjs --save-dev
import resolve from '@rollup/plugin-node-resolve'
import commonjs from '@rollup/plugin-commonjs'
export default {
input: 'src/index.js',
output: {
file: 'dist/bundle.js',
format: 'esm',
},
plugins: [resolve(), commonjs()],
}

注意顺序:resolve 要在 commonjs 前面。

@rollup/plugin-typescript#

处理 TypeScript:

Terminal window
npm install @rollup/plugin-typescript typescript tslib --save-dev
import typescript from '@rollup/plugin-typescript'
export default {
input: 'src/index.ts',
output: {
file: 'dist/bundle.js',
format: 'esm',
},
plugins: [typescript()],
}

@rollup/plugin-babel#

用 Babel 转换代码:

Terminal window
npm install @rollup/plugin-babel @babel/core @babel/preset-env --save-dev
import { babel } from '@rollup/plugin-babel'
export default {
input: 'src/index.js',
output: {
file: 'dist/bundle.js',
format: 'esm',
},
plugins: [
babel({
babelHelpers: 'bundled',
presets: ['@babel/preset-env'],
}),
],
}

@rollup/plugin-terser#

压缩代码:

Terminal window
npm install @rollup/plugin-terser --save-dev
import terser from '@rollup/plugin-terser'
export default {
input: 'src/index.js',
output: {
file: 'dist/bundle.min.js',
format: 'esm',
},
plugins: [terser()],
}

@rollup/plugin-json#

导入 JSON 文件:

Terminal window
npm install @rollup/plugin-json --save-dev
import json from '@rollup/plugin-json'
export default {
input: 'src/index.js',
output: {
file: 'dist/bundle.js',
format: 'esm',
},
plugins: [json()],
}

代码中就可以:

import pkg from './package.json'
console.log(pkg.version)

@rollup/plugin-replace#

替换代码中的字符串:

import replace from '@rollup/plugin-replace'
export default {
input: 'src/index.js',
output: {
file: 'dist/bundle.js',
format: 'esm',
},
plugins: [
replace({
'preventAssignment': true,
'process.env.NODE_ENV': JSON.stringify('production'),
}),
],
}

完整的库打包配置#

一个典型的 npm 库配置:

rollup.config.js
import resolve from '@rollup/plugin-node-resolve'
import commonjs from '@rollup/plugin-commonjs'
import typescript from '@rollup/plugin-typescript'
import terser from '@rollup/plugin-terser'
import { readFileSync } from 'fs'
const pkg = JSON.parse(readFileSync('./package.json', 'utf8'))
// 不打包的依赖
const external = [
...Object.keys(pkg.dependencies || {}),
...Object.keys(pkg.peerDependencies || {}),
]
export default [
// ESM 版本
{
input: 'src/index.ts',
output: {
file: pkg.module,
format: 'esm',
sourcemap: true,
},
external,
plugins: [resolve(), commonjs(), typescript()],
},
// CommonJS 版本
{
input: 'src/index.ts',
output: {
file: pkg.main,
format: 'cjs',
sourcemap: true,
exports: 'named',
},
external,
plugins: [resolve(), commonjs(), typescript()],
},
// UMD 压缩版本(用于 CDN)
{
input: 'src/index.ts',
output: {
file: 'dist/index.umd.min.js',
format: 'umd',
name: 'MyLibrary',
globals: {
'react': 'React',
'react-dom': 'ReactDOM',
},
},
external: ['react', 'react-dom'],
plugins: [resolve(), commonjs(), typescript(), terser()],
},
]

对应的 package.json

{
"name": "my-library",
"version": "1.0.0",
"main": "dist/index.cjs.js",
"module": "dist/index.esm.js",
"types": "dist/index.d.ts",
"exports": {
".": {
"import": "./dist/index.esm.js",
"require": "./dist/index.cjs.js",
"types": "./dist/index.d.ts"
}
},
"files": ["dist"],
"scripts": {
"build": "rollup -c",
"dev": "rollup -c -w"
},
"peerDependencies": {
"react": ">=17.0.0"
},
"devDependencies": {
"@rollup/plugin-commonjs": "^25.0.0",
"@rollup/plugin-node-resolve": "^15.0.0",
"@rollup/plugin-terser": "^0.4.0",
"@rollup/plugin-typescript": "^11.0.0",
"rollup": "^4.0.0",
"typescript": "^5.0.0",
"tslib": "^2.6.0"
}
}

插件开发#

Rollup 插件是一个返回对象的函数,对象包含各种钩子。

基本结构#

export default function myPlugin(options = {}) {
return {
name: 'my-plugin', // 必须,用于错误信息
// 构建开始时
buildStart() {
console.log('构建开始')
},
// 解析模块路径
resolveId(source, importer) {
if (source === 'virtual-module') {
return source // 返回值作为模块 ID
}
return null // 返回 null 让其他插件处理
},
// 加载模块内容
load(id) {
if (id === 'virtual-module') {
return 'export default "这是虚拟模块"'
}
return null
},
// 转换代码
transform(code, id) {
if (id.endsWith('.custom')) {
return {
code: `export default ${JSON.stringify(code)}`,
map: null,
}
}
return null
},
// 构建结束时
buildEnd() {
console.log('构建结束')
},
// 输出生成时
generateBundle(options, bundle) {
// 可以修改或添加输出文件
for (const fileName in bundle) {
console.log('输出文件:', fileName)
}
},
}
}

实战:添加 banner#

export default function banner(text) {
return {
name: 'banner',
renderChunk(code) {
return `/*!\n * ${text}\n */\n${code}`
},
}
}
// 使用
import banner from './plugins/banner.js'
export default {
input: 'src/index.js',
output: {
file: 'dist/bundle.js',
format: 'esm',
},
plugins: [banner('My Library v1.0.0 | MIT License')],
}

实战:处理 CSS#

import { createFilter } from '@rollup/pluginutils'
import { readFileSync, writeFileSync, mkdirSync } from 'fs'
import { dirname, basename } from 'path'
export default function css(options = {}) {
const filter = createFilter(options.include || ['**/*.css'], options.exclude)
const styles = {}
return {
name: 'css',
transform(code, id) {
if (!filter(id)) return null
styles[id] = code
return {
code: `export default ${JSON.stringify(code)}`,
map: { mappings: '' },
}
},
generateBundle(opts) {
const cssContent = Object.values(styles).join('\n')
if (cssContent) {
this.emitFile({
type: 'asset',
fileName: 'styles.css',
source: cssContent,
})
}
},
}
}

Tree Shaking 深入#

Rollup 的 Tree Shaking 基于 ES Module 的静态结构。但有些情况需要注意。

副作用标记#

如果模块有副作用(比如修改全局变量),需要在 package.json 里标记:

{
"sideEffects": ["*.css", "src/polyfills.js"]
}

或者标记为无副作用:

{
"sideEffects": false
}

纯函数注释#

告诉 Rollup 某个函数调用是纯的,可以安全删除:

const result = /*#__PURE__*/ someFunction()

如果 result 没被使用,整个调用会被删除。

保留导出#

默认情况下,未使用的导出会被删除。如果需要保留:

export default {
input: 'src/index.js',
output: {
file: 'dist/bundle.js',
format: 'esm',
},
preserveEntrySignatures: 'strict', // 保留入口模块的所有导出
}

代码分割#

Rollup 支持动态导入的代码分割:

src/index.js
export async function loadFeature() {
const { feature } = await import('./feature.js')
return feature()
}

配置:

export default {
input: 'src/index.js',
output: {
dir: 'dist',
format: 'esm',
},
}

会生成多个文件:

dist/
├── index.js
└── feature-xxxxx.js

手动指定分割点#

export default {
input: {
main: 'src/index.js',
vendor: 'src/vendor.js',
},
output: {
dir: 'dist',
format: 'esm',
manualChunks: {
lodash: ['lodash-es'],
react: ['react', 'react-dom'],
},
},
}

Watch 模式#

开发时自动重新构建:

Terminal window
rollup -c -w
# 或者
rollup -c --watch

配置 watch 选项:

export default {
input: 'src/index.js',
output: {
file: 'dist/bundle.js',
format: 'esm',
},
watch: {
include: 'src/**',
exclude: 'node_modules/**',
clearScreen: false,
},
}

与其他工具对比#

对比项RollupWebpackesbuildParcel
主要用途打包库打包应用极速构建零配置打包
Tree Shaking最佳良好良好良好
输出体积最小较大中等中等
代码分割支持强大基础自动
HMR需插件内置内置
配置复杂度中等极低
插件生态丰富最丰富发展中一般

常见问题#

1. 循环依赖警告#

(!) Circular dependency: src/a.js -> src/b.js -> src/a.js

虽然 ES Module 支持循环依赖,但最好避免。如果确定没问题,可以忽略:

export default {
onwarn(warning, warn) {
if (warning.code === 'CIRCULAR_DEPENDENCY') return
warn(warning)
},
}

2. 找不到模块#

[!] Error: Could not resolve './utils' from src/index.js

检查是否安装了 @rollup/plugin-node-resolve,以及文件扩展名是否正确。

3. CommonJS 模块问题#

Error: 'default' is not exported by node_modules/xxx

需要 @rollup/plugin-commonjs,并检查导入方式:

// 可能需要改成
import * as xxx from 'xxx'
// 或者
import xxx from 'xxx'

4. 外部依赖没有被排除#

确保 external 配置正确:

export default {
external: ['react', 'react-dom', /^lodash\//],
}

可以用正则表达式匹配模块路径。

5. 类型声明文件#

@rollup/plugin-typescript 默认不生成 .d.ts 文件。需要配置:

typescript({
declaration: true,
declarationDir: 'dist/types',
})

或者用 tsc 单独生成。

参考资料#