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`)
})
}