结合源码
详解 Vite 原理
深入 Vite 源码仓库,理解其原生 ESM 驱动的开发服务器、毫秒级 HMR 与 Rollup 生产构建是如何协同工作, 彻底重塑前端开发体验的。
传统打包器的困境
Webpack 等工具将所有模块打包为 Bundle 再交给浏览器,项目越大,启动越慢。
Vite 源码架构全景
Vite 的源码仓库包含多个核心模块,每个模块各司其职,构成完整的工具链。
创建开发服务器
当你在终端执行 vite 时, 入口文件是 packages/vite/src/node/cli.ts, 它最终调用 createServer() 创建实例:
export async function createServer(
inlineConfig: InlineConfig = {}
): Promise<ViteDevServer> {
// 1. 解析配置:合并默认配置 + 用户 vite.config.ts
const config = await resolveConfig(inlineConfig, 'serve')
// 2. 创建中间件容器(基于 connect)
const middlewares = connect()
// 3. 创建 WebSocket 服务器(用于 HMR 通信)
const ws = createWebSocketServer(httpServer, config)
// 4. 初始化插件容器
const pluginContainer = await createPluginContainer(config)
// 5. 注册内置中间件(静态文件、HTML回退、转换等)
// ── 被封装在 internalPlugins 中
for (const plugin of config.plugins) {
if (plugin.configureServer) {
await plugin.configureServer(server)
}
}
// 6. 返回完整的 server 实例
const server: ViteDevServer = {
config,
middlewares,
ws,
pluginContainer,
transformRequest(url) { /* ... */ },
listen(port) { /* ... */ },
// ...
}
return server
}关键设计点
为什么用 connect 而不是 Express?
connect 极轻量(无路由层),Vite 只需中间件管道,不需 Express 的额外抽象。
pluginContainer 是什么?
它是 Rollup 插件钩子的运行容器,负责 resolveId → load → transform 的管线调度。
configureServer 钩子的作用?
允许插件在服务启动前注入自定义中间件或拦截请求,如 vite-plugin-mock。
依赖预构建 (Pre-Bundling)
?为什么需要预构建?
npm 上的大多数包仍然以 CommonJS 或 UMD 格式发布。 浏览器的原生 ESM 无法直接 import 这些模块。
此外,像 lodash-es 这样的包包含 600+ 个内部模块文件。 如果逐个请求,浏览器会发起大量 HTTP 请求,导致严重的瀑布流延迟。
预构建的核心目标:将 CJS→ESM 转换 + 内部模块合并为单文件, 结果缓存到 node_modules/.vite/deps/。
async function runOptimizeDeps(config) {
// 1. 扫描入口文件,收集裸模块引用
const deps = await scanImports(config)
// deps = { 'react': 'react',
// 'react-dom': 'react-dom', ... }
// 2. 使用 esbuild 进行打包
const result = await esbuild.build({
entryPoints: Object.keys(deps),
bundle: true, // 合并内部模块
format: 'esm', // 输出 ESM 格式
outdir: depsCacheDir, // .vite/deps/
splitting: true, // 代码分割
plugins: [
cjsToEsmPlugin() // CJS → ESM 转换
],
})
// 3. 写入元数据(哈希 + 依赖映射)
writeDepsMetadata(depsCacheDir, {
hash: getDepHash(deps, config),
optimized: deps,
})
}预构建流程图解
模块请求处理管线
当浏览器遇到 import App from './App.tsx' 时, 会向 Vite Dev Server 发起 HTTP 请求。Vite 通过 中间件 + 插件管线 处理该请求。
async function transformRequest(
url: string,
server: ViteDevServer,
options?: TransformResult
): Promise<TransformResult | null> {
// 1. resolve:将 URL 转换为文件路径
const id = (await server.pluginContainer.resolveId(url))?.id
?? url
// 2. load:读取文件内容
const loadResult = await server.pluginContainer.load(id)
const code = loadResult?.code ?? await fs.readFile(id, 'utf-8')
// 3. transform:应用所有插件的 transform 钩子
const result = await server.pluginContainer.transform(code, id)
// 4. 缓存结果(后续相同请求直接返回)
moduleGraph.ensureEntryFromUrl(url)
return {
code: result?.code,
map: result?.map,
etag: getEtag(result?.code),
}
}HMR 热模块替换原理
HMR 三步走
① 文件监听 → 检测变更
Vite 使用 chokidar 监听文件系统。当文件发生变化时,获取与变更模块关联的 HMR 边界。
watcher.on('change', async (file) => {
const mods = moduleGraph.getModulesByFile(file)
hmr(mods, file)
})② 推送更新消息
通过 WebSocket 向客户端推送类型为 update 的消息,包含变更模块的路径和时间戳。
ws.send({
type: 'update',
updates: [{
type: 'js-update',
path: '/src/App.tsx',
acceptedPath: '/src/App.tsx',
timestamp: Date.now()
}]
})③ 客户端接受更新
浏览器端 Vite HMR Runtime 收到消息后,重新 import 更新后的模块,并调用 accept 回调。
// 浏览器端 HMR Runtime
const newMod = await import(
modPath + '?t=' + timestamp
)
if (mod.callbacks) {
mod.callbacks.forEach(cb => cb(newMod))
}Module Graph(模块图)
Vite 维护一个内存中的模块依赖图。每个模块记录其 importers(谁引入了它)和 imported(它引入了谁), HMR 更新时据此精确计算影响边界。
class ModuleNode {
url: string
file: string | null
importers = new Set<ModuleNode>()
importedModules = new Set<ModuleNode>()
acceptedHmrDeps = new Set<ModuleNode>()
transformResult: TransformResult | null
lastHMRTimestamp = 0
}
class ModuleGraph {
urlToModuleMap = new Map<string, ModuleNode>()
fileToModulesMap = new Map<string, Set<ModuleNode>>()
async ensureEntryFromUrl(rawUrl: string) {
// URL → ModuleNode 的双向映射
// 同时建立 import 关系图
}
invalidateModule(mod: ModuleNode) {
mod.transformResult = null
mod.lastHMRTimestamp = Date.now()
// 递归标记所有 importers 为 stale
}
}HMR 更新边界示意
插件系统的设计哲学
export async function createPluginContainer(config: ResolvedConfig) {
// 按 enforce 排序: pre → normal → post
const sortedPlugins = sortPlugins(config.plugins)
const container = {
async resolveId(rawId: string, importer?: string) {
let id = rawId
for (const plugin of sortedPlugins) {
if (!plugin.resolveId) continue
const result = await plugin.resolveId.call(
ctx, id, importer, { ssr: false }
)
if (result) {
id = typeof result === 'string' ? result : result.id
}
}
return { id }
},
async load(id: string) {
for (const plugin of sortedPlugins) {
if (!plugin.load) continue
const result = await plugin.load.call(ctx, id)
if (result !== null) return result
}
return null
},
async transform(code: string, id: string) {
for (const plugin of sortedPlugins) {
if (!plugin.transform) continue
const result = await plugin.transform.call(ctx, code, id)
if (result !== null) {
code = typeof result === 'string' ? result : result.code
}
}
return { code }
}
}
return container
}生产构建:Vite + Rollup
在生产环境中,Vite 不再使用 esbuild 进行打包,而是直接调用 Rollup 作为构建工具。 这是因为 Rollup 提供了更成熟的代码分割、Tree-shaking 和 Chunk 优化能力。
CSS 代码分割
自动提取异步 chunk 的 CSS,避免加载闪烁。
异步 Chunk 加载优化
自动预加载关联 chunk,减少瀑布流。
CSS Codegen
内联关键 CSS,异步加载非关键样式。
多页面应用支持
自动检测 HTML 入口并生成对应 bundle。
import { rollup } from 'rollup'
export async function build(inlineConfig) {
const config = await resolveConfig(inlineConfig, 'build')
// Vite 内置插件自动注入
const vitePlugins = [
viteResolvePlugin(config), // 路径解析
viteCssPlugin(config), // CSS 处理
viteAssetPlugin(config), // 静态资源
viteHtmlPlugin(config), // HTML 处理
viteDefinePlugin(config), // 环境变量
...(config.plugins), // 用户插件
]
// 调用 Rollup 构建
const bundle = await rollup({
input: config.build.rollupOptions.input,
plugins: vitePlugins,
...config.build.rollupOptions,
})
// 写入产物
await bundle.write({
dir: config.build.outDir,
format: 'es', // ESM 输出
sourcemap: true,
manualChunks: config.build.rollupOptions
.manualChunks,
})
}