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 处理器抑制特定警告。最佳实践是重构代码以消除循环依赖。

这篇文章有帮助吗?

保持更新

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

无垃圾邮件,随时退订。

合作推荐

赞助这篇文章

把你的产品放到这个开发者主题旁边,并追踪点击效果。

咨询文章赞助

试试这些相关工具

本站使用 Cookie 进行流量分析与广告展示。继续浏览即视为同意。 隐私政策