从整体角度来看,vite 热更新主要分为三步
创建模块依赖图:服务启动时创建 ModuleGraph 实例,执行 transform 钩子时创建 ModuleNode 实例,记录模块间的依赖关系
服务端收集更新模块:服务启动时通过 chokidar 创建监听器,当文件发生变化时收集需要热更新的模块,将需要更新的模块信息通过 websocket 发送给客户端
客户端派发更新:服务器启动时会在 index.html 注入一段客户端代码,创建一个 websocket 服务监听服务端端发送的热更新信息,在收到服务端的信息后根据模块依赖关系进行模块热更新
在 vite 中,主要通过 ModuleGraph 和 ModuleNode 来建立各模块依赖关系,ModuleGraph 记录模块及模块的所有依赖,ModuleNode 记录模块节点具体信息
模块依赖图在项目启动时通过 ModuleGraph 类创建一个实例
const moduleGraph: ModuleGraph = new ModuleGraph((url, ssr) =>
container.resolveId(url, undefined, { ssr })
);
ModuleGraph 主要通过三个 Map 和一个 Set 来记录模块信息,包括
urlToModuleMap:原始请求 url 到模块节点的映射,如 /src/index.tsx(vite 中的每个模块 url 是唯一的)
idToModuleMap:模块 id 到模块节点的映射,id 是原始请求 url 经过 resolveId 钩子解析后的结果
fileToModulesMap:文件到模块节点的映射,由于单文件可能包含多个模块,如 .vue 文件,因此 Map 的 value 值为一个集合
safeModulesPath:记录被认为是“安全”的模块路径,安全路径不需要模块转换和处理
// 目录:packages/vite/src/node/server/moduleGraph.ts
export class ModuleGraph {
urlToModuleMap = new Map<string, ModuleNode>()
idToModuleMap = new Map<string, ModuleNode>()
fileToModulesMap = new Map<string, Set<ModuleNode>>()
safeModulesPath = new Set<string>()
}
ModuleGraph 三个 map 中存储的就是 ModuleNode 模块节点的信息,ModuleNode 中记录了三个和热更新相关的重要属性
clientImportedModules:当前模块依赖的其他模块
acceptedHmrDeps:其他模块对当前模块的依赖关系,发生热更新时,根据 acceptedHmrDeps 记录的信息通知其他模块信息热更新
export class ModuleNode {
// 原始请求 url
url: string
// 文件绝对路径 + query
id: string | null = null
// 文件绝对路径
file: string | null = null
type: 'js' | 'css'
info?: ModuleInfo
// resolveId 钩子返回结构的元数据
meta?: Record<string, any>
// 重要:当前模块被哪些模块引用
importers = new Set<ModuleNode>()
// 重要:当前模块依赖的其他模块
clientImportedModules = new Set<ModuleNode>()
// 接收热更新的模块
acceptedHmrDeps = new Set<ModuleNode>()
acceptedHmrExports: Set<string> | null = null
importedBindings: Map<string, Set<string>> | null = null
// 是否为 接受自身模块更新
isSelfAccepting?: boolean
// 经过 transform 钩子编译后的结果
transformResult: TransformResult | null = null
// 上一次热更新时间戳
lastHMRTimestamp = 0
lastInvalidationTimestamp = 0
constructor(url: string, setIsSelfAccepting = true) {
this.url = url
this.type = isDirectCSSRequest(url) ? 'css' : 'js'
if (setIsSelfAccepting) {
this.isSelfAccepting = false
}
}
}
服务端收集更新模块
在服务启动阶段,使用 chokidar 的 watch 方法创建文件监听器,监听文件的修改、新增、删除操作
const watcher = chokidar.watch(
[root, ...config.configFileDependencies, config.envDir],
resolvedWatchOptions,
) as FSWatcher
当文件修改时,有三个执行步骤
通过 moduleGraph 实例的 onFileChange 方法移除文件缓存信息
// 监听文件修改操作
watcher.on("change", async (file) => {
// 标准化文件路径
file = normalizePath(file);
// 移除文件缓存信息
moduleGraph.onFileChange(file);
// 执行热更新方法
await onHMRUpdate(file, false);
});
对于文件的新增和删除,使用的同一个方法,执行步骤和文件修改类似,只是第二步的方法有所不同,但本质上都是使用 moduleGraph 的 onFileChange 方法移除文件缓存信息,再执行热更新方法 onHMRUpdate
// 监听文件新增和删除操作
const onFileAddUnlink = async (file: string) => {
// 标准化文件路径
file = normalizePath(file);
// 处理新增和修改文件操作,本质也是移除文件缓存信息
await handleFileAddUnlink(file, server);
// 执行热更新方法
await onHMRUpdate(file, true);
};
// 监听文件新增
watcher.on("add", onFileAddUnlink);
// 监听文件删除
watcher.on("unlink", onFileAddUnlink);
在服务启动阶段,会通过 chokidar 的 watch 方法方法创建一个文件监听器,当文件发生修改、新增和删除操作时,执行热更新操作
热更新操作前会调用 moduleGraph 实例的 onFileChange 方法,清理文件的缓存信息
通过 updateModules 执行收集需要热更新的模块,通过 websocket 向客户端发送需要热更新的模块
客户端派发更新
script 脚本 /@vite/client 会向客户端注入一段默认的代码,代码中执行的 setupWebSocket 方法会创建一个 websocket 服务用于监听服务端发送的热更新信息,接收到的信息会通过 handleMessage 方法处理
function setupWebSocket(
protocol: string,
hostAndPath: string,
onCloseWithoutOpen?: () => void
) {
const socket = new WebSocket(`${protocol}://${hostAndPath}`, "vite-hmr");
let isOpened = false;
// 开启事件
socket.addEventListener(
"open",
() => {
isOpened = true;
notifyListeners("vite:ws:connect", { webSocket: socket });
},
{ once: true }
);
socket.addEventListener("message", async ({ data }) => {
// 接收并处理服务端的热更新信息
handleMessage(JSON.parse(data));
});
return socket;
}