1. 介绍
Rollup 是一个用于 JavaScript 的模块打包工具
2.安装
npm install --global rollup
//文件目录
├── package.json
├── pnpm-lock.yaml
├── rollup.config.js
└── src
├── index.js
└── util.js
- 基础打包命令
# 浏览器用 - 打包成 IIFE 格式
rollup src/main.js --file dist/bundle.js --format iife
# Node.js 用 - 打包成 CommonJS 格式
rollup src/main.js --file dist/bundle.js --format cjs
# 同时支持浏览器和Node - 打包成 UMD 格式
rollup src/main.js --file dist/bundle.js --format umd --name "MyBundle"
{
// rollup 打包命令,`-c` 表示使用配置文件中的配置
"build": "rollup -c"
}
3. 配置文件 rollup.config.js
// 创建 rollup.config.js
export default {
//external:['lodash'] 忽略模块不打包
//plugins,
// 以下三个配置项都可以使用这些占位符:
// 1. [name]: 去除文件后缀后的文件名
// 2. [hash]: 根据文件名和文件内容生成的 hash 值
// 3. [format]: 产物模块格式,如 es、cjs
// 4. [extname]: 产物后缀名(带`.`)
// 入口模块的输出文件名
entryFileNames: `[name].js`,
// 非入口模块(如动态 import)的输出文件名
chunkFileNames: 'chunk-[hash].js',
// 静态资源文件输出文件名
assetFileNames: 'assets/[name]-[hash][extname]',
input: 'src/main.js', // 入口文件
output: {
file: 'dist/bundle.js', // 输出文件
format: 'es', // 输出格式
name: 'MyLibrary' // UMD格式需要的全局变量名
}
};
3.1 多入口\输出口配置
// 多入口打包
export default [
{
input: 'src/main-a.js',
output: { file: 'dist/a.js', format: 'es' }
},
{
input: 'src/main-b.js',
output: [
{ file: 'dist/b-cjs.js', format: 'cjs' },
{ file: 'dist/b-es.js', format: 'es' }
]
}
]
3.2 ts写入配置
npm install @rollup/plugin-typescript --save-dev
import { RollupOptions } from 'rollup'
const config: RollupOptions = {
input: 'src/main.ts',
output: {
file: 'dist/bundle.js',
format: 'es'
}
}
export default config
3.3 命令行工具
# 基本打包(输出到控制台)
rollup src/main.js
# 打包到文件(CommonJS格式)
rollup src/main.js --file dist/bundle.js --format cjs
# 使用配置文件
rollup --config rollup.config.js
# 监听文件变化自动打包
rollup --config --watch
4.核心API介绍
4.1 rollup.rollup() 打包阶段
const bundle = await rollup({
input: 'src/index.js', // 入口文件
plugins: [/* 插件数组 */],
external: ['lodash'] // 外部依赖
});
作用 构建模块依赖图 执行tree-shaking 不生产最终输出文件
4.2 bundle.generate() 内存输出
const { output } = await bundle.generate({
format: 'es',
sourcemap: true
});
output.forEach(chunk => {
if (chunk.type === 'chunk') {
console.log(chunk.code); // 生成的代码
console.log(chunk.map); // sourcemap
}
});
4.3 bundle.write()写入磁盘
await bundle.write({
file: 'dist/bundle.js',
format: 'cjs',
banner: '/* 我的库 v1.0 */'
});
eg:
import { rollup } from 'rollup'
// 常用 inputOptions 配置
const inputOptions = {
input: './src/main.js',
external: [],
plugins: []
}
const outputOptionsList = [
// 常用 outputOptions 配置
{
dir: 'dist/es',
entryFileNames: `[name].[hash].js`, // 入口模块的输出文件名
chunkFileNames: 'chunk-[hash].js', // 非入口模块(如动态 import)的输出文件名
assetFileNames: 'assets/[name]-[hash][extname]',
format: 'es',
sourcemap: true,
globals: {
lodash: '_'
}
}
// 省略其它的输出配置
]
async function build() {
let bundle
let buildFailed = false
try {
// 1. 调用 rollup.rollup 生成 bundle 对象
bundle = await rollup(inputOptions)
for (const outputOptions of outputOptionsList) {
// 2. 拿到 bundle 对象,根据每一份输出配置,调用 generate 和 write 方法分别生成和写入产物
const { output } = await bundle.generate(outputOptions)
console.log(output)
await bundle.write(outputOptions)
}
} catch (error) {
buildFailed = true
console.error(error)
}
if (bundle) {
console.log(bundle)
// 最后调用 bundle.close 方法结束打包
await bundle.close()
}
process.exit(buildFailed ? 1 : 0)
}
build()
执行node build.js 可以看到打包结果
4.4 rollup watch
rollup.watch 即每次源文件变动后自动进行重新打包
// watch.js
import { watch } from 'rollup'
// 配置监控系统
const watcher = watch({
// 基本打包配置(和rollup.config.js一样)
input: './src/main.js', // 主入口文件位置
output: [
{
dir: 'dist/es', // (ES模块格式)
format: 'esm'
},
{
dir: 'dist/cjs', //(CommonJS格式)
format: 'cjs'
}
],
// 监控专用配置
watch: {
exclude: ['node_modules/**'], // 不监控文件(node_modules)
include: ['src/**'] // 只监控文件(src目录)
}
})
// 设置监控警报(事件监听)
watcher.on('restart', () => {
console.log('🔄 检测到文件变化,正在重建...')
})
watcher.on('change', (id) => {
console.log(`📁 发现变动的文件: ${id}`)
})
watcher.on('event', (e) => {
if (e.code === 'BUNDLE_START') {
console.log('👨🍳 开始构建新版本...')
}
if (e.code === 'BUNDLE_END') {
console.log(`✅ 构建完成!耗时 ${e.duration}ms`)
console.log('产出位置:', e.output)
}
if (e.code === 'ERROR') {
console.error('🔥 构建失败!', e.error)
}
})
process.on('SIGINT', () => {
watcher.close()
process.exit(0)
})
执行node watch.js 每次保存文件自动打包
4.插件
- 考虑
模块打包之外的问题,比如路径别名(alias) 、全局变量注入和代码压缩 - Rollup 的打包过程中,会定义一套完整的构建生命周期,从开始打包到产物输出,中途会经历一些标志性的阶段,并且在不同阶段会自动执行对应的插件钩子函数(Hook)。对 Rollup 插件来讲,最重要的部分是钩子函数,一方面它定义了插件的执行逻辑,也就是"做什么";另一方面也声明了插件的作用阶段,即"什么时候做"
在执行 rollup 命令之后,在 cli 内部的主要逻辑简化如下:
// Build 阶段
const bundle = await rollup.rollup(inputOptions)
// Output 阶段
await Promise.all(outputOptions.map(bundle.write))
// 构建结束
await bundle.close()
- Build 阶段主要负责创建模块依赖图,初始化各个模块的 AST 以及模块之间的依赖关系
- 真正进行打包的过程会在
Output阶段进行,即在bundle对象的generate或者write方法中进行 - ** Rollup 会先进入到 Build 阶段,解析各模块的内容及依赖关系,然后进入
Output阶段,完成打包及输出的过程。对于不同的阶段,Rollup 插件会有不同的插件工作流程。
🤔:rollup 插件钩子VS核心API 1.角色不同
- 核心API,提供程序化控制打包流程的方法 处理整体构建流程(初始化配置 → 构建依赖图 → 输出产物)控制中枢
- 插件钩子:是扩展能力接入点 在不同阶
- 段插入自定义逻辑 不是控制事间流 而是根据事间流去做一些事
4.1 插件钩子
- 根据构建阶段主要分为构建钩子和输出生产钩子
- 根据不同的 Hook 执行方式
Async、Sync同步异步 - 执行顺序分类
- first 多个插件执行 按顺序运行直到有插件返回null或undefined值
- sequential - 多个插件按顺序运行,异步钩子会等待前一个完成
- parallel - 多个插件按顺序运行,但异步钩子会并行执行
// @filename: rollup-plugin-my-example.js
export default function myExample () {
return {
name: 'my-example', // 此名称将出现在警告和错误中
resolveId ( source ) {
if (source === 'virtual-module') {
// 这表示 rollup 不应询问其他插件或
// 从文件系统检查以找到此 ID
return source;
}
return null; // 其他ID应按通常方式处理
},
load ( id ) {
if (id === 'virtual-module') {
// "virtual-module"的源代码
return 'export default "This is virtual!"';
}
return null; // 其他ID应按通常方式处理
}
};
}
注意:名称 rollup-plugin-前缀 使用虚拟模块 \0 前缀模块ID
4.2构建钩子
export default function rollupPluginTest() {
return {
name: 'rollup-plugin-test',
//初始配置钩子
options(opts) {
// 可以修改或扩展配置
return {
...opts,
treeshake: true, // 强制开启摇树优化
external: ['react'] // 添加外部依赖
}
},
buildStart(options) {
console.log('构建启动!')
console.log('入口文件:', options.input)
console.log('输出格式:', options.output?.[0]?.format)
},
//解析模块路径
resolveId(source, importer) {
// 示例1:将 '@utils' 解析为实际路径
if (source === '@utils') {
return path.resolve(__dirname, 'src/utils/index.js')
}
// 返回null表示使用默认解析
return null
},
load(id) {
// 示例1:提供虚拟模块
if (id === 'virtual-module') {
return 'export default "这是虚拟模块内容"'
}
return null // 其他文件正常加载
},
transform(code, id) {
// 示例1:移除调试代码
if (process.env.NODE_ENV === 'production') {
code = code.replace(/console\.log\(.*?\);?/g, '')
}
return code // 返回转换后的代码
},
//moduleParsed钩子 模块解析后触发
moduleParsed(moduleInfo) {
// 可以在此收集依赖信息用于分析
console.log(`模块解析完成: ${moduleInfo.id}`)
console.log('导入的模块:', moduleInfo.importedIds)
console.log('动态导入的模块:', moduleInfo.dynamicallyImportedIds)
},
//所有模块处理完成后触发
buildEnd(error) {
if (error) {
console.error('构建失败:', error.message)
} else {
console.log('构建成功完成!')
// 可以在这里生成构建报告
}
},
//在 watch 模式下,Rollup 会额外触发两个钩子:
watchChange(id, change) {
console.log(`文件变更检测: ${id}`)
console.log('变更类型:', change.event) // 'create'|'update'|'delete'
// 可以在这里添加自定义的watch逻辑
},
closeWatcher() {
console.log('监视器关闭')
// 清理资源
},
//dynamicImport 钩子可以用来处理动态导入的模块,
resolveDynamicImport(specifier, importer) {
// 示例:处理特殊的动态导入模式
if (typeof specifier === 'string' && specifier.startsWith('pages/')) {
return path.resolve(__dirname, `src/${specifier}.js`)
}
// 返回null让Rollup继续正常处理
return null
}
}
}
4.3 输出生成钩子
export default function modifyOutput() {
return {
name: 'modify-output',
//修改输出配置
outputOptions(options) {
return {
...options,
sourcemap: true
}
},
//输出开始的时候调用
renderStart(outputOptions) {
console.log('开始生成输出:', outputOptions.format)
},
//修改chunk的哈希值
augmentChunkHash(chunkInfo) {
if (chunkInfo.name === 'main') {
return Date.now().toString() // 基于时间戳修改哈希
}
},
//转化单个chunk代码
renderChunk(code, chunkInfo) {
return `/* ${chunk.fileName} */\n${code}`
},
//所有chunks生产后调用 可以修改最终输出
generateBundle(options, bundle) {
console.log('生成输出完成:', options.output.file)
console.log('输出文件:', Object.keys(bundle))
},
//文件写入磁盘后调用
async writeBundle(options, bundle) {
await sendNotification(`构建完成,生成 ${Object.keys(bundle).length} 个文件`)
}
};
}
4.4 钩子配置选项了解
钩子可以是函数 也可以是handler属性对象
export default function myPlugin() {
return {
name: 'my-plugin',
// 简单函数形式
buildStart() { /*...*/ },
// 对象形式
resolveId: {
order: 'pre', // 执行顺序
handler(source) { /*...*/ }
},
writeBundle: {
sequential: true, // 顺序执行
order: 'post', // 最后执行
async handler({ dir }) { /*...*/ }
},
transform: {
filter: { id: '*.jsx', code: '<Custom' }, // 过滤条件
handler(code) { /*...*/ }
}
};
}
-
order: 控制执行顺序
- 'pre' - 在其他插件之前执行
- 'post' - 在其他插件之后执行
- null - 按插件顺序执行(默认)
-
sequential: 仅用于 parallel 钩子,使当前插件钩子顺序执行
-
filter: 过滤条件,仅对特定 ID 或代码内容执行钩子
4.5 插件例子
- resolveId:确定文件在哪
- load:读取文件内容
- transform:修改内容
- renderChunk
- generateBundle
4.5.1 路径解析: resolveId
- resolveId 当代码中出现 import xxx from 'module-a' 时,它负责确定这个 module-a 到底在哪里。
- 返回值为 null 时,会默认交给下一个插件的 resolveId 钩子处理。
- 返回值为 string 时,则停止后续插件的处理。
4.5.2 load
import { readFileSync } from 'fs'
import { extname } from 'path'
// 支持的图片类型及其MIME类型
const DEFAULT_MIME_TYPES = {
'.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg',
'.png': 'image/png',
'.gif': 'image/gif',
'.svg': 'image/svg+xml',
'.webp': 'image/webp'
}
// 默认配置
const DEFAULT_OPTIONS = {
dom: false,
exclude: undefined,
include: undefined,
mimeTypes: DEFAULT_MIME_TYPES
}
export default function rollupPluginImg(opts = {}) {
const options = { ...DEFAULT_OPTIONS, ...opts }
return {
name: 'rollup-plugin-image',
load(id) {
try {
console.log('id', id)
// 1. 检查文件扩展名是否匹配
const ext = extname(id)
const mime = options.mimeTypes[ext]
console.log('ext', ext)
console.log('mime', mime)
// 如果不是图片类型,返回 null
if (!mime) return null
// 2. 检查包含/排除规则
if (options.exclude && options.exclude.test(id)) return null
if (options.include && !options.include.test(id)) return null
// 3. 读取文件内容
const isSvg = mime === 'image/svg+xml'
const format = isSvg ? 'utf-8' : 'base64'
const source = readFileSync(id, format).replace(/[\r\n]+/gm, '')
// 4. 生成Data URI
const dataUri = `data:${mime};${format},${source}`
// 5. 根据配置生成不同的导出代码
const code = options.dom ? generateDomCode(dataUri) : generateConstCode(dataUri)
return code.trim()
} catch (error) {
// 6. 错误处理
this.warn(`Failed to load image ${id}: ${error.message}`)
return null
}
}
}
}
// 生成DOM元素的代码
function generateDomCode(dataUri) {
return `
var img = new Image();
img.src = '${dataUri}';
export default img;
`
}
// 生成常量导出的代码
function generateConstCode(dataUri) {
return `export default '${dataUri}';`
}
- load 作用通过resolveId解析后的路径加载模块
- 如果返回值为 null,则交给下一个插件处理;
- 如果返回值为 string 或者对象,则终止后续插件的处理,如果是对象可以包含 SourceMap
4.5.3 代码转化 transform
transform 异步串行钩子,作用是对加载后的模块内容进行自定义的转换
- 单个模块加载
- 当前模块信息 有官方插件@rollup/plugin-replace 可以替换文件目标字符串
// rollup.config.js
import replace from '@rollup/plugin-replace'
export default {
plugins: [
replace({
__VERSION__: '"1.0.0"', // 注意:要替换为字符串需要额外加引号
__DEV__: false, // 可以直接替换布尔值
'process.env.NODE_ENV': JSON.stringify('production')
})
]
}
import MagicString from 'magic-string'
//高效操作字符串并生成 SourceMap 的 JS 库
//id -当前文件路径
//options - 替换配置
//code - 原始代码
//magicString - MagicString 对象
function executeReplacement(code, id, options) {
const magicString = new MagicString(code)
// 2. 遍历所有需要替换的键值对
Object.entries(options).forEach(([key, value]) => {
// 确保key是有效的标识符
const escapedKey = key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
const pattern = new RegExp(escapedKey, 'g')
let match
// 3. 查找所有匹配项
while ((match = pattern.exec(code))) {
const start = match.index
const end = start + match[0].length
// 确保替换值是字符串,并处理引号情况
const stringValue =
typeof value === 'string'
? value.startsWith('"') || value.startsWith("'")
? value // 已经是带引号的字符串
: `'${value}'` // 已经是带引号的字符串
: JSON.stringify(value) // 其他类型转为JSON字符串
//执行替换
magicString.overwrite(start, end, stringValue)
}
})
return {
code: magicString.toString(),
map: magicString.generateMap()
}
}
//两个钩子都执行 原始模块中的代码可能被其他插件修改 最终组合后的代码最好也执行一次
export default function rollupPluginReplace(options = {}) {
return {
name: 'rollup-plugin-replace',
transform(code, id) {
return executeReplacement(code, id, options)
},
renderChunk(code, chunk) {
return executeReplacement(code, chunk.fileName, options)
}
}
}
4.5.4 renderChunk
- 整个模块转化完成 整个chunk元信息 可以全局优化 统一处理
export default function productionClean() {
return {
name: 'production-clean',
renderChunk(code) {
if (process.env.NODE_ENV === 'production') {
return code.replace(/console\.(log|warn|info)\(.*?\);/g, '')
}
if (chunk.isEntry) {
return `
const start = performance.now();
${code}
console.log('执行耗时:', performance.now() - start);
`
}
return code
}
}
}
4.5.5 generateBundle
打败产物最终加工站 触发时机所有代码转化完成后 写入磁盘前 可以通过this.emitFile()添加文件 可以生成html 分析包大小 版本信息
eg 生成版本信息
// version-plugin.js
export default function rollupPluginVersion() {
const version = Date.now() // 使用时间戳作为版本号
return {
name: 'rollup-plugin-version',
generateBundle(_, bundle) {
const manifest = {}
// 1. 给JS文件添加版本号并收集文件信息
Object.entries(bundle).forEach(([fileName, file]) => {
// 只处理JS文件
if (fileName.endsWith('.js')) {
// 创建带版本号的新文件名
const newName = fileName.replace('.js', `.${version}.js`)
// 添加新版本文件到bundle
bundle[newName] = file
// 从bundle中移除旧文件
delete bundle[fileName]
// 更新manifest使用新文件名
fileName = newName
}
// 记录文件信息到manifest
manifest[fileName] = {
size: file.code?.length || file.source?.length || 0,
type: file.type,
fileName: fileName
}
})
// 3. 生成manifest文件
this.emitFile({
type: 'asset',
fileName: 'manifest.json',
source: JSON.stringify(
{
version,
buildTime: new Date().toISOString(),
files: manifest
},
null,
2
)
})
}
}
}
eg 生成html文件
export default function rollupPluginHtml() {
return {
name: 'rollup-plugin-html',
async generateBundle(outputOptions, bundle) {
// 1. 筛选需要注入的JS文件
const jsFiles = Object.keys(bundle).filter((name) => name.endsWith('.js'))
// 2. 生成HTML内容
let html = `<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>My App</title>
</head>
<body>
<div id="app"></div>`
// 3. 注入JS脚本
jsFiles.forEach((file) => {
html += `\n <script src="${file}"></script>`
})
html += '\n</body>\n</html>'
// 4. 输出HTML文件
this.emitFile({
type: 'asset',
fileName: 'index.html',
source: html
})
}
}
}
- bundle对象结构
interface Bundle {
[fileName: string]: {
type: 'chunk' | 'asset'; // 文件类型
name?: string; // chunk名称
fileName?: string; // 输出文件名
code?: string; // JS代码内容
source?: string | Buffer; // 资源内容
// ...其他元信息
}
}
- emitFile方法
this.emitFile({
type: 'asset', // 文件类型(asset/chunk)
name: 'file-name', // 资源名称
fileName: 'path/file', // 输出路径
source: '内容' // 文件内容
})
5.rollup 组件库打包
graph TD
A[入口文件] --> B[resolve]
B --> C[commonjs]
C --> D[typescript]
D --> E[postcss]
E --> F[输出文件]
B[resolve] --> B1[解析第三方模块]
C[commonjs] --> C1[转换CommonJS为ESM]
D[typescript] --> D1[编译TypeScript]
E[postcss] --> E1[处理CSS]
E1 --> E2[CSS模块化]
E1 --> E3[提取CSS文件]
import postcss from 'rollup-plugin-postcss'
import typescript from '@rollup/plugin-typescript'
import resolve from '@rollup/plugin-node-resolve'
import commonjs from '@rollup/plugin-commonjs'
import replace from '@rollup/plugin-replace'
/** @type {import("rollup").RollupOptions} */
export default {
input: 'src/index.ts',
external: ['react', 'react-dom'],
output: [
{
file: 'dist/esm.js',
format: 'esm'
},
{
file: 'dist/cjs.js',
format: 'cjs'
},
{
file: 'dist/umd.js',
name: 'ivy',
format: 'umd',
globals: {
react: 'React',
'react-dom': 'ReactDOM'
}
}
],
plugins: [
resolve(),
commonjs(),
typescript({
tsconfig: 'tsconfig.json'
}),
postcss({
extract: true,
extract: 'index.css'
}),
replace({
'process.env.NODE_ENV': '"production"'
})
]
}
6. 代码分割
6.1自动分割
动态导入
// 动态导入示例(点菜时才要食谱)
export function cookDish() {
import('./recipe.js').then(module => {
console.log('开始做:', module.dishName)
})
}
6.2.手动分割
manualChunks配置
// rollup.config.js
export default {
output: {
manualChunks: {
// 把lodash单独打包
'my-lodash': ['lodash'],
// 把utils目录打包在一起
'my-utils': ['src/utils/*']
}
}
}
6.3 配置输出文件名字
// rollup.config.js
export default {
output: {
chunkFileNames: 'chunks/[name]-[hash].js',
entryFileNames: '[name].js'
}
}