Vite内核解析-第5章 开发服务器架构

《Vite 设计与实现》完整目录

第5章 开发服务器架构

开篇引言

Vite 的开发服务器是整个项目中最复杂的子系统。它不只是一个"启动一个 HTTP 服务器然后返回文件"的简单程序------它是一个精密的运行时环境,需要在接收到浏览器请求的瞬间完成模块解析、代码转换、依赖预构建、HMR 通知等一系列操作,且全部延迟控制在毫秒级。

src/node/server/index.ts 是整个服务器的入口和骨架,不到 1100 行代码编排了十几个中间件、多个环境实例、WebSocket 服务器、文件监听器的协同工作。这个文件的结构揭示了 Vite 开发体验的全部秘密:为什么首次启动那么快?为什么文件修改后浏览器几乎瞬间更新?为什么大型单仓项目也不会让开发服务器变慢?

答案在于三个关键设计:

  1. 基于 Connect 的中间件栈:不是路由匹配,而是管线式处理。每个请求从头到尾经过十几个中间件,每个中间件只关注自己能处理的请求类型,其余放行给下一个。这种架构天然支持横切关注点的分离。

  2. 按需编译 :浏览器请求哪个模块,服务器才编译哪个模块。没有预先打包的步骤,也没有全量扫描。模块的 resolveId -> load -> transform 链条只在首次请求时执行,结果缓存在模块图中。

  3. 多环境架构 :每个 DevEnvironment(client、ssr 等)拥有独立的模块图和插件容器,互不干扰。这使得同一个服务器实例可以同时为客户端渲染和服务端渲染提供服务。

本章将从 _createServer 函数出发,逐步拆解开发服务器的每一个构成要素。

:::tip 本章要点

  1. _createServer 是开发服务器的核心工厂函数,负责组装所有子系统
  2. Connect 中间件栈包含 15+ 个中间件,按精确顺序排列,分为安全层、配置层、静态资源层、转换层、回退层
  3. WebSocket 服务器既可以共享 HTTP 端口,也可以独立监听,通过 token 机制防止跨站劫持
  4. Chokidar 文件监听器触发的变更事件驱动整个 HMR 管线
  5. DevEnvironment 封装了每个运行环境的模块图、插件容器、依赖优化器
  6. 服务器支持中间件模式(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 参数来区分首次创建和重启场景。重启时会传入 previousEnvironmentspreviousShortcutsState 等状态,避免重复初始化。

5.1.2 初始化序列

_createServer 的执行可以分为七个阶段:

graph TD A["1. 解析配置 resolveConfig"] --> B["2. 创建基础设施"] B --> C["3. 创建环境实例"] C --> D["4. 组装 ViteDevServer 对象"] D --> E["5. 注册文件监听器"] E --> F["6. 安装中间件栈"] F --> G["7. 等待服务器就绪"] B --> B1["HTTP Server"] B --> B2["Connect App"] B --> B3["WebSocket Server"] B --> B4["Chokidar Watcher"] B --> B5["Public Files"]

让我们逐一展开:

阶段 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。默认配置下会创建 clientssr 两个环境。

阶段 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>
  // ...
}

几个值得注意的设计细节:

  • pluginContainermoduleGraph 使用 getter 并标记了弃用警告 :这是因为它们实际上是 client 环境的代理,为了向后兼容而保留。新代码应使用 server.environments.client.pluginContainer
  • wshotws 是底层的 WebSocket 服务器,hot 是标准化的 HMR 通道。两者在当前版本中指向同一个对象
  • environments 的类型声明使用了 Record<'client' | 'ssr' | (string & {}), DevEnvironment>,这个巧妙的类型既提供了 clientssr 的自动补全,又允许自定义环境名

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: 100000streamResetRate: 33 :放宽流重置速率限制,防止快速导航时浏览器取消大量请求触发 ERR_HTTP2_PROTOCOL_ERROR
  • allowHTTP1: 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 请求依次传递给一系列中间件函数。没有路由表、没有内置解析器、没有模板引擎。

这个选择背后的考量是:

  1. 最小依赖:Connect 本身几乎零依赖
  2. 完全控制:没有框架魔法,中间件的执行顺序完全由代码决定
  3. 嵌入友好 :Connect 应用本身就是一个标准的 (req, res, next) 函数,可以直接嵌入到任何 Node.js HTTP 框架中

5.4.2 完整的中间件注册顺序

以下是 _createServer 中中间件的完整注册顺序。我从源码中逐行提取,每个中间件标注了其职责:

graph TD REQ["浏览器请求"] --> M1 subgraph "安全层 Security Layer" M1["1. timeMiddleware
(仅 DEBUG 模式)"] M2["2. rejectInvalidRequestMiddleware
拒绝非法请求方法"] M3["3. rejectNoCorsRequestMiddleware
拒绝缺少正确 Origin 的请求"] M4["4. corsMiddleware
CORS 头部注入"] M5["5. hostValidationMiddleware
DNS 重绑定攻击防护"] end subgraph "配置层 Configuration Layer" M6["6. configureServer pre hooks
插件的前置服务器配置"] end subgraph "缓存层 Cache Layer" M7["7. cachedTransformMiddleware
304 Not Modified 快速路径"] end subgraph "代理和基础路径 Proxy & Base" M8["8. proxyMiddleware
API 代理"] M9["9. baseMiddleware
base 路径重写"] end subgraph "工具层 Utility Layer" M10["10. launchEditorMiddleware
点击错误打开编辑器"] M11["11. viteHMRPingMiddleware
HMR 心跳检测"] end subgraph "静态资源层 Static Layer" M12["12. servePublicMiddleware
public 目录文件"] end subgraph "转换层 Transform Layer" M13["13. transformMiddleware
核心模块转换"] M14["14. serveRawFsMiddleware
/@fs/ 路径文件"] M15["15. serveStaticMiddleware
项目根目录静态文件"] end subgraph "回退层 Fallback Layer" M16["16. htmlFallbackMiddleware
SPA 路由回退到 index.html"] M17["17. configureServer post hooks
插件的后置服务器配置"] M18["18. indexHtmlMiddleware
HTML 转换和注入"] M19["19. notFoundMiddleware
404 响应"] end subgraph "错误层 Error Layer" M20["20. errorMiddleware
错误捕获和展示"] end M1 --> M2 --> M3 --> M4 --> M5 M5 --> M6 --> M7 --> M8 --> M9 M9 --> M10 --> M11 --> M12 M12 --> M13 --> M14 --> M15 M15 --> M16 --> M17 --> M18 --> M19 --> M20

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 请求处理时序

一个典型的模块请求的完整处理流程:

sequenceDiagram participant Browser participant Connect as Connect Stack participant Cache as cachedTransformMiddleware participant Transform as transformMiddleware participant PC as PluginContainer participant MG as ModuleGraph Browser->>Connect: GET /src/App.tsx Connect->>Cache: 检查 If-None-Match alt ETag 匹配 Cache-->>Browser: 304 Not Modified else ETag 不匹配或无缓存 Cache->>Transform: next() Transform->>MG: 查找已缓存的 TransformResult alt 缓存命中 MG-->>Transform: 返回缓存结果 else 缓存未命中 Transform->>PC: resolveId('/src/App.tsx') PC-->>Transform: '/absolute/path/src/App.tsx' Transform->>PC: load('/absolute/path/src/App.tsx') PC-->>Transform: 原始文件内容 Transform->>PC: transform(code, id) PC-->>Transform: 转换后的代码 + sourcemap Transform->>MG: 缓存 TransformResult end Transform-->>Browser: 200 OK + 转换后的 JS end

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)
  }
}
graph TD A{"config.server.ws === false?"} -->|"是"| B["返回 noop 实现"] A -->|"否"| C{"HMR 端口与服务器端口兼容?"} C -->|"是"| D["共享 HTTP 服务器
监听 upgrade 事件"] C -->|"否"| E["创建独立 HTTP 服务器
监听独立端口"] D --> F["WebSocket Server Ready"] E --> F

默认情况下(模式 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
}

关键设计点:

  1. timingSafeEqual:使用恒定时间比较防止时序攻击
  2. vite-ping 豁免:心跳探测协议允许无 token 连接,因为它只用于检测服务器是否在线,不传输敏感数据,且连接会立即关闭
  3. 非浏览器请求豁免 :没有 Origin 头的请求(如 Node.js 客户端)被认为是可信的------因为如果攻击者能直接发起无 SOP 限制的请求,他也可以发送普通 HTTP 请求,WebSocket 验证无法提供额外保护
  4. 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 变更事件的传播顺序

sequenceDiagram participant FS as File System participant Chokidar participant PC as PluginContainer participant MG as ModuleGraph participant HMR as HMR Engine participant WS as WebSocket participant Browser FS->>Chokidar: 文件变更 Chokidar->>PC: watchChange(file, { event }) Note over PC: 通知所有环境的插件容器 Chokidar->>MG: onFileChange(file) Note over MG: 使受影响的模块失效 Chokidar->>HMR: handleHMRUpdate(type, file, server) HMR->>HMR: 确定更新边界 HMR->>WS: 发送 update/full-reload 消息 WS->>Browser: HMR Payload Browser->>Browser: 应用更新或刷新页面

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 有三个生命周期阶段:

stateDiagram-v2 [*] --> Created : new DevEnvironment() Created --> Initiated : init() Initiated --> Listening : listen() Listening --> Closed : close() Closed --> [*] state Created { [*] --> moduleGraph_created moduleGraph_created --> hot_configured hot_configured --> depsOptimizer_created } state Initiated { [*] --> pluginContainer_created pluginContainer_created --> ready } state Listening { [*] --> hot_listening hot_listening --> depsOptimizer_inited depsOptimizer_inited --> warmup_started }

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 模块转换

transformRequestDevEnvironment 最核心的方法。它接受一个 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 嵌入到外部框架

middlewareModetrue 或一个对象时,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.jssrc/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 等截然不同的运行目标,每个目标拥有独立的模块处理管线和依赖优化策略。

相关推荐
杨艺韬6 小时前
Vite内核解析-第1章 为什么需要理解 Vite
agent
杨艺韬6 小时前
Vite内核解析-第8章 依赖预构建
agent
杨艺韬6 小时前
Vite内核解析-第3章 配置系统
agent
杨艺韬7 小时前
Claude Code设计与实现-第5章 流式消息与状态机
agent
杨艺韬7 小时前
Claude Code设计与实现-第10章 Bash 安全与沙箱
agent
杨艺韬7 小时前
Claude Code设计与实现-第14章 多 Agent 协调与 Swarm
agent
杨艺韬7 小时前
Claude Code设计与实现-第8章 核心工具实现剖析
agent
杨艺韬7 小时前
Claude Code设计与实现-第9章 多模式权限模型
agent
杨艺韬7 小时前
Claude Code设计与实现-第17章 React + Ink 终端 UI
agent