Vite 原理与实践
Vite 在开发和生产阶段采用不同的处理方式。开发时,dev server 根据浏览器请求按需转换源码模块,以缩短启动和更新等待;构建时,Vite 从入口出发处理完整依赖图,生成适合部署的静态资源。这两条链路共同构成了 Vite 的工作方式。
Vite 8 的工具链变化
Vite 8 延续了这套开发与生产分工,主要更新集中在底层工具链。依赖预构建和生产构建统一使用 Rolldown,原有的 Rollup 插件大多可以继续使用;新增构建配置应写入 rolldownOptions。JS、TS 和 JSX 转换以及生产环境的 JS 压缩主要由 Oxc 完成,CSS 则默认由 Lightning CSS 压缩。
底层工具更换后,Vite 的插件接口仍以 resolveId、load、transform 等钩子为核心,并可通过 hook filters 在进入 JavaScript 钩子前过滤模块。Vite 8 还加入了 resolve.tsconfigPaths、server.forwardConsole 等配置,分别用于解析 tsconfig.json 中的路径映射,以及把浏览器运行时错误和日志转发到 dev server 终端。这些变化分别作用于开发和生产流程。
开发阶段
dev server 串起整个开发流程,可以拆成四段:启动 dev server、首次打开页面、按需加载模块和热更新。
启动 dev server
执行 vite dev 后,Vite 会先加载配置、解析插件,并准备 HTTP 服务、WebSocket 通道和文件监听。这里准备的是一个按需响应模块请求的服务,不会提前处理所有页面、路由和业务模块,因此冷启动通常较快。
启动阶段还会准备依赖预构建。Vite 发现 import React from 'react' 这类裸模块导入后,会先通过 Rolldown 预构建依赖,再把导入地址改写为 /node_modules/.vite/deps/react.js?v=hash 这类浏览器可以请求的 URL。预构建主要解决两个问题:
- 兼容性:将 CJS、UMD 或具有复杂入口的依赖转换为 ESM;
- 请求数量:有些 ESM 依赖包含大量小模块,浏览器逐个请求会拖慢页面加载。预构建会将这些模块合并,减少请求数量。
预构建产物保存在 node_modules/.vite/deps/。后续启动时,如果 lockfile 和相关配置没有变化,Vite 会复用已有产物;如果运行过程中发现新的裸模块导入,则会重新执行预构建,并在需要时刷新页面。通常无需手动配置,只有自动识别结果不符合预期时,才需要通过 optimizeDeps.include 指定要预构建的依赖,或通过 optimizeDeps.exclude 排除不需要预构建的依赖。
首次打开页面
浏览器首次请求 index.html 时,dev server 会转换 HTML、注入 HMR 客户端,再将结果返回给浏览器。HMR 客户端随后与服务端建立 WebSocket 连接,用于接收模块更新、错误信息和页面重载指令。
浏览器收到 HTML 后,会从入口脚本开始沿 ESM 导入关系请求源码模块。Vite 不会在启动时生成完整 bundle,而是在浏览器请求模块时按需处理。
按需加载模块
以 /src/App.tsx 为例,下面的流程图展示 dev server 从收到请求到返回 ESM 的处理过程:
这条链路中,resolveId 把导入路径解析为模块 id,load 读取或生成模块内容,transform 负责转换源码。转换结束后,Vite 会分析 import 语句,将导入地址改写为浏览器可以请求的 URL,并把依赖关系记录到模块图中。转换结果会作为 transformResult 缓存在对应的模块节点上。
CSS 和静态资源也会按需处理。导入 CSS 时,Vite 会将其包装成模块,并支持样式热更新;导入图片或字体时,Vite 会返回对应的资源 URL。public 目录中的文件不经过转换,直接按原路径提供。
模块缓存与模块图
Vite 会结合浏览器缓存和 dev server 的转换缓存,避免重复处理模块。源码请求使用 HTTP 协商缓存,预构建依赖则通过带版本参数的 URL 进行强缓存。dev server 会把转换结果保存在模块节点中,并在缓存未失效时复用。
为了管理转换结果和模块之间的导入关系,Vite 会维护模块图。下面的图展示了模块图如何定位节点,以及每个节点保存的主要信息:
图中的主要数据结构及字段职责如下:
EnvironmentModuleGraph:通过 URL、id、文件路径或 ETag 定位模块节点;EnvironmentModuleNode:保存转换结果、失效状态和 HMR 边界信息;importers:记录哪些模块导入了当前模块;importedModules:记录当前模块导入了哪些模块。
再次请求尚未失效的模块时,Vite 可以复用转换结果;文件变化后,则沿导入关系使受影响的模块失效,并计算 HMR 更新范围。
热更新
文件保存后,chokidar 会把变化通知 Vite。Vite 定位受影响的模块节点,并通过 handleHotUpdate 让插件调整本次更新范围,随后沿 importers 向上寻找 HMR 边界并使相关模块失效。找到边界时推送局部更新,否则触发整页刷新。完整流程如下:
实际开发中,HMR 边界通常由 React、Vue 等框架插件在转换阶段注入或标记。Vite 负责 HMR 通信和模块更新,框架插件负责将更新接入框架的刷新机制。以 React 为例,组件状态能否保留取决于 React Fast Refresh 的边界判断。
为了说明 HMR 边界如何接收更新,下面用底层 API 展示一个简化示例。假设 utils 导出了 formatMessage,可以在它更新后替换当前使用的函数:
生产阶段
构建流程
vite build 从入口开始解析、加载和转换模块,构建完整依赖图,最终生成可部署的静态资源。标准应用默认以 index.html 为入口,也可以通过 build.rolldownOptions.input 指定其它入口。
Vite 8 使用 Rust 编写的 Rolldown 完成生产构建。Rolldown 会在模块处理和产物生成过程中执行相应的插件钩子,并兼容 Rollup 插件 API,因此大多数现有 Vite 插件可以继续使用。完整流程如下:
优化策略
基于完整依赖图,Vite 和 Rolldown 默认会执行以下优化:
- Tree Shaking:基于模块的静态结构移除未使用代码;
- Code Splitting:根据入口、动态导入和自定义规则生成 chunk;
- CSS Code Splitting:提取异步 chunk 关联的 CSS,并确保 CSS 加载完成后再执行该 chunk;
- Module Preload:预加载入口依赖和动态导入所需的 chunk,减少串行请求;
- 资源哈希:根据内容生成带 hash 的文件名,支持长期缓存;
- 压缩:客户端构建默认使用 Oxc Minifier 压缩 JS,使用 Lightning CSS 压缩 CSS。
这些策略通常不需要额外配置。下面以 React 项目为例,展示几项可按实际情况调整的配置:
插件机制
钩子分类
Vite 插件基于 Rolldown 插件接口,并提供配置解析、dev server 扩展、HTML 转换和 HMR 处理等专属钩子。resolveId、load、transform 等通用钩子同时用于 dev 和 build,generateBundle 等产物生成钩子则只在 build 中执行。
按执行阶段和职责划分,常用钩子如下:
插件可以通过 apply: 'serve' | 'build' 限制在 dev 或 build 中生效,也可以通过 hook filters 在调用前过滤模块。为兼容不支持 hook filters 的旧版 Vite,钩子内部仍需保留相同检查。下面的 alias 插件只处理 @/ 导入,并将其映射到 src/:
常用插件
Module Federation
Vite 本身没有内置 Module Federation,需要跨应用共享模块时,可以接入 @module-federation/vite。remote 负责暴露独立构建的模块,host 负责注册远程入口并通过别名导入。
remote 配置如下:
host 端通过 remotes 注册远程入口:
如果 host 要加载 Webpack 产出的 var remote,要把入口格式和全局容器名写清楚,同时确认共享依赖版本和运行时版本:
然后在 host 里按「导入前缀/暴露模块名」导入:
dev 与 build 差异
dev 和 build 共用配置与插件体系,但处理范围和输出目标不同:dev 按浏览器请求转换模块,build 则从入口出发分析完整依赖图并生成部署产物。差异主要体现在以下方面:
-
处理范围:未访问的路由或模块可能不会在 dev 中被转换,build 会处理所有可达模块,因此可能暴露模块解析、语法或变量动态导入问题。变量动态导入需使用相对路径和明确扩展名,变量只能表示一层文件名;更复杂的场景可使用
import.meta.glob; -
转换目标:dev 面向现代浏览器,尽量保留源码语法;build 则根据
build.target转换语法,并执行 Tree Shaking、代码分块和压缩; -
依赖处理:依赖预构建只用于 dev,build 会重新分析并打包依赖。非标准的 CJS/ESM 导出可能暴露导入错误,错误的
sideEffects声明也可能导致必要代码被移除; -
资源路径:dev server 直接提供本地资源,build 产物中的 JS、CSS 和图片 URL 则根据
base生成。部署到 CDN 或子路径时,需要按实际路径配置并验证产物。
因此,提交或部署前应运行 vite build,再通过 vite preview 或真实环境检查产物。Vite 只转译 TypeScript,不执行类型检查,TypeScript 项目还需要运行 tsc --noEmit 或相应的检查工具。