阅读视图

发现新文章,点击刷新页面。

vite 源码 - 创建 ws 服务

截屏2025-10-07 11.13.51.png

wss 配置

vite 中,对于 ws 服务相关配置指的是 server.hmr 。这个字段接收两个类型的参数:

  • 布尔值(当值为 false 时则禁用热更新)
  • 对象

其中对象支持以下几种属性:

export interface HmrOptions {
  protocol?: string // 协议 (`ws` 或 `wss`)
  host?: string // 主机
  port?: number // 端口
  clientPort?: number // 客户端端口
  path?: string // 路径
  timeout?: number // 超时时间
  overlay?: boolean // 是否在浏览器上显示 vite 错误覆盖层
  server?: HttpServer // node 服务
}

创建 ws 服务

vite 中调用 createWebSocketServer 函数来创建 ws 服务。首先学习这个函数的定义。

function createWebSocketServer(
  server: HttpServer | null,
  config: ResolvedConfig,
  httpsOptions?: HttpsServerOptions,
): WebSocketServer

根据这个函数的定义,可以知道这个函数接收三个参数,分别是:

  • server: 之前创建的 node 服务
  • config: 处理好的配置对象
  • httpsOptions: https 配置对象即相关文件

返回一个 WebSocketServer

了解了这个函数定义之后,再结合之前 vite 的配置,可以推测出需要根据不同的配置返回不同的 WebSocketServer

首先需要处理的是用户禁用 hmr 即配置为 false 的情况。

按理来说,对于这种情况,可以直接返回一个 null,但是这里并没有返回一个 null 而是返回了一个对象,这个对象中存在 WebSocketServer 的方法和属性,其中的方法值为 noop ,用来模拟 WebSocketServer。这样做的目的有几个:

  • 避免判空,在其他地方需要调用 ws 服务时,就不用写 ifelse 判断。
  • 符合类型安全
// 返回的对象
{
      [isWebSocketServer]: true,
      get clients() {
        return new Set<WebSocketClient>()
      },
      async close() {},
      on: noop as any as WebSocketServer['on'],
      off: noop as any as WebSocketServer['off'],
      setInvokeHandler: noop,
      handleInvoke: async () => ({
        error: {
          name: 'TransportError',
          message: 'handleInvoke not implemented',
          stack: new Error().stack,
        },
      }),
      listen: noop,
      send: noop,
}

再处理了禁用 hmr 的情况后,需要真正的创建 ws 服务了。

这里也分为两种情况。

在分析这两种情况之前,需要了解 HTTPWebSocket 协议。

核心关系:WebSocket 通过 HTTP 协议“升级”而来

  • WebSocket 连接的建立必须经过一次 HTTP 请求,称为 “WebSocket 握手(Handshake)”
  • 客户端发送一个特殊的 HTTP GET 请求,包含特定的头部;
  • 服务器如果支持 WebSocket,就返回 101 Switching Protocols 响应;
  • 之后,TCP 连接从 HTTP 协议“升级”为 WebSocket 协议,双方开始全双工通信。

所以,要完成 hmr ,需要一个 HTTP 服务和 WebSocket 服务。

这两个服务在代码中对应两个变量:

  • wsServer: 底层 HTTP 服务器,用于接收upgrade请求
  • wss: WebSocket 连接管理器,处理具体 WebSocket 逻辑

wsServer 的来源就是 hmr 配置中的 server 字段,如果传入一个 servervite 会复用这个 server

存在 wsServer

直接使用该 wsServer ,监听 upgrade 事件。

let hmrBase = config.base
const hmrPath = hmr ? hmr.path : undefined
if (hmrPath) {
  hmrBase = path.posix.join(hmrBase, hmrPath)
}
hmrServerWsListener = (req, socket, head) => {
  // 安全校验
  const protocol = req.headers['sec-websocket-protocol']!
  const parsedUrl = new URL(`http://example.com${req.url!}`)
  if (
    [HMR_HEADER, 'vite-ping'].includes(protocol) &&
    parsedUrl.pathname === hmrBase
  ) {
    handleUpgrade(req, socket as Socket, head, protocol === 'vite-ping')
  }
}
wsServer.on('upgrade', hmrServerWsListener)

不存在 wsServer

由之前的内容,可以推断出这里的处理逻辑。

  1. 创建 wsServer
  2. 使用 wsServer 监听 upgrade 事件
  3. 当触发 upgrade 事件, 执行 handleUpgrade 函数
// **HTTP 服务器的请求回调**,用于处理**所有非 WebSocket 的普通 HTTP 请求**。
// 总是返回 426 升级协议
const route = ((_, res) => {
  const statusCode = 426
  const body = STATUS_CODES[statusCode]
  if (!body)
    throw new Error(`No body text found for the ${statusCode} status code`)

  res.writeHead(statusCode, {
    'Content-Length': body.length,
    'Content-Type': 'text/plain',
  })
  res.end(body)
}) as Parameters<typeof createHttpServer>[1]
if (httpsOptions) {
  wsHttpServer = createHttpsServer(httpsOptions, route)
} else {
  wsHttpServer = createHttpServer(route)
}
wsHttpServer.on('upgrade', (req, socket, head) => {
  const protocol = req.headers['sec-websocket-protocol']!
  // 防止服务未就绪时客户端连接
  if (protocol === 'vite-ping' && server && !server.listening) {
    req.destroy()
    return
  }
  handleUpgrade(req, socket as Socket, head, protocol === 'vite-ping')
})

handleUpgrade

这里的逻辑很简单,主要就是建立 websocket 连接。

const handleUpgrade = (
    req: IncomingMessage,
    socket: Duplex,
    head: Buffer,
    isPing: boolean,
) => {
    wss.handleUpgrade(req, socket as Socket, head, (ws) => {
      if (isPing) {
        ws.close(/* Normal Closure */ 1000)
        return
      }
      wss.emit('connection', ws, req)
    })
}

handleUpgrade 函数中,手动触发了 connection 事件,那么就需要实现 connection 事件的回调函数。

wss.on('connection', (socket) => {
    socket.on('message', (raw) => {
      if (!customListeners.size) return
      let parsed: any
      try {
        parsed = JSON.parse(String(raw))
      } catch {}
      if (!parsed || parsed.type !== 'custom' || !parsed.event) return
      const listeners = customListeners.get(parsed.event)
      if (!listeners?.size) return
      const client = getSocketClient(socket)
      listeners.forEach((listener) =>
        listener(parsed.data, client, parsed.invoke),
      )
    })
    socket.on('error', (err) => {
      // 错误处理,日志打印
    })
    socket.send(JSON.stringify({ type: 'connected' }))
    // bufferedError 处理
  })

从代码中可以看出,如果忽略对参数的校验和处理,核心逻辑其实是根据 parsed.eventcustomListeners 中取得所有的监听函数,并全部执行。那么该如何添加监听函数呢?

在最后的部分,vitews 的功能进行的封装和增强。通过一个对象放回,通过调用这些函数,可以添加监听函数。

// normalizeHotChannel 对**底层 HMR(热模块替换)通信通道(`HotChannel`)进行标准化封装**
const normalizedHotChannel = normalizeHotChannel(
    {
      send(payload) {
        if (payload.type === 'error' && !wss.clients.size) {
          bufferedError = payload
          return
        }

        const stringified = JSON.stringify(payload)
        wss.clients.forEach((client) => {
          // readyState 1 means the connection is open
          if (client.readyState === 1) {
            client.send(stringified)
          }
        })
      },
      on(event: string, fn: any) {
        if (!customListeners.has(event)) {
          customListeners.set(event, new Set())
        }
        customListeners.get(event)!.add(fn)
      },
      off(event: string, fn: any) {
        customListeners.get(event)?.delete(fn)
      },
      listen() {
        wsHttpServer?.listen(port, host)
      },
      close() {
        // should remove listener if hmr.server is set
        // otherwise the old listener swallows all WebSocket connections
        if (hmrServerWsListener && wsServer) {
          wsServer.off('upgrade', hmrServerWsListener)
        }
        return new Promise<void>((resolve, reject) => {
          wss.clients.forEach((client) => {
            client.terminate()
          })
          wss.close((err) => {
            if (err) {
              reject(err)
            } else {
              if (wsHttpServer) {
                wsHttpServer.close((err) => {
                  if (err) {
                    reject(err)
                  } else {
                    resolve()
                  }
                })
              } else {
                resolve()
              }
            }
          })
        })
      },
    },
    config.server.hmr !== false,
    false,
  )
  return {
    ...normalizedHotChannel,

    on: ((event: string, fn: any) => {
      if (wsServerEvents.includes(event)) {
        wss.on(event, fn)
        return
      }
      normalizedHotChannel.on(event, fn)
    }) as WebSocketServer['on'],
    off: ((event: string, fn: any) => {
      if (wsServerEvents.includes(event)) {
        wss.off(event, fn)
        return
      }
      normalizedHotChannel.off(event, fn)
    }) as WebSocketServer['off'],
    async close() {
      await normalizedHotChannel.close()
    },

    [isWebSocketServer]: true,
    get clients() {
      return new Set(Array.from(wss.clients).map(getSocketClient))
    },
  }

vite 源码 - 创建服务

截屏2025-10-07 11.13.51.png

缓存配置

vite 中有一个全局变量,叫 usedConfigs,是一个 WeakSet,作用是缓存配置,避免重复创建。

const usedConfigs = new WeakSet<ResolvedConfig>()
if (usedConfigs.has(config)) {
    throw new Error(`There is already a server associated with the config.`)
}

usedConfigs.add(config)

初始化静态文件

在这一步做的工作其实就一句话,即递归读取静态资源文件夹(public)所有文件,并返回层级为 1 的文件路径数组。

// 缓存配置对应的静态资源文件
const publicFilesMap = new WeakMap<ResolvedConfig, Set<string>>()
export async function initPublicFiles(config) {
  let fileNames
  // 递归读取文件
  fileNames = await recursiveReaddir(config.publicDir)
  // 提取文件名和在静态资源文件夹中的路径
  const publicFiles = new Set(
    fileNames.map((fileName) => fileName.slice(config.publicDir.length)),
  )
  publicFilesMap.set(config, publicFiles)
  return publicFiles
}
const initPublicFilesPromise = initPublicFiles(config)
const publicFiles = await initPublicFilesPromise

处理 https 配置

这里的逻辑很简单,就是把相关的证书等文件读取到内存中,不存在则返回 undefined

export async function resolveHttpsConfig(https) {
  if (!https) return undefined

  const [ca, cert, key, pfx] = await Promise.all([
    readFileIfExists(https.ca),
    readFileIfExists(https.cert),
    readFileIfExists(https.key),
    readFileIfExists(https.pfx),
  ])
  return { ...https, ca, cert, key, pfx }
}
const httpsOptions = await resolveHttpsConfig(config.server.https)

处理输出目录配置

这里的输出目录指的是打包后的资源放置的文件目录,对应配置中的 outDir 属性。但是在 vite 中同样支持 rollupOptionsrollup 相关配置。所以在这里需要兼顾 rollupOptions.output 字段。

export function getResolvedOutDirs(root: string,outDir: string,outputOptions) {
  const resolvedOutDir = path.resolve(root, outDir)
  if (!outputOptions) return new Set([resolvedOutDir])

  return new Set(
    // arraify 的作用是判断是否为数组,否则用数组包装并返回
    arraify(outputOptions).map(({ dir }) =>
      dir ? path.resolve(root, dir) : resolvedOutDir,
    ),
  )
}
const resolvedOutDirs = getResolvedOutDirs(
    config.root,
    config.build.outDir,
    config.build.rollupOptions.output,
)

是否清空输出目录

vite 中在构建前是否清空输出目录 outDir 默认为 true,如果显式传入了 emptyOutDir 字段那么会直接返回 emptyOutDir,如果没有,那么会判断每一个输出目录是否在项目中,如果有一个不在,则返回 false

export function resolveEmptyOutDir(
  emptyOutDir: boolean | null,
  root: string,
  outDirs: Set<string>,
  logger?: Logger,
): boolean {
  if (emptyOutDir != null) return emptyOutDir

  for (const outDir of outDirs) {
    if (!normalizePath(outDir).startsWith(withTrailingSlash(root))) {
      return false
    }
  }
  return true
}
const emptyOutDir = resolveEmptyOutDir(
    config.build.emptyOutDir,
    config.root,
    resolvedOutDirs,
)

处理文件监听配置

这里主要做的是对监听需要忽略的文件做初始化。对应到配置中则是 ignored 数组。在这里会加入默认需要忽略的文件,如 **/.git/** **/node_modules/**,还会加入开发者自定义文件,最后加入所有输出目录。

export function resolveChokidarOptions(
  options: WatchOptions | undefined,
  resolvedOutDirs: Set<string>,
  emptyOutDir: boolean,
  cacheDir: string,
): WatchOptions {
  const { ignored: ignoredList, ...otherOptions } = options ?? {}
  const ignored: WatchOptions['ignored'] = [
    '**/.git/**',
    '**/node_modules/**',
    '**/test-results/**', // Playwright
    escapePath(cacheDir) + '/**',
    ...arraify(ignoredList || []),
  ]
  if (emptyOutDir) {
    ignored.push(
      ...[...resolvedOutDirs].map((outDir) => escapePath(outDir) + '/**'),
    )
  }

  const resolvedWatchOptions: WatchOptions = {
    ignored,
    ignoreInitial: true,
    ignorePermissionErrors: true,
    ...otherOptions,
  }

  return resolvedWatchOptions
}

初始化 connect 实例

connect 是一个极简的中间件框架。它提供一个“洋葱式/链式”的中间件堆栈:每个请求进来后按顺序执行 middleware(req, res, next),你在中间件里处理、转发(next())、或直接结束响应(res.end())。

var connect = require('connect');
var http = require('http');

var app = connect();

// gzip/deflate outgoing responses
var compression = require('compression');
app.use(compression());

//create node.js http server and listen on port
http.createServer(app).listen(3000);

在 vite 中,直接调用。

const middlewares = connect() as Connect.Server

创建 http 服务器

这个创建 http 服务器的逻辑相对简单,直接看代码。

export async function resolveHttpServer(
  { proxy }: CommonServerOptions,
  app: Connect.Server,
  httpsOptions?: HttpsServerOptions,
): Promise<HttpServer> {
  // 如果没有传入 httpsOptions ,直接使用 http1.1 创建 server
  if (!httpsOptions) {
    const { createServer } = await import('node:http')
    return createServer(app)
  }

  // 如果存在代理,使用 http1.1 创建 server,避免兼容性问题
  if (proxy) {
    const { createServer } = await import('node:https')
    return createServer(httpsOptions, app)
  } else { // 使用 http2 创建 server
    const { createSecureServer } = await import('node:http2')
    return createSecureServer(
      {
        // 提高每个 HTTP/2 会话的内存上限,避免大量请求时触发 `ENHANCE_YOUR_CALM`(过载)
        // 导致的 502。
        maxSessionMemory: 1000,
        ...httpsOptions,
        allowHTTP1: true,
      },
      app,
    )
  }
}

结束

接下来,会分析 webSocket 服务器的创建,这里的逻辑相对复杂,放后面看。

❌