Read OSS

开发服务器:从 HTTP 请求到转换后的模块

高级

前置知识

  • 第 1 篇:架构与代码库导航
  • 第 2 篇:配置与环境系统
  • 理解 Node.js connect 中间件模式

开发服务器:从 HTTP 请求到转换后的模块

Vite 的开发服务器正是"即时"开发体验的核心所在。它不会在启动时打包整个应用,而是按需提供模块,在浏览器请求时实时对其进行转换。但"按需"并不等于"简单"——背后有一套严格有序的中间件栈、一套带有过期检测的请求去重机制,以及一个模拟 Rolldown 钩子执行模型的插件容器。

本文将追踪完整的请求生命周期:从 createServer() 初始化,经过中间件链,直到将 TypeScript 源码转换为浏览器可执行 JavaScript 的转换流水线。

服务器创建与初始化

一切从 server/index.ts 中的 _createServer 开始。该函数解析配置后,逐步组装服务器的各个组件:

sequenceDiagram
    participant User
    participant _createServer
    participant Config
    participant HTTP
    participant Environments
    participant Middlewares

    User->>_createServer: createServer(inlineConfig)
    _createServer->>Config: resolveConfig(inlineConfig, 'serve')
    _createServer->>HTTP: resolveHttpServer(middlewares, https)
    _createServer->>_createServer: createWebSocketServer(httpServer, config)
    _createServer->>_createServer: chokidar.watch(root, configDeps, envFiles)
    _createServer->>Environments: Create environments via dev.createEnvironment()
    _createServer->>Environments: Initialize each (pluginContainer, moduleGraph)
    _createServer->>Middlewares: Register 18 middleware layers
    _createServer-->>User: ViteDevServer

第 560–579 行,各环境被并行创建和初始化:

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

正如第 2 篇所介绍的,每个环境都有独立的 DevEnvironment 实例,拥有各自的模块图和插件容器。previousInstance 参数则支持无缝重启服务器——环境可以从旧实例中迁移状态。

环境创建完成后,第 583 行会设置一个向后兼容的 ModuleGraph 包装器,将客户端和 SSR 模块图合并,供仍使用旧 API 的插件使用。

18 层中间件栈

第 920–1031 行的中间件注册,是整个代码库中排序最为精心设计的部分之一。完整的中间件栈如下:

flowchart TD
    A["1. timeMiddleware (DEBUG only)"] --> B["2. rejectInvalidRequestMiddleware"]
    B --> C["3. rejectNoCorsRequestMiddleware"]
    C --> D["4. CORS middleware"]
    D --> E["5. Host validation middleware"]
    E --> F["6. Plugin configureServer hooks (pre)"]
    F --> G["7. cachedTransformMiddleware"]
    G --> H["8. Proxy middleware"]
    H --> I["9. Base path middleware"]
    I --> J["10. Open-in-editor (__open-in-editor)"]
    J --> K["11. HMR ping handler"]
    K --> L["12. Public file serving"]
    L --> M["13. transformMiddleware ★"]
    M --> N["14. Raw FS serving (/@fs/)"]
    N --> O["15. Static file serving"]
    O --> P["16. HTML fallback (SPA/MPA)"]
    P --> Q["17. Plugin configureServer hooks (post)"]
    Q --> R["18. Index HTML transform"]
    R --> S["19. 404 handler"]
    S --> T["20. Error handler"]

这套顺序有其深刻的设计意图:

  • 安全优先(第 2–5 层):非法请求、CORS 违规以及 DNS 重绑定攻击在进入任何处理逻辑之前即被拦截。
  • 插件钩子分层(第 6 层与第 17 层):configureServer 钩子可以返回一个函数,作为"post"钩子执行。这正是钩子被拆分的原因——pre 钩子在内部中间件之前运行,post 钩子则在静态文件服务之后、HTML 转换之前执行。
  • 缓存优先于转换(第 7 层):cachedTransformMiddleware 会检查请求是否有匹配的 etag,若命中则直接返回 304,完全跳过转换流水线。
  • 转换是核心(第 13 层):这里是模块按需解析、加载和转换的地方。
  • HTML 转换最后执行(第 18 层):HTML 文件在所有其他处理完成后才进行处理,以便注入 script 标签并解析 import map。

提示: 排查中间件顺序问题时,可以设置 DEBUG=connect:dispatcher,观察每个请求由哪个中间件处理。Vite 为所有内部中间件都提供了具名标识(如 viteCachedTransformMiddlewareviteHMRPingMiddleware),正是为了方便调试。

转换流水线:请求去重与缓存

当请求到达转换中间件且未命中缓存时,会调用 transformRequest。该函数实现了带过期检测的请求去重机制:

sequenceDiagram
    participant Browser
    participant transformRequest
    participant _pendingRequests
    participant doTransform
    participant loadAndTransform

    Browser->>transformRequest: GET /src/App.tsx
    transformRequest->>transformRequest: Record timestamp
    transformRequest->>_pendingRequests: Check for pending request
    alt Pending request exists and is fresh
        _pendingRequests-->>transformRequest: Return existing promise
    else Pending request is stale
        transformRequest->>_pendingRequests: Abort stale, start new
    end
    transformRequest->>doTransform: Process URL
    doTransform->>doTransform: moduleGraph.getModuleByUrl(url)
    alt Cache hit (fresh transformResult)
        doTransform-->>transformRequest: Return cached result
    else Cache miss
        doTransform->>doTransform: pluginContainer.resolveId(url)
        doTransform->>loadAndTransform: Load and transform
        loadAndTransform->>loadAndTransform: pluginContainer.load(id)
        loadAndTransform->>loadAndTransform: pluginContainer.transform(code, id)
        loadAndTransform->>loadAndTransform: Store in moduleGraph
        loadAndTransform-->>transformRequest: TransformResult
    end

第 110–127 行的去重逻辑处理了一个微妙的竞态条件:当一个模块正在处理过程中被失效(由 HMR 或优化器更新触发),待处理的请求就会变得过期。此时函数会将请求的时间戳与 module.lastInvalidationTimestamp 进行比较,决定是复用已有的处理结果,还是中止并重新开始。

doTransform 函数提供了两次缓存命中机会:先按 URL 查找,再按解析后的 ID 查找(因为不同 URL 可能解析到同一个文件)。只有两次都未命中,才会进入 loadAndTransform

loadAndTransform 按照经典的 Rollup 风格依次执行钩子:loadtransform。加载的代码会先经过文件系统访问策略校验,再依次通过每个插件的 transform 钩子处理。

EnvironmentPluginContainer

插件容器是 Vite 历史上最值得关注的组件之一。pluginContainer.ts 文件头部明确记录了它的来源:

This file is refactored into TypeScript based on
https://github.com/preactjs/wmr/blob/main/packages/wmr/src/lib/rollup-plugin-container.js

这个源自 WMR 的插件容器,在开发模式下模拟了 Rolldown 的插件钩子执行过程。由于开发模式并不实际打包,容器需要模拟插件所期望的 resolveIdloadtransform 钩子链。

flowchart TD
    REQ["transformRequest(url)"] --> RID["resolveId hooks<br/>(first non-null wins)"]
    RID --> LOAD["load hooks<br/>(first non-null wins)"]
    LOAD -->|"No plugin loaded"| FS["Read from file system"]
    LOAD -->|"Plugin returned code"| TRANSFORM
    FS --> TRANSFORM["transform hooks<br/>(sequential, each sees previous output)"]
    TRANSFORM --> RESULT["TransformResult<br/>{code, map, etag}"]

容器为每个插件创建独立的 PluginContext,提供插件所需的 this.resolve()this.emitFile()this.getModuleInfo() 方法。模块信息通过 Proxy 实现懒计算——只有当插件实际调用 this.getModuleInfo() 时才会求值。

模块图:按环境隔离与兼容性适配

EnvironmentModuleGraph 维护了四个索引以支持快速查找:

urlToModuleMap: Map<string, EnvironmentModuleNode>    // URL → module
idToModuleMap: Map<string, EnvironmentModuleNode>     // resolved ID → module
etagToModuleMap: Map<string, EnvironmentModuleNode>   // etag → module
fileToModulesMap: Map<string, Set<EnvironmentModuleNode>>  // file → modules

每个 EnvironmentModuleNode 都记录了模块的导入关系(importersimportedModules)、HMR 状态(acceptedHmrDepsisSelfAccepting)、缓存的转换结果以及失效时间戳。invalidationState 字段区分了两种失效类型:软失效(仅需更新时间戳)和硬失效(需要完整重新转换)。

为了向后兼容,mixedModuleGraph.ts 中的 ModuleNode 包装器同时持有 _clientModule_ssrModule 的引用,并通过 _get 辅助方法优先返回客户端模块的值,回退到 SSR:

_get<T extends keyof EnvironmentModuleNode>(
  prop: T,
): EnvironmentModuleNode[T] {
  return (this._clientModule?.[prop] ?? this._ssrModule?.[prop])!
}

这种双引用设计让仍在使用 server.moduleGraph(现已废弃)的插件能够继续正常工作,同时为整个生态系统向按环境模块图的迁移提供过渡支持。

下一步预告

我们已经追踪了一个请求从浏览器出发,经过 18 层中间件,进入转换流水线,穿越插件钩子,最终落入模块图的完整旅程。但插件内部究竟发生了什么,我们还没有涉及。下一篇文章将深入探讨 Vite 的插件系统:扩展自 Rolldown 的 Plugin 接口、双层排序机制、提前跳过钩子的过滤器优化,以及核心插件的内部实现——包括超过 3500 行的 CSS 插件,以及负责重写裸导入的 import analysis 插件。