开发服务器:从 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 为所有内部中间件都提供了具名标识(如viteCachedTransformMiddleware、viteHMRPingMiddleware),正是为了方便调试。
转换流水线:请求去重与缓存
当请求到达转换中间件且未命中缓存时,会调用 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 风格依次执行钩子:load → transform。加载的代码会先经过文件系统访问策略校验,再依次通过每个插件的 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 的插件钩子执行过程。由于开发模式并不实际打包,容器需要模拟插件所期望的 resolveId → load → transform 钩子链。
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 都记录了模块的导入关系(importers、importedModules)、HMR 状态(acceptedHmrDeps、isSelfAccepting)、缓存的转换结果以及失效时间戳。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 插件。