0 Like
Vite 源码解析(2) - 模块依赖图

Vite 源码解析(2) - 模块依赖图

7 PV0 LikeVite
由于现代前端都是模块化开发, 因此各个模块之间会产生各种各样多对多的依赖关系. 为此, 各个打包器都要以 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

urlToModuleMap
urlToModuleMap

idToModuleMap

idToModuleMap
idToModuleMap

fileToModulesMap

fileToModulesMap
fileToModulesMap

safeModulesPath

safeModulesPath
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];   } }
Vite 源码解析(1) - 配置篇

PREVIOUS POST

Vite 源码解析(1) - 配置篇

Vite 源码解析(3) - 插件篇

NEXT POST

Vite 源码解析(3) - 插件篇

    Search by