Skip to content

Rollup ​

运行机制 ​

Rollup 的设计理念是 "编译时打包",适合构建库或需要高效 Tree-shaking 的应用。与 Webpack 相比,它更专注于 ES 模块的优化,生成的代码更简洁。

  1. 读取配置文件,确定入口文件和输出格式。
  2. 构建模块依赖图,分析模块之间的关系。
  3. 应用插件进行转换和优化。
  4. 生成输出文件,支持多种格式(如 ES 模块、CommonJS、UMD 等)。

Rollup vs Webpack vs Vite ​

模块处理 ​

  • Rollup:
    • 原生支持 ES 模块(ESM),对 CommonJS 模块需通过 @rollup/plugin-commonjs 转换。
    • 基于静态分析解析模块依赖,不支持动态 require(如 require(someVar))。
  • Webpack:
    • 支持 ESM、CommonJS、AMD 等所有模块规范,通过内置逻辑统一处理。
    • 基于动态依赖图解析(运行时分析),支持动态导入(import())和动态 require。
  • Vite:
    • 开发环境:直接使用浏览器原生 ESM(不打包,依赖浏览器解析 import)。
    • 生产环境:基于 Rollup 打包,支持 ESM 和 CommonJS(通过 Rollup 插件转换)。

Tree-shaking ​

  • Rollup:
    • 强大的 Tree-shaking 能力,基于静态分析移除未使用的代码。
    • 生成的代码更小、更高效。
  • Weabpack:
    • Tree-shaking 依赖于 ES 模块语法(如 import/export),但不如 Rollup 高效。
    • 需要配置mode 为 production 才能启用。
  • Vite:
    • 生产环境使用 Rollup 进行 Tree-shaking,效果类似于 Rollup。
    • 开发环境不进行打包,依赖浏览器原生 ESM。

非JS文件处理 ​

  • Rollup:
    • 原生仅支持 JS/ESM,非 JS 文件(CSS、图片、JSON 等)需通过插件处理(如 rollup-plugin-postcss 处理 CSS,@rollup/plugin-url 处理图片)。
    • 插件生态专注于 “库开发” 场景,对复杂资源(如视频、字体)的处理插件较少。
  • Webpack:
    • 内置对各种资源的处理能力(CSS、图片、字体等),通过 loader 机制处理。
    • 插件生态丰富,支持复杂资源的处理。
  • Vite
    • 开发环境:通过原生 ESM 配合插件处理非 JS 文件(如 @vitejs/plugin-react 处理 JSX,内置 CSS 处理)。
    • 生产环境:复用 Rollup 插件生态(如 rollup-plugin-svg 处理 SVG)

热更新 ​

  • Rollup:
    • 原生不支持热更新(HMR),需通过 rollup-plugin-livereload 等插件实现简单刷新,开发体验较弱。
    • 启动速度中等(需全量打包),适合库开发(开发频率低于应用)。
  • Webpack:
    • 支持 HMR,但依赖 “打包后更新”(需重新构建修改的模块及依赖链),大型项目启动和热更新速度较慢。
    • 可通过 webpack-dev-server 或 webpack serve 启动开发服务器,配置复杂。
  • Vite:
    • 启动速度极快:开发环境不打包,直接基于原生 ESM 加载文件,避免全量构建。
    • 热更新(HMR)高效:只更新修改的模块,无需重新打包依赖链,支持 React/Vue 等框架的组件级 HMR。
    • 内置开发服务器,配置简单(默认支持 ESM、HMR、CSS 处理等)。

代码分割 ​

  • Rollup:
    • 支持代码分割,但需通过动态导入(import()) 触发,主要用于生成多 chunk(如库的按需加载模块)。
    • 分割逻辑较简单,不支持 Webpack 级别的 “自动分割公共依赖”。需要通过插件或手动配置实现。
  • Webpack:
    • 动态导入分割(import());
    • 自动分割公共依赖(splitChunks 配置);
    • 入口分割(多入口应用)。
    • 分割逻辑灵活,适合大型应用的性能优化(减少重复代码)。
  • Vite:
    • 开发环境:基于原生 ESM 天然支持 “按需加载”(无需打包,浏览器直接请求模块)。
    • 生产环境:依赖 Rollup 的代码分割能力(与 Rollup 一致,通过动态导入触发)。

Rollup 怎么实现的Tree-shaking ​

基于静态分析的死代码消除

1. 构建模块依赖图 ​

从入口文件开始,递归解析所有 import 和 export 语句,构建无环的依赖关系图。

2. 标记活代码(Live Code) ​

从入口模块出发,反向追踪所有被引用的导出(exports)。 未被追踪到的导出即为 “死代码”(Dead Code)。

3. 消除死代码 ​

在生成最终代码时,跳过所有未被标记的导出和语句。

为什么Rollup 能做到更彻底的 Tree-shaking? ​

  1. 模块包装机制:
    • Webpack 将每个模块包装在函数中(如 webpack_require),导致静态分析难以跨越模块边界。
  2. 动态导入兼容性:
    • 为支持动态导入(如 import('./module').then(...)),Webpack 需要保留完整的模块结构。
  3. v副作用保守处理:
    • 即使 package.json 中标记了 sideEffects: false,Webpack 仍可能保留未使用的导出,以防运行时依赖。

举个例子: ​

  1. utils.js
javascript
export const add = (a, b) => a + b;
export const subtract = (a, b) => a - b;
export const multiply = (a, b) => a * b;

// 模拟有副作用的函数(实际未被使用)
export const init = () => {
  console.log('Initializing...');
};
  1. math.js
javascript
// 间接导出 utils.js 的所有内容
export * from './utils.js';

// 额外导出一个组合函数
export const calculate = (a, b) => add(a, b) * multiply(a, b);
  1. main.js
javascript
// 仅引用 add 函数
import { add } from './math.js';

console.log(add(1, 2));

Rollup 打包结果 ​

js
// dist/bundle.js
const add = (a, b) => a + b;
console.log(add(1, 2));

Webpack 打包结果 ​

js
// dist/main.js(简化版)
(() => {
  // utils.js 模块
  const utils = {
    add: (a, b) => a + b,
    subtract: (a, b) => a - b,
    multiply: (a, b) => a * b,
    init: () => console.log('Initializing...')
  };

  // math.js 模块
  const math = {
    ...utils, // 保留所有导出,即使未被使用
    calculate: (a, b) => utils.add(a, b) * utils.multiply(a, b)
  };

  // main.js 模块
  console.log(math.add(1, 2));
})();

Rollup 的插件机制 ​

Rollup 插件通过 钩子函数 介入打包流程的不同阶段,类似 “事件监听” 模式。核心流程包括:

  1. 模块解析:从入口文件开始,递归解析所有依赖模块。
  2. 模块转换:对每个模块的内容进行转换(如编译 TypeScript、处理 CSS)。
  3. 生成依赖图:构建模块间的依赖关系。
  4. 代码生成:根据依赖图生成最终打包文件。

插件可以在上述任一阶段插入自定义逻辑,修改输入或输出内容。

具体包括以下钩子

  1. 构建钩子(Build Hooks) 在模块解析和转换阶段执行:
    • resolveId:拦截模块路径解析,可自定义模块定位逻辑(如处理别名)。
    • load:加载模块内容,可替代默认文件读取逻辑(如从数据库加载)。
    • transform:转换模块内容(如编译 TypeScript、压缩代码)。(支持异步)
    • buildEnd:所有模块处理完成后执行(如清理临时文件)。
  2. 输出钩子(Output Hooks) 在生成最终代码阶段执行:
    • generateBundle:生成打包文件前执行,可修改输出内容。
    • writeBundle:文件写入磁盘后执行(如生成额外的元数据文件)。

一个简单的插件示例 ​

javascript
// rollup-plugin-markdown.js
import { createFilter } from '@rollup/pluginutils';
import marked from 'marked';

export default function markdown(options = {}) {
  const filter = createFilter(options.include || /\.md$/, options.exclude);
  
  return {
    name: 'markdown', // 插件名称(用于错误提示)
    
    // 转换钩子:将 Markdown 转换为 JS 模块
    transform(code, id) {
      if (!filter(id)) return null; // 只处理 .md 文件
      
      // 将 Markdown 转换为 HTML
      const html = marked(code);
      
      // 返回转换后的 JS 模块代码
      return {
        code: `export default ${JSON.stringify(html)};`,
        map: { mappings: '' }, // 可选:提供 sourcemap
      };
    },
  };
}

Rollup 可能存在的问题 ​

1. CommonJS 模块处理不彻底 ​

使用 @rollup/plugin-commonjs 转换 CommonJS 模块。

2. 插件生态不如 Webpack 丰富 ​

3. 动态导入支持有限 ​

在处理动态导入时,Rollup 需要静态分析模块依赖,可能无法处理复杂的动态 require 语句。
Rollup 的代码分割依赖 manualChunks 配置,相比 Webpack 的 splitChunks 缺乏自动化,需手动指定分割规则,否则可能导致重复打包:

4. 构建可能更慢 ​

rollup 仅支持压缩和转译部分并发,可能更慢。