Vite

Vite 这几年之所以迅速流行,不是因为它“更时髦”,而是因为它重新拆分了前端构建的两个阶段:

  • 开发阶段:尽量不做整包构建,直接利用浏览器原生 ESM
  • 生产阶段:再交给 Rollup 做完整打包和优化

这个设计直接解决了传统打包工具在大型项目中的一个核心痛点:冷启动太慢,改一行代码也要重新经过大量构建流程

一、先理解 Vite 解决的是什么问题

Webpack 时代,一个典型开发过程通常是:

  1. 读取入口文件
  2. 分析依赖图
  3. 经过一系列 loaderplugin
  4. 生成 bundle
  5. 启动 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

开发时追求快,生产时追求稳和优化能力。因此 Vitebuild 阶段并不自己发明一整套打包器,而是站在 Rollup 之上:

  • 代码分包
  • Tree Shaking
  • Chunk 拆分
  • 插件生态
  • 库模式打包

这也是为什么 Vite 在工程体验和产物质量之间取得了不错平衡。

三、为什么 Vite 会感觉“特别快”

很多人说 Vite 快,但只停留在结论层面。更重要的是理解它快在哪。

1. 冷启动快

因为开发阶段不需要先把整个项目全部打包完成,服务几乎可以很快启动起来。

2. 热更新快

当你改一个文件时,Vite 通常只需要让受影响的模块失效并重新请求,而不是重新构建整棵依赖图。

3. 转译快

依赖预构建大量依赖 esbuild,而 esbuild 基于 Go 实现,速度极快。

但要注意,Vite 快主要是开发体验快,不是所有场景都天然更快。

四、Vite 的开发流程长什么样

可以把一次本地开发过程理解为下面几步:

  1. 启动开发服务
  2. 扫描项目依赖并预构建第三方包
  3. 浏览器请求入口模块
  4. 服务端按需转换源码并返回浏览器
  5. 文件变更时,仅使受影响模块失效
  6. 通过 HMR 推送更新给浏览器

这和传统“先全量 bundle 再启动”的模型非常不同。

五、HMR 为什么在 Vite 里体验更好

1. 模块粒度更细

传统方案里,一个模块变更可能会波及更大范围的重建;而 Vite 倾向于以模块为单位进行更新传播。

2. 框架层做了额外适配

VueReact 等框架中,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、或者包含特殊文件类型时,可能会出现预构建异常。

这时常见排查方向是:

  • optimizeDeps
  • build.commonjsOptions
  • 包本身的 exports 字段
  • 是否存在浏览器不兼容代码

八、Vite 适合什么,不适合什么

适合

  • 中大型前端应用开发
  • 追求更好本地开发体验的项目
  • VueReactSvelte 等现代前端项目
  • 需要快速搭建和迭代的团队项目

不适合的场景并不是“不能用”,而是要多考虑

  • 对极特殊构建流程依赖很深的遗留项目
  • 大量依赖老旧 Webpack loader/plugin 生态的系统
  • 多年历史包袱、模块格式不统一的大型单仓项目

Vite 的优势很大,但迁移成本也要纳入评估。

九、一个实际案例:为什么本地能跑,构建后路径错了

这是 Vite 项目里很典型的问题。

例如:

  • 本地开发访问资源正常
  • 打包部署后静态资源 404

这类问题通常要优先检查:

  • base 配置是否正确
  • 部署路径是根路径还是子路径
  • 资源引用是相对路径还是绝对路径
  • 是否用了与部署环境不兼容的路由 history 模式

这类问题说明:开发阶段快,并不等于构建产物可以忽略部署语义。

十、Vite 和 Webpack、Rollup 的关系

Vite 和 Webpack

  • Webpack 更像一个高度通用的“全流程打包平台”
  • Vite 更强调现代浏览器能力下的开发体验优化

Vite 和 Rollup

  • Vite 开发时不是 Rollup
  • Vite 生产构建时大量复用 Rollup

理解这点很重要,不然会误以为 Vite 是“另一个打包器”,实际上它更像是现代前端开发与构建的一整套方案。

十一、核心源码怎么读

如果你想真正读懂 Vite,建议不要一开始就钻进所有细节,而是先抓住几条主线:

  • 开发服务是怎么启动的
  • 一个模块请求是怎么被处理的
  • HMR 更新是怎么传播的
  • 生产构建为什么会走到 Rollup

1. 先抓入口:createServer

开发态最值得先看的入口就是 createServer。从职责上理解,它大致做了这些事情:

  1. 解析用户配置
  2. 创建插件容器
  3. 创建模块图 ModuleGraph
  4. 初始化文件监听器
  5. 组装中间件链
  6. 创建 WebSocket 通道用于 HMR
  7. 返回开发服务器实例

可以把它想象成一个“把所有能力装配起来的工厂函数”。

它的伪代码大致像这样:

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 并不是简单读取文件返回,而是会经过一条比较清晰的处理链:

  1. 解析请求 URL
  2. 判断是不是特殊请求,例如 /@vite/client/@id/xxx
  3. 定位到真实文件路径
  4. 调用插件链执行 resolveId
  5. 调用插件链执行 load
  6. 调用插件链执行 transform
  7. 注入 HMR、Source Map、import 重写等开发态能力
  8. 返回给浏览器

这里最关键的不是某一行代码,而是你要理解:

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 不是“全量重建”,而是:

  1. 找到受影响模块
  2. 沿反向依赖往上追踪
  3. 判断是否命中 HMR 边界
  4. 决定是热更新、局部失效,还是整页刷新

这正是 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 可以概括成:

  1. 解析构建配置
  2. 注入 Vite 自己的内部插件
  3. 把用户配置转换成 RollupOptions
  4. 调用 Rollup 执行打包
  5. 对产物做清单、资源路径、HTML 注入等后处理

也就是说,Vite build 更像一层“面向现代前端项目的构建编排器”。

2. 为什么看懂 Vite build 一定会接触 Rollup

因为最终真正负责:

  • chunk 拆分
  • 输出格式组织
  • 产物生成
  • renderChunk/generateBundle

这些核心工作的大头,依然在 Rollup 那一侧。

十三、读 Vite 源码最推荐的顺序

  1. 先看配置解析,不急着记细节
  2. 再看 createServer
  3. 再看 pluginContainerresolve/load/transform
  4. 再看 ModuleGraphHMR
  5. 最后再看 build 如何转接到 Rollup

十四、学习 Vite 最该掌握的能力

  • 理解开发态 ESM 服务模型
  • 理解依赖预构建
  • 理解 HMR 更新边界
  • 理解 vite.config.ts 的职责
  • 理解开发与生产构建不是同一套机制

总结

Vite 的价值,不只是“快”,而是它重新定义了现代前端开发环境的默认体验:

  • 开发阶段少做无意义工作
  • 利用浏览器原生能力
  • 用更轻的方式处理模块更新
  • 生产阶段继续借助成熟打包能力

如果你已经习惯传统打包工具,真正应该学习的不是几个配置项,而是背后的构建模型变化。

上次更新:
贡献者: Joe, joe