一、Vite的核心设计理念
Vite(法语"快速")的诞生是为了解决Webpack在开发环境下的两大顽疾:缓慢的启动速度(HMR)和冗余的打包过程。与Webpack在Dev模式也走完整Bundle流程不同,Vite做了根本性的架构切割。
1.1 两种开发模式的本质差异
// Webpack Dev模式的痛点:
// $ webpack --mode development
// [==] 编译中... ███████████████████████░░░░░░░ 85%
// 实际行为:即使只改了一个文件,也要重建整个依赖图
// 模块数量达到1000+时,首次启动 >30秒
// Vite的设计哲学:按需编译 + esm原生支持
// ┌──────────────────────────────────────────────┐
// │ Vite Dev Server │
// │ │
// │ ┌─────────────────┐ ┌────────────────┐ │
// │ │ Dev Middleware │ │ Pre-bundling │ │
// │ │ (请求拦截) │ │ (依赖预构建) │ │
// │ └────────┬────────┘ └───────┬────────┘ │
// │ │ │ │
// │ ▼ ▼ │
// │ ┌─────────────────────────────────────────┐ │
// │ │ Rollup + esbuild (CJS转ESM) │ │
// │ │ 单文件按需编译 │ │
// │ └─────────────────────────────────────────┘ │
// └──────────────────────────────────────────────┘
// Vite生产模式的构建:
// vite build → Rollup(完整Tree-shaking + 代码分割)
// 这与开发模式是两个完全不同的构建引擎
// 核心指标对比
// Webpack Vite
// 冷启动(500模块) 32秒 0.8秒 ⚡ 40x
// HMR(单文件) 1.5秒 50ms ⚡ 30x
// 生产构建 45秒 12秒 ⚡ 3.7x
// 产出大小(gzip) 420KB 380KB (Rollup优化)
二、esbuild依赖预构建(Pre-bundling)
2.1 为什么需要预构建
// 依赖预构建解决的问题:
// ① CJS → ESM 转换(CommonJS依赖太多)
// ② 大量小文件合并(减少HTTP请求数)
// ③ 路径规范化(处理exports/exportsField)
// Vue3的依赖结构(仅计算dependencies):
// node_modules/vue/
// ├── package.json
// ├── dist/vue.runtime.esm-bundler.js ← esm bundler版
// ├── dist/vue.esm-bundler.js ← esm bundler完整版
// └── dist/vue.cjs.js ← CommonJS版
// 浏览器直接import时,exportsField可能导致加载错误版本
// 预构建后的产物(vite.config.js: optimizeDeps)
// .vite/deps/vue.js ← 预打包后的单文件
// .vite/deps/_metadata.json ← 缓存元信息
// 触发预构建的条件:
// 1. 首次运行(.vite目录不存在)
// 2. package.json变化(dependencies增减)
// 3. lock文件变化(npm install更新了版本)
// esbuild的配置(源码位于 vite/src/node/optimizer)
// vite optimizeDeps.include = ['vue', 'react', 'lodash']
// 强制将某些包加入预构建(避免动态import问题)
2.2 预构建的缓存机制
// 预构建缓存路径:node_modules/.vite/deps/
// 缓存失效条件(immutable cache):
// .vite/deps/_metadata.json 记录:
{
"hash": "a3f2c1b8", // 根据 node_modules 内容计算
"browserHash": "d4e5f6a7", // 根据 package.json deps 计算
"optimized": {
"vue": {
"file": "vue.js",
"src": "vue/dist/vue.esm-bundler.js",
"needsInterop": false
}
}
}
// ⚠️ 常见问题:预构建失效导致"cannot import from"
import _ from 'lodash-es'; // lodash-es vs lodash
// 解决:预构建时指定正确的入口
// vite.config.js
export default defineConfig({
optimizeDeps: {
include: ['lodash-es/lodash.min.js']
}
});
三、Rollup插件机制与Vite插件链
3.1 Rollup插件的数据流
// Rollup插件是"转换函数"的组合,数据流如下:
//
// Source Code
// │
// ▼
// ① load(id) ← 按ID加载模块内容
// │ (返回null则进入下一插件)
// ▼
// ② transform(code, id) ← AST转换(babel/terser/typescript)
// │ (返回null则跳过)
// ▼
// ③ parse(id) ← 生成AST(供后续分析)
// │
// ▼
// ④ resolveId(importPath) ← 路径解析
// │ (返回null则继续)
// ▼
// ⑤ transform(...) ← 再次transform(可链式)
// │
// ▼
// ⑥ generate/bundle ← 输出最终产物
// Rollup插件结构(标准五件套)
export default function myPlugin() {
return {
name: 'my-plugin', // 插件名(用于日志)
enforce: 'pre' | 'post', // 执行顺序:pre在Vite内置插件前
// 钩子签名
resolveId(source, importer) { // 路径解析
if (source === 'virtual-module') {
return source; // 返回ID表示已处理
}
return null; // 返回null表示交给下一个插件处理
},
load(id) { // 加载模块
if (id === 'virtual-module') {
return 'export default "from virtual module"';
}
return null;
},
transform(code, id) { // 转换代码
if (!id.endsWith('.special')) return null;
return {
code: code.replace(/old/g, 'new'),
map: null, // 源码映射
deps: [], // 新增依赖
assets: [] // 静态资源
};
},
generateBundle(options, bundle) { // 输出前最后修改
// 在这里可以修改输出文件
const chunk = bundle['main.js'];
chunk.code = '/* injected by plugin */\n' + chunk.code;
}
};
}
3.2 Vite插件与Rollup插件的差异
// Vite独有钩子(开发服务器专用)
// 这些钩子在 rollupOptions.plugins 中无法使用
export default function viteOnlyPlugin() {
return {
name: 'vite-only',
// HTTP请求拦截(配置服务器中间件)
configureServer(server) {
server.middlewares.use('/custom-api', (req, res) => {
res.end(JSON.stringify({ from: 'vite-plugin' }));
});
},
// 热更新自定义(ViteHMRClient通信)
handleHotUpdate(ctx) {
// ctx = { file, modules, server, timestamp, read }
if (ctx.file.includes('shared')) {
// 通知所有相关模块热更新
ctx.server.ws.send({ type: 'full-reload' });
}
},
// 开发服务器启动前
configurePreviewServer(server) { },
// 构建前执行
apply: 'build' | 'serve' | undefined
};
}
// Vite插件扩展了Rollup钩子的三个场景
// ① SSR构建:transformIndexHtml → 注入SSR bundle标签
// ② 模块预构建:resolveDependencies → 自定义依赖解析
// ③ 路径规范化:normalizePath → 跨平台路径处理
四、生产构建与代码分割
4.1 Rollup的代码分割策略
// Rollup支持三种代码分割策略:
// ① 动态import() 自动分割
// ② manualChunks 函数精确控制
// ③ facade chunks(入口与代码分离)
// 策略一:动态import自动分割
// index.js
import('./heavy-module.js').then(m => m.run());
// 编译后自动生成独立chunk:
// dist/
// ├── index-[hash].js ← 主入口
// ├── heavy-module-[hash].js ← 自动分割
// └── index-[hash].html
// 策略二:manualChunks精细控制
// vite.config.js
export default defineConfig({
build: {
rollupOptions: {
output: {
manualChunks(id) {
// 按目录分组
if (id.includes('node_modules/vue')) return 'vendor-vue';
if (id.includes('node_modules/react')) return 'vendor-react';
if (id.includes('/components/')) return 'ui-components';
if (id.includes('/utils/')) return 'utils';
}
}
}
}
});
// 策略三:facade chunk(按入口分割)
// 每个HTML入口对应一个facade + 共享chunk
// 适用:多页应用(MPA)
// vite.config.js
export default defineConfig({
build: {
rollupOptions: {
input: {
main: 'index.html',
admin: 'admin.html',
docs: 'docs.html'
},
output: {
// 产出结构:
// main-[hash].js ← 主入口facade
// admin-[hash].js ← 管理后台facade
// shared-[hash].js ← 公共库(自动提取)
}
}
}
});
五、自定义插件实战:从设计到实现
// 实战:创建一个"SVG组件化"插件
// 功能:import './icon.svg?component' → 返回React/Vue组件
// 使用:import Icon from './icon.svg?component'
import { readFileSync } from 'fs';
import { basename } from 'path';
// 方案一:纯Vite插件(推荐)
export function svgComponentPlugin(preamble = '') {
const svgRegex = /\.svg\?component$/;
return {
name: 'vite-plugin-svg-component',
// 解析:识别特殊后缀
resolveId(source) {
if (svgRegex.test(source)) {
// 返回处理后的ID(去掉?component后缀)
return source.replace('?component', '');
}
return null;
},
// 加载:将SVG转为React/Vue组件代码
load(id) {
if (!svgRegex.test(id) && !id.endsWith('.svg')) return null;
const raw = readFileSync(id, 'utf-8');
// 提取SVG属性(用于保留原始样式)
const attrs = raw.match(/