Rollup 的定位很清晰:打包 JavaScript 库。如果你在写一个 npm 包,想让它体积小、tree shaking 效果好,Rollup 基本是首选。
Vue、React、Three.js、D3 这些知名库都用 Rollup 打包。Vite 的生产构建也是基于 Rollup。
与 Webpack 的区别#
Webpack 的设计目标是打包应用,要处理各种资源(图片、CSS、字体),还要考虑代码分割、懒加载、HMR。功能很全,但也复杂。
Rollup 只关心 JavaScript 模块。它假设你的代码都是标准的 ES Module,然后做最极致的优化。
举个例子,假设有这样的代码:
export function add(a, b) { return a + b}
export function multiply(a, b) { return a * b}
// index.jsimport { 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——未使用的代码不仅被标记为无用,而且根本不会出现在输出里。
安装与基本使用#
npm install rollup --save-dev命令行使用#
# 打包,输出到 stdoutrollup src/index.js
# 输出到文件rollup src/index.js --file dist/bundle.js
# 指定输出格式rollup src/index.js --file dist/bundle.js --format esm
# 生成 sourcemaprollup 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, },}运行:
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 = addNode.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 里的模块:
npm install @rollup/plugin-node-resolve --save-devimport 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:
npm install @rollup/plugin-commonjs --save-devimport 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:
npm install @rollup/plugin-typescript typescript tslib --save-devimport typescript from '@rollup/plugin-typescript'
export default { input: 'src/index.ts', output: { file: 'dist/bundle.js', format: 'esm', }, plugins: [typescript()],}@rollup/plugin-babel#
用 Babel 转换代码:
npm install @rollup/plugin-babel @babel/core @babel/preset-env --save-devimport { 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#
压缩代码:
npm install @rollup/plugin-terser --save-devimport 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 文件:
npm install @rollup/plugin-json --save-devimport 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 库配置:
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 支持动态导入的代码分割:
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 模式#
开发时自动重新构建:
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, },}与其他工具对比#
| 对比项 | Rollup | Webpack | esbuild | Parcel |
|---|---|---|---|---|
| 主要用途 | 打包库 | 打包应用 | 极速构建 | 零配置打包 |
| 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 单独生成。