Frontend · #vite#build-tool#esbuild#rollup

Vite构建工具原理与插件开发

2025.07.30 7 min 2.8k
// 目录 · contents

引言

Vite(法语”快”的意思)已经成为现代前端项目的首选构建工具。它通过开发环境使用原生 ESM + esbuild 预构建,生产环境使用 Rollup 打包的双引擎架构,实现了极速的开发体验和优化的生产构建。本文将从架构设计讲起,深入分析 Dev Server 的工作原理、HMR 机制、生产构建流程,并手把手教你开发自定义 Vite 插件。

Vite 架构概览

graph TB
    subgraph "Development 开发模式"
        A[浏览器请求] --> B[Vite Dev Server]
        B --> C[esbuild 预构建<br/>node_modules → ESM]
        B --> D[原生 ESM<br/>按需编译源码]
        B --> E[HMR WebSocket<br/>热模块替换]
    end

    subgraph "Production 生产模式"
        F[vite build] --> G[Rollup 打包]
        G --> H[代码分割 Code Splitting]
        G --> I[Tree Shaking]
        G --> J[CSS 提取 & 压缩]
        G --> K[资源处理 & Hash]
    end

    style B fill:#646cff,color:#fff
    style C fill:#ffcf00,color:#000
    style G fill:#ef4444,color:#fff

为什么 Vite 这么快?

传统打包工具(如 Webpack)在开发模式下需要先打包所有模块,然后才能启动开发服务器。而 Vite 利用了两个关键优势:

  1. 原生 ESM:浏览器原生支持 ES Module,Vite 直接按需提供模块,无需打包
  2. esbuild 预构建:用 Go 编写的 esbuild 比 JavaScript 打包器快 10-100 倍
sequenceDiagram
    participant B as 浏览器
    participant V as Vite Dev Server
    participant E as esbuild
    participant FS as 文件系统

    Note over V: 启动时: 预构建 node_modules
    V->>E: 预构建依赖 (react, lodash...)
    E-->>V: ESM 格式的依赖缓存

    B->>V: GET /src/App.tsx
    V->>FS: 读取 App.tsx
    FS-->>V: 源码
    V->>V: 按需编译 (esbuild transform)
    V-->>B: 编译后的 ESM 模块

    B->>V: GET /node_modules/.vite/deps/react.js
    V-->>B: 预构建的 React ESM (缓存)

    Note over B,V: 文件修改
    FS->>V: 文件变更事件
    V->>V: 判断 HMR 边界
    V->>B: WebSocket: HMR update
    B->>V: GET /src/App.tsx?t=123456
    V-->>B: 更新后的模块

Dev Server 深入

依赖预构建

Vite 在首次启动时,会使用 esbuild 将 node_modules 中的依赖预构建为 ESM 格式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// vite.config.ts
import { defineConfig } from 'vite';

export default defineConfig({
optimizeDeps: {
// Force include dependencies that aren't auto-detected
include: ['lodash-es', 'axios'],

// Exclude dependencies (e.g., already ESM)
exclude: ['@vueuse/core'],

// esbuild options for pre-bundling
esbuildOptions: {
target: 'esnext',
plugins: [/* custom esbuild plugins */],
},
},
});

预构建解决了两个问题: 1. CommonJS/UMD 转 ESM:浏览器只认 ESM 2. 依赖合并lodash-es 有 600+ 个 ESM 文件,预构建合并为单个模块,减少 HTTP 请求

模块解析与转换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// When browser requests /src/App.tsx, Vite:

// 1. Resolves bare imports to pre-built deps
// Input:
import React from 'react';
import { useState } from 'react';
import dayjs from 'dayjs';

// Output (rewritten by Vite):
import React from '/node_modules/.vite/deps/react.js?v=abc123';
import { useState } from '/node_modules/.vite/deps/react.js?v=abc123';
import dayjs from '/node_modules/.vite/deps/dayjs.js?v=def456';

// 2. Transforms TypeScript/JSX on the fly (using esbuild)
// 3. Handles CSS imports as side-effect modules
// 4. Converts asset imports to URLs
import logo from './logo.png';
// → const logo = '/src/logo.png';

HMR 机制

HMR API

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// Vite's HMR API (import.meta.hot)
if (import.meta.hot) {
// Accept self-update
import.meta.hot.accept((newModule) => {
// Re-execute with updated module
console.log('Module updated:', newModule);
});

// Accept dependency updates
import.meta.hot.accept('./utils.js', (newUtils) => {
// Re-run dependent logic
recompute(newUtils);
});

// Cleanup side effects
import.meta.hot.dispose((data) => {
// Store state for next update
data.count = currentCount;
clearInterval(timer);
});

// Restore state after update
if (import.meta.hot.data.count) {
currentCount = import.meta.hot.data.count;
}

// Invalidate: propagate update to parent
import.meta.hot.invalidate();

// Full reload when HMR can't handle the change
import.meta.hot.on('vite:beforeFullReload', () => {
console.log('Full reload triggered');
});
}

框架 HMR 集成

graph TD
    A[文件修改] --> B{文件类型?}
    B -->|.vue| C["@vitejs/plugin-vue<br/>SFC HMR"]
    B -->|.tsx/.jsx| D["@vitejs/plugin-react<br/>React Fast Refresh"]
    B -->|.css| E[CSS HMR<br/>替换 style 标签]
    B -->|.module.css| F[CSS Modules HMR<br/>更新 JS 模块]

    C --> G[保持组件状态]
    D --> G
    E --> H[无需 JS 执行]
    F --> I[更新样式映射]

    style A fill:#646cff,color:#fff
    style G fill:#2ecc71,color:#fff
    style H fill:#2ecc71,color:#fff
1
2
3
4
5
6
7
8
9
10
11
12
// React Fast Refresh integration (simplified)
// @vitejs/plugin-react automatically adds:
if (import.meta.hot) {
// Register component for React Fast Refresh
window.$RefreshReg$(Component, 'Component');
window.$RefreshSig$();

import.meta.hot.accept((mod) => {
// React Refresh runtime handles state preservation
// Only re-renders changed components
});
}

生产构建(Rollup)

构建配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

export default defineConfig({
plugins: [react()],

build: {
// Output directory
outDir: 'dist',

// Rollup options
rollupOptions: {
input: {
main: './index.html',
admin: './admin.html', // Multi-page app
},
output: {
// Manual chunk splitting
manualChunks: {
vendor: ['react', 'react-dom'],
charts: ['chart.js', 'd3'],
},

// Or use a function for more control
manualChunks(id) {
if (id.includes('node_modules')) {
if (id.includes('react')) return 'vendor-react';
if (id.includes('lodash')) return 'vendor-lodash';
return 'vendor'; // All other dependencies
}
},

// Asset naming
assetFileNames: 'assets/[name]-[hash][extname]',
chunkFileNames: 'js/[name]-[hash].js',
entryFileNames: 'js/[name]-[hash].js',
},
},

// Minification
minify: 'terser', // or 'esbuild' (default, faster)
terserOptions: {
compress: {
drop_console: true,
drop_debugger: true,
},
},

// CSS
cssCodeSplit: true, // Split CSS per async chunk
cssMinify: 'lightningcss', // Faster CSS minifier

// Source maps
sourcemap: true, // or 'hidden' for production

// Chunk size warnings
chunkSizeWarningLimit: 500,

// Target browsers
target: 'es2020',
},
});

构建分析

1
2
3
4
5
6
7
8
9
10
11
12
13
// Install: npm i -D rollup-plugin-visualizer
import { visualizer } from 'rollup-plugin-visualizer';

export default defineConfig({
plugins: [
visualizer({
open: true,
gzipSize: true,
brotliSize: true,
filename: 'dist/stats.html',
}),
],
});

插件 API

Vite 插件兼容 Rollup 插件接口,并扩展了 Vite 特有的钩子:

graph TD
    A[Vite 插件钩子] --> B[通用钩子<br/>兼容 Rollup]
    A --> C[Vite 特有钩子]

    B --> B1[buildStart]
    B --> B2[resolveId]
    B --> B3[load]
    B --> B4[transform]
    B --> B5[buildEnd]

    C --> C1[config]
    C --> C2[configResolved]
    C --> C3[configureServer]
    C --> C4[transformIndexHtml]
    C --> C5[handleHotUpdate]

    style A fill:#646cff,color:#fff
    style B fill:#ef4444,color:#fff
    style C fill:#10b981,color:#fff

自定义插件开发

示例一:虚拟模块插件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// plugins/vite-plugin-build-info.js
export default function buildInfoPlugin() {
const virtualModuleId = 'virtual:build-info';
const resolvedVirtualModuleId = '\0' + virtualModuleId;

return {
name: 'vite-plugin-build-info',

resolveId(id) {
if (id === virtualModuleId) {
return resolvedVirtualModuleId;
}
},

load(id) {
if (id === resolvedVirtualModuleId) {
return `
export const buildTime = '${new Date().toISOString()}';
export const nodeVersion = '${process.version}';
export const mode = '${process.env.NODE_ENV || 'development'}';
`;
}
},
};
}

// Usage in app:
// import { buildTime, nodeVersion } from 'virtual:build-info';

示例二:Markdown 转换插件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// plugins/vite-plugin-markdown.js
import { marked } from 'marked';

export default function markdownPlugin() {
return {
name: 'vite-plugin-markdown',

// Only apply during dev and build
enforce: 'pre',

// Transform .md files
transform(code, id) {
if (!id.endsWith('.md')) return null;

const html = marked(code);

// Return as a JS module
return {
code: `export default ${JSON.stringify(html)};`,
map: null,
};
},
};
}

// Usage:
// import content from './README.md';
// element.textContent = content;

示例三:API 自动导入插件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// plugins/vite-plugin-auto-import-api.js
import fs from 'fs';
import path from 'path';

export default function autoImportApiPlugin(options = {}) {
const { apiDir = 'src/api', prefix = '/api' } = options;

return {
name: 'vite-plugin-auto-import-api',

// Configure dev server
configureServer(server) {
server.middlewares.use(async (req, res, next) => {
if (!req.url.startsWith(prefix)) return next();

const apiPath = req.url.replace(prefix, '');
const filePath = path.resolve(apiDir, `${apiPath}.ts`);

if (!fs.existsSync(filePath)) return next();

try {
// Load the API module via Vite's module graph
const mod = await server.ssrLoadModule(filePath);
const handler = mod.default || mod;

// Parse request body
let body = '';
req.on('data', chunk => { body += chunk; });
req.on('end', async () => {
const result = await handler({
method: req.method,
body: body ? JSON.parse(body) : undefined,
query: new URL(req.url, 'http://localhost').searchParams,
});

res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify(result));
});
} catch (err) {
res.statusCode = 500;
res.end(JSON.stringify({ error: err.message }));
}
});
},
};
}

示例四:HMR 感知插件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// plugins/vite-plugin-i18n-hot.js
export default function i18nHotPlugin() {
return {
name: 'vite-plugin-i18n-hot',

// Custom HMR handling
handleHotUpdate({ file, server }) {
if (file.endsWith('.json') && file.includes('/locales/')) {
// Send custom HMR event
server.ws.send({
type: 'custom',
event: 'i18n-update',
data: { file },
});
// Return empty array to prevent default HMR
return [];
}
},

// Inject client-side HMR listener
transformIndexHtml(html) {
return html.replace(
'</body>',
`<script type="module">
if (import.meta.hot) {
import.meta.hot.on('i18n-update', (data) => {
console.log('i18n updated:', data.file);
// Reload translations without full page refresh
window.dispatchEvent(new CustomEvent('i18n-reload'));
});
}
</script></body>`
);
},
};
}

优化策略

开发环境优化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
export default defineConfig({
server: {
// Pre-transform frequently used modules
warmup: {
clientFiles: [
'./src/components/**/*.tsx',
'./src/pages/Home.tsx',
],
},
},

// Speed up dependency pre-bundling
optimizeDeps: {
holdUntilCrawlEnd: false, // Start serving before crawl completes
},

// Use SWC instead of Babel for React
plugins: [
react({ babel: false }), // Uses SWC, faster transforms
],
});

生产环境优化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
export default defineConfig({
build: {
// Modern browsers only — smaller output
target: 'esnext',

// Use Rollup's tree shaking
rollupOptions: {
treeshake: {
moduleSideEffects: false,
propertyReadSideEffects: false,
},
},

// Compress assets
reportCompressedSize: true,
},

// CSS optimization
css: {
transformer: 'lightningcss',
lightningcss: {
targets: { chrome: 100 },
},
},
});

Vite vs 其他构建工具

特性 Vite Webpack Turbopack esbuild
开发启动速度 极快 极快
HMR 速度 极快 无 HMR
生产构建 快(Rollup) 开发中 极快但功能少
插件生态 丰富 最丰富 有限 有限
配置复杂度
CSS 处理 内置 需要 loader 内置 基础
SSR 支持 内置 需要配置 通过 Next.js

总结

Vite 的双引擎架构(开发用 esbuild + 原生 ESM,生产用 Rollup)在开发体验和构建质量之间找到了最佳平衡点。理解其原理有助于更好地调优配置和排查问题。插件开发方面,Vite 兼容 Rollup 插件接口并提供了额外的开发服务器钩子,使得自定义扩展变得简单。随着 Rolldown(Rust 版 Rollup)的推进,Vite 未来有望在开发和生产环境统一使用同一个打包器,进一步消除行为差异。

作者 · authorzt
发布 · date2025-07-30
篇幅 · length2.8k 字 · 7 min
许可 · licenseCC BY-SA 4.0
$ echo "comments" · 评论