Appearance
第5章 开发服务器架构
开篇引言
Vite 的开发服务器是整个项目中最复杂的子系统。它不只是一个"启动一个 HTTP 服务器然后返回文件"的简单程序——它是一个精密的运行时环境,需要在接收到浏览器请求的瞬间完成模块解析、代码转换、依赖预构建、HMR 通知等一系列操作,且全部延迟控制在毫秒级。
src/node/server/index.ts 是整个服务器的入口和骨架,不到 1100 行代码编排了十几个中间件、多个环境实例、WebSocket 服务器、文件监听器的协同工作。这个文件的结构揭示了 Vite 开发体验的全部秘密:为什么首次启动那么快?为什么文件修改后浏览器几乎瞬间更新?为什么大型单仓项目也不会让开发服务器变慢?
答案在于三个关键设计:
基于 Connect 的中间件栈:不是路由匹配,而是管线式处理。每个请求从头到尾经过十几个中间件,每个中间件只关注自己能处理的请求类型,其余放行给下一个。这种架构天然支持横切关注点的分离。
按需编译:浏览器请求哪个模块,服务器才编译哪个模块。没有预先打包的步骤,也没有全量扫描。模块的
resolveId -> load -> transform链条只在首次请求时执行,结果缓存在模块图中。多环境架构:每个
DevEnvironment(client、ssr 等)拥有独立的模块图和插件容器,互不干扰。这使得同一个服务器实例可以同时为客户端渲染和服务端渲染提供服务。
本章将从 _createServer 函数出发,逐步拆解开发服务器的每一个构成要素。
本章要点
_createServer是开发服务器的核心工厂函数,负责组装所有子系统- Connect 中间件栈包含 15+ 个中间件,按精确顺序排列,分为安全层、配置层、静态资源层、转换层、回退层
- WebSocket 服务器既可以共享 HTTP 端口,也可以独立监听,通过 token 机制防止跨站劫持
- Chokidar 文件监听器触发的变更事件驱动整个 HMR 管线
DevEnvironment封装了每个运行环境的模块图、插件容器、依赖优化器- 服务器支持中间件模式(
middlewareMode),可以嵌入到 Express/Koa 等框架中
5.1 服务器创建流程
5.1.1 入口函数
开发服务器的创建从 createServer 开始,它只是 _createServer 的薄包装:
typescript
// src/node/server/index.ts
export function createServer(
inlineConfig: InlineConfig | ResolvedConfig = {},
): Promise<ViteDevServer> {
return _createServer(inlineConfig, { listen: true })
}_createServer 是真正的工厂函数,它接受一个 options 参数来区分首次创建和重启场景。重启时会传入 previousEnvironments 和 previousShortcutsState 等状态,避免重复初始化。
5.1.2 初始化序列
_createServer 的执行可以分为七个阶段:
让我们逐一展开:
阶段 1:解析配置
typescript
const config = isResolvedConfig(inlineConfig)
? inlineConfig
: await resolveConfig(inlineConfig, 'serve')如果传入的是已解析的配置(重启场景),直接使用;否则调用 resolveConfig 进行完整的配置解析流程。
阶段 2:创建基础设施
typescript
const middlewares = connect() as Connect.Server
const httpServer = middlewareMode
? null // 中间件模式不创建 HTTP 服务器
: await resolveHttpServer(middlewares, httpsOptions)
const ws = createWebSocketServer(httpServer, config, httpsOptions)
const watcher = watchEnabled
? chokidar.watch([root, ...configFileDependencies, ...envFiles], resolvedWatchOptions)
: createNoopWatcher(resolvedWatchOptions)这段代码创建了四个核心对象:Connect 应用、HTTP 服务器、WebSocket 服务器、文件监听器。注意 middlewareMode 下不创建 HTTP 服务器——Vite 将作为中间件嵌入到外部服务器中。
阶段 3:创建环境实例
typescript
const environments: Record<string, DevEnvironment> = {}
await Promise.all(
Object.entries(config.environments).map(
async ([name, environmentOptions]) => {
const environment = await environmentOptions.dev.createEnvironment(
name, config, { ws },
)
environments[name] = environment
await environment.init({ watcher, previousInstance: options.previousEnvironments?.[name] })
},
),
)环境实例的创建是并行的。每个环境在 init 时创建自己的 EnvironmentPluginContainer(如第 4 章所述)和 EnvironmentModuleGraph。默认配置下会创建 client 和 ssr 两个环境。
阶段 4-5 会在后续章节详述。
阶段 6:安装中间件栈(下一节详述)
阶段 7:等待服务器就绪
typescript
const initServer = async (onListen: boolean) => {
if (serverInited) return
// 启动 client 环境的 pluginContainer
await environments.client.pluginContainer.buildStart()
// 启动所有环境的 hot channel 和依赖优化器
await Promise.all(Object.values(environments).map((e) => e.listen(server)))
}buildStart 只对 client 环境调用一次——这是为了向后兼容。如果插件设置了 perEnvironmentStartEndDuringDev: true,则每个环境都会收到 buildStart 调用。
5.2 ViteDevServer 接口
ViteDevServer 接口定义了开发服务器对外暴露的所有能力:
typescript
export interface ViteDevServer {
config: ResolvedConfig
middlewares: Connect.Server
httpServer: HttpServer | null
watcher: FSWatcher
ws: WebSocketServer
hot: NormalizedHotChannel
pluginContainer: PluginContainer
environments: Record<'client' | 'ssr' | (string & {}), DevEnvironment>
moduleGraph: ModuleGraph
transformRequest(url: string, options?: TransformOptions): Promise<TransformResult | null>
warmupRequest(url: string, options?: TransformOptions): Promise<void>
transformIndexHtml(url: string, html: string, originalUrl?: string): Promise<string>
listen(port?: number, isRestart?: boolean): Promise<ViteDevServer>
close(): Promise<void>
restart(forceOptimize?: boolean): Promise<void>
// ...
}几个值得注意的设计细节:
pluginContainer和moduleGraph使用 getter 并标记了弃用警告:这是因为它们实际上是client环境的代理,为了向后兼容而保留。新代码应使用server.environments.client.pluginContainerws和hot:ws是底层的 WebSocket 服务器,hot是标准化的 HMR 通道。两者在当前版本中指向同一个对象environments的类型声明使用了Record<'client' | 'ssr' | (string & {}), DevEnvironment>,这个巧妙的类型既提供了client和ssr的自动补全,又允许自定义环境名
5.3 HTTP/HTTPS 服务器创建
src/node/http.ts 封装了 HTTP 和 HTTPS 服务器的创建逻辑:
typescript
// src/node/http.ts
export async function resolveHttpServer(
app: Connect.Server,
httpsOptions?: HttpsServerOptions,
): Promise<HttpServer> {
if (!httpsOptions) {
const { createServer } = await import('node:http')
return createServer(app)
}
const { createSecureServer } = await import('node:http2')
return createSecureServer(
{
maxSessionMemory: 1000,
streamResetBurst: 100000,
streamResetRate: 33,
...httpsOptions,
allowHTTP1: true,
},
app,
)
}当启用 HTTPS 时,Vite 使用 HTTP/2 的 createSecureServer 而非 HTTPS 的 createServer。HTTP/2 的多路复用特性对开发服务器特别有利——浏览器可以同时请求数十个模块而不受连接数限制。
三个硬编码的参数值得说明:
maxSessionMemory: 1000(默认 10 MB):大幅提高到 1000 MB,防止大型项目中大量并发请求导致502 ENHANCE_YOUR_CALM错误streamResetBurst: 100000和streamResetRate: 33:放宽流重置速率限制,防止快速导航时浏览器取消大量请求触发ERR_HTTP2_PROTOCOL_ERRORallowHTTP1: true:保持对 HTTP/1.1 客户端的兼容
5.3.1 端口管理
httpServerStart 实现了智能端口选择:
typescript
export async function httpServerStart(httpServer, serverOptions): Promise<number> {
const { port: startPort, strictPort, host, logger } = serverOptions
for (let port = startPort; port <= MAX_PORT; port++) {
if (await isPortAvailable(port)) {
const result = await tryBindServer(httpServer, port, host)
if (result.success) return port
if (result.error.code !== 'EADDRINUSE') throw result.error
}
if (strictPort) throw new Error(`Port ${port} is already in use`)
logger.info(`Port ${port} is in use, trying another one...`)
}
throw new Error(`No available ports found`)
}注意 isPortAvailable 的实现——它检查通配符地址(0.0.0.0 和 ::)上的端口可用性,而不仅仅是目标地址。这避免了一个微妙的问题:如果另一个进程绑定了 0.0.0.0:3000,即使 tryBindServer 尝试绑定 localhost:3000 也可能成功(在某些操作系统上),但实际上该端口已被占用。
5.3.2 客户端错误处理
typescript
export function setClientErrorHandler(server: HttpServer, logger: Logger): void {
server.on('clientError', (err, socket) => {
let msg = '400 Bad Request'
if ((err as any).code === 'HPE_HEADER_OVERFLOW') {
msg = '431 Request Header Fields Too Large'
logger.warn('Server responded with status code 431...')
}
if ((err as any).code === 'ECONNRESET' || !socket.writable) return
socket.end(`HTTP/1.1 ${msg}\r\n\r\n`)
})
}HPE_HEADER_OVERFLOW 是开发中常见的问题——当 Cookie 或自定义 Header 过大时 Node.js 的 HTTP parser 会抛出此错误。Vite 给出了友好的错误提示和文档链接。
5.4 Connect 中间件栈
5.4.1 为什么选择 Connect
Vite 没有使用 Express、Koa 或 Fastify,而是选择了 Connect——一个极其简单的中间件框架。Connect 的核心只有一个功能:将 HTTP 请求依次传递给一系列中间件函数。没有路由表、没有内置解析器、没有模板引擎。
这个选择背后的考量是:
- 最小依赖:Connect 本身几乎零依赖
- 完全控制:没有框架魔法,中间件的执行顺序完全由代码决定
- 嵌入友好:Connect 应用本身就是一个标准的
(req, res, next)函数,可以直接嵌入到任何 Node.js HTTP 框架中
5.4.2 完整的中间件注册顺序
以下是 _createServer 中中间件的完整注册顺序。我从源码中逐行提取,每个中间件标注了其职责:
5.4.3 关键中间件详解
安全层(中间件 1-5)
安全中间件排在最前面,确保恶意请求在进入业务逻辑之前被拦截。rejectInvalidRequestMiddleware 拒绝非标准 HTTP 方法,hostValidationMiddleware 通过检查 Host 头防止 DNS 重绑定攻击(仅 HTTP 模式,HTTPS 不受此攻击影响)。
configureServer 钩子的双阶段执行(中间件 6 和 17)
typescript
const postHooks: ((() => void) | void)[] = []
for (const hook of config.getSortedPluginHooks('configureServer')) {
postHooks.push(await hook.call(configureServerContext, reflexServer))
}
// ... 安装内部中间件 ...
postHooks.forEach((fn) => fn && fn())这个双阶段设计意味着:
- 在
configureServer中直接调用server.middlewares.use()注册的中间件排在内部中间件之前 - 返回的后置函数注册的中间件排在
htmlFallbackMiddleware之后、indexHtmlMiddleware之前
这个位置非常巧妙——后置中间件可以拦截 SPA 路由回退后的请求,在 indexHtmlMiddleware 处理 HTML 之前提供自定义内容。
cachedTransformMiddleware(中间件 7)
这是性能的第一道防线。它检查请求的 If-None-Match 头是否匹配模块的 ETag:
typescript
// 如果 ETag 匹配,直接返回 304,无需任何文件 I/O 或代码转换
if (etag && req.headers['if-none-match'] === etag) {
res.statusCode = 304
return res.end()
}transformMiddleware(中间件 13)
这是整个中间件栈中最核心的中间件。它拦截模块请求(.js、.ts、.vue、.css 等),通过环境的插件容器进行完整的 resolveId -> load -> transform 管线处理,并将结果缓存到模块图中。第 6 章(模块图)和第 7 章(HMR)会深入分析它的实现。
htmlFallbackMiddleware(中间件 16)
对于 SPA 应用,所有非文件请求都需要回退到 index.html。这个中间件检查请求的 Accept 头是否包含 text/html,如果是且路径不匹配任何文件,就将请求重写为 index.html 的路径。
errorMiddleware(中间件 20)
错误中间件在栈的最末端,捕获前面所有中间件抛出的异常。它将错误格式化为浏览器可以渲染的 HTML 页面(开发模式下),或者在中间件模式下将错误传递给外部框架的错误处理器。
5.4.4 请求处理时序
一个典型的模块请求的完整处理流程:
5.5 WebSocket 服务器
5.5.1 创建策略
src/node/server/ws.ts 中的 createWebSocketServer 支持三种创建模式:
typescript
export function createWebSocketServer(
server: HttpServer | null,
config: ResolvedConfig,
httpsOptions?: HttpsServerOptions,
): WebSocketServer {
// 模式 1:禁用 WebSocket
if (config.server.ws === false) {
return noopWebSocketServer
}
const hmr = isObject(config.server.hmr) && config.server.hmr
const wsServer = hmr?.server || (portsAreCompatible && server)
if (wsServer) {
// 模式 2:共享 HTTP 服务器(默认)
wsServer.on('upgrade', hmrServerWsListener)
} else {
// 模式 3:独立 WebSocket 服务器
wsHttpServer = httpsOptions
? createHttpsServer(httpsOptions, route)
: createHttpServer(route)
wsHttpServer.on('upgrade', handleUpgrade)
}
}默认情况下(模式 2),WebSocket 服务器复用 HTTP 服务器的端口。当浏览器发起 WebSocket 升级请求时,Connect 的 HTTP 处理管线会忽略它(因为它不是标准 HTTP 请求),由 upgrade 事件监听器接管。
模式 3 在以下场景激活:用户通过 hmr.port 指定了不同于 HTTP 服务器的端口。这在某些代理环境中是必要的。
5.5.2 安全机制:Token 验证
WebSocket 连接面临跨站 WebSocket 劫持(Cross-site WebSocket Hijacking)的风险。恶意网页可以向 ws://localhost:5173 发起 WebSocket 连接,如果没有验证机制,它就能接收到 HMR 消息,甚至可能利用自定义事件执行恶意操作。
Vite 的防护方案是 Token 验证:
typescript
function hasValidToken(config: ResolvedConfig, url: URL) {
const token = url.searchParams.get('token')
if (!token) return false
try {
return crypto.timingSafeEqual(
Buffer.from(token),
Buffer.from(config.webSocketToken),
)
} catch {}
return false
}
const shouldHandle = (req: IncomingMessage) => {
const protocol = req.headers['sec-websocket-protocol']!
// vite-ping 允许任何来源连接
if (protocol === 'vite-ping') return true
// 检查 Host 头
if (allowedHosts !== true && !isHostAllowed(req.headers.host, allowedHosts)) {
return false
}
// 如果有 Origin 头(浏览器请求),必须携带有效 token
if (req.headers.origin) {
const parsedUrl = new URL(`http://example.com${req.url!}`)
return hasValidToken(config, parsedUrl)
}
// 非浏览器请求(如 CLI 工具)可以无 token 连接
return true
}关键设计点:
timingSafeEqual:使用恒定时间比较防止时序攻击vite-ping豁免:心跳探测协议允许无 token 连接,因为它只用于检测服务器是否在线,不传输敏感数据,且连接会立即关闭- 非浏览器请求豁免:没有
Origin头的请求(如 Node.js 客户端)被认为是可信的——因为如果攻击者能直接发起无 SOP 限制的请求,他也可以发送普通 HTTP 请求,WebSocket 验证无法提供额外保护 - Token 通过 URL 查询参数传递:虽然 token 可能出现在服务器日志中,但这被认为是可接受的风险,因为 token 每次进程启动都会重新生成
5.5.3 连接处理
当验证通过后,handleUpgrade 将连接升级为 WebSocket:
typescript
const handleUpgrade = (req, socket, head, isPing) => {
wss.handleUpgrade(req, socket, head, (ws) => {
if (isPing) {
ws.close(1000) // Normal Closure
return
}
wss.emit('connection', ws, req)
})
}vite-ping 连接在升级后立即关闭——它的唯一目的是探测服务器是否可达,不需要保持连接。
5.6 文件监听系统
5.6.1 Chokidar 配置
Vite 使用 Chokidar 监听文件系统变更。监听的路径包括:
typescript
const watcher = chokidar.watch(
[
...(config.experimental.bundledDev ? [] : [root]), // 项目根目录
...config.configFileDependencies, // 配置文件依赖
...getEnvFilesForMode(config.mode, config.envDir), // .env 文件
...(publicDir && publicFiles ? [publicDir] : []), // public 目录
],
resolvedWatchOptions,
)当 server.watch 设置为 null 时,Vite 使用 createNoopWatcher 创建一个什么都不做的虚拟监听器,这在 CI 环境或只读文件系统中很有用。
5.6.2 变更事件处理
文件变更触发三种事件,每种有不同的处理逻辑:
typescript
// 文件修改
watcher.on('change', async (file) => {
file = normalizePath(file)
reloadOnTsconfigChange(server, file) // tsconfig 变更时重载
// 通知所有环境的插件容器
await Promise.all(
Object.values(server.environments).map((environment) =>
environment.pluginContainer.watchChange(file, { event: 'update' }),
),
)
// 使模块图中对应模块失效
for (const environment of Object.values(server.environments)) {
environment.moduleGraph.onFileChange(file)
}
// 触发 HMR
await onHMRUpdate('update', file)
})
// 文件创建
watcher.on('add', (file) => onFileAddUnlink(file, false))
// 文件删除
watcher.on('unlink', (file) => onFileAddUnlink(file, true))onFileAddUnlink 有一个巧妙的逻辑——当 public 目录中新增了一个文件,如果模块图中存在同路径的模块(例如 /logo.svg),它会清除该模块的 ETag 缓存。这确保下次请求该路径时,public 目录中的文件优先于模块图中的结果:
typescript
if (publicDir && publicFiles) {
if (file.startsWith(publicDir)) {
const path = file.slice(publicDir.length)
publicFiles[isUnlink ? 'delete' : 'add'](path)
if (!isUnlink) {
const moduleWithSamePath = await clientModuleGraph.getModuleByUrl(path)
const etag = moduleWithSamePath?.transformResult?.etag
if (etag) {
clientModuleGraph.etagToModuleMap.delete(etag)
}
}
}
}5.6.3 变更事件的传播顺序
5.7 DevEnvironment 类
5.7.1 结构概览
DevEnvironment(定义在 src/node/server/environment.ts)是每个运行环境在开发模式下的完整封装:
typescript
export class DevEnvironment extends BaseEnvironment {
mode = 'dev' as const
moduleGraph: EnvironmentModuleGraph
depsOptimizer?: DepsOptimizer
hot: NormalizedHotChannel
_pluginContainer: EnvironmentPluginContainer<DevEnvironment> | undefined
_pendingRequests: Map<string, { request: Promise<TransformResult | null>; timestamp: number; abort: () => void }>
_crawlEndFinder: CrawlEndFinder
constructor(name: string, config: ResolvedConfig, context: DevEnvironmentContext) {
// 创建模块图
this.moduleGraph = new EnvironmentModuleGraph(name, (url) =>
this.pluginContainer.resolveId(url, undefined),
)
// 配置 HMR 通道
this.hot = context.transport ? normalizeHotChannel(context.transport, context.hot) : normalizeHotChannel({}, context.hot)
// 创建依赖优化器
if (!isDepOptimizationDisabled(optimizeDeps)) {
this.depsOptimizer = (optimizeDeps.noDiscovery ? createExplicitDepsOptimizer : createDepsOptimizer)(this)
}
}
}5.7.2 初始化和生命周期
DevEnvironment 有三个生命周期阶段:
init() 阶段创建插件容器:
typescript
async init(options?: { watcher?: FSWatcher; previousInstance?: DevEnvironment }): Promise<void> {
this._pluginContainer = await createEnvironmentPluginContainer(
this,
this.config.plugins,
options?.watcher,
)
}listen() 阶段启动 HMR 通道、依赖优化器和预热:
typescript
async listen(server: ViteDevServer): Promise<void> {
this.hot.listen()
await this.depsOptimizer?.init()
warmupFiles(server, this)
}5.7.3 模块转换
transformRequest 是 DevEnvironment 最核心的方法。它接受一个 URL,返回转换后的代码:
typescript
transformRequest(url: string, options?: TransformOptionsInternal): Promise<TransformResult | null> {
return transformRequest(this, url, options)
}实际的 transformRequest 函数(定义在 transformRequest.ts 中)协调了完整的模块处理管线:URL 解析 -> 缓存检查 -> resolveId -> load -> transform -> 结果缓存。这个过程的详细机制将在第 6 章展开。
5.7.4 请求去重
_pendingRequests Map 实现了请求去重——如果同一个模块的转换请求正在处理中,后续请求会等待已有的 Promise 而不是发起新的转换:
typescript
_pendingRequests: Map<string, {
request: Promise<TransformResult | null>
timestamp: number
abort: () => void
}>这在页面首次加载时尤为重要:浏览器可能同时请求同一个模块的多个版本(例如通过不同的 import 路径),去重机制确保每个模块只被转换一次。
5.8 中间件模式
5.8.1 嵌入到外部框架
当 middlewareMode 为 true 或一个对象时,Vite 不创建自己的 HTTP 服务器:
typescript
const httpServer = middlewareMode
? null
: await resolveHttpServer(middlewares, httpsOptions)此时,server.middlewares(Connect 应用)可以作为中间件嵌入到 Express、Koa 或其他框架中:
typescript
// 在 Express 中使用 Vite
const app = express()
const vite = await createServer({
server: { middlewareMode: true },
})
app.use(vite.middlewares)
app.listen(3000)5.8.2 WebSocket 端口共享
middlewareMode 支持传入父服务器实例以实现 WebSocket 端口共享:
typescript
middlewareMode?: boolean | {
server: HttpServer // 父服务器实例
}当传入 server 时,WebSocket 的 upgrade 事件会注册在父服务器上,而不是创建独立的 WebSocket 服务器。这避免了端口冲突,同时保持 HMR 功能正常。
5.9 服务器重启机制
restart 方法的实现揭示了一个优雅的状态传递机制:
typescript
async restart(forceOptimize?: boolean) {
if (!server._restartPromise) {
server._forceOptimizeOnRestart = !!forceOptimize
server._restartPromise = restartServer(server).finally(() => {
server._restartPromise = null
server._forceOptimizeOnRestart = false
})
}
return server._restartPromise
}restartServer 函数关闭旧服务器,创建新服务器,并将旧实例的状态传递给新实例:
typescript
// 重启时传递的状态
_createServer(inlineConfig, {
listen: true,
previousEnvironments: server.environments, // 环境实例
previousShortcutsState: server._shortcutsState, // CLI 快捷键状态
previousRestartPromise: server._restartPromise, // 重启 Promise
})为了保持外部引用的有效性,Vite 使用了一个 Proxy 技巧:
typescript
const reflexServer = new Proxy(server, {
get: (_, property) => server[property],
set: (_, property, value) => { server[property] = value; return true },
})所有对外暴露的 server 引用实际上是这个 Proxy。当 server 变量在重启过程中被替换为新实例时,Proxy 自动指向新实例,外部持有的引用无需更新。这个模式也用在 configureServer 钩子中——传给插件的是 reflexServer,确保插件在服务器重启后仍然能访问到正确的实例。
5.10 设计决策
5.10.1 为什么不用路由表而用中间件栈?
路由表(如 Express 的 app.get('/path', handler))适合 API 服务器,因为每个端点有明确的路径模式。但开发服务器的请求模式是动态的:
/src/App.tsx-> 模块转换/public/logo.svg-> 静态文件/@fs/node_modules/react/index.js-> 文件系统访问/-> HTML 回退- 任意路径 -> 可能匹配 public 文件、可能需要转换、可能需要代理
中间件栈让每个关注点独立判断"这个请求是否属于我",不需要预先枚举所有可能的路径模式。这也使得插件可以在任意位置注入自定义处理逻辑。
5.10.2 为什么 public 文件中间件排在 transform 之前?
servePublicMiddleware 排在 transformMiddleware 之前,这意味着 public 目录中的文件优先于需要转换的源码文件。如果 public/app.js 和 src/app.js 同时存在,对 /app.js 的请求会直接返回 public 目录中的文件,不会触发模块转换。
这个设计反映了 public 目录的语义:它包含的是不需要任何处理的静态资源,应该以最高优先级、最低延迟返回。
5.10.3 为什么 buildStart 只对 client 环境调用?
typescript
// 只对 client 环境调用 buildStart
await environments.client.pluginContainer.buildStart()这是一个向后兼容的决策。在 Environment API 引入之前,buildStart 在服务器启动时只调用一次。为了不破坏现有插件的行为,默认仍然只对 client 环境调用。插件可以通过设置 perEnvironmentStartEndDuringDev: true 来启用按环境调用。
这体现了 Vite 团队的渐进式迁移策略:新功能通过 opt-in 的方式引入,现有行为保持不变,给生态留出迁移时间。
5.11 小结
Vite 的开发服务器是一个精心编排的协作系统。_createServer 工厂函数将 HTTP 服务器、Connect 中间件栈、WebSocket 服务器、Chokidar 文件监听器、多个 DevEnvironment 实例组装为一个统一的 ViteDevServer。
中间件栈的设计是理解开发服务器的关键。15+ 个中间件按照"安全 -> 配置 -> 缓存 -> 代理 -> 静态资源 -> 模块转换 -> HTML 回退 -> 错误处理"的逻辑分层排列,每一层都有明确的职责边界。插件通过 configureServer 钩子可以在内部中间件之前或之后注入自定义处理。
WebSocket 服务器的 Token 验证机制和 Chokidar 的变更事件传播链共同构成了 HMR 的基础设施。文件变更从文件系统出发,经过插件通知、模块图失效、HMR 边界计算,最终通过 WebSocket 推送到浏览器。
DevEnvironment 作为环境的运行时封装,将模块图、插件容器、依赖优化器、HMR 通道聚合在一起,为多环境并行开发提供了清晰的隔离边界。这种架构使得同一个 Vite 实例可以同时服务于客户端渲染、服务端渲染、甚至 Edge Worker 等截然不同的运行目标,每个目标拥有独立的模块处理管线和依赖优化策略。