FORMA

工作原理

Vite 是面向现代浏览器的前端构建工具:开发阶段利用原生 ES 模块与按需编译;生产构建由 Rollup 完成。对比见 Webpack 与 Vite工程化概览

下面从开发服务器、预构建、插件与 HMR 四方面说明核心机制。

一、开发服务器原生 ESM 架构

传统的 Webpack 等打包工具在开发模式下需要构建整个应用的依赖图,启动慢、热更新慢。Vite 将开发模式下的模块处理交给了现代浏览器。

1. 利用浏览器原生 ES 模块

Vite 启动开发服务器后直接提供源码。浏览器通过 <script type="module"> 加载入口,遇到 import 再请求对应模块,由 Vite 拦截并返回编译后的结果。

  • 无需将整个应用打包成一个 bundle,浏览器自行管理模块加载。
  • 开发服务器只处理当前请求的模块,极大的冷启动速度。

2. 按需编译:只编译请求的模块

当浏览器请求某个 .vue.jsx.ts 文件时,Vite 才会对该文件进行实时编译(转换 TS、编译 JSX、解析 Vue SFC 等)。这意味着项目中有大量文件时,不会在启动时全部编译,只编译请求的模块,启动时间与项目规模解耦。

3. 依赖预构建(Pre-bundling)的作用

虽然 Vite 提倡使用原生 ESM,但 node_modules 中的依赖通常有两种问题:

  • 多模块依赖:许多 CommonJS 或 UMD 库无法直接在浏览器中使用。
  • 大量内部模块:一个依赖可能包含成百上千个模块(如 lodash-es),直接引入会导致请求瀑布。

预构建(pre-bundling)使用 esbuild 将依赖统一打包成单个或少量 ESM 模块,然后提供给浏览器。好处:

  • 兼容性:将 CommonJS/UMD 转换成 ESM。
  • 性能:将多个内部模块合并,减少请求数量。
  • 缓存:预构建产物写入 node_modules/.vite/deps,下次启动直接使用。

二、预构建机制详解

1. 预构建触发条件

  • 首次启动:自动扫描 index.html 中的 <script type="module"> 以及入口中的 optimizeDeps.entries 配置,收集依赖并预构建。
  • 手动强制:使用 server.force 配置或 vite --force 命令。
  • 依赖变更:当 package.json 中的依赖版本变化或锁定文件变化,Vite 会重新预构建(通过 _metadata.json 的哈希判断)。
  • 未预构建的依赖被导入:如果某些依赖未被预构建,Vite 也会在运行时触发预构建。

2. _metadata.json 缓存机制

node_modules/.vite/deps/_metadata.json 文件记录了预构建的依赖列表、文件哈希、配置哈希等信息。Vite 在启动时对比当前依赖和缓存的哈希,若一致则直接使用缓存,否则重新预构建。

3. 预构建产物存放位置与手动清除

产物存放在 node_modules/.vite/deps/ 目录下,文件名包含内容哈希。如果需要强制重新构建,可以删除该目录或执行 vite --force

4. 自定义 optimizeDeps.include / exclude

某些第三方库可能因为动态导入或特殊的 CommonJS 导出导致预构建失败,可以通过配置强制包含或排除:

js
// vite.config.js
export default {
  optimizeDeps: {
    include: ["some-package"], // 强制预构建
    exclude: ["@vue/runtime-core"], // 排除,让浏览器直接加载原始 ESM
  },
};

三、插件架构与 hooks 执行顺序

Vite 的插件体系兼容 Rollup 插件,并扩展了一些 Vite 特有的钩子。插件生命周期主要分为三个阶段:解析 → 加载 → 转换 → HTML 转换 → 生成 / 写入

1. 插件关键 hooks

钩子作用执行时机
resolveId解析模块 ID(如 import 'vue' 转为真实路径)每个模块导入语句
load加载模块内容(返回源码字符串)在 resolve 之后,transform 之前
transform转换加载后的模块内容(编译 TS、JSX 等)每个模块加载后
transformIndexHtml转换 index.html 内容开发服务器请求 HTML 时或构建打包时
generateBundle在 bundle 生成后、写入前修改产物构建阶段,Rollup 特有
writeBundlebundle 写入后执行构建阶段

2. 插件执行顺序

通过 enforce 属性控制:

  • enforce: 'pre' → 在 Vite 核心钩子之前执行(如 alias)。
  • enforce → 正常顺序。
  • enforce: 'post' → 在 Vite 核心钩子之后执行(如压缩、校验)。

同一组内按插件注册顺序执行。

js
// 示例插件结构
const myPlugin = {
  name: 'my-plugin',
  enforce: 'pre',  // 可选 'pre'、'post'
  resolveId(source) { ... },
  load(id) { ... },
  transform(code, id) { ... },
};

3. Vite 插件与 Rollup 插件的兼容性

绝大多数 Rollup 插件可以在 Vite 中直接使用,因为 Vite 的生产构建就是基于 Rollup。但需要注意:

  • 某些 Rollup 插件假设在打包阶段(如 generateBundle),在开发阶段可能不会调用。
  • Vite 开发环境会调用 resolveIdloadtransform 等钩子,因此耗时插件可能影响开发性能。
  • 可以通过 apply: 'build' | 'serve' 限定插件只在特定模式生效。

四、HMR(热模块替换)原理

Vite 的 HMR 基于原生 ES 模块,利用 WebSocket 实现增量更新,更新速度快且精确。

1. 基于 WebSocket 的模块更新通知

  • 开发服务器启动后,会建立 WebSocket 连接。
  • 当文件被编辑时,Vite 会判断受影响的模块链,并向客户端发送更新消息(包含模块 ID 及更新类型)。
  • 客户端接收消息后,重新请求对应的模块,并在运行时处理替换逻辑。

2. 精确的 HMR 边界:import.meta.hot API

Vite 暴露了 import.meta.hot 对象,允许框架和模块定义自定义热更新行为:

js
if (import.meta.hot) {
  import.meta.hot.accept((newModule) => {
    // 模块更新后执行,可以重新渲染
  });
  import.meta.hot.dispose(() => {
    // 模块废弃前的清理工作
  });
}

对于无 HMR 适配的模块,Vite 会通过 fallback 进行页面重载。

3. 与框架(Vue/React)的深度集成

  • @vitejs/plugin-vue:针对 Vue 单文件组件实现了精细的 HMR。当修改 <template><style> 时,只更新对应部分,保留组件状态;修改 <script> 时,会触发组件重新加载。
  • @vitejs/plugin-react:利用 React Refresh(官方 HMR 方案),保持组件状态。它会在编译时将组件转换为支持热更新的代码,并注入 import.meta.hot 逻辑。

这些框架插件通过分析文件变更范围,将更新粒度缩小到组件级别,从而保留状态。

五、总结

核心机制关键点收益
原生 ESM 开发服务器浏览器直接加载模块,按需编译冷启动极快,无打包开销
依赖预构建esbuild 打包依赖,合并模块、转 ESM减少请求,兼容 CommonJS
插件体系兼容 Rollup 钩子 + enforce 顺序强扩展性,复用 Rollup 生态
HMRWebSocket 推送 + import.meta.hot API模块级热更新,保有状态

Vite 通过充分利用浏览器原生能力、智能预构建和精细化的热更新,为现代前端开发提供了极致的开发体验。在生产环境下,它通过 Rollup 打包输出优化后的产物,兼顾运行性能。

参考文献

以下链接在编写时均可正常访问:

资料说明
Vite:工作原理官方
依赖预构建optimizeDeps
HMR APIimport.meta.hot

Series

vite

6 / 6

插件开发