rullup

120 阅读9分钟

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 执行方式AsyncSync同步异步
  • 执行顺序分类
    • 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'
  }
}