阅读视图
vite 源码 - 创建 ws 服务
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
服务了。
这里也分为两种情况。
在分析这两种情况之前,需要了解 HTTP
和 WebSocket
协议。
核心关系: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
字段,如果传入一个 server
,vite
会复用这个 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
由之前的内容,可以推断出这里的处理逻辑。
- 创建
wsServer
- 使用
wsServer
监听upgrade
事件 - 当触发
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.event
从 customListeners
中取得所有的监听函数,并全部执行。那么该如何添加监听函数呢?
在最后的部分,vite
对 ws
的功能进行的封装和增强。通过一个对象放回,通过调用这些函数,可以添加监听函数。
// 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 源码 - 创建服务
缓存配置
在 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
中同样支持 rollupOptions
即 rollup
相关配置。所以在这里需要兼顾 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 服务器的创建,这里的逻辑相对复杂,放后面看。