跳到主要内容

Rollup 与库打包

问题

Rollup 是什么?它和 Webpack 有什么区别?如何使用 Rollup 打包一个 npm 库?

答案

Rollup 是一个 JavaScript 模块打包器,专注于将小段代码编译为更大更复杂的库或应用。它的核心设计理念是 面向 ES Module,天然支持 Tree Shaking,是目前打包 JavaScript 库的首选工具。Vue、React、Svelte、D3 等知名开源项目都使用 Rollup 进行打包。

一句话总结

Rollup = 面向库开发的 ESM 打包器,以 Tree Shaking 和干净的产物著称。

核心概念

Rollup 的配置围绕以下几个核心概念展开:

概念说明
input入口文件路径,Rollup 从这里开始分析依赖图
output输出配置,包括文件路径、格式(format)、全局变量名等
format输出格式:esm(ES Module)、cjs(CommonJS)、umd(通用)、iife(立即执行)
external外部依赖,不打包进产物,由使用者提供
plugins插件,扩展 Rollup 的功能(如解析 node_modules、编译 TypeScript 等)
各输出格式对比
// ESM — 现代浏览器和打包器使用
export function add(a: number, b: number): number {
return a + b
}

// CJS — Node.js 使用
module.exports = {
add: function (a, b) {
return a + b
},
}

// UMD — 兼容 AMD、CJS、全局变量,适合 CDN 直接引入
;(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined'
? factory(exports)
: typeof define === 'function' && define.amd
? define(['exports'], factory)
: factory((global.MyLib = {}))
})(this, function (exports) {
exports.add = function (a, b) {
return a + b
}
})

// IIFE — 自执行函数,适合 <script> 标签直接引入
var MyLib = (function () {
function add(a, b) {
return a + b
}
return { add }
})()
format 选择建议
  • 库打包:同时输出 esm + cjs,确保现代和传统环境都能使用
  • 需要 CDN 引入:额外输出 umdiife 格式
  • 纯 Node.js 工具:只需输出 cjs(或仅 esm,如面向 Node 18+)

Rollup vs Webpack 对比

维度RollupWebpack
设计目标面向库打包,输出干净的 ESM 代码面向应用打包,处理各种资源类型
Tree Shaking原生支持,基于 ESM 静态分析,效果彻底支持,但不如 Rollup 彻底
代码分割支持(动态 import),但功能较基础成熟强大(SplitChunksPlugin)
产物体积小而干净,无多余运行时代码包含模块加载运行时(__webpack_require__)
产物格式支持 ESM / CJS / UMD / IIFE / AMD主要输出自有格式,ESM 输出实验性支持
静态资源需要插件支持原生 loader 机制,开箱即用
HMR不支持(需 Vite 等封装)原生支持
CSS 处理需要插件原生 loader 支持
插件生态精简聚焦丰富庞大
配置复杂度简单直观配置灵活但复杂
适用场景JS 库、组件库、SDKWeb 应用、大型 SPA、MPA
选型建议
  • 打包库 / SDK / 组件库 → 选 Rollup(或基于 Rollup 的 tsup、unbuild)
  • 打包应用(SPA / MPA) → 选 Webpack 或 Vite
  • 两者都需要 → 选 Vite(开发用原生 ESM,生产用 Rollup)

Rollup 配置示例

安装 Rollup

npm install rollup @rollup/plugin-node-resolve @rollup/plugin-commonjs @rollup/plugin-typescript rollup-plugin-terser rollup-plugin-dts -D

完整配置(多格式输出)

rollup.config.ts
import { defineConfig } from 'rollup'
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 dts from 'rollup-plugin-dts'
import { readFileSync } from 'fs'

const pkg = JSON.parse(readFileSync('./package.json', 'utf-8'))

// 将 dependencies 和 peerDependencies 标记为外部依赖,不打包进产物
const external = [
...Object.keys(pkg.dependencies || {}),
...Object.keys(pkg.peerDependencies || {}),
]

export default defineConfig([
// 主构建:输出 ESM + CJS + UMD
{
input: 'src/index.ts',
output: [
{
file: pkg.module, // dist/index.esm.js
format: 'esm',
sourcemap: true,
},
{
file: pkg.main, // dist/index.cjs.js
format: 'cjs',
sourcemap: true,
exports: 'named',
},
{
file: pkg.unpkg, // dist/index.umd.js
format: 'umd',
name: 'MyLib', // 全局变量名
sourcemap: true,
globals: {
// UMD 格式中外部依赖的全局变量映射
react: 'React',
'react-dom': 'ReactDOM',
},
plugins: [terser()], // UMD 产物压缩
},
],
external,
plugins: [
resolve(), // 解析 node_modules
commonjs(), // 转换 CJS 为 ESM
typescript({
tsconfig: './tsconfig.json',
declaration: false, // 类型声明单独处理
}),
],
},
// 类型声明:输出 .d.ts
{
input: 'src/index.ts',
output: {
file: 'dist/index.d.ts',
format: 'esm',
},
plugins: [dts()],
},
])

常用插件

插件作用
@rollup/plugin-node-resolve解析 node_modules 中的第三方模块
@rollup/plugin-commonjs将 CommonJS 模块转换为 ESM,使其可以被 Rollup 处理
@rollup/plugin-typescript编译 TypeScript
@rollup/plugin-babel使用 Babel 转译代码
@rollup/plugin-json允许 import JSON 文件
@rollup/plugin-alias路径别名
@rollup/plugin-replace替换代码中的变量(如 process.env.NODE_ENV
rollup-plugin-terser代码压缩(基于 Terser)
rollup-plugin-dts打包合并 .d.ts 类型声明文件
rollup-plugin-visualizer产物可视化分析
@rollup/plugin-image导入图片资源
rollup-plugin-postcssCSS 处理(PostCSS、CSS Modules、Sass 等)
最小化插件配置

打包一个纯 TypeScript 库,最常用的三件套是:@rollup/plugin-node-resolve + @rollup/plugin-commonjs + @rollup/plugin-typescript

Rollup 插件机制

Rollup 的插件通过 钩子函数(Hooks) 介入打包流程。钩子分为两大类:

  1. Build Hooks(构建钩子):在构建阶段(rollup.rollup())触发,负责模块解析、加载、转换
  2. Output Generation Hooks(输出钩子):在生成阶段(bundle.generate() / bundle.write())触发,负责产物处理

钩子执行顺序

钩子类型

每个钩子还有不同的执行类型:

类型说明示例
async异步钩子,可返回 PromiseresolveIdloadtransform
first多个插件返回值时,取第一个非 null 的值resolveIdload
sequential按顺序执行,前一个插件的返回值传给下一个transform
parallel并行执行,互不影响buildStartbuildEnd

手写一个 Rollup 插件

Rollup 插件本质上就是一个 返回包含钩子函数的对象 的函数。下面以一个"自动注入版本号"插件为例:

plugins/rollup-plugin-version.ts
import type { Plugin } from 'rollup'
import { readFileSync } from 'fs'

interface VersionPluginOptions {
/** 要替换的占位符,默认 __VERSION__ */
placeholder?: string
}

// 插件就是一个返回对象的函数
export function versionPlugin(options: VersionPluginOptions = {}): Plugin {
const { placeholder = '__VERSION__' } = options
const pkg = JSON.parse(readFileSync('./package.json', 'utf-8'))

return {
// name 是必需的,用于错误信息和调试
name: 'rollup-plugin-version',

// transform 钩子:在每个模块代码被解析后调用
transform(code: string, id: string) {
// 只处理 JS/TS 文件
if (!/\.[jt]sx?$/.test(id)) return null

// 如果代码中不包含占位符,直接跳过
if (!code.includes(placeholder)) return null

// 替换占位符为实际版本号
return {
code: code.replace(
new RegExp(placeholder, 'g'),
JSON.stringify(pkg.version)
),
map: null, // 简单示例省略 sourcemap
}
},

// buildStart 钩子:构建开始时打印信息
buildStart() {
console.log(`📦 Building v${pkg.version}...`)
},

// generateBundle 钩子:在产物生成时,添加一个额外文件
generateBundle() {
// 通过 this.emitFile 向产物中添加文件
this.emitFile({
type: 'asset',
fileName: 'version.txt',
source: pkg.version,
})
},
}
}

使用方式:

rollup.config.ts
import { defineConfig } from 'rollup'
import { versionPlugin } from './plugins/rollup-plugin-version'

export default defineConfig({
input: 'src/index.ts',
output: { file: 'dist/index.js', format: 'esm' },
plugins: [
versionPlugin({ placeholder: '__VERSION__' }),
],
})
src/index.ts
// 源码中使用占位符
export const VERSION = __VERSION__ // 构建后会被替换为 "1.0.0"

库打包最佳实践

package.json 的导出字段

正确配置 package.json 中的导出字段是库打包的关键:

package.json
{
"name": "my-lib",
"version": "1.0.0",
// 传统字段
"main": "dist/index.cjs.js", // CJS 入口(Node.js require)
"module": "dist/index.esm.js", // ESM 入口(打包器优先使用)
"types": "dist/index.d.ts", // TypeScript 类型声明
"unpkg": "dist/index.umd.js", // CDN(unpkg)

// 现代导出方式(Node.js 12.11+ / 打包器)
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.esm.js",
"require": "./dist/index.cjs.js"
},
"./utils": {
"types": "./dist/utils.d.ts",
"import": "./dist/utils.esm.js",
"require": "./dist/utils.cjs.js"
}
},

// 标识包是否有副作用(影响 Tree Shaking)
"sideEffects": false,

// 指定发布到 npm 的文件
"files": ["dist"],

// 外部化依赖:使用者需自行安装
"peerDependencies": {
"react": ">=18.0.0",
"react-dom": ">=18.0.0"
},

"devDependencies": {
"rollup": "^4.0.0",
"@rollup/plugin-node-resolve": "^15.0.0",
"@rollup/plugin-commonjs": "^25.0.0",
"@rollup/plugin-typescript": "^11.0.0",
"rollup-plugin-terser": "^7.0.0",
"rollup-plugin-dts": "^6.0.0",
"typescript": "^5.0.0"
}
}
main vs module vs exports
字段用途使用场景
mainCJS 入口Node.js require()
moduleESM 入口(非标准但广泛支持)Webpack、Rollup 等打包器
types类型声明入口TypeScript 编译器
exports条件导出(现代标准)Node.js 12.11+、现代打包器

推荐 同时配置 main + module + exports,兼顾所有环境。

输出多种格式

库应该同时输出 ESM 和 CJS 格式,必要时加上 UMD:

rollup.config.ts — 多格式输出策略
import { defineConfig } from 'rollup'

export default defineConfig({
input: 'src/index.ts',
output: [
// ESM — 给现代打包器和 <script type="module"> 使用
{ file: 'dist/index.esm.js', format: 'esm', sourcemap: true },

// CJS — 给 Node.js require() 使用
{ file: 'dist/index.cjs.js', format: 'cjs', sourcemap: true, exports: 'named' },

// UMD — 给 CDN <script> 标签使用
{ file: 'dist/index.umd.js', format: 'umd', name: 'MyLib', sourcemap: true },
],
})

外部化依赖

打包库时,应将 dependenciespeerDependencies 外部化,避免重复打包:

rollup.config.ts — external 配置
import { defineConfig } from 'rollup'
import { readFileSync } from 'fs'

const pkg = JSON.parse(readFileSync('./package.json', 'utf-8'))

export default defineConfig({
input: 'src/index.ts',
external: [
// 所有 dependencies 和 peerDependencies 都不打入产物
...Object.keys(pkg.dependencies || {}),
...Object.keys(pkg.peerDependencies || {}),
// 也需要匹配子路径导入,如 'react/jsx-runtime'
/^react(\/.*)?$/,
/^lodash(\/.*)?$/,
],
output: { file: 'dist/index.esm.js', format: 'esm' },
})
为什么要外部化?

如果不将 react 标记为 external,Rollup 会将整个 React 打包进你的库产物中。当用户使用你的库时,页面上就会出现 两份 React 实例,导致 Hooks 报错、状态不共享等严重问题。

生成类型声明文件

使用 rollup-plugin-dts 将所有 .d.ts 文件打包合并为一个声明文件:

rollup.config.ts — 类型声明打包
import { defineConfig } from 'rollup'
import dts from 'rollup-plugin-dts'

export default defineConfig({
input: 'src/index.ts',
output: {
file: 'dist/index.d.ts',
format: 'esm',
},
// rollup-plugin-dts 会把所有分散的 .d.ts 合并为单个文件
plugins: [dts()],
})
为什么要合并类型声明?
  1. 用户只需引用一个 .d.ts 文件,而非整个类型目录
  2. 避免 dist 中暴露内部类型结构
  3. 减少 npm 包体积

现代替代方案

Rollup 虽然强大,但配置相对繁琐。社区已经衍生出多个基于 Rollup 的零配置或低配置工具:

工具底层特点适用场景
tsupesbuild零配置、极速、开箱支持 TypeScript小型库、CLI 工具
unbuildRollup + esbuild自动推断配置、支持 stub 开发模式Monorepo 中的包
Vite Library ModeRollup复用 Vite 生态和配置已使用 Vite 的项目
RolldownRustRollup 兼容的高性能替代,Vite 未来底层未来趋势
tsup.config.ts — 零配置打包库
import { defineConfig } from 'tsup'

export default defineConfig({
entry: ['src/index.ts'],
format: ['esm', 'cjs'], // 同时输出 ESM 和 CJS
dts: true, // 自动生成类型声明
splitting: false, // 不做代码分割
sourcemap: true,
clean: true, // 构建前清理 dist
})
vite.config.ts — Vite Library Mode
import { defineConfig } from 'vite'
import { resolve } from 'path'
import dts from 'vite-plugin-dts'

export default defineConfig({
plugins: [dts()],
build: {
lib: {
entry: resolve(__dirname, 'src/index.ts'),
name: 'MyLib',
formats: ['es', 'cjs', 'umd'],
fileName: (format) => `index.${format}.js`,
},
rollupOptions: {
external: ['react', 'react-dom'],
output: {
globals: {
react: 'React',
'react-dom': 'ReactDOM',
},
},
},
},
})
选型指南
  • 追求极简和速度tsup(底层 esbuild,几乎零配置)
  • Monorepo 多包管理unbuild(自动推断 package.json 中的配置)
  • 已有 Vite 项目 → Vite Library Mode(配置统一)
  • 需要完全掌控构建流程 → 直接使用 Rollup

常见面试问题

Q1: Rollup 和 Webpack 的区别是什么?各自适用什么场景?

答案

Rollup 和 Webpack 的核心区别在于设计目标和产物质量

1. 设计理念不同:

  • Rollup 围绕 ESM 设计,目标是生成尽可能 干净、紧凑 的代码,它把所有模块"铺平"到一个作用域中(Scope Hoisting),减少函数包装和运行时代码
  • Webpack 围绕"万物皆模块"设计,目标是处理应用中的 所有类型资源(JS、CSS、图片、字体等),通过 loader 机制统一处理

2. Tree Shaking 能力:

Tree Shaking 效果对比
// 源码
export function used() { return 'used' }
export function unused() { return 'unused' }

// Rollup 产物 — 只保留 used,unused 被彻底移除
function used() { return 'used' }
export { used }

// Webpack 产物 — 包含模块系统运行时
/******/ (() => {
/******/ var __webpack_modules__ = ({
/******/ "./src/index.js": ((__unused, __exports, __webpack_require__) => {
/******/ __webpack_require__.d(__exports, { used: () => used })
function used() { return 'used' }
/******/ })
/******/ })
/******/ // ... 模块加载运行时代码
/******/ })()

3. 适用场景:

场景推荐工具原因
JS 库 / SDKRollup产物干净、体积小、Tree Shaking 彻底
组件库Rollup / Vite Library Mode需要输出多种格式(ESM + CJS)
Web 应用(SPA)Webpack / Vite需要 HMR、代码分割、资源处理
大型老项目Webpack生态成熟、兼容性好
新项目Vite底层使用 Rollup,兼顾开发体验和构建质量
面试加分点

Vite 实际上是 Rollup 和 Webpack 优势的结合:开发时利用原生 ESM 实现极速 HMR(解决了 Rollup 没有 dev server 的问题),生产时使用 Rollup 打包(保证产物质量)。理解这一点可以体现你对前端构建工具全局的认知。

Q2: 如何使用 Rollup 打包一个 npm 库?(完整流程)

答案

以打包一个 React 工具库为例,完整流程如下:

第一步:初始化项目并安装依赖

npm install rollup @rollup/plugin-node-resolve @rollup/plugin-commonjs @rollup/plugin-typescript rollup-plugin-terser rollup-plugin-dts typescript -D

第二步:配置 tsconfig.json

tsconfig.json
{
"compilerOptions": {
"target": "ES2018",
"module": "ESNext",
"moduleResolution": "Node",
"declaration": true,
"declarationDir": "dist/types",
"outDir": "dist",
"strict": true,
"esModuleInterop": true,
"jsx": "react-jsx",
"sourceMap": true
},
"include": ["src"],
"exclude": ["node_modules", "dist"]
}

第三步:编写库源码

src/index.ts
export { useDebounce } from './hooks/useDebounce'
export { useThrottle } from './hooks/useThrottle'
export { formatDate } from './utils/date'
export type { DebounceOptions, ThrottleOptions } from './types'

第四步:配置 rollup.config.ts

rollup.config.ts
import { defineConfig } from 'rollup'
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 dts from 'rollup-plugin-dts'
import { readFileSync } from 'fs'

const pkg = JSON.parse(readFileSync('./package.json', 'utf-8'))

const external = [
...Object.keys(pkg.dependencies || {}),
...Object.keys(pkg.peerDependencies || {}),
/^react(\/.*)?$/, // 匹配 react 及其子路径
]

export default defineConfig([
// 主构建
{
input: 'src/index.ts',
output: [
{ file: 'dist/index.esm.js', format: 'esm', sourcemap: true },
{ file: 'dist/index.cjs.js', format: 'cjs', sourcemap: true, exports: 'named' },
],
external,
plugins: [
resolve(),
commonjs(),
typescript({ tsconfig: './tsconfig.json', declaration: false }),
terser(),
],
},
// 类型声明
{
input: 'src/index.ts',
output: { file: 'dist/index.d.ts', format: 'esm' },
plugins: [dts()],
},
])

第五步:配置 package.json

package.json
{
"name": "my-react-hooks",
"version": "1.0.0",
"main": "dist/index.cjs.js",
"module": "dist/index.esm.js",
"types": "dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.esm.js",
"require": "./dist/index.cjs.js"
}
},
"sideEffects": false,
"files": ["dist"],
"peerDependencies": {
"react": ">=18.0.0"
},
"scripts": {
"build": "rollup -c rollup.config.ts --configPlugin typescript",
"prepublishOnly": "npm run build"
}
}

第六步:构建并发布

# 构建
npm run build

# 发布前检查产物
ls dist/
# index.esm.js index.cjs.js index.d.ts index.esm.js.map index.cjs.js.map

# 发布到 npm
npm publish

产物结构:

dist/
├── index.esm.js # ESM 格式(打包器使用)
├── index.esm.js.map # ESM sourcemap
├── index.cjs.js # CJS 格式(Node.js require 使用)
├── index.cjs.js.map # CJS sourcemap
└── index.d.ts # 合并的类型声明

Q3: Rollup 的插件机制是怎样的?

答案

Rollup 的插件机制基于 钩子系统(Hooks),插件通过实现特定的钩子函数来介入打包的各个阶段。

1. 插件的基本结构:

Rollup 插件结构
import type { Plugin } from 'rollup'

function myPlugin(options: Record<string, unknown> = {}): Plugin {
return {
name: 'my-plugin', // 必需:用于错误提示和调试

// Build Hooks — 构建阶段
resolveId(source: string, importer: string | undefined) { /* ... */ },
load(id: string) { /* ... */ },
transform(code: string, id: string) { /* ... */ },

// Output Generation Hooks — 输出阶段
renderChunk(code: string, chunk: RenderedChunk) { /* ... */ },
generateBundle(options: OutputOptions, bundle: OutputBundle) { /* ... */ },
}
}

2. 核心钩子说明:

钩子阶段作用典型场景
optionsBuild修改输入配置动态调整配置
buildStartBuild构建开始时调用初始化、清理
resolveIdBuild自定义模块路径解析虚拟模块、路径别名
loadBuild自定义模块内容加载虚拟模块、内存文件
transformBuild转换模块代码代码替换、注入
buildEndBuild构建结束时调用错误汇总
renderChunkOutput处理每个输出 chunk代码压缩、banner 注入
generateBundleOutput所有 chunk 生成后调用添加额外文件、修改产物
writeBundleOutput产物写入磁盘后调用后置处理、通知

3. 钩子的执行类型:

不同钩子有不同的执行方式,理解这一点对编写插件至关重要:

钩子执行类型示例
import type { Plugin } from 'rollup'

function examplePlugin(): Plugin {
return {
name: 'example',

// first 类型:多个插件都实现了 resolveId,取第一个非 null 返回值
resolveId(source: string) {
if (source === 'virtual:env') {
return '\0virtual:env' // 返回非 null,后续插件不再处理
}
return null // 返回 null,交给下一个插件处理
},

// sequential 类型:按顺序执行,前一个的输出作为后一个的输入
transform(code: string, id: string) {
// 多个插件的 transform 串联执行
// 插件 A 的输出 → 插件 B 的输入 → 插件 C 的输入
return code.replace('__DEV__', 'false')
},

// parallel 类型:并行执行,互不依赖
buildStart() {
console.log('Build started!')
// 所有插件的 buildStart 并行执行
},
}
}

4. 虚拟模块插件实战:

plugins/rollup-plugin-virtual-env.ts
import type { Plugin } from 'rollup'

// 虚拟模块前缀约定:使用 \0 前缀防止其他插件处理
const VIRTUAL_ID = 'virtual:env'
const RESOLVED_ID = '\0virtual:env'

interface EnvOptions {
mode: 'development' | 'production'
}

export function virtualEnvPlugin(options: EnvOptions): Plugin {
return {
name: 'rollup-plugin-virtual-env',

// 解析:将虚拟模块 ID 映射为内部 ID
resolveId(source: string) {
if (source === VIRTUAL_ID) {
return RESOLVED_ID
}
},

// 加载:为虚拟模块返回代码内容
load(id: string) {
if (id === RESOLVED_ID) {
return `
export const MODE = ${JSON.stringify(options.mode)}
export const IS_DEV = ${options.mode === 'development'}
export const IS_PROD = ${options.mode === 'production'}
`
}
},
}
}

// 使用时:
// import { MODE, IS_DEV } from 'virtual:env'
面试要点总结
  1. Rollup 插件是一个 返回钩子对象的函数
  2. 钩子分为 Build Hooks(构建阶段)和 Output Hooks(输出阶段)
  3. 最核心的三个钩子:resolveId(解析路径)→ load(加载内容)→ transform(转换代码)
  4. Vite 的插件机制基于 Rollup 扩展,学会 Rollup 插件约等于学会 Vite 插件

相关链接