在前端工程化领域,Vite 凭借其极致的开发体验和强大的构建能力,已成为新一代开发工具链的事实标准。随着 Vite 8 的正式发布,这套工具在性能和架构上再次实现突破——底层打包器统一为 Rust 编写的 Rolldown,开发环境启动速度和热更新响应迈入毫秒级时代。而作为开发者每天最常接触的命令行入口,vite dev 和 vite serve 背后承载着怎样的设计理念?它们又有哪些鲜为人知的细节?本文将为你一一揭晓。
命令本质:开发服务器的统一入口
在 Vite 8 中,vite dev 与 vite serve 实际上是同一个命令的两种不同叫法,二者完全等价。
vite
vite dev
vite serve
之所以保留两个名称,主要是为了兼容过往的习惯(如 serve 源自早期版本,而 dev 更直观地表达开发用途)。
vite 启动通用的命令行参数?
// 定义 Vite 命令行工具
const cli = cac('vite')
cli
.option('-c, --config <file>', `[string] use specified config file`)
.option('--base <path>', `[string] public base path (default: /)`, {
type: [convertBase],
})
.option('-l, --logLevel <level>', `[string] info | warn | error | silent`)
.option('--clearScreen', `[boolean] allow/disable clear screen when logging`)
.option(
'--configLoader <loader>',
`[string] use 'bundle' to bundle the config with Rolldown, or 'runner' (experimental) to process it on the fly, or 'native' (experimental) to load using the native runtime (default: bundle)`,
)
.option('-d, --debug [feat]', `[string | boolean] show debug logs`)
.option('-f, --filter <filter>', `[string] filter debug logs`)
.option('-m, --mode <mode>', `[string] set env mode`)
# 指定配置文件路径
vite dev --config my.config.js
# 设置公共路径,默认 /
vite dev --base /my-app/
# 设置日志级别
vite dev --logLevel error` # 只输出错误
vite dev --clearScreen # 启用清屏
vite dev --no-clearScreen # 禁用清屏
# `bundle`(默认):使用 Rolldown 将配置文件打包后执行。
# `runner`(实验性):使用动态 `import()` 即时处理配置文件。
# `native`(实验性):使用原生 Node.js 模块加载(需配置文件为 ESM)。
vite dev --configLoader runner # 使用 Rolldown 将配置文件打包后执行
vite dev --debug # 开启全部调试日志
vite dev --debug vite:hmr # 仅显示 HMR 相关调试信息
# 指定运行模式(如 `development`、`production`、`staging`)。
# Vite 会加载对应的环境变量文件(例如 `.env.[mode]`),并影响 `import.meta.env.MODE` 的值。
vite dev --mode staging
vite dev 启动接收的命令行参数
在当前目录下启动 Vite 开发服务器。vite dev 和 vite serve 是 vite 的别名。
cli
.command('[root]', 'start dev server') // default command
.alias('serve') // the command is called 'serve' in Vite's API
.alias('dev') // alias to align with the script name
.option('--host [host]', `[string] specify hostname`, { type: [convertHost] })
.option('--port <port>', `[number] specify port`)
.option('--open [path]', `[boolean | string] open browser on startup`)
.option('--cors', `[boolean] enable CORS`)
.option('--strictPort', `[boolean] exit if specified port is already in use`)
.option(
'--force',
`[boolean] force the optimizer to ignore the cache and re-bundle`,
)
.option(
'--experimentalBundle',
`[boolean] use experimental full bundle mode (this is highly experimental)`,
)
# 指定项目的根目录。如果不提供,默认使用当前工作目录(`process.cwd()`)
vite dev ./my-project
vite dev --host # 监听所有接口
vite dev --host localhost # 仅监听本地
vite dev --port 3000
vite dev --open # 打开 http://localhost:5173/
vite dev --open /admin # 打开 http://localhost:5173/admin
# 强制依赖优化器忽略缓存,重新预构建所有依赖(`optimizeDeps`)
vite dev --force
# 启用实验性的“全量打包开发模式”(`bundledDev`)
vite dev --experimentalBundle
启用实验性的“全量打包开发模式”,文件会被打包。会减少大量请求。
命令行执行 vite 后做了什么?
- 创建 server 实例
- 启动监听端口
async (
root: string,
options: ServerOptions & ExperimentalDevOptions & GlobalCLIOptions,
) => {
filterDuplicateOptions(options)
// output structure is preserved even after bundling so require()
// is ok here
// 动态导入并创建开发服务器
const { createServer } = await import('./server')
try {
const server = await createServer({
root,
base: options.base,
mode: options.mode,
configFile: options.config,
configLoader: options.configLoader,
logLevel: options.logLevel,
clearScreen: options.clearScreen,
server: cleanGlobalCLIOptions(options),
forceOptimizeDeps: options.force,
experimental: {
bundledDev: options.experimentalBundle,
},
})
// 校验服务器实例并启动
if (!server.httpServer) {
throw new Error('HTTP server not available')
}
// 启动 HTTP 服务器监听指定端口
await server.listen()
// 输出启动日志
const info = server.config.logger.info
const modeString =
// 非 development 模式,输出环境模式
options.mode && options.mode !== 'development'
? ` ${colors.bgGreen(` ${colors.bold(options.mode)} `)}`
: ''
// 启动耗时(计算从 Vite 启动到服务器就绪的时间)
const viteStartTime = global.__vite_start_time ?? false
const startupDurationString = viteStartTime
? colors.dim(
`ready in ${colors.reset(
colors.bold(Math.ceil(performance.now() - viteStartTime)),
)} ms`,
)
: ''
// 检查是否有已存在的日志输出(避免重复打印)
const hasExistingLogs =
process.stdout.bytesWritten > 0 || process.stderr.bytesWritten > 0
// 输出核心启动日志(Vite 版本 + 模式 + 启动耗时)
info(
`\n ${colors.green(
`${colors.bold('VITE')} v${VERSION}`,
)}${modeString} ${startupDurationString}\n`,
{
clear: !hasExistingLogs,
},
)
// 打印服务器访问地址(如 http://localhost:3000/)
server.printUrls()
const customShortcuts: CLIShortcut<typeof server>[] = []
if (profileSession) {
customShortcuts.push({
key: 'p',
description: 'start/stop the profiler',
async action(server) {
if (profileSession) {
await stopProfiler(server.config.logger.info)
} else {
const inspector = await import('node:inspector').then(
(r) => r.default,
)
await new Promise<void>((res) => {
profileSession = new inspector.Session()
profileSession.connect()
profileSession.post('Profiler.enable', () => {
profileSession!.post('Profiler.start', () => {
server.config.logger.info('Profiler started')
res()
})
})
})
}
},
})
}
// 绑定快捷键到服务器(print: true 表示打印快捷键说明)
server.bindCLIShortcuts({ print: true, customShortcuts })
} catch (e) {
const logger = createLogger(options.logLevel)
logger.error(
colors.red(`error when starting dev server:\n${inspect(e)}`),
{
error: e,
},
)
await stopProfiler(logger.info)
process.exit(1)
}
},
// 启动 HTTP 服务器监听指定端口
async listen(port?: number, isRestart?: boolean) {
// 解析主机名
const hostname = await resolveHostname(config.server.host)
if (httpServer) {
httpServer.prependListener('listening', () => {
// 解析服务器监听的 URL 地址
server.resolvedUrls = resolveServerUrls(
httpServer,
config.server,
hostname,
httpsOptions,
config,
)
})
}
// 启动 HTTP 服务器
await startServer(server, hostname, port)
if (httpServer) {
// 如果不是重启,配置了 open 选项打开浏览器
if (!isRestart && config.server.open) server.openBrowser()
}
return server
},
createServer 函数做了什么工作?
- 参数解析与配置校验。
- 服务器基础设施创建(HTTP/WS/中间件/文件监听)。
- 多环境(environments)初始化。
- 服务器对象构建与向后兼容。
- 中间件栈构建。
- 文件变化监听与 HMR。
- 启动服务器逻辑。
- 返回 server 实例。
一、config 解析
- 加载配置文件:读取
vite.config.js/vite.config.ts(可通过--config指定其他文件)。如果文件是 TypeScript,Vite 会使用esbuild或rolldown动态编译。 - 合并命令行参数:命令行选项优先级高于配置文件。
- 应用默认值:补充未提供的选项(如
root默认为process.cwd(),base默认为/)。 - 加载环境变量:根据
mode(默认development)读取.env和.env.[mode]文件,注入process.env和import.meta.env。 - 加载插件:收集用户配置中的
plugins数组,调用每个插件的config钩子(允许插件修改配置),最后调用configResolved钩子通知插件配置已解析完成。 - 生成
ResolvedConfig:输出完整的、只读的配置对象,包含server、build、optimizeDeps、environments等字段。
二、服务器基础设施创建(HTTP/WS/中间件/文件监听)
// 3、网络服务构建
const middlewares = connect() as Connect.Server
// middlewareMode 为 true 时,不解析 HTTP 服务器,以中间件模式创建;否则解析 HTTP 服务器
const httpServer = middlewareMode
? null
: await resolveHttpServer(middlewares, httpsOptions)
// 创建 WebSocket 服务器
const ws = createWebSocketServer(httpServer, config, httpsOptions)
新建 HTTP 服务
async function resolveHttpServer(
app: Connect.Server,
httpsOptions?: HttpsServerOptions,
): Promise<HttpServer> {
// 如果没有 httpsOptions,创建 HTTP 服务器
if (!httpsOptions) {
// http 模块在 net 的基础上增加了 HTTP 协议解析和封装能力。
// 当你创建一个 HTTP 服务器时,实际底层是一个 net.Server
const { createServer } = await import('node:http')
return createServer(app) // 创建 HTTP 服务器
}
// 如果有 httpsOptions,创建 HTTPS 服务器
const { createSecureServer } = await import('node:http2')
return createSecureServer(
{
// Manually increase the session memory to prevent 502 ENHANCE_YOUR_CALM
// errors on large numbers of requests
maxSessionMemory: 1000, // 增加会话内存,防止 502 错误
// Increase the stream reset rate limit to prevent net::ERR_HTTP2_PROTOCOL_ERROR
// errors on large numbers of requests
streamResetBurst: 100000, // 增加流重置突发量,防止 net::ERR_HTTP2_PROTOCOL_ERROR 错误
streamResetRate: 33, // 增加流重置速率,防止 net::ERR_HTTP2_PROTOCOL_ERROR 错误
...httpsOptions, // 合并 httpsOptions 选项
allowHTTP1: true, // 允许 HTTP/1 协议
},
// @ts-expect-error TODO: is this correct?
app,
)
}
三、 多环境(environments)初始化
Vite 8 引入了多环境(Environments)概念,每个环境(如 client、ssr)拥有独立的模块图、插件容器和依赖优化器。
const environments: Record<string, DevEnvironment> = {}
// 多环境(Environments)初始化
await Promise.all(
Object.entries(config.environments).map(
async ([name, environmentOptions]) => {
const environment = await environmentOptions.dev.createEnvironment(
name,
config,
{
ws,
},
)
environments[name] = environment
const previousInstance =
options.previousEnvironments?.[environment.name]
await environment.init({ watcher, previousInstance })
},
),
)
四、环境向后兼容
在 Vite 8 引入多环境(environments)之前,Vite 只有一个全局的模块图。升级到 Vite 8 后,每个环境(client、ssr)有了自己独立的模块图,但为了不破坏现有的插件和 API,Vite 需要提供一个兼容层,使得老代码依然可以通过 server.moduleGraph 访问模块图。
五、中间件栈构建
- 请求计时器(仅 DEBUG 模式)
- 拒绝无效请求(过滤包含空格等非法字符的请求)
- CORS 中间件(默认启用)
- 主机验证(防止 DNS 重绑定攻击)
- 用户插件
configureServer钩子(允许插件注入自定义中间件) - 缓存转换中间件(若未启用
bundledDev) - 代理中间件(将
/api等请求转发到后端服务器) - Base 路径中间件(处理
base配置) - 编辑器打开支持(
/__open-in-editor) - HMR Ping 处理(响应客户端心跳)
public目录静态服务(直接返回public下的文件)- 转换中间件(核心) :拦截对
.js、.vue、.ts等文件的请求,调用插件链进行转换,返回最终代码。 - 静态文件服务:返回项目根目录下未被转换的静态资源。
- HTML fallback(SPA 模式下,未匹配路径返回
index.html) index.html转换中间件:注入客户端脚本(/@vite/client)和环境变量。- 404 处理
- 错误处理中间件
六、利用 chokidar,文件变化监听
// 9、文件变更事件处理
// 监听文件变化事件
watcher.on('change', async (file) => {
file = normalizePath(file)
// 检查是否是 TypeScript 配置文件变化,如果是则重启服务器
reloadOnTsconfigChange(server, file)
await Promise.all(
Object.values(server.environments).map((environment) =>
// 通知所有环境的插件容器文件已更新
environment.pluginContainer.watchChange(file, { event: 'update' }),
),
)
// invalidate module graph cache on file change
for (const environment of Object.values(server.environments)) {
environment.moduleGraph.onFileChange(file)
}
// 触发热模块替换更新,将变更同步到客户端
await onHMRUpdate('update', file)
})
// 监听文件添加事件
watcher.on('add', (file) => {
onFileAddUnlink(file, false)
})
// 监听文件删除事件
watcher.on('unlink', (file) => {
onFileAddUnlink(file, true)
})
修改 tsconfig.app.json
修改 tsconfig.json文件
会全量刷新,执行 location.reload()。
4:13:20 PM [vite] changed tsconfig file detected: /Users/xxxx/Documents/code/cloudcode/vue3-vite-cube/tsconfig.json - Clearing cache and forcing full-reload to ensure TypeScript is compiled with updated config values. (x2)
{
"type": "custom",
"event": "file-changed",
"data": {
"file": "/Users/xxx/Documents/code/cloudcode/vue3-vite-cube/tsconfig.json"
}
}
修改 vue 页面的 script setup 块和 template 块
{
"type": "custom",
"event": "file-changed",
"data": {
"file": "/Users/xxx/Documents/code/cloudcode/vue3-vite-cube/src/pages/home/index.vue"
}
}
{
"type": "update",
"updates": [
{
"type": "js-update",
"timestamp": 1775036864943,
"path": "/src/pages/home/index.vue",
"acceptedPath": "/src/pages/home/index.vue",
"explicitImportRequired": false,
"isWithinCircularImport": false
}
]
}
修改 vue 页面的 style 块
{
"type": "custom",
"event": "file-changed",
"data": {
"file": "/Users/xxxxxx/Documents/code/cloudcode/vue3-vite-cube/src/pages/home/index.vue"
}
}
{
"type": "update",
"updates": [
{
"type": "js-update",
"timestamp": 1775037234261,
"path": "/src/pages/home/index.vue",
"acceptedPath": "/src/pages/home/index.vue",
"explicitImportRequired": false,
"isWithinCircularImport": false
},
{
"type": "js-update",
"timestamp": 1775037234261,
"path": "/src/pages/home/index.vue?vue&type=style&index=0&scoped=2c5296db&lang.css",
"acceptedPath": "/src/pages/home/index.vue?vue&type=style&index=0&scoped=2c5296db&lang.css",
"explicitImportRequired": false, // 示是否需要显式动态导入新模块
"isWithinCircularImport": false // 表示是否处于循环依赖中
}
]
}
七、启动服务器逻辑
真正启动服务器在 cli 中 server.listen 执行。
这里只是重写 listen方法 ,待启动服务器时执行。
- 调用
server.listen() - 监听配置的端口(默认 5173)
- 启动完成后执行回调
- 自动打开浏览器(如果配置
server.open) - 终端打印
let initingServer: Promise<void> | undefined
let serverInited = false // 标记服务器是否已初始化
if (!middlewareMode && httpServer) {
// overwrite listen to init optimizer before server start
const listen = httpServer.listen.bind(httpServer)
// 重写 listen 方法,确保在服务器启动前初始化优化器
httpServer.listen = (async (port: number, ...args: any[]) => {
try {
await initServer(true)
} catch (e) {
httpServer.emit('error', e)
return
}
// 调用原始 listen 方法启动服务器
return listen(port, ...args)
}) as any
} else {
await initServer(false)
}
const initServer = async (onListen: boolean) => {
if (serverInited) return // 如果服务器已初始化,直接返回
if (initingServer) return initingServer // 如果服务器正在初始化,直接返回
initingServer = (async function () {
// 如果没有配置 bundledDev,则在初始化服务器时调用 buildStart 方法
if (!config.experimental.bundledDev) {
// For backward compatibility, we call buildStart for the client
// environment when initing the server. For other environments
// buildStart will be called when the first request is transformed
await environments.client.pluginContainer.buildStart()
}
// ensure ws server started
// 确保 WebSocket 服务器已启动
if (onListen || options.listen) {
await Promise.all(
// 确保所有环境的服务器都启动
Object.values(environments).map((e) => e.listen(server)),
)
}
initingServer = undefined // 清空初始化 Promise
serverInited = true // 标记服务器已初始化
})()
return initingServer
}
热更新
Vite 的热更新(HMR)基于原生 ES 模块和 WebSocket 实现,能在文件修改后仅更新受影响的模块,无需刷新页面,从而保留应用状态。其原理可分为服务端和客户端两个阶段。
服务端:变化检测与消息推送
一、文件检测
Vite 使用 chokidar 库来监听文件系统的变化。在 _createServer 函数中,会创建一个文件监听器(watcher),监听范围包括:
- 项目根目录(
root) - 配置文件依赖(
config.configFileDependencies) - 环境变量文件(
.env等) public目录
(chokidar.watch(
// config file dependencies and env file might be outside of root
[
...(config.experimental.bundledDev ? [] : [root]),
...config.configFileDependencies,
...getEnvFilesForMode(config.mode, config.envDir),
// Watch the public directory explicitly because it might be outside
// of the root directory.
...(publicDir && publicFiles ? [publicDir] : []),
],
resolvedWatchOptions,
) as FSWatcher)
二、模块图与依赖分析
1、 模块图的数据结构
urlToModuleMap:根据 URL 查找模块节点。fileToModulesMap:根据文件路径查找对应的模块节点(一个文件可能对应多个模块,如?import和?url查询)。- 每个模块节点(
ModuleNode)记录了:importers:依赖该模块的模块(即父模块)。importedModules:该模块导入的子模块。
2、依赖分析
当文件发生变化时,handleHMRUpdate 会执行以下步骤:
- 根据文件路径找到对应的模块节点(
moduleGraph.getModulesByFile(file))。 - 遍历这些模块节点,收集所有受影响的模块(包括自己以及所有
importers链上的模块)。 - 通过模块图向上追溯,找到所有依赖该模块的模块,直到没有更多依赖者为止
三、重新编译与生成更新消息
对于每个受影响的模块,Vite 调用 environment.transformRequest(url) 重新进行转换。该函数会经过完整的插件链(resolveId → load → transform),生成新的模块代码和 source map,并更新模块图中的 transformResult 缓存。
编译过程中,Vite 会记录一个时间戳(timestamp),用于客户端绕过浏览器缓存。
四、Websocket 推送消息
Vite 开发服务器内置了一个 WebSocket 服务器,用于与客户端通信。当 update 消息生成后,Vite 会通过 WebSocket 将其推送给所有已连接的客户端。
客户端:接收消息并执行更新
一、客户端初始化与 Websocket 连接
1、注入客户端脚本
当浏览器请求 index.html 时,Vite 的 indexHtmlMiddleware 会调用 clientPlugin 的 transformIndexHtml 钩子,在 HTML 中自动注入 <script type="module" src="/@vite/client">,该脚本负责建立 WebSocket 连接,暴露 HMR API。
2、建立 WebSocket 连接
客户端脚本首先会创建一个 WebSocket 连接指向开发服务器(默认地址 ws://localhost:5173)。同时,它会监听 open、message、close、error 等事件。
连接成功后,服务端会发送 { type: 'connected' } 消息,客户端收到后标记为就绪状态。
3、暴露 import.meta.hot API
客户端在全局维护了几个 Map 结构,用于存储每个模块注册的 HMR 回调(accept、dispose 等)。同时,它定义了一个 createHotContext 函数,该函数返回一个包含 accept、dispose、invalidate 等方法的对象。
二、接收消息与类型分发
客户端 WebSocket 的 message 事件处理函数负责解析 JSON 消息,并根据 type 字段分发到不同的处理逻辑。
客户端 WebSocket 收到消息后,根据 type 进行处理:
connected, 标记就绪,可发送预热请求。update:遍历updates数组,对每个更新执行热替换。full-reload:调用location.reload()刷新页面。prune,custom,自定义事件。error:在页面上显示错误覆盖层。ping,不做处理。
async function handleMessage(payload: HotPayload) {
switch (payload.type) {
// WebSocket 和服务器握手成功,打印日志。
case 'connected':
console.debug(`[vite] connected.`)
break
// JS/CSS 热更新
case 'update':
// 通知所有插件 / 监听:马上要热更新了
// 用于在热更新前执行自定义逻辑,例如刷新页面
await hmrClient.notifyListeners('vite:beforeUpdate', payload)
if (hasDocument) {
// if this is the first update and there's already an error overlay, it
// means the page opened with existing server compile error and the whole
// module script failed to load (since one of the nested imports is 500).
// in this case a normal update won't work and a full reload is needed.
// 首次更新容错 + 清理错误
if (isFirstUpdate && hasErrorOverlay()) {
// 如果页面一打开就报错(编译失败),第一次热更新直接全页刷新,确保能正常运行
location.reload() // 刚打开页面就报错,直接刷新修复
return
} else {
if (enableOverlay) {
clearErrorOverlay() // 清空之前的报错
}
isFirstUpdate = false
}
}
// 所有文件更新并行处理,速度极快
await Promise.all(
payload.updates.map(async (update): Promise<void> => {
if (update.type === 'js-update') {
return hmrClient.queueUpdate(update) // 交给核心引擎更新JS
}
// css-update
// this is only sent when a css file referenced with <link> is updated
const { path, timestamp } = update
const searchUrl = cleanUrl(path)
// can't use querySelector with `[href*=]` here since the link may be
// using relative paths so we need to use link.href to grab the full
// URL for the include check.
// 找到页面对应的旧 <link> 标签
// 页面 <link href="style.css"> 是相对路径
// e.href 会返回 http://localhost:5173/src/style.css 完整 URL
const el = Array.from(
document.querySelectorAll<HTMLLinkElement>('link'),
).find(
(e) =>
!outdatedLinkTags.has(e) && cleanUrl(e.href).includes(searchUrl),
)
if (!el) {
return
}
// 拼接带时间戳的新 CSS 路径
const newPath = `${base}${searchUrl.slice(1)}${
searchUrl.includes('?') ? '&' : '?'
}t=${timestamp}`
// rather than swapping the href on the existing tag, we will
// create a new link tag. Once the new stylesheet has loaded we
// will remove the existing link tag. This removes a Flash Of
// Unstyled Content that can occur when swapping out the tag href
// directly, as the new stylesheet has not yet been loaded.
return new Promise((resolve) => {
// 克隆新 link 标签,不直接改旧 href
const newLinkTag = el.cloneNode() as HTMLLinkElement
newLinkTag.href = new URL(newPath, el.href).href
const removeOldEl = () => {
el.remove()
console.debug(`[vite] css hot updated: ${searchUrl}`)
resolve()
}
// 等新 CSS 加载完成后,再删除旧标签
newLinkTag.addEventListener('load', removeOldEl)
newLinkTag.addEventListener('error', removeOldEl)
// 缓存新标签,避免重复删除
outdatedLinkTags.add(el)
// 插入新标签到旧标签后面
el.after(newLinkTag)
})
}),
)
// 触发更新完成事件
// 通知插件 / 框架:热更新完成
await hmrClient.notifyListeners('vite:afterUpdate', payload)
break
// 处理 custom 自定义消息
case 'custom': {
await hmrClient.notifyListeners(payload.event, payload.data)
if (payload.event === 'vite:ws:disconnect') {
// dom环境,且页面未卸载
if (hasDocument && !willUnload) {
console.log(`[vite] server connection lost. Polling for restart...`)
const socket = payload.data.webSocket as WebSocket
const url = new URL(socket.url)
url.search = '' // remove query string including `token`
await waitForSuccessfulPing(url.href) // 轮询等待服务器重启
location.reload() // 服务器回来后,自动刷新页面
}
}
break
}
// 处理 full-reload 全页刷新
case 'full-reload':
await hmrClient.notifyListeners('vite:beforeFullReload', payload)
if (hasDocument) {
if (payload.path && payload.path.endsWith('.html')) {
// if html file is edited, only reload the page if the browser is
// currently on that page.
const pagePath = decodeURI(location.pathname)
const payloadPath = base + payload.path.slice(1)
if (
pagePath === payloadPath ||
payload.path === '/index.html' ||
(pagePath.endsWith('/') && pagePath + 'index.html' === payloadPath)
) {
pageReload()
}
return
} else {
pageReload()
}
}
break
// 处理 prune 清理模块
case 'prune':
await hmrClient.notifyListeners('vite:beforePrune', payload)
await hmrClient.prunePaths(payload.paths)
break
// 显示红色错误遮罩
case 'error': {
await hmrClient.notifyListeners('vite:error', payload)
if (hasDocument) {
const err = payload.err
if (enableOverlay) {
createErrorOverlay(err)
} else {
console.error(
`[vite] Internal Server Error\n${err.message}\n${err.stack}`,
)
}
}
break
}
// 处理 ping 消息,心跳检测,不处理任何逻辑
case 'ping': // noop
break
// 处理默认情况
default: {
const check: never = payload
return check
}
}
}
三、处理 update 消息(热更新)
- 请求新模块(带时间戳),每个
update对象包含path、acceptedPath、timestamp等字段。客户端构造新的 UR,利用?t=timestamp强制绕过浏览器缓存。使用动态import()获取模块的导出对象。 - 执行
dispose回调(清理旧资源),在替换模块之前,需要先执行旧模块注册的dispose回调(如果有),以便清理定时器、事件监听等。 - 找到接受更新的模块,
- 针对css处理。如果
update.type === 'css-update',客户端不会通过import()请求,而是直接替换页面中的<link>或<style>标签。 - 失败回退(
full-reload),如果更新过程中发生错误(例如网络请求失败、回调抛出异常),或者找不到任何接受回调,客户端会发送full-reload指令,刷新整个页面以确保应用状态正确。
客户端执行 js-update
importUpdatedModule 是 Vite HMR 的模块更新加载器:拼接带时间戳的最新 URL,动态加载新代码 ,循环依赖异常时自动刷新。
// 普通 ESM 模式
// 动态加载最新的模块代码 → 解决浏览器缓存 → 处理循环依赖错误
async function importUpdatedModule({
acceptedPath, // 要更新的模块路径
timestamp, // 模块更新时间戳
explicitImportRequired, // 是否显式导入
isWithinCircularImport, // 是否在循环依赖里
}) {
// 拆分路径
const [acceptedPathWithoutQuery, query] = acceptedPath.split(`?`)
const importPromise = import(
/* @vite-ignore */ // 告诉 vite 不解析这个动态导入,由浏览器负责加载
base +
// 移除前导斜杠,确保路径正确
acceptedPathWithoutQuery.slice(1) +
// timestamp 用于刷新浏览器缓存,确保加载最新代码
`?${explicitImportRequired ? 'import&' : ''}t=${timestamp}${
query ? `&${query}` : ''
}`
)
if (isWithinCircularImport) {
// 循环依赖, 热更失败 → 自动刷新页面
importPromise.catch(() => {
console.info(
`[hmr] ${acceptedPath} failed to apply HMR as it's within a circular import. Reloading page to reset the execution order. ` +
`To debug and break the circular import, you can run \`vite --debug hmr\` to log the circular dependency path if a file change triggered it.`,
)
pageReload()
})
}
// 返回模块
return await importPromise
}
importUpdatedModule 负责用原生 ESM 加载最新代码,通知 Rolldown 运行时更新模块导出,循环依赖异常自动刷新页面。
// 打包开发模式(bundledDev)
async function importUpdatedModule({
url,
acceptedPath,
isWithinCircularImport, // 是否在循环依赖里
}) {
// 加载新代码,并通知 Rolldown 运行时更新模块
// import(base + url!) 浏览器原生 ESM 动态导入
// 浏览器发起网络请求 → 访问 Vite 开发服务器
// url 已带时间戳 → 强制不缓存,加载最新版
const importPromise = import(base + url!).then(() =>
// @ts-expect-error globalThis.__rolldown_runtime__
// 全局运行时.loadExports
// __rolldown_runtime__:Rolldown 运行时(Vite 新一代底层打包 / 运行核心)
// loadExports(acceptedPath)
// → 告诉运行时:重新收集这个模块的最新导出
// → 运行时会自动更新所有引用该模块的地方
globalThis.__rolldown_runtime__.loadExports(acceptedPath),
)
// 循环依赖容错
if (isWithinCircularImport) {
// 热更失败 → 自动刷新页面
importPromise.catch(() => {
console.info(
`[hmr] ${acceptedPath} failed to apply HMR as it's within a circular import. Reloading page to reset the execution order. ` +
`To debug and break the circular import, you can run \`vite --debug hmr\` to log the circular dependency path if a file change triggered it.`,
)
pageReload()
})
}
return await importPromise
}
更新文件
/**
* 处理 HMR 更新
* @param type 文件操作类型
* @param file 文件路径
*/
const onHMRUpdate = async (
type: 'create' | 'delete' | 'update',
file: string,
) => {
// 如果 HMR 已启用,则处理 HMR 更新
if (serverConfig.hmr !== false) {
await handleHMRUpdate(type, file, server)
}
}
新增文件/删除文件
/**
* 处理文件添加或删除
* @param file 文件路径
* @param isUnlink 是否删除文件
*/
const onFileAddUnlink = async (file: string, isUnlink: boolean) => {
file = normalizePath(file)
// 「检测文件是否为 tsconfig.json/jsconfig.json,若是则触发服务器重启」
// 因为这类配置文件变更会影响模块解析规则,必须重启才能生效。
reloadOnTsconfigChange(server, file)
await Promise.all(
// 通知所有环境的插件容器,同步文件变更事件
Object.values(server.environments).map((environment) =>
// 对每个环境,调用其插件容器的 watchChange 方法
// 传递文件路径和事件类型('delete' 或 'create')
environment.pluginContainer.watchChange(file, {
event: isUnlink ? 'delete' : 'create',
}),
),
)
if (publicDir && publicFiles) {
if (file.startsWith(publicDir)) {
const path = file.slice(publicDir.length)
publicFiles[isUnlink ? 'delete' : 'add'](path)
// 新增文件时:清理同名模块的 ETag 缓存,保证公共文件优先响应
// Vite 会为模块生成 ETag(实体标签),用于「ETag 快速路径」—— 客户端请求时,若 ETag 未变,直接返回缓存的模块内容
if (!isUnlink) {
// 获取客户端环境的模块图实例
const clientModuleGraph = server.environments.client.moduleGraph
// 根据路径 path(如 /image.png)查找模块图中是否存在同名模块
const moduleWithSamePath =
await clientModuleGraph.getModuleByUrl(path)
const etag = moduleWithSamePath?.transformResult?.etag
// 如果有etag ,则删除。
// 保证 public 下文件等优先级
if (etag) {
// The public file should win on the next request over a module with the
// same path. Prevent the transform etag fast path from serving the module
clientModuleGraph.etagToModuleMap.delete(etag)
}
}
}
}
// 文件删除时,清理模块依赖图缓存
if (isUnlink) {
// invalidate module graph cache on file change
for (const environment of Object.values(server.environments)) {
environment.moduleGraph.onFileDelete(file)
}
}
// 触发 HMR 更新,同步变更到客户端
await onHMRUpdate(isUnlink ? 'delete' : 'create', file)
}
禁止热更新
server: {
ws: false,
}
修改文件浏览器内容不会自动更新。
重启服务器
什么场景会触发开发服务器重启?
- 修改
vite.config.js配置文件。 - 依赖文件修改,如
package.json。 - 创建/修改
.env环境文件。 - 插件中调用
server.restart。
// 配置文件、配置文件依赖、环境文件变化时,自动重启服务器
if (isConfig || isConfigDependency || isEnv) {
// auto restart server
debugHmr?.(`[config change] ${colors.dim(shortFile)}`)
// 打印日志
config.logger.info(
colors.green(
`${normalizePath(
path.relative(process.cwd(), file),
)} changed, restarting server...`,
),
{ clear: true, timestamp: true },
)
try {
// 重启服务器
await restartServerWithUrls(server)
} catch (e) {
config.logger.error(colors.red(e))
}
return
}
server.restart
重启服务器前,会先关闭服务器(包含 停止HTTP服务,停止Websocket 服务,关闭文件监听,关闭所有环境的 DevEnvironment 实例,释放模块图、插件容器、依赖优化器等资源)。
// 重启 Vite 开发服务器,同时处理并发重启请求,确保同一时间只有一个重启操作在执行。
async restart(forceOptimize?: boolean) {
// 如果没有重启 Promise,创建一个
if (!server._restartPromise) {
// 设置是否强制优化依赖
server._forceOptimizeOnRestart = !!forceOptimize
// 重启服务器
server._restartPromise = restartServer(server).finally(() => {
// 重启完成后,重置重启 Promise 和强制优化依赖
server._restartPromise = null
server._forceOptimizeOnRestart = false
})
}
// 如果存在,说明已经有一个重启操作在进行中,直接返回该 Promise
return server._restartPromise
},
全量更新
什么场景会触发全量刷新?
- 修改
index.html文件。 - 修改
main.ts文件。 - 修改路由配置
router/index.ts文件。
{
"type": "full-reload",
"triggeredBy": "/Users/xxxxxx/Documents/code/cloudcode/vue3-vite-cube/src/common/utils.ts",
"path": "*"
}
// (dev only) the client itself cannot be hot updated.
// Vite 客户端自身文件变更 → 不能热更 → 必须整页刷新
if (file.startsWith(withTrailingSlash(normalizedClientDir))) {
environments.forEach(({ hot }) =>
hot.send({
type: 'full-reload',
path: '*',
triggeredBy: path.resolve(config.root, file),
}),
)
return
}