Skip to content

第18章 设计模式与架构决策

开篇引言

在前面十七章中,我们从源码层面深入分析了 Vite 的每一个核心子系统。现在是时候退后一步,从更高的抽象层次审视 Vite 的设计智慧了。

Vite 不仅是一个构建工具,更是一本优秀的软件架构教材。它在有限的领域中展示了大量可迁移的设计模式:中间件栈模式处理请求管线,基于图的失效传播驱动 HMR,插件 Hook 管线提供了灵活的扩展点,按需转换架构消除了不必要的计算,多环境隔离通过类型层次实现了关注点分离,缓存策略在多个层次上优化了性能。

本章将提炼这些设计模式,分析其动机、权衡和适用场景,最终讨论如何借鉴这些思想构建自己的开发工具。

本章要点

  • 提炼 Vite 中的七大可迁移设计模式
  • 理解每种模式的动机、实现策略和适用边界
  • 分析关键架构决策背后的权衡
  • 掌握从 Vite 架构中可借鉴的工具构建方法论

18.1 中间件栈模式

18.1.1 模式描述

Vite 的开发服务器使用 Connect 中间件栈处理 HTTP 请求。每个中间件是一个函数,按顺序执行,可以选择处理请求或将其传递给下一个中间件。

18.1.2 设计要点

中间件栈模式的核心优势在于:

关注点分离:每个中间件只负责一种类型的请求处理。CORS 中间件只管跨域头,Proxy 中间件只管代理转发,Transform 中间件只管代码转换。它们之间通过 next() 函数实现松耦合。

顺序敏感的处理管线:中间件的注册顺序决定了处理优先级。例如 Proxy 中间件必须在 Transform 中间件之前,否则代理目标的请求会被错误地当作本地模块处理。

短路退出:当某个中间件已经处理了请求(发送了响应),后续中间件不再执行。这避免了不必要的计算。

可组合性:中间件可以自由添加和移除。Vite 允许用户通过 server.middlewareMode 关闭内置中间件,或通过配置 Hook 添加自定义中间件。

18.1.3 适用场景

中间件栈模式适用于任何需要"多步骤、有序处理"的场景:

  • HTTP 请求处理(最经典的应用)
  • 日志管线(格式化 -> 过滤 -> 输出)
  • 数据验证管线(类型检查 -> 范围检查 -> 业务规则检查)
  • 图像处理管线(解码 -> 滤镜 -> 编码)

18.1.4 实现要点

typescript
// 简化的中间件栈实现
class MiddlewareStack<Context> {
  private middlewares: Array<(ctx: Context, next: () => Promise<void>) => Promise<void>> = []

  use(middleware: (ctx: Context, next: () => Promise<void>) => Promise<void>) {
    this.middlewares.push(middleware)
  }

  async handle(ctx: Context) {
    let index = 0
    const next = async () => {
      if (index < this.middlewares.length) {
        await this.middlewares[index++](ctx, next)
      }
    }
    await next()
  }
}

18.2 基于图的失效传播

18.2.1 模式描述

Vite 的 HMR 系统建立在模块依赖图之上。当一个文件变更时,系统沿着依赖图的反向边(importers)传播失效信号,直到找到能够自我接受更新的模块(HMR boundary)或到达根节点(触发完整刷新)。

18.2.2 设计要点

双向边:模块图中每个节点同时维护 importedModules(依赖了谁)和 importers(被谁依赖)。这种双向关系使得失效传播能够高效地进行,无需遍历整个图。

边界检测:传播在 HMR boundary(声明了 import.meta.hot.accept 的模块)处停止。这是一种"最小化影响范围"的策略 -- 只重新加载真正需要更新的模块子树。

时间戳去重:每个模块节点维护 lastHMRTimestamp,防止同一次文件变更触发重复的更新传播。

环隔离:在 Vite 6 的多环境架构中,失效传播只在单个环境的模块图内进行,不会跨环境。客户端的文件变更不会影响 SSR 环境的模块状态(除非显式配置)。

18.2.3 通用化抽象

这个模式可以推广为一种通用的"变更传播"机制:

适用于任何具有"依赖关系"且需要"增量更新"的系统:

  • 构建系统(Make, Bazel):源文件变更触发依赖它的目标重新构建
  • 响应式系统(Vue, MobX):数据变更触发依赖它的计算和视图更新
  • 缓存失效(CDN, Redis):源数据变更触发依赖它的缓存键失效

18.2.4 实现要点

typescript
// 通用的图失效传播
interface GraphNode<T> {
  id: string
  data: T
  dependencies: Set<string>   // 我依赖谁
  dependents: Set<string>     // 谁依赖我
  isBoundary: boolean         // 是否为传播边界
}

function propagateInvalidation<T>(
  graph: Map<string, GraphNode<T>>,
  changedId: string,
): { boundaries: GraphNode<T>[]; needsFullRefresh: boolean } {
  const boundaries: GraphNode<T>[] = []
  const visited = new Set<string>()
  const queue = [changedId]

  while (queue.length > 0) {
    const id = queue.shift()!
    if (visited.has(id)) continue
    visited.add(id)

    const node = graph.get(id)
    if (!node) continue

    if (node.isBoundary && id !== changedId) {
      boundaries.push(node)
      continue  // 不再向上传播
    }

    if (node.dependents.size === 0 && id !== changedId) {
      return { boundaries: [], needsFullRefresh: true }
    }

    for (const depId of node.dependents) {
      queue.push(depId)
    }
  }

  return { boundaries, needsFullRefresh: false }
}

18.3 插件 Hook 管线

18.3.1 模式描述

Vite 的插件系统定义了一系列 Hook,每个 Hook 在构建管线的特定阶段被调用。多个插件可以为同一个 Hook 提供实现,它们按照插件注册顺序和 enforce 优先级依次执行。

基于 VitePress 构建