
Vite 源码解析(2) - 模块依赖图
由于现代前端都是模块化开发, 因此各个模块之间会产生各种各样多对多的依赖关系. 为此, 各个打包器都要以 entry 作为起点, 去寻找整个 APP 的模块依赖图. vite 也不例外, 我们这篇文章就来学习下 vite 的 ModuleGraph 是如何实现的.
ModuleGraph 实例
在 createServer 中, 我们看到会创建一个 ModuleGraph 实例, 并把它作为参数传递到 createPluginContainer(我们下一章重点来讲 vite 的插件机制) 函数中, 供一些插件使用. 此外, 该实例也会传递到 devServer(我们下下章重点来讲 vite 的开发服务及中间件机制) 对象中, 供后续的 HMR 等使用.
const moduleGraph: ModuleGraph = new ModuleGraph((url, ssr) =>
container.resolveId(url, undefined, { ssr })
);
const container = await createPluginContainer(config, moduleGraph, watcher);
const server: ViteDevServer = {
config,
middlewares,
httpServer,
watcher,
pluginContainer: container,
ws,
moduleGraph,
// ...
};
ModuleNode
ModuleNode 是每个模块节点的原子信息, vite 正是通过它将所有的模块关联起来, 形成模块依赖图. 下面我们来简单介绍下各个属性.
export class ModuleNode {
url: string; // 以 / 开头的相对路径
id: string | null = null; // 模块的绝对路径, 但可能带着 hash 和 query
file: string | null = null; // 模块的绝对路径, 不带 hash 和 query
type: "js" | "css"; // 如果路径上带着 &direct, 则为 css, 否则为 js
info?: ModuleInfo; // 模块信息, 来自 rollup, 有 ast, 源码字符串等, 详情: https://rollupjs.org/guide/en/#thisgetmoduleinfo
meta?: Record<string, any>; // 自定义的元信息
importers = new Set<ModuleNode>(); // 导入当前模块的模块的集合
importedModules = new Set<ModuleNode>(); // 当前模块的导入模块集合(或者说谁用到)
acceptedHmrDeps = new Set<ModuleNode>(); // 接收的热更新依赖的集合, 我们放在热更新那一章来讲
isSelfAccepting?: boolean; // 是否为模块自更新
transformResult: TransformResult | null = null; // 通过插件构建后的结果
ssrTransformResult: TransformResult | null = null;
ssrModule: Record<string, any> | null = null;
ssrError: Error | null = null;
lastHMRTimestamp = 0; // HMR 最后更新时间, 也就给给模块 url 上附上 &t=xxxxxxxxxxxxx 的那个时间戳
lastInvalidationTimestamp = 0; // 最后失效的时间, 如果你的模块时间戳超过它, 说明就过期了
constructor(url: string) {
this.url = url;
this.type = isDirectCSSRequest(url) ? "css" : "js"; // 判断是 js 还是 css, 注意这里的 css 也可能是 sass, less 等等
// #7870
// The `isSelfAccepting` value is set by importAnalysis, but some
// assets don't go through importAnalysis.
// 过滤掉 html 文件和不需要被导入分析的模块(json, sourcemap, direct css)
// 这些模块不需要关注自更新
if (isHTMLRequest(url) || canSkipImportAnalysis(url)) {
this.isSelfAccepting = false;
}
}
}
ModuleGraph 各个属性, 方法一览
export class ModuleGraph {
urlToModuleMap: Map<string, ModuleNode>; // key 为相对路径, value 为 ModuleNode 的集合
idToModuleMap: Map<string, ModuleNode>; // key 为模块的绝对路径(但可能带着 hash 和 query), value 为 ModuleNode 的集合
fileToModulesMap: Map<string, Set<ModuleNode>>; // key 为模块的绝对路径(不带 hash 和 query), value 为 ModuleNode 的集合
safeModulesPath: Set<string>; // 哪些模块是安全模块, 后面讲 importAnalysis 插件时再做讲解
constructor(
private resolveId: (
url: string,
ssr: boolean
) => Promise<PartialResolvedId | null>
);
async getModuleByUrl(
rawUrl: string,
ssr?: boolean
): Promise<ModuleNode | undefined>;
getModuleById(id: string): ModuleNode | undefined;
getModulesByFile(file: string): Set<ModuleNode> | undefined;
onFileChange(file: string): void;
invalidateModule(
mod: ModuleNode,
seen?: Set<ModuleNode>,
timestamp?: number
): void;
invalidateAll(): void;
async updateModuleInfo(
mod: ModuleNode,
importedModules: Set<string | ModuleNode>,
acceptedModules: Set<string | ModuleNode>,
isSelfAccepting: boolean,
ssr?: boolean
): Promise<Set<ModuleNode> | undefined>;
async ensureEntryFromUrl(rawUrl: string, ssr?: boolean): Promise<ModuleNode>;
createFileOnlyEntry(file: string): ModuleNode;
resolveUrl(url: string, ssr?: boolean): Promise<ResolvedUrl>;
}
urlToModuleMap

idToModuleMap

fileToModulesMap

safeModulesPath

getModuleByUrl
根据一个 url 获取对应的 ModuleNode. url 为模块的相对路径.
export class ModuleGraph {
async getModuleByUrl(
rawUrl: string,
ssr?: boolean
): Promise<ModuleNode | undefined> {
const [url] = await this.resolveUrl(rawUrl, ssr);
return this.urlToModuleMap.get(url);
}
}
getModuleById
根据一个 id 获取对应的 ModuleNode. id 为模块的绝对路径(可能带着 hash 和 query)
export class ModuleGraph {
getModuleById(id: string): ModuleNode | undefined {
// 我们知道 vite 为了判断源码模块的新鲜度. 给 url 加了一个 ?t=xxxxxxxxxxxxx
// removeTimestampQuery 就是把这个 query 去掉
return this.idToModuleMap.get(removeTimestampQuery(id));
}
}
getModulesByFile
根据一个 file 获取对应的 ModuleNode 集合. file 为模块的绝对路径(不带 hash 和 query).
export class ModuleGraph {
getModulesByFile(file: string): Set<ModuleNode> | undefined {
return this.fileToModulesMap.get(file);
}
}
onFileChange
当文件发生变化时, 就把旧的文件批量过期掉.
export class ModuleGraph {
onFileChange(file: string): void {
const mods = this.getModulesByFile(file);
if (mods) {
const seen = new Set<ModuleNode>();
mods.forEach((mod) => {
this.invalidateModule(mod, seen);
});
}
}
}
invalidateModule
将模块过期, 思路很简单, 把时间改了即可.
export class ModuleGraph {
invalidateModule(
mod: ModuleNode,
seen: Set<ModuleNode> = new Set(),
timestamp: number = Date.now()
): void {
// Save the timestamp for this invalidation, so we can avoid caching the result of possible already started
// processing being done for this module
// lastInvalidationTimestamp 上面说了, 是最后失效的时间, 如果你的模块时间戳超过它, 说明就过期了
// 所以把当前模块时间戳赋值给 lastInvalidationTimestamp, 那这个模块就过期了
mod.lastInvalidationTimestamp = timestamp;
// Don't invalidate mod.info and mod.meta, as they are part of the processing pipeline
// Invalidating the transform result is enough to ensure this module is re-processed next time it is requested
// 把经过插件转换后的结果废弃掉
mod.transformResult = null;
mod.ssrTransformResult = null;
invalidateSSRModule(mod, seen);
}
}
invalidateAll
没啥说的, 循环废掉当前 id 下的所有模块.
export class ModuleGraph {
invalidateAll(): void {
const timestamp = Date.now();
const seen = new Set<ModuleNode>();
this.idToModuleMap.forEach((mod) => {
this.invalidateModule(mod, seen, timestamp);
});
}
}
updateModuleInfo
当导入关系发生变化时, 需要更新模块之间的关系. 这也是 ModuleGraph 最重要的一个方法.
export class ModuleGraph {
/**
* Update the module graph based on a module's updated imports information
* If there are dependencies that no longer have any importers, they are
* returned as a Set.
*/
async updateModuleInfo(
mod: ModuleNode, // 当前模块对应的 ModuleNode 对象
importedModules: Set<string | ModuleNode>, // 当前模块导入的模块
acceptedModules: Set<string | ModuleNode>, // 当前模块接收热更新模块的合集
isSelfAccepting: boolean, // 如果是自身更新则为 true
ssr?: boolean
): Promise<Set<ModuleNode> | undefined> {
mod.isSelfAccepting = isSelfAccepting;
// 先把 importedModules(当前模块导入的模块集合) 保存一份
const prevImports = mod.importedModules;
// 创建一个空的 Set
const nextImports = (mod.importedModules = new Set());
// 不再导入的模块集合
let noLongerImported: Set<ModuleNode> | undefined;
// update import graph
// 遍历新的导入的 modules
for (const imported of importedModules) {
const dep =
typeof imported === "string"
? await this.ensureEntryFromUrl(imported, ssr)
: imported;
// 将当前模块(mod)添加到被导入模块(dep) 的 importer 上
dep.importers.add(mod);
// 把这个被导入的模块(dep) 添加到 nextImports 中
nextImports.add(dep);
}
// remove the importer from deps that were imported but no longer are.
prevImports.forEach((dep) => {
// 如果 nextImports 中没有这个 dep
if (!nextImports.has(dep)) {
// 反过来说明 dep 没在当前模块中导入, 所以把 dep 的 importers 删除掉当前 mod
dep.importers.delete(mod);
// 如果 dep 的 importers 为空
if (!dep.importers.size) {
// 说明 dep 没有被任何模块导入, 于是把它归类到 noLongerImported 中
// dependency no longer imported
(noLongerImported || (noLongerImported = new Set())).add(dep);
}
}
});
// update accepted hmr deps
// 将 import.meta.hot.accept() 中设置的模块添加到 mod.acceptedModules 中
const deps = (mod.acceptedHmrDeps = new Set());
for (const accepted of acceptedModules) {
const dep =
typeof accepted === "string"
? await this.ensureEntryFromUrl(accepted, ssr)
: accepted;
deps.add(dep);
}
// 返回不再被任何模块导入的模块的集合
return noLongerImported;
}
}
ensureEntryFromUrl
目的就是如果找到这个模块, 就把这个模块返回, 否则创建一个新的 ModuleNode, 并放置到 urlToModuleMap, idToModuleMap, fileToModulesMap.
export class ModuleGraph {
async ensureEntryFromUrl(rawUrl: string, ssr?: boolean): Promise<ModuleNode> {
const [url, resolvedId, meta] = await this.resolveUrl(rawUrl, ssr);
let mod = this.urlToModuleMap.get(url);
// 如果没获取 mod
if (!mod) {
// new 一个新的 ModuleNode
mod = new ModuleNode(url);
if (meta) mod.meta = meta;
// 加入到 urlToModuleMap
this.urlToModuleMap.set(url, mod);
mod.id = resolvedId;
// 加入到 idToModuleMap
this.idToModuleMap.set(resolvedId, mod);
// 清除掉 hash, query 作为 file
const file = (mod.file = cleanUrl(resolvedId));
let fileMappedModules = this.fileToModulesMap.get(file);
// 如果没有 fileMappedModules Set
if (!fileMappedModules) {
// 初始化一个
fileMappedModules = new Set();
this.fileToModulesMap.set(file, fileMappedModules);
}
// 否则添加进去即可
fileMappedModules.add(mod);
}
// 返回 mod
return mod;
}
}
createFileOnlyEntry
对于像 @import 这种也要放在模块依赖图中.
export class ModuleGraph {
// some deps, like a css file referenced via @import, don't have its own
// url because they are inlined into the main css import. But they still
// need to be represented in the module graph so that they can trigger
// hmr in the importing css file.
createFileOnlyEntry(file: string): ModuleNode {
file = normalizePath(file);
let fileMappedModules = this.fileToModulesMap.get(file);
if (!fileMappedModules) {
fileMappedModules = new Set();
this.fileToModulesMap.set(file, fileMappedModules);
}
const url = `${FS_PREFIX}${file}`;
for (const m of fileMappedModules) {
if (m.url === url || m.id === file) {
return m;
}
}
const mod = new ModuleNode(url);
mod.file = file;
fileMappedModules.add(mod);
return mod;
}
}
resolveUrl
export class ModuleGraph {
// for incoming urls, it is important to:
// 1. remove the HMR timestamp query (?t=xxxx)
// 2. resolve its extension so that urls with or without extension all map to
// the same module
// 1. 去掉热更新带着的 import=xxxx 和 ?t=xxxxxx 参数
// 2. 解析其扩展名,以便带有或不带有扩展名的 url 都映射到同一个模块
async resolveUrl(url: string, ssr?: boolean): Promise<ResolvedUrl> {
url = removeImportQuery(removeTimestampQuery(url));
// resolveId 是 rollup 插件体系的, 它就是获取当前模块在文件系统的绝对路径, 下一章讲插件会说到
const resolved = await this.resolveId(url, !!ssr);
const resolvedId = resolved?.id || url;
const ext = extname(cleanUrl(resolvedId));
const { pathname, search, hash } = parseUrl(url);
if (ext && !pathname!.endsWith(ext)) {
url = pathname + ext + (search || "") + (hash || "");
}
return [url, resolvedId, resolved?.meta];
}
}