DevToolBox免费
博客

Rollup.js 完全指南:现代 JavaScript 模块打包

18 min read作者 DevToolBox Team

Rollup 是一个 JavaScript 模块打包器,将小段代码编译成更大、更复杂的包。它是 JavaScript 库的首选打包器,以产生干净、高效的输出和出色的 tree-shaking 而闻名。Rollup 原生使用 ES 模块标准,这意味着它可以静态分析你的导入和导出,比依赖 CommonJS 的打包器更有效地消除死代码。如果你正在为 npm 构建库、框架组件或精简的应用包,Rollup 是一个值得掌握的出色工具。

TL;DR

Rollup 是一个针对库和精简应用优化的模块打包器。它提供一流的 tree-shaking,输出 ESM/CJS/UMD/IIFE,并拥有丰富的插件生态系统。当你需要干净、高效的包时使用 Rollup -- 特别是 npm 包。对于需要 HMR 的复杂应用,考虑使用 Vite(其生产构建底层使用 Rollup)。

关键要点

  • 由于原生 ES 模块支持和卓越的 tree-shaking,Rollup 在主要打包器中产生最小的包。
  • 插件生态系统(@rollup/plugin-*)覆盖所有需求:TypeScript、CommonJS 转换、Babel、压缩等。
  • 单一配置的多输出格式(ESM、CJS、UMD、IIFE)使其成为库作者的理想选择。
  • 带动态导入的代码分割支持应用构建的延迟加载。
  • Vite 使用 Rollup 进行生产构建,因此 Rollup 插件在两个生态系统中都可用。

安装和入门

Rollup 可以全局安装或作为项目依赖安装。推荐的方法是本地安装并通过 npx 或 package.json 脚本使用。Rollup 支持 CLI 和配置文件两种用法。

# Install Rollup globally
npm install -g rollup

# Or as a dev dependency (recommended)
npm install --save-dev rollup

# Verify installation
npx rollup --version

# Bundle a file (CLI)
npx rollup src/index.js --file dist/bundle.js --format esm

# Bundle with a config file
npx rollup --config rollup.config.mjs

# Watch mode
npx rollup --config --watch

配置文件

rollup.config.mjs 文件是配置 Rollup 的标准方式。它导出一个配置对象或配置数组。使用 .mjs 扩展名确保文件被视为 ES 模块。你可以定义单个或多个输出,这对需要同时发布 ESM、CommonJS 和 UMD 构建的库作者非常有用。

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

// Multiple outputs (library authors)
export default {
  input: 'src/index.js',
  output: [
    {
      file: 'dist/my-lib.esm.js',
      format: 'esm',
      sourcemap: true,
    },
    {
      file: 'dist/my-lib.cjs.js',
      format: 'cjs',
      sourcemap: true,
    },
    {
      file: 'dist/my-lib.umd.js',
      format: 'umd',
      name: 'MyLib',
      sourcemap: true,
    },
  ],
};

核心插件

Rollup 在 @rollup/ 范围下拥有丰富的官方插件生态系统。最常用的插件包括:node-resolve(解析 node_modules 导入)、commonjs(将 CJS 转换为 ESM)、terser(压缩)、babel(转译)、typescript(TS 编译)和 json(导入 JSON 文件)。

// rollup.config.mjs with essential plugins
import resolve from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import terser from '@rollup/plugin-terser';
import babel from '@rollup/plugin-babel';
import typescript from '@rollup/plugin-typescript';
import json from '@rollup/plugin-json';

export default {
  input: 'src/index.ts',
  output: {
    file: 'dist/bundle.js',
    format: 'esm',
    sourcemap: true,
  },
  plugins: [
    // Resolve node_modules imports
    resolve({
      browser: true,
      preferBuiltins: false,
    }),

    // Convert CommonJS modules to ESM
    commonjs(),

    // Compile TypeScript
    typescript({
      tsconfig: './tsconfig.json',
    }),

    // Transpile with Babel
    babel({
      babelHelpers: 'bundled',
      presets: [['@babel/preset-env', {
        targets: '>0.25%, not dead',
      }]],
      exclude: 'node_modules/**',
    }),

    // Import JSON files
    json(),

    // Minify for production
    terser({
      compress: { drop_console: true },
    }),
  ],
};

Tree-Shaking(死代码消除)

Tree-shaking 是 Rollup 的标志性功能。因为 Rollup 原生理解 ES 模块的 import/export 语句,它可以静态确定哪些导出被使用并从最终包中移除未使用的代码。在 package.json 中设置 "sideEffects: false" 有助于 Rollup 消除整个未使用的模块。

// math.js - only used functions are included
export function add(a, b) {
  return a + b;
}

export function subtract(a, b) {
  return a - b;
}

export function multiply(a, b) {
  return a * b;
}

// index.js - only imports add
import { add } from './math.js';
console.log(add(2, 3));

// Rollup output: subtract and multiply are removed!
// This is tree-shaking in action.

// Mark side-effect-free modules in package.json:
// {
//   "sideEffects": false
// }
//
// Or specify files with side effects:
// {
//   "sideEffects": ["./src/polyfills.js", "*.css"]
// }

代码分割和动态导入

Rollup 通过多入口点和动态 import() 表达式支持代码分割。使用代码分割时,输出必须使用 "dir" 选项而非 "file"。Rollup 自动为多个入口点共用的代码创建共享块。

// rollup.config.mjs - code splitting with multiple entry points
export default {
  input: {
    main: 'src/main.js',
    admin: 'src/admin.js',
    vendor: 'src/vendor.js',
  },
  output: {
    dir: 'dist',
    format: 'esm',
    entryFileNames: '[name]-[hash].js',
    chunkFileNames: 'chunks/[name]-[hash].js',
    manualChunks: {
      // Force specific modules into named chunks
      lodash: ['lodash-es'],
      react: ['react', 'react-dom'],
    },
  },
};

// Dynamic import for lazy loading
// src/main.js
async function loadChart() {
  const { Chart } = await import('./chart.js');
  return new Chart('#canvas');
}

document.getElementById('btn').addEventListener('click', loadChart);

输出格式:ESM、CJS、UMD 和 IIFE

Rollup 支持四种主要输出格式。ESM 使用 import/export 语法,是浏览器和 Node.js 的现代标准。CJS 使用 require() 用于 Node.js 兼容性。UMD 在浏览器、Node.js 和 AMD 中通用。IIFE 用于直接 script 标签引入。

// ESM - Modern browsers and Node.js (recommended)
// Uses import/export syntax
{
  output: {
    file: 'dist/lib.esm.js',
    format: 'esm',     // or 'es'
  }
}

// CommonJS - Node.js require() style
{
  output: {
    file: 'dist/lib.cjs.js',
    format: 'cjs',
    exports: 'auto',   // 'default', 'named', 'none'
  }
}

// UMD - Universal (browsers + Node.js + AMD)
{
  output: {
    file: 'dist/lib.umd.js',
    format: 'umd',
    name: 'MyLibrary',  // global variable name
    globals: {
      react: 'React',
      'react-dom': 'ReactDOM',
    },
  },
  external: ['react', 'react-dom'],
}

// IIFE - Immediately Invoked Function Expression
// For <script> tags, no module loader needed
{
  output: {
    file: 'dist/lib.iife.js',
    format: 'iife',
    name: 'MyLibrary',
  }
}

库打包最佳实践

Rollup 擅长为 npm 打包库。关键原则:将所有依赖外部化、生成 ESM 和 CJS 输出、发出 TypeScript 声明文件、正确配置 package.json exports 映射。

// Complete library bundling config
// rollup.config.mjs
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')
);

export default {
  input: 'src/index.ts',
  // Externalize peer dependencies
  external: [
    ...Object.keys(pkg.peerDependencies || {}),
    ...Object.keys(pkg.dependencies || {}),
  ],
  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',
    },
  ],
  plugins: [
    resolve(),
    commonjs(),
    typescript({ declaration: true, declarationDir: 'dist/types' }),
    terser(),
  ],
};

// Matching package.json fields:
// {
//   "main": "dist/index.cjs.js",
//   "module": "dist/index.esm.js",
//   "types": "dist/types/index.d.ts",
//   "exports": {
//     ".": {
//       "import": "./dist/index.esm.js",
//       "require": "./dist/index.cjs.js",
//       "types": "./dist/types/index.d.ts"
//     }
//   },
//   "files": ["dist"]
// }

Watch 模式和开发工作流

Rollup 包含内置的 watch 模式,当源文件更改时自动重建。你可以配置要监视的文件、设置重建延迟,并使用编程 API 进行自定义构建管道。

// rollup.config.mjs with watch options
export default {
  input: 'src/index.js',
  output: {
    file: 'dist/bundle.js',
    format: 'esm',
  },
  watch: {
    include: 'src/**',
    exclude: 'node_modules/**',
    clearScreen: false,
    // Rebuild delay (ms)
    buildDelay: 100,
  },
};

// package.json scripts
// {
//   "scripts": {
//     "build": "rollup -c",
//     "dev": "rollup -c --watch",
//     "build:prod": "NODE_ENV=production rollup -c"
//   }
// }

// Programmatic API
import { watch } from 'rollup';

const watcher = watch({
  input: 'src/index.js',
  output: { file: 'dist/bundle.js', format: 'esm' },
});

watcher.on('event', (event) => {
  if (event.code === 'BUNDLE_END') {
    console.log('Built in ' + event.duration + 'ms');
  }
  if (event.code === 'ERROR') {
    console.error(event.error);
  }
});

// Close watcher when done
// watcher.close();

高级配置

高级 Rollup 配置通常包括路径别名、环境变量替换、条件插件、包可视化、自定义警告处理器和 banner/footer 注入。

// Advanced rollup.config.mjs
import resolve from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import terser from '@rollup/plugin-terser';
import replace from '@rollup/plugin-replace';
import alias from '@rollup/plugin-alias';
import { visualizer } from 'rollup-plugin-visualizer';

const isProduction = process.env.NODE_ENV === 'production';

export default {
  input: 'src/index.js',
  output: {
    dir: 'dist',
    format: 'esm',
    sourcemap: !isProduction,
    banner: '/* My Library v1.0.0 */',
  },
  plugins: [
    // Path aliases (like Webpack resolve.alias)
    alias({
      entries: [
        { find: '@', replacement: './src' },
        { find: '@utils', replacement: './src/utils' },
      ],
    }),

    // Environment variable replacement
    replace({
      preventAssignment: true,
      'process.env.NODE_ENV': JSON.stringify(
        process.env.NODE_ENV || 'development'
      ),
    }),

    resolve(),
    commonjs(),

    // Only minify in production
    isProduction && terser(),

    // Bundle visualization
    isProduction && visualizer({
      filename: 'stats.html',
      gzipSize: true,
    }),
  ].filter(Boolean),

  // Suppress specific warnings
  onwarn(warning, warn) {
    if (warning.code === 'CIRCULAR_DEPENDENCY') return;
    warn(warning);
  },
};

编写自定义插件

Rollup 插件遵循简单的基于钩子的 API。最重要的钩子包括:buildStart、resolveId、load、transform 和 generateBundle。

// Writing a custom Rollup plugin
function myPlugin(options = {}) {
  return {
    name: 'my-plugin',

    // Called on each build start
    buildStart() {
      console.log('Build started...');
    },

    // Resolve custom import paths
    resolveId(source) {
      if (source === 'virtual:config') {
        return source; // handle this id
      }
      return null; // let other plugins handle it
    },

    // Provide content for resolved ids
    load(id) {
      if (id === 'virtual:config') {
        return 'export default { version: "1.0" };';
      }
      return null;
    },

    // Transform individual modules
    transform(code, id) {
      if (id.endsWith('.css')) {
        return {
          code: 'export default ' + JSON.stringify(code),
          map: null,
        };
      }
    },

    // Post-processing the final bundle
    generateBundle(options, bundle) {
      for (const [name, chunk] of Object.entries(bundle)) {
        console.log(name + ': ' + chunk.code.length + ' bytes');
      }
    },
  };
}

Rollup vs Webpack vs Vite

每个打包器有不同的优势。Rollup 产生最干净的输出,最适合库。Webpack 拥有最丰富的生态系统,最适合复杂应用。Vite 在生产中使用 Rollup,在开发中使用 esbuild,提供最佳开发体验。

FeatureRollupWebpackVite
Primary use caseLibrariesApplicationsApplications + DX
Tree-shakingExcellent (native ESM)Good (since v5)Excellent (Rollup)
Dev server / HMRPlugin neededBuilt-in (WDS)Built-in (instant)
Output cleanlinessVery cleanWrapped in runtimeClean (Rollup)
Config complexitySimpleComplexMinimal
Build speedFastModerateVery fast (esbuild)
Code splittingYesYes (advanced)Yes (Rollup)
Plugin ecosystemModerateVery largeGrowing (Rollup compat)
Bundle sizeSmallestLargerSmall (Rollup)

最佳实践

  • 构建库时始终将 peer 依赖外部化。将 React 等 peer 依赖打包到库中会导致消费应用中出现重复副本。
  • 使用 package.json "exports" 字段的 "import" 和 "require" 条件来支持 ESM 和 CJS 消费者。
  • 开发期间启用 sourcemap,但考虑在生产构建中禁用以减小输出大小。
  • 当库没有副作用时,在 package.json 中设置 "sideEffects: false" 以启用最大化 tree-shaking。
  • 使用 rollup-plugin-visualizer 分析包大小并识别优化机会。
  • 优先使用 @rollup/plugin-typescript 而非 rollup-plugin-ts -- 官方插件维护更好、更可靠。

常见问题

什么时候应该使用 Rollup 而不是 Webpack?

构建 JavaScript 库、框架组件或任何发布到 npm 的包时使用 Rollup。Rollup 产生更干净、更小的包,具有更好的 tree-shaking。对于复杂应用使用 Webpack。对于新应用项目,Vite(底层使用 Rollup)通常是最佳选择。

Rollup 支持 TypeScript 吗?

支持。使用 @rollup/plugin-typescript 编译 TypeScript 文件。它与 tsconfig.json 集成并可以生成声明文件(.d.ts)。

Rollup 的 tree-shaking 如何工作?

Rollup 静态分析 ES 模块的 import/export 语句来确定哪些导出被使用。未使用的导出及其关联代码会从最终包中移除。

Rollup 能处理 CSS 和图片吗?

可以,通过插件。使用 rollup-plugin-postcss 处理 CSS,@rollup/plugin-image 处理图片文件。

ESM 和 CJS 输出有什么区别?

ESM 使用 import/export 语法,支持 tree-shaking 和静态分析。CJS 使用 require()/module.exports,是传统 Node.js 格式。ESM 是现代标准,应作为主要格式。

Vite 和 Rollup 是什么关系?

Vite 使用 Rollup 作为其生产打包器。开发期间 Vite 使用 esbuild 进行快速模块转换。Rollup 插件通常与 Vite 兼容。

Rollup 可以用于应用打包吗?

可以,但有注意事项。Rollup 可以处理代码分割和动态导入,但缺少内置 HMR 和开发服务器功能。对于应用,Vite 提供更好的开发体验。

如何处理 Rollup 中的循环依赖?

Rollup 支持 ES 模块中的循环依赖,但会发出警告。你可以使用配置中的 onwarn 处理器抑制特定警告。最佳实践是重构代码以消除循环依赖。

相关工具

𝕏 Twitterin LinkedIn
这篇文章有帮助吗?

保持更新

获取每周开发技巧和新工具通知。

无垃圾邮件,随时退订。

试试这些相关工具

{ }JSON FormatterJSON Validator

相关文章

esbuild 完全指南:最快的 JavaScript 打包工具

掌握 esbuild 超快打包,包括 CLI、JavaScript API、插件、加载器、压缩与生产优化。

Prettier 完全指南:统一代码格式化的最佳实践

掌握 Prettier 代码格式化工具,包括配置、ESLint 集成、编辑器设置、预提交钩子与 CI/CD 流水线。

TypeScript 类型守卫:运行时类型检查完全指南

掌握 TypeScript 类型守卫:typeof、instanceof、in、自定义类型守卫和可辨识联合。