Rollup 与库打包
问题
Rollup 是什么?它和 Webpack 有什么区别?如何使用 Rollup 打包一个 npm 库?
答案
Rollup 是一个 JavaScript 模块打包器,专注于将小段代码编译为更大更复杂的库或应用。它的核心设计理念是 面向 ES Module,天然支持 Tree Shaking,是目前打包 JavaScript 库的首选工具。Vue、React、Svelte、D3 等知名开源项目都使用 Rollup 进行打包。
Rollup = 面向库开发的 ESM 打包器,以 Tree Shaking 和干净的产物著称。
核心概念
Rollup 的配置围绕以下几个核心概念展开:
| 概念 | 说明 |
|---|---|
| input | 入口文件路径,Rollup 从这里开始分析依赖图 |
| output | 输出配置,包括文件路径、格式(format)、全局变量名等 |
| format | 输出格式:esm(ES Module)、cjs(CommonJS)、umd(通用)、iife(立即执行) |
| external | 外部依赖,不打包进产物,由使用者提供 |
| plugins | 插件,扩展 Rollup 的功能(如解析 node_modules、编译 TypeScript 等) |
// ESM — 现代浏览器和打包器使用
export function add(a: number, b: number): number {
return a + b
}
// CJS — Node.js 使用
module.exports = {
add: function (a, b) {
return a + b
},
}
// UMD — 兼容 AMD、CJS、全局变量,适合 CDN 直接引入
;(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined'
? factory(exports)
: typeof define === 'function' && define.amd
? define(['exports'], factory)
: factory((global.MyLib = {}))
})(this, function (exports) {
exports.add = function (a, b) {
return a + b
}
})
// IIFE — 自执行函数,适合 <script> 标签直接引入
var MyLib = (function () {
function add(a, b) {
return a + b
}
return { add }
})()
- 库打包:同时输出
esm+cjs,确保现代和传统环境都能使用 - 需要 CDN 引入:额外输出
umd或iife格式 - 纯 Node.js 工具:只需输出
cjs(或仅esm,如面向 Node 18+)
Rollup vs Webpack 对比
| 维度 | Rollup | Webpack |
|---|---|---|
| 设计目标 | 面向库打包,输出干净的 ESM 代码 | 面向应用打包,处理各种资源类型 |
| Tree Shaking | 原生支持,基于 ESM 静态分析,效果彻底 | 支持,但不如 Rollup 彻底 |
| 代码分割 | 支持(动态 import),但功能较基础 | 成熟强大(SplitChunksPlugin) |
| 产物体积 | 小而干净,无多余运行时代码 | 包含模块加载运行时(__webpack_require__) |
| 产物格式 | 支持 ESM / CJS / UMD / IIFE / AMD | 主要输出自有格式,ESM 输出实验性支持 |
| 静态资源 | 需要插件支持 | 原生 loader 机制,开箱即用 |
| HMR | 不支持(需 Vite 等封装) | 原生支持 |
| CSS 处理 | 需要插件 | 原生 loader 支持 |
| 插件生态 | 精简聚焦 | 丰富庞大 |
| 配置复杂度 | 简单直观 | 配置灵活但复杂 |
| 适用场景 | JS 库、组件库、SDK | Web 应用、大型 SPA、MPA |
- 打包库 / SDK / 组件库 → 选 Rollup(或基于 Rollup 的 tsup、unbuild)
- 打包应用(SPA / MPA) → 选 Webpack 或 Vite
- 两者都需要 → 选 Vite(开发用原生 ESM,生产用 Rollup)
Rollup 配置示例
安装 Rollup
- npm
- Yarn
- pnpm
- Bun
npm install rollup @rollup/plugin-node-resolve @rollup/plugin-commonjs @rollup/plugin-typescript rollup-plugin-terser rollup-plugin-dts -D
yarn add rollup @rollup/plugin-node-resolve @rollup/plugin-commonjs @rollup/plugin-typescript rollup-plugin-terser rollup-plugin-dts --dev
pnpm add rollup @rollup/plugin-node-resolve @rollup/plugin-commonjs @rollup/plugin-typescript rollup-plugin-terser rollup-plugin-dts -D
bun add rollup @rollup/plugin-node-resolve @rollup/plugin-commonjs @rollup/plugin-typescript rollup-plugin-terser rollup-plugin-dts --dev
完整配置(多格式输出)
import { defineConfig } from 'rollup'
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 dts from 'rollup-plugin-dts'
import { readFileSync } from 'fs'
const pkg = JSON.parse(readFileSync('./package.json', 'utf-8'))
// 将 dependencies 和 peerDependencies 标记为外部依赖,不打包进产物
const external = [
...Object.keys(pkg.dependencies || {}),
...Object.keys(pkg.peerDependencies || {}),
]
export default defineConfig([
// 主构建:输出 ESM + CJS + UMD
{
input: 'src/index.ts',
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',
},
{
file: pkg.unpkg, // dist/index.umd.js
format: 'umd',
name: 'MyLib', // 全局变量名
sourcemap: true,
globals: {
// UMD 格式中外部依赖的全局变量映射
react: 'React',
'react-dom': 'ReactDOM',
},
plugins: [terser()], // UMD 产物压缩
},
],
external,
plugins: [
resolve(), // 解析 node_modules
commonjs(), // 转换 CJS 为 ESM
typescript({
tsconfig: './tsconfig.json',
declaration: false, // 类型声明单独处理
}),
],
},
// 类型声明:输出 .d.ts
{
input: 'src/index.ts',
output: {
file: 'dist/index.d.ts',
format: 'esm',
},
plugins: [dts()],
},
])
常用插件
| 插件 | 作用 |
|---|---|
@rollup/plugin-node-resolve | 解析 node_modules 中的第三方模块 |
@rollup/plugin-commonjs | 将 CommonJS 模块转换为 ESM,使其可以被 Rollup 处理 |
@rollup/plugin-typescript | 编译 TypeScript |
@rollup/plugin-babel | 使用 Babel 转译代码 |
@rollup/plugin-json | 允许 import JSON 文件 |
@rollup/plugin-alias | 路径别名 |
@rollup/plugin-replace | 替换代码中的变量(如 process.env.NODE_ENV) |
rollup-plugin-terser | 代码压缩(基于 Terser) |
rollup-plugin-dts | 打包合并 .d.ts 类型声明文件 |
rollup-plugin-visualizer | 产物可视化分析 |
@rollup/plugin-image | 导入图片资源 |
rollup-plugin-postcss | CSS 处理(PostCSS、CSS Modules、Sass 等) |
打包一个纯 TypeScript 库,最常用的三件套是:@rollup/plugin-node-resolve + @rollup/plugin-commonjs + @rollup/plugin-typescript。
Rollup 插件机制
Rollup 的插件通过 钩子函数(Hooks) 介入打包流程。钩子分为两大类:
- Build Hooks(构建钩子):在构建阶段(
rollup.rollup())触发,负责模块解析、加载、转换 - Output Generation Hooks(输出钩子):在生成阶段(
bundle.generate()/bundle.write())触发,负责产物处理
钩子执行顺序
钩子类型
每个钩子还有不同的执行类型:
| 类型 | 说明 | 示例 |
|---|---|---|
| async | 异步钩子,可返回 Promise | resolveId、load、transform |
| first | 多个插件返回值时,取第一个非 null 的值 | resolveId、load |
| sequential | 按顺序执行,前一个插件的返回值传给下一个 | transform |
| parallel | 并行执行,互不影响 | buildStart、buildEnd |
手写一个 Rollup 插件
Rollup 插件本质上就是一个 返回包含钩子函数的对象 的函数。下面以一个"自动注入版本号"插件为例:
import type { Plugin } from 'rollup'
import { readFileSync } from 'fs'
interface VersionPluginOptions {
/** 要替换的占位符,默认 __VERSION__ */
placeholder?: string
}
// 插件就是一个返回对象的函数
export function versionPlugin(options: VersionPluginOptions = {}): Plugin {
const { placeholder = '__VERSION__' } = options
const pkg = JSON.parse(readFileSync('./package.json', 'utf-8'))
return {
// name 是必需的,用于错误信息和调试
name: 'rollup-plugin-version',
// transform 钩子:在每个模块代码被解析后调用
transform(code: string, id: string) {
// 只处理 JS/TS 文件
if (!/\.[jt]sx?$/.test(id)) return null
// 如果代码中不包含占位符,直接跳过
if (!code.includes(placeholder)) return null
// 替换占位符为实际版本号
return {
code: code.replace(
new RegExp(placeholder, 'g'),
JSON.stringify(pkg.version)
),
map: null, // 简单示例省略 sourcemap
}
},
// buildStart 钩子:构建开始时打印信息
buildStart() {
console.log(`📦 Building v${pkg.version}...`)
},
// generateBundle 钩子:在产物生成时,添加一个额外文件
generateBundle() {
// 通过 this.emitFile 向产物中添加文件
this.emitFile({
type: 'asset',
fileName: 'version.txt',
source: pkg.version,
})
},
}
}
使用方式:
import { defineConfig } from 'rollup'
import { versionPlugin } from './plugins/rollup-plugin-version'
export default defineConfig({
input: 'src/index.ts',
output: { file: 'dist/index.js', format: 'esm' },
plugins: [
versionPlugin({ placeholder: '__VERSION__' }),
],
})
// 源码中使用占位符
export const VERSION = __VERSION__ // 构建后会被替换为 "1.0.0"
库打包最佳实践
package.json 的导出字段
正确配置 package.json 中的导出字段是库打包的关键:
{
"name": "my-lib",
"version": "1.0.0",
// 传统字段
"main": "dist/index.cjs.js", // CJS 入口(Node.js require)
"module": "dist/index.esm.js", // ESM 入口(打包器优先使用)
"types": "dist/index.d.ts", // TypeScript 类型声明
"unpkg": "dist/index.umd.js", // CDN(unpkg)
// 现代导出方式(Node.js 12.11+ / 打包器)
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.esm.js",
"require": "./dist/index.cjs.js"
},
"./utils": {
"types": "./dist/utils.d.ts",
"import": "./dist/utils.esm.js",
"require": "./dist/utils.cjs.js"
}
},
// 标识包是否有副作用(影响 Tree Shaking)
"sideEffects": false,
// 指定发布到 npm 的文件
"files": ["dist"],
// 外部化依赖:使用者需自行安装
"peerDependencies": {
"react": ">=18.0.0",
"react-dom": ">=18.0.0"
},
"devDependencies": {
"rollup": "^4.0.0",
"@rollup/plugin-node-resolve": "^15.0.0",
"@rollup/plugin-commonjs": "^25.0.0",
"@rollup/plugin-typescript": "^11.0.0",
"rollup-plugin-terser": "^7.0.0",
"rollup-plugin-dts": "^6.0.0",
"typescript": "^5.0.0"
}
}
| 字段 | 用途 | 使用场景 |
|---|---|---|
main | CJS 入口 | Node.js require() |
module | ESM 入口(非标准但广泛支持) | Webpack、Rollup 等打包器 |
types | 类型声明入口 | TypeScript 编译器 |
exports | 条件导出(现代标准) | Node.js 12.11+、现代打包器 |
推荐 同时配置 main + module + exports,兼顾所有环境。
输出多种格式
库应该同时输出 ESM 和 CJS 格式,必要时加上 UMD:
import { defineConfig } from 'rollup'
export default defineConfig({
input: 'src/index.ts',
output: [
// ESM — 给现代打包器和 <script type="module"> 使用
{ file: 'dist/index.esm.js', format: 'esm', sourcemap: true },
// CJS — 给 Node.js require() 使用
{ file: 'dist/index.cjs.js', format: 'cjs', sourcemap: true, exports: 'named' },
// UMD — 给 CDN <script> 标签使用
{ file: 'dist/index.umd.js', format: 'umd', name: 'MyLib', sourcemap: true },
],
})
外部化依赖
打包库时,应将 dependencies 和 peerDependencies 外部化,避免重复打包:
import { defineConfig } from 'rollup'
import { readFileSync } from 'fs'
const pkg = JSON.parse(readFileSync('./package.json', 'utf-8'))
export default defineConfig({
input: 'src/index.ts',
external: [
// 所有 dependencies 和 peerDependencies 都不打入产物
...Object.keys(pkg.dependencies || {}),
...Object.keys(pkg.peerDependencies || {}),
// 也需要匹配子路径导入,如 'react/jsx-runtime'
/^react(\/.*)?$/,
/^lodash(\/.*)?$/,
],
output: { file: 'dist/index.esm.js', format: 'esm' },
})
如果不将 react 标记为 external,Rollup 会将整个 React 打包进你的库产物中。当用户使用你的库时,页面上就会出现 两份 React 实例,导致 Hooks 报错、状态不共享等严重问题。
生成类型声明文件
使用 rollup-plugin-dts 将所有 .d.ts 文件打包合并为一个声明文件:
import { defineConfig } from 'rollup'
import dts from 'rollup-plugin-dts'
export default defineConfig({
input: 'src/index.ts',
output: {
file: 'dist/index.d.ts',
format: 'esm',
},
// rollup-plugin-dts 会把所有分散的 .d.ts 合并为单个文件
plugins: [dts()],
})
- 用户只需引用一个
.d.ts文件,而非整个类型目录 - 避免
dist中暴露内部类型结构 - 减少 npm 包体积
现代替代方案
Rollup 虽然强大,但配置相对繁琐。社区已经衍生出多个基于 Rollup 的零配置或低配置工具:
| 工具 | 底层 | 特点 | 适用场景 |
|---|---|---|---|
| tsup | esbuild | 零配置、极速、开箱支持 TypeScript | 小型库、CLI 工具 |
| unbuild | Rollup + esbuild | 自动推断配置、支持 stub 开发模式 | Monorepo 中的包 |
| Vite Library Mode | Rollup | 复用 Vite 生态和配置 | 已使用 Vite 的项目 |
| Rolldown | Rust | Rollup 兼容的高性能替代,Vite 未来底层 | 未来趋势 |
import { defineConfig } from 'tsup'
export default defineConfig({
entry: ['src/index.ts'],
format: ['esm', 'cjs'], // 同时输出 ESM 和 CJS
dts: true, // 自动生成类型声明
splitting: false, // 不做代码分割
sourcemap: true,
clean: true, // 构建前清理 dist
})
import { defineConfig } from 'vite'
import { resolve } from 'path'
import dts from 'vite-plugin-dts'
export default defineConfig({
plugins: [dts()],
build: {
lib: {
entry: resolve(__dirname, 'src/index.ts'),
name: 'MyLib',
formats: ['es', 'cjs', 'umd'],
fileName: (format) => `index.${format}.js`,
},
rollupOptions: {
external: ['react', 'react-dom'],
output: {
globals: {
react: 'React',
'react-dom': 'ReactDOM',
},
},
},
},
})
- 追求极简和速度 →
tsup(底层 esbuild,几乎零配置) - Monorepo 多包管理 →
unbuild(自动推断 package.json 中的配置) - 已有 Vite 项目 → Vite Library Mode(配置统一)
- 需要完全掌控构建流程 → 直接使用 Rollup
常见面试问题
Q1: Rollup 和 Webpack 的区别是什么?各自适用什么场景?
答案:
Rollup 和 Webpack 的核心区别在于设计目标和产物质量:
1. 设计理念不同:
- Rollup 围绕 ESM 设计,目标是生成尽可能 干净、紧凑 的代码,它把所有模块"铺平"到一个作用域中(Scope Hoisting),减少函数包装和运行时代码
- Webpack 围绕"万物皆模块"设计,目标是处理应用中的 所有类型资源(JS、CSS、图片、字体等),通过 loader 机制统一处理
2. Tree Shaking 能力:
// 源码
export function used() { return 'used' }
export function unused() { return 'unused' }
// Rollup 产物 — 只保留 used,unused 被彻底移除
function used() { return 'used' }
export { used }
// Webpack 产物 — 包含模块系统运行时
/******/ (() => {
/******/ var __webpack_modules__ = ({
/******/ "./src/index.js": ((__unused, __exports, __webpack_require__) => {
/******/ __webpack_require__.d(__exports, { used: () => used })
function used() { return 'used' }
/******/ })
/******/ })
/******/ // ... 模块加载运行时代码
/******/ })()
3. 适用场景:
| 场景 | 推荐工具 | 原因 |
|---|---|---|
| JS 库 / SDK | Rollup | 产物干净、体积小、Tree Shaking 彻底 |
| 组件库 | Rollup / Vite Library Mode | 需要输出多种格式(ESM + CJS) |
| Web 应用(SPA) | Webpack / Vite | 需要 HMR、代码分割、资源处理 |
| 大型老项目 | Webpack | 生态成熟、兼容性好 |
| 新项目 | Vite | 底层使用 Rollup,兼顾开发体验和构建质量 |
Vite 实际上是 Rollup 和 Webpack 优势的结合:开发时利用原生 ESM 实现极速 HMR(解决了 Rollup 没有 dev server 的问题),生产时使用 Rollup 打包(保证产物质量)。理解这一点可以体现你对前端构建工具全局的认知。
Q2: 如何使用 Rollup 打包一个 npm 库?(完整流程)
答案:
以打包一个 React 工具库为例,完整流程如下:
第一步:初始化项目并安装依赖
- npm
- Yarn
- pnpm
- Bun
npm install rollup @rollup/plugin-node-resolve @rollup/plugin-commonjs @rollup/plugin-typescript rollup-plugin-terser rollup-plugin-dts typescript -D
yarn add rollup @rollup/plugin-node-resolve @rollup/plugin-commonjs @rollup/plugin-typescript rollup-plugin-terser rollup-plugin-dts typescript --dev
pnpm add rollup @rollup/plugin-node-resolve @rollup/plugin-commonjs @rollup/plugin-typescript rollup-plugin-terser rollup-plugin-dts typescript -D
bun add rollup @rollup/plugin-node-resolve @rollup/plugin-commonjs @rollup/plugin-typescript rollup-plugin-terser rollup-plugin-dts typescript --dev
第二步:配置 tsconfig.json
{
"compilerOptions": {
"target": "ES2018",
"module": "ESNext",
"moduleResolution": "Node",
"declaration": true,
"declarationDir": "dist/types",
"outDir": "dist",
"strict": true,
"esModuleInterop": true,
"jsx": "react-jsx",
"sourceMap": true
},
"include": ["src"],
"exclude": ["node_modules", "dist"]
}
第三步:编写库源码
export { useDebounce } from './hooks/useDebounce'
export { useThrottle } from './hooks/useThrottle'
export { formatDate } from './utils/date'
export type { DebounceOptions, ThrottleOptions } from './types'
第四步:配置 rollup.config.ts
import { defineConfig } from 'rollup'
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 dts from 'rollup-plugin-dts'
import { readFileSync } from 'fs'
const pkg = JSON.parse(readFileSync('./package.json', 'utf-8'))
const external = [
...Object.keys(pkg.dependencies || {}),
...Object.keys(pkg.peerDependencies || {}),
/^react(\/.*)?$/, // 匹配 react 及其子路径
]
export default defineConfig([
// 主构建
{
input: 'src/index.ts',
output: [
{ file: 'dist/index.esm.js', format: 'esm', sourcemap: true },
{ file: 'dist/index.cjs.js', format: 'cjs', sourcemap: true, exports: 'named' },
],
external,
plugins: [
resolve(),
commonjs(),
typescript({ tsconfig: './tsconfig.json', declaration: false }),
terser(),
],
},
// 类型声明
{
input: 'src/index.ts',
output: { file: 'dist/index.d.ts', format: 'esm' },
plugins: [dts()],
},
])
第五步:配置 package.json
{
"name": "my-react-hooks",
"version": "1.0.0",
"main": "dist/index.cjs.js",
"module": "dist/index.esm.js",
"types": "dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.esm.js",
"require": "./dist/index.cjs.js"
}
},
"sideEffects": false,
"files": ["dist"],
"peerDependencies": {
"react": ">=18.0.0"
},
"scripts": {
"build": "rollup -c rollup.config.ts --configPlugin typescript",
"prepublishOnly": "npm run build"
}
}
第六步:构建并发布
# 构建
npm run build
# 发布前检查产物
ls dist/
# index.esm.js index.cjs.js index.d.ts index.esm.js.map index.cjs.js.map
# 发布到 npm
npm publish
产物结构:
dist/
├── index.esm.js # ESM 格式(打包器使用)
├── index.esm.js.map # ESM sourcemap
├── index.cjs.js # CJS 格式(Node.js require 使用)
├── index.cjs.js.map # CJS sourcemap
└── index.d.ts # 合并的类型声明
Q3: Rollup 的插件机制是怎样的?
答案:
Rollup 的插件机制基于 钩子系统(Hooks),插件通过实现特定的钩子函数来介入打包的各个阶段。
1. 插件的基本结构:
import type { Plugin } from 'rollup'
function myPlugin(options: Record<string, unknown> = {}): Plugin {
return {
name: 'my-plugin', // 必需:用于错误提示和调试
// Build Hooks — 构建阶段
resolveId(source: string, importer: string | undefined) { /* ... */ },
load(id: string) { /* ... */ },
transform(code: string, id: string) { /* ... */ },
// Output Generation Hooks — 输出阶段
renderChunk(code: string, chunk: RenderedChunk) { /* ... */ },
generateBundle(options: OutputOptions, bundle: OutputBundle) { /* ... */ },
}
}
2. 核心钩子说明:
| 钩子 | 阶段 | 作用 | 典型场景 |
|---|---|---|---|
options | Build | 修改输入配置 | 动态调整配置 |
buildStart | Build | 构建开始时调用 | 初始化、清理 |
resolveId | Build | 自定义模块路径解析 | 虚拟模块、路径别名 |
load | Build | 自定义模块内容加载 | 虚拟模块、内存文件 |
transform | Build | 转换模块代码 | 代码替换、注入 |
buildEnd | Build | 构建结束时调用 | 错误汇总 |
renderChunk | Output | 处理每个输出 chunk | 代码压缩、banner 注入 |
generateBundle | Output | 所有 chunk 生成后调用 | 添加额外文件、修改产物 |
writeBundle | Output | 产物写入磁盘后调用 | 后置处理、通知 |
3. 钩子的执行类型:
不同钩子有不同的执行方式,理解这一点对编写插件至关重要:
import type { Plugin } from 'rollup'
function examplePlugin(): Plugin {
return {
name: 'example',
// first 类型:多个插件都实现了 resolveId,取第一个非 null 返回值
resolveId(source: string) {
if (source === 'virtual:env') {
return '\0virtual:env' // 返回非 null,后续插件不再处理
}
return null // 返回 null,交给下一个插件处理
},
// sequential 类型:按顺序执行,前一个的输出作为后一个的输入
transform(code: string, id: string) {
// 多个插件的 transform 串联执行
// 插件 A 的输出 → 插件 B 的输入 → 插件 C 的输入
return code.replace('__DEV__', 'false')
},
// parallel 类型:并行执行,互不依赖
buildStart() {
console.log('Build started!')
// 所有插件的 buildStart 并行执行
},
}
}
4. 虚拟模块插件实战:
import type { Plugin } from 'rollup'
// 虚拟模块前缀约定:使用 \0 前缀防止其他插件处理
const VIRTUAL_ID = 'virtual:env'
const RESOLVED_ID = '\0virtual:env'
interface EnvOptions {
mode: 'development' | 'production'
}
export function virtualEnvPlugin(options: EnvOptions): Plugin {
return {
name: 'rollup-plugin-virtual-env',
// 解析:将虚拟模块 ID 映射为内部 ID
resolveId(source: string) {
if (source === VIRTUAL_ID) {
return RESOLVED_ID
}
},
// 加载:为虚拟模块返回代码内容
load(id: string) {
if (id === RESOLVED_ID) {
return `
export const MODE = ${JSON.stringify(options.mode)}
export const IS_DEV = ${options.mode === 'development'}
export const IS_PROD = ${options.mode === 'production'}
`
}
},
}
}
// 使用时:
// import { MODE, IS_DEV } from 'virtual:env'
- Rollup 插件是一个 返回钩子对象的函数
- 钩子分为 Build Hooks(构建阶段)和 Output Hooks(输出阶段)
- 最核心的三个钩子:
resolveId(解析路径)→load(加载内容)→transform(转换代码) - Vite 的插件机制基于 Rollup 扩展,学会 Rollup 插件约等于学会 Vite 插件