Vite
Vite 这几年之所以迅速流行,不是因为它“更时髦”,而是因为它重新拆分了前端构建的两个阶段:
- 开发阶段:尽量不做整包构建,直接利用浏览器原生
ESM - 生产阶段:再交给
Rollup做完整打包和优化
这个设计直接解决了传统打包工具在大型项目中的一个核心痛点:冷启动太慢,改一行代码也要重新经过大量构建流程。
一、先理解 Vite 解决的是什么问题
在 Webpack 时代,一个典型开发过程通常是:
- 读取入口文件
- 分析依赖图
- 经过一系列
loader和plugin - 生成 bundle
- 启动 dev server
这个过程在中小项目里问题不大,但当项目越来越大时,开发体验会明显下降:
- 启动慢
- 热更新慢
- 改动范围越大,重新构建越重
- 配置越来越复杂,排查问题成本高
Vite 的核心改进是:开发时不急着把整个项目打成一个 bundle。
二、Vite 的核心思路
1. 开发时基于浏览器原生 ESM
现代浏览器已经支持 ES Module,所以 Vite 在开发阶段尽量让浏览器直接请求源码模块。
例如:
import { createApp } from 'vue'
import App from './App.vue'
在开发环境下,Vite 不会像传统打包器那样先整体打包成一个大文件,而是:
- 浏览器请求入口模块
- 发现入口模块还依赖别的模块
- 浏览器继续按需请求这些模块
Vite在服务端按请求实时转换
这使得启动阶段非常轻。
2. 依赖预构建
如果完全把所有内容都交给浏览器按模块加载,也会有新问题:
node_modules里大量包是CommonJS- 深层依赖请求太多会拖慢加载
因此 Vite 会先对第三方依赖做一次预构建,通常借助 esbuild:
- 把
CommonJS转为ESM - 合并深层依赖,减少浏览器请求数
- 提高依赖加载效率
这一步是 Vite 快的关键之一。
3. 生产构建交给 Rollup
开发时追求快,生产时追求稳和优化能力。因此 Vite 在 build 阶段并不自己发明一整套打包器,而是站在 Rollup 之上:
- 代码分包
- Tree Shaking
- Chunk 拆分
- 插件生态
- 库模式打包
这也是为什么 Vite 在工程体验和产物质量之间取得了不错平衡。
三、为什么 Vite 会感觉“特别快”
很多人说 Vite 快,但只停留在结论层面。更重要的是理解它快在哪。
1. 冷启动快
因为开发阶段不需要先把整个项目全部打包完成,服务几乎可以很快启动起来。
2. 热更新快
当你改一个文件时,Vite 通常只需要让受影响的模块失效并重新请求,而不是重新构建整棵依赖图。
3. 转译快
依赖预构建大量依赖 esbuild,而 esbuild 基于 Go 实现,速度极快。
但要注意,Vite 快主要是开发体验快,不是所有场景都天然更快。
四、Vite 的开发流程长什么样
可以把一次本地开发过程理解为下面几步:
- 启动开发服务
- 扫描项目依赖并预构建第三方包
- 浏览器请求入口模块
- 服务端按需转换源码并返回浏览器
- 文件变更时,仅使受影响模块失效
- 通过 HMR 推送更新给浏览器
这和传统“先全量 bundle 再启动”的模型非常不同。
五、HMR 为什么在 Vite 里体验更好
1. 模块粒度更细
传统方案里,一个模块变更可能会波及更大范围的重建;而 Vite 倾向于以模块为单位进行更新传播。
2. 框架层做了额外适配
在 Vue、React 等框架中,Vite 配合对应插件后可以尽可能保留应用状态,减少整页刷新。
3. 更新路径更短
因为开发阶段尽量减少不必要的打包动作,所以改动到反馈的链路更短。
六、Vite 的配置应该怎么理解
很多人初学时会把 vite.config.ts 看作“又一个复杂配置文件”,其实更好的理解方式是:
它是对以下几类能力的声明:
- 项目根目录和别名解析
- 开发服务器行为
- 插件扩展能力
- 构建产物行为
- 代理、环境变量、CSS 处理等工程能力
一个常见配置示例:
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'path'
export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
'@': path.resolve(__dirname, 'src'),
},
},
server: {
port: 3000,
open: true,
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
},
},
},
build: {
sourcemap: true,
},
})
七、Vite 中最值得理解的几个点
1. 为什么路径别名有时能跑,有时不能跑
你需要区分几层解析能力:
- TypeScript 编译器的路径解析
- Vite 运行时的模块解析
- 测试工具的路径解析
- 编辑器智能提示的路径解析
如果只配了其中一层,就会出现“编辑器没报错,但运行失败”或“构建能过,但测试挂掉”的情况。
2. 环境变量为什么读不到
Vite 对环境变量有显式约定。前端能访问的变量通常需要以 VITE_ 前缀暴露,例如:
VITE_API_BASE=/api
然后在代码中读取:
const apiBase = import.meta.env.VITE_API_BASE
如果不理解这一层封装,很容易误以为“环境变量失效了”。
3. 为什么某些依赖会触发预构建问题
当依赖包本身导出方式复杂、混合 ESM / CJS、或者包含特殊文件类型时,可能会出现预构建异常。
这时常见排查方向是:
optimizeDepsbuild.commonjsOptions- 包本身的
exports字段 - 是否存在浏览器不兼容代码
八、Vite 适合什么,不适合什么
适合
- 中大型前端应用开发
- 追求更好本地开发体验的项目
Vue、React、Svelte等现代前端项目- 需要快速搭建和迭代的团队项目
不适合的场景并不是“不能用”,而是要多考虑
- 对极特殊构建流程依赖很深的遗留项目
- 大量依赖老旧
Webpack loader/plugin生态的系统 - 多年历史包袱、模块格式不统一的大型单仓项目
Vite 的优势很大,但迁移成本也要纳入评估。
九、一个实际案例:为什么本地能跑,构建后路径错了
这是 Vite 项目里很典型的问题。
例如:
- 本地开发访问资源正常
- 打包部署后静态资源 404
这类问题通常要优先检查:
base配置是否正确- 部署路径是根路径还是子路径
- 资源引用是相对路径还是绝对路径
- 是否用了与部署环境不兼容的路由 history 模式
这类问题说明:开发阶段快,并不等于构建产物可以忽略部署语义。
十、Vite 和 Webpack、Rollup 的关系
Vite 和 Webpack
Webpack更像一个高度通用的“全流程打包平台”Vite更强调现代浏览器能力下的开发体验优化
Vite 和 Rollup
Vite开发时不是RollupVite生产构建时大量复用Rollup
理解这点很重要,不然会误以为 Vite 是“另一个打包器”,实际上它更像是现代前端开发与构建的一整套方案。
十一、核心源码怎么读
如果你想真正读懂 Vite,建议不要一开始就钻进所有细节,而是先抓住几条主线:
- 开发服务是怎么启动的
- 一个模块请求是怎么被处理的
HMR更新是怎么传播的- 生产构建为什么会走到
Rollup
1. 先抓入口:createServer
开发态最值得先看的入口就是 createServer。从职责上理解,它大致做了这些事情:
- 解析用户配置
- 创建插件容器
- 创建模块图
ModuleGraph - 初始化文件监听器
- 组装中间件链
- 创建 WebSocket 通道用于
HMR - 返回开发服务器实例
可以把它想象成一个“把所有能力装配起来的工厂函数”。
它的伪代码大致像这样:
async function createServer(inlineConfig) {
const config = await resolveConfig(inlineConfig, 'serve')
const pluginContainer = await createPluginContainer(config)
const moduleGraph = new ModuleGraph()
const watcher = chokidar.watch(config.root)
const middlewares = connect()
setupTransformMiddleware(middlewares, pluginContainer, moduleGraph)
setupStaticMiddleware(middlewares)
setupHmr(watcher, moduleGraph)
return {
config,
pluginContainer,
moduleGraph,
middlewares,
watcher,
}
}
真正的源码当然更复杂,但你先建立这个装配视角,后面就不容易迷路。
2. 模块请求主链路:从 URL 到转换结果
浏览器请求一个模块时,Vite 并不是简单读取文件返回,而是会经过一条比较清晰的处理链:
- 解析请求 URL
- 判断是不是特殊请求,例如
/@vite/client、/@id/xxx - 定位到真实文件路径
- 调用插件链执行
resolveId - 调用插件链执行
load - 调用插件链执行
transform - 注入
HMR、Source Map、import 重写等开发态能力 - 返回给浏览器
这里最关键的不是某一行代码,而是你要理解:
Vite开发态本质上是“一个按请求实时做模块解析和转换的服务器”。
3. pluginContainer 是开发态的核心中枢
Vite 源码里一个非常重要的抽象就是插件容器。你可以把它理解成:
- 统一调度插件钩子
- 把
resolve/load/transform串联起来 - 在开发态复用一套接近
Rollup plugin的心智模型
例如一个非常典型的调用链是:
const resolved = await pluginContainer.resolveId(url)
const loaded = await pluginContainer.load(resolved.id)
const transformed = await pluginContainer.transform(code, resolved.id)
这也是为什么学过 Rollup 插件系统后,再看 Vite 会顺很多。
4. ModuleGraph 为什么重要
如果没有模块图,Vite 很难高效完成 HMR。
ModuleGraph 主要维护几类关系:
- 一个 URL 对应哪个模块记录
- 一个模块依赖了哪些模块
- 哪些模块反向依赖当前模块
- 当前模块是否有
HMR边界
当文件变化时,Vite 不是“全量重建”,而是:
- 找到受影响模块
- 沿反向依赖往上追踪
- 判断是否命中
HMR边界 - 决定是热更新、局部失效,还是整页刷新
这正是 Vite 热更新体验好的核心基础。
5. HMR 更新为什么快
从源码视角看,HMR 重点不是“推送一条消息”,而是“找到最小更新闭包”。
一个简化流程大致是:
watcher.on('change', async (file) => {
const mods = moduleGraph.getModulesByFile(file)
const boundaries = propagateUpdate(mods)
if (boundaries.length) {
ws.send({ type: 'update', updates: boundaries })
} else {
ws.send({ type: 'full-reload' })
}
})
这里最值得学习的,不是 API 名,而是这套设计:
- 文件系统变化 -> 模块图定位 -> 依赖传播 -> 决策更新边界
6. 依赖预构建为什么单独存在
Vite 源码里,依赖预构建和普通源码转换是两条不同关注线:
- 业务源码走按请求转换
- 第三方依赖先做预构建缓存
背后原因是:
- 第三方依赖相对稳定
- 预构建后可以复用缓存
- 可以提前处理
CommonJS -> ESM
这也是为什么第一次启动和后续启动速度体验不同:第一次常常在“准备依赖缓存”。
十二、生产构建源码脉络
很多人学 Vite 时只看开发态,但 build 同样值得理解。
1. build 阶段的关键步骤
从源码思路上看,Vite build 可以概括成:
- 解析构建配置
- 注入
Vite自己的内部插件 - 把用户配置转换成
RollupOptions - 调用
Rollup执行打包 - 对产物做清单、资源路径、HTML 注入等后处理
也就是说,Vite build 更像一层“面向现代前端项目的构建编排器”。
2. 为什么看懂 Vite build 一定会接触 Rollup
因为最终真正负责:
- chunk 拆分
- 输出格式组织
- 产物生成
renderChunk/generateBundle
这些核心工作的大头,依然在 Rollup 那一侧。
十三、读 Vite 源码最推荐的顺序
- 先看配置解析,不急着记细节
- 再看
createServer - 再看
pluginContainer的resolve/load/transform - 再看
ModuleGraph与HMR - 最后再看
build如何转接到Rollup
十四、学习 Vite 最该掌握的能力
- 理解开发态
ESM服务模型 - 理解依赖预构建
- 理解 HMR 更新边界
- 理解
vite.config.ts的职责 - 理解开发与生产构建不是同一套机制
总结
Vite 的价值,不只是“快”,而是它重新定义了现代前端开发环境的默认体验:
- 开发阶段少做无意义工作
- 利用浏览器原生能力
- 用更轻的方式处理模块更新
- 生产阶段继续借助成熟打包能力
如果你已经习惯传统打包工具,真正应该学习的不是几个配置项,而是背后的构建模型变化。
