vite hmr 的过程
模块依赖图构建
ModuleGraph 类
ts
import { PartialResolvedId, TransformResult } from "rollup";
import { cleanUrl } from "./utils";
export class ModuleNode {
// 资源访问 url
url: string;
// 资源绝对路径
id: string | null = null;
//哪些包导入了该包
importers = new Set<ModuleNode>();
//该包被哪些包导入
importedModules = new Set<ModuleNode>();
//该模块的内容
transformResult: TransformResult | null = null;
lastHMRTimestamp = 0;
constructor(url: string) {
this.url = url;
}
}
export class ModuleGraph {
// 资源 url 到 ModuleNode 的映射表
urlToModuleMap = new Map<string, ModuleNode>();
// 资源绝对路径到 ModuleNode 的映射表
idToModuleMap = new Map<string, ModuleNode>();
constructor(
private resolveId: (url: string) => Promise<PartialResolvedId | null>
) {}
getModuleById(id: string): ModuleNode | undefined {
return this.idToModuleMap.get(id);
}
async getModuleByUrl(rawUrl: string): Promise<ModuleNode | undefined> {
const { url } = await this._resolve(rawUrl);
return this.urlToModuleMap.get(url);
}
//从url构建一个ModuleNode
async ensureEntryFromUrl(rawUrl: string): Promise<ModuleNode> {
const { url, resolvedId } = await this._resolve(rawUrl);
// 首先检查缓存
if (this.urlToModuleMap.has(url)) {
return this.urlToModuleMap.get(url) as ModuleNode;
}
// 若无缓存,更新 urlToModuleMap 和 idToModuleMap
const mod = new ModuleNode(url);
mod.id = resolvedId;
this.urlToModuleMap.set(url, mod);
this.idToModuleMap.set(resolvedId, mod);
return mod;
}
// 根据mod和其导入的包进行更新图
async updateModuleInfo(
mod: ModuleNode,
importedModules: Set<string | ModuleNode>
) {
const prevImports = mod.importedModules;
for (const curImports of importedModules) {
//是string就生成mod节点
const dep =
typeof curImports === "string"
? await this.ensureEntryFromUrl(cleanUrl(curImports))
: curImports;
if (dep) {
mod.importedModules.add(dep);
dep.importers.add(mod);
}
}
// 清除已经不再被引用的依赖
for (const prevImport of prevImports) {
if (!importedModules.has(prevImport.url)) {
prevImport.importers.delete(mod);
}
}
}
// HMR 触发时会执行这个方法
invalidateModule(file: string) {
const mod = this.idToModuleMap.get(file);
if (mod) {
// 更新时间戳
mod.lastHMRTimestamp = Date.now();
//去除mod的内容使其失活
mod.transformResult = null;
mod.importers.forEach((importer) => {
this.invalidateModule(importer.id!);
});
}
}
private async _resolve(
url: string
): Promise<{ url: string; resolvedId: string }> {
const resolved = await this.resolveId(url);
const resolvedId = resolved?.id || url;
return { url, resolvedId };
}
}
import { PartialResolvedId, TransformResult } from "rollup";
import { cleanUrl } from "./utils";
export class ModuleNode {
// 资源访问 url
url: string;
// 资源绝对路径
id: string | null = null;
//哪些包导入了该包
importers = new Set<ModuleNode>();
//该包被哪些包导入
importedModules = new Set<ModuleNode>();
//该模块的内容
transformResult: TransformResult | null = null;
lastHMRTimestamp = 0;
constructor(url: string) {
this.url = url;
}
}
export class ModuleGraph {
// 资源 url 到 ModuleNode 的映射表
urlToModuleMap = new Map<string, ModuleNode>();
// 资源绝对路径到 ModuleNode 的映射表
idToModuleMap = new Map<string, ModuleNode>();
constructor(
private resolveId: (url: string) => Promise<PartialResolvedId | null>
) {}
getModuleById(id: string): ModuleNode | undefined {
return this.idToModuleMap.get(id);
}
async getModuleByUrl(rawUrl: string): Promise<ModuleNode | undefined> {
const { url } = await this._resolve(rawUrl);
return this.urlToModuleMap.get(url);
}
//从url构建一个ModuleNode
async ensureEntryFromUrl(rawUrl: string): Promise<ModuleNode> {
const { url, resolvedId } = await this._resolve(rawUrl);
// 首先检查缓存
if (this.urlToModuleMap.has(url)) {
return this.urlToModuleMap.get(url) as ModuleNode;
}
// 若无缓存,更新 urlToModuleMap 和 idToModuleMap
const mod = new ModuleNode(url);
mod.id = resolvedId;
this.urlToModuleMap.set(url, mod);
this.idToModuleMap.set(resolvedId, mod);
return mod;
}
// 根据mod和其导入的包进行更新图
async updateModuleInfo(
mod: ModuleNode,
importedModules: Set<string | ModuleNode>
) {
const prevImports = mod.importedModules;
for (const curImports of importedModules) {
//是string就生成mod节点
const dep =
typeof curImports === "string"
? await this.ensureEntryFromUrl(cleanUrl(curImports))
: curImports;
if (dep) {
mod.importedModules.add(dep);
dep.importers.add(mod);
}
}
// 清除已经不再被引用的依赖
for (const prevImport of prevImports) {
if (!importedModules.has(prevImport.url)) {
prevImport.importers.delete(mod);
}
}
}
// HMR 触发时会执行这个方法
invalidateModule(file: string) {
const mod = this.idToModuleMap.get(file);
if (mod) {
// 更新时间戳
mod.lastHMRTimestamp = Date.now();
//去除mod的内容使其失活
mod.transformResult = null;
mod.importers.forEach((importer) => {
this.invalidateModule(importer.id!);
});
}
}
private async _resolve(
url: string
): Promise<{ url: string; resolvedId: string }> {
const resolved = await this.resolveId(url);
const resolvedId = resolved?.id || url;
return { url, resolvedId };
}
}
首先在服务启动前,我们需要初始化 ModuleGraph 实例:
diff
// src/node/server/index.ts
+ import { ModuleGraph } from "../ModuleGraph";
export interface ServerContext {
root: string;
pluginContainer: PluginContainer;
app: connect.Server;
plugins: Plugin[];
+ moduleGraph: ModuleGraph;
}
export async function startDevServer() {
+ const moduleGraph = new ModuleGraph((url) => pluginContainer.resolveId(url));
const pluginContainer = createPluginContainer(plugins);
const serverContext: ServerContext = {
root: process.cwd(),
app,
pluginContainer,
plugins,
+ moduleGraph
};
}
// src/node/server/index.ts
+ import { ModuleGraph } from "../ModuleGraph";
export interface ServerContext {
root: string;
pluginContainer: PluginContainer;
app: connect.Server;
plugins: Plugin[];
+ moduleGraph: ModuleGraph;
}
export async function startDevServer() {
+ const moduleGraph = new ModuleGraph((url) => pluginContainer.resolveId(url));
const pluginContainer = createPluginContainer(plugins);
const serverContext: ServerContext = {
root: process.cwd(),
app,
pluginContainer,
plugins,
+ moduleGraph
};
}
然后在加载完模块后,也就是调用插件容器的 load 方法后,我们需要通过 ensureEntryFromUrl 方法注册模块 当我们对 JS 模块分析完 import 语句之后,需要更新模块之间的依赖关系
服务器
- 创建文件监听器,以监听文件的变动
- 创建 WebSocket 服务端,负责和客户端进行通信
ts
import connect from "connect";
import { red } from "picocolors";
import { WebSocketServer, WebSocket } from "ws";
import { HMR_PORT } from "./constants";
export function createWebSocketServer(server: connect.Server): {
send: (msg: string) => void;
close: () => void;
} {
let wss: WebSocketServer;
wss = new WebSocketServer({ port: HMR_PORT });
wss.on("connection", (socket) => {
socket.send(JSON.stringify({ type: "connected" }));
});
wss.on("error", (e: Error & { code: string }) => {
if (e.code !== "EADDRINUSE") {
console.error(red(`WebSocket server error:\n${e.stack || e.message}`));
}
});
return {
send(payload: Object) {
const stringified = JSON.stringify(payload);
//发送给所有客户端
wss.clients.forEach((client) => {
if (client.readyState === WebSocket.OPEN) {
client.send(stringified);
}
});
},
close() {
wss.close();
},
};
}
import connect from "connect";
import { red } from "picocolors";
import { WebSocketServer, WebSocket } from "ws";
import { HMR_PORT } from "./constants";
export function createWebSocketServer(server: connect.Server): {
send: (msg: string) => void;
close: () => void;
} {
let wss: WebSocketServer;
wss = new WebSocketServer({ port: HMR_PORT });
wss.on("connection", (socket) => {
socket.send(JSON.stringify({ type: "connected" }));
});
wss.on("error", (e: Error & { code: string }) => {
if (e.code !== "EADDRINUSE") {
console.error(red(`WebSocket server error:\n${e.stack || e.message}`));
}
});
return {
send(payload: Object) {
const stringified = JSON.stringify(payload);
//发送给所有客户端
wss.clients.forEach((client) => {
if (client.readyState === WebSocket.OPEN) {
client.send(stringified);
}
});
},
close() {
wss.close();
},
};
}
客户端
HMR 客户端指的是我们向浏览器中注入的一段 JS 脚本,这段脚本中会做如下的事情: 相关脚本
ts
// src/client/client.ts
console.log("[vite] connecting...");
// 1. 创建客户端 WebSocket 实例
// 其中的 __HMR_PORT__ 之后会被 no-bundle 服务编译成具体的端口号
const socket = new WebSocket(`ws://localhost:__HMR_PORT__`, "vite-hmr");
// 2. 接收服务端的更新信息
socket.addEventListener("message", async ({ data }) => {
handleMessage(JSON.parse(data)).catch(console.error);
});
// 3. 根据不同的更新类型进行更新
async function handleMessage(payload: any) {
switch (payload.type) {
case "connected":
console.log(`[vite] connected.`);
// 心跳检测
setInterval(() => socket.send("ping"), 1000);
break;
case "update":
// 进行具体的模块更新
payload.updates.forEach((update: Update) => {
if (update.type === "js-update") {
// 具体的更新逻辑,后续来开发
}
});
break;
}
}
// src/client/client.ts
console.log("[vite] connecting...");
// 1. 创建客户端 WebSocket 实例
// 其中的 __HMR_PORT__ 之后会被 no-bundle 服务编译成具体的端口号
const socket = new WebSocket(`ws://localhost:__HMR_PORT__`, "vite-hmr");
// 2. 接收服务端的更新信息
socket.addEventListener("message", async ({ data }) => {
handleMessage(JSON.parse(data)).catch(console.error);
});
// 3. 根据不同的更新类型进行更新
async function handleMessage(payload: any) {
switch (payload.type) {
case "connected":
console.log(`[vite] connected.`);
// 心跳检测
setInterval(() => socket.send("ping"), 1000);
break;
case "update":
// 进行具体的模块更新
payload.updates.forEach((update: Update) => {
if (update.type === "js-update") {
// 具体的更新逻辑,后续来开发
}
});
break;
}
}
Vite 帮我们在模块最顶层注入了 i
ts
import { createHotContext as __vite__createHotContext } from "/@vite/client";
import.meta.hot = __vite__createHotContext("/src/App.tsx");
import { createHotContext as __vite__createHotContext } from "/@vite/client";
import.meta.hot = __vite__createHotContext("/src/App.tsx");
注入之后我们来实现createHotContext
ts
interface HotModule {
id: string;
callbacks: HotCallback[];
}
interface HotCallback {
deps: string[];
fn: (modules: object[]) => void;
}
// HMR 模块表
const hotModulesMap = new Map<string, HotModule>();
// 不在生效的模块表
const pruneMap = new Map<string, (data: any) => void | Promise<void>>();
export const createHotContext = (ownerPath: string) => {
const mod = hotModulesMap.get(ownerPath);
if (mod) {
mod.callbacks = [];
}
function acceptDeps(deps: string[], callback: any) {
const mod: HotModule = hotModulesMap.get(ownerPath) || {
id: ownerPath,
callbacks: [],
};
// callbacks 属性存放 accept 的依赖、依赖改动后对应的回调逻辑
mod.callbacks.push({
deps,
fn: callback,
});
hotModulesMap.set(ownerPath, mod);
}
return {
accept(deps: any, callback?: any) {
// 这里仅考虑接受自身模块更新的情况
// import.meta.hot.accept()
if (typeof deps === "function" || !deps) {
acceptDeps([ownerPath], ([mod]) => deps && deps(mod));
}
},
// 模块不再生效的回调
// import.meta.hot.prune(() => {})
prune(cb: (data: any) => void) {
pruneMap.set(ownerPath, cb);
},
};
};
interface HotModule {
id: string;
callbacks: HotCallback[];
}
interface HotCallback {
deps: string[];
fn: (modules: object[]) => void;
}
// HMR 模块表
const hotModulesMap = new Map<string, HotModule>();
// 不在生效的模块表
const pruneMap = new Map<string, (data: any) => void | Promise<void>>();
export const createHotContext = (ownerPath: string) => {
const mod = hotModulesMap.get(ownerPath);
if (mod) {
mod.callbacks = [];
}
function acceptDeps(deps: string[], callback: any) {
const mod: HotModule = hotModulesMap.get(ownerPath) || {
id: ownerPath,
callbacks: [],
};
// callbacks 属性存放 accept 的依赖、依赖改动后对应的回调逻辑
mod.callbacks.push({
deps,
fn: callback,
});
hotModulesMap.set(ownerPath, mod);
}
return {
accept(deps: any, callback?: any) {
// 这里仅考虑接受自身模块更新的情况
// import.meta.hot.accept()
if (typeof deps === "function" || !deps) {
acceptDeps([ownerPath], ([mod]) => deps && deps(mod));
}
},
// 模块不再生效的回调
// import.meta.hot.prune(() => {})
prune(cb: (data: any) => void) {
pruneMap.set(ownerPath, cb);
},
};
};
在 accept 方法中,我们会用hotModulesMap这张表记录该模块所 accept 的模块,以及 accept 的模块更新之后回调逻辑。
然后我们再来看fetchUpdate
ts
async function fetchUpdate({ path, timestamp }: Update) {
const mod = hotModulesMap.get(path);
if (!mod) return;
const moduleMap = new Map();
const modulesToUpdate = new Set<string>();
modulesToUpdate.add(path);
await Promise.all(
Array.from(modulesToUpdate).map(async (dep) => {
const [path, query] = dep.split(`?`);
try {
// 通过动态 import 拉取最新模块
const newMod = await import(
path + `?t=${timestamp}${query ? `&${query}` : ""}`
);
moduleMap.set(dep, newMod);
} catch (e) {}
})
);
return () => {
// 拉取最新模块后执行更新回调
for (const { deps, fn } of mod.callbacks) {
fn(deps.map((dep: any) => moduleMap.get(dep)));
}
console.log(`[vite] hot updated: ${path}`);
};
}
async function fetchUpdate({ path, timestamp }: Update) {
const mod = hotModulesMap.get(path);
if (!mod) return;
const moduleMap = new Map();
const modulesToUpdate = new Set<string>();
modulesToUpdate.add(path);
await Promise.all(
Array.from(modulesToUpdate).map(async (dep) => {
const [path, query] = dep.split(`?`);
try {
// 通过动态 import 拉取最新模块
const newMod = await import(
path + `?t=${timestamp}${query ? `&${query}` : ""}`
);
moduleMap.set(dep, newMod);
} catch (e) {}
})
);
return () => {
// 拉取最新模块后执行更新回调
for (const { deps, fn } of mod.callbacks) {
fn(deps.map((dep: any) => moduleMap.get(dep)));
}
console.log(`[vite] hot updated: ${path}`);
};
}
然后拉取了行的模块又交给了中间件的处理就达到了热更新