Rollup
运行机制
Rollup 的设计理念是 "编译时打包",适合构建库或需要高效 Tree-shaking 的应用。与 Webpack 相比,它更专注于 ES 模块的优化,生成的代码更简洁。
- 读取配置文件,确定入口文件和输出格式。
- 构建模块依赖图,分析模块之间的关系。
- 应用插件进行转换和优化。
- 生成输出文件,支持多种格式(如 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?
- 模块包装机制:
- Webpack 将每个模块包装在函数中(如 webpack_require),导致静态分析难以跨越模块边界。
- 动态导入兼容性:
- 为支持动态导入(如 import('./module').then(...)),Webpack 需要保留完整的模块结构。
- v副作用保守处理:
- 即使 package.json 中标记了 sideEffects: false,Webpack 仍可能保留未使用的导出,以防运行时依赖。
举个例子:
- 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...');
};- math.js
javascript
// 间接导出 utils.js 的所有内容
export * from './utils.js';
// 额外导出一个组合函数
export const calculate = (a, b) => add(a, b) * multiply(a, b);- 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 插件通过 钩子函数 介入打包流程的不同阶段,类似 “事件监听” 模式。核心流程包括:
- 模块解析:从入口文件开始,递归解析所有依赖模块。
- 模块转换:对每个模块的内容进行转换(如编译 TypeScript、处理 CSS)。
- 生成依赖图:构建模块间的依赖关系。
- 代码生成:根据依赖图生成最终打包文件。
插件可以在上述任一阶段插入自定义逻辑,修改输入或输出内容。
具体包括以下钩子
- 构建钩子(Build Hooks) 在模块解析和转换阶段执行:
- resolveId:拦截模块路径解析,可自定义模块定位逻辑(如处理别名)。
- load:加载模块内容,可替代默认文件读取逻辑(如从数据库加载)。
- transform:转换模块内容(如编译 TypeScript、压缩代码)。(支持异步)
- buildEnd:所有模块处理完成后执行(如清理临时文件)。
- 输出钩子(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 仅支持压缩和转译部分并发,可能更慢。