每种构建工具都会有一个 Graph 用于维护模块之间的引用和模块信息,这篇文章就是分析 Vite 的 Module Graph 是什么样子的
创建 Module Graph 实例
初始化过程发生在createServer
过程中
const container = await createPluginContainer(config, watcher)
const moduleGraph = new ModuleGraph(container)
const server: ViteDevServer = {
moduleGraph,
// ...
}
创建一个 ModuleGraph
实例,并传入创建的插件容器container
export class ModuleGraph {
urlToModuleMap = new Map<string, ModuleNode>()
idToModuleMap = new Map<string, ModuleNode>()
fileToModulesMap = new Map<string, Set<ModuleNode>>()
safeModulesPath = new Set<string>()
container: PluginContainer
constructor(container: PluginContainer) {
this.container = container
}
async getModuleByUrl(rawUrl: string): Promise<ModuleNode | undefined> {}
getModuleById(id: string): ModuleNode | undefined {}
getModulesByFile(file: string): Set<ModuleNode> | undefined {}
onFileChange(file: string): void {}
invalidateModule(): void {}
invalidateAll(): void {}
async updateModuleInfo(): Promise<Set<ModuleNode> | undefined> {}
async ensureEntryFromUrl(rawUrl: string): Promise<ModuleNode> {}
createFileOnlyEntry(file: string): ModuleNode {}
async resolveUrl(url: string): Promise<[string, string]> {}
}
初始化过程就是将传入的插件容器container
挂载到this
上,并初始化 4 个属性urlToModuleMap
、idToModuleMap
、fileToModulesMap
、safeModulesPath
接下来分别看下每个方法的作用
resolveUrl
async resolveUrl(url: string): Promise<[string, string]> {
// 去掉 ?import 和 t=xxx
url = removeImportQuery(removeTimestampQuery(url))
// 这里
const resolvedId = (await this.container.resolveId(url))?.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]
}
这个方法的作用是调用所有插件的resolveId
钩子函数,根据当前被请求模块的url,获取该文件的绝对路径,最后返回[url, 文件绝对路径]
ensureEntryFromUrl
async ensureEntryFromUrl(rawUrl: string): Promise<ModuleNode> {
// 获取文件 url 和 绝对路径
const [url, resolvedId] = await this.resolveUrl(rawUrl)
// 根据 url 获取该url对应的 ModuleNode 实例
let mod = this.urlToModuleMap.get(url)
if (!mod) {
// 初始化 ModuleNode 实例
mod = new ModuleNode(url)
// 将 mod 添加到 urlToModuleMap 中
this.urlToModuleMap.set(url, mod)
// 设置 id
mod.id = resolvedId
// 设置 idToModuleMap
this.idToModuleMap.set(resolvedId, mod)
const file = (mod.file = cleanUrl(resolvedId))
let fileMappedModules = this.fileToModulesMap.get(file)
if (!fileMappedModules) {
fileMappedModules = new Set()
// 设置 fileToModulesMap
this.fileToModulesMap.set(file, fileMappedModules)
}
fileMappedModules.add(mod)
}
return mod
}
根据模块路径创建ModuleNode
对象,将对象收集到ModuleGraph
的属性中;最后返回这个对象
- 添加到
urlToModuleMap
中,键是文件url
;值是模块对应的MoudleNode
对象 - 添加到
idToModuleMap
中,键是文件绝对路径;值是模块对应的MoudleNode
对象 - 添加到
fileToModulesMap
中,键是去掉query
和hash
的文件绝对路径;值是Set
实例,里面添加的是模块对应的MoudleNode
对象
对象内就是关于这个模块的一些信息和模块之间的关系;看下对象属性
- url: 以 / 开头的 url,比如 /src/assets/logo.png
- id: 模块绝对路径,可能带有 query 和 hash
- file: 不带 query 和 hash 的模块绝对路径
- type: 如果是 css 文件并且路径上带有 direct 参数则为'css',否则为 'js'
- lastHMRTimestamp: HMR 更新时间
- importers: 导入该模块的模块合集 Set,元素是 ModuleNode 对象
- importedModules: 当前模块的导入模块合集 Set,元素是 ModuleNode 对象
- transformResult: {
code: 源码,
map: sourcemap 相关,
etag: 唯一值,和对比缓存有关
}
下面的和 import.meta.hot.accept() 有关
- isSelfAccepting: 如果是模块自更新,则为 true
- acceptedHmrDeps: 当前模块接收热更新的模块合集 Set,元素是 ModuleNode 对象;和import.meta.hot.accept() 有关
获取 ModuleNode
// 根据 url 获取模块对应的 ModuleGraph 对象
async getModuleByUrl(rawUrl: string): Promise<ModuleNode | undefined> {
const [url] = await this.resolveUrl(rawUrl)
return this.urlToModuleMap.get(url)
}
// 根据 可能有参数的绝对路径 获取模块对应的 ModuleGraph 对象
getModuleById(id: string): ModuleNode | undefined {
return this.idToModuleMap.get(removeTimestampQuery(id))
}
// 根据 没有参数的绝对路径 获取模块对应的 ModuleGraph 对象集合 Set
getModulesByFile(file: string): Set<ModuleNode> | undefined {
return this.fileToModulesMap.get(file)
}
清空 ModuleNode
// 清空 ModuleGraph 对象的 transformResult
invalidateModule(mod: ModuleNode, seen: Set<ModuleNode> = new Set()): void {
mod.transformResult = null
}
// 清空所有 ModuleGraph 对象的 transformResult
invalidateAll(): void {
const seen = new Set<ModuleNode>()
this.idToModuleMap.forEach((mod) => {
this.invalidateModule(mod, seen)
})
}
onFileChange
onFileChange(file: string): void {
const mods = this.getModulesByFile(file)
if (mods) {
const seen = new Set<ModuleNode>()
mods.forEach((mod) => {
this.invalidateModule(mod, seen)
})
}
}
根据传入的file
获取并清空对应ModuleNode
对象的transformResult
属性值
updateModuleInfo
最重要的一个方法,用于构建和更新模块之间的引用关系
async updateModuleInfo(
mod: ModuleNode, // 当前模块对应的 ModuleNode 对象
importedModules: Set<string | ModuleNode>, // 当前模块导入的模块
acceptedModules: Set<string | ModuleNode>, // 当前模块接收热更新模块的合集
isSelfAccepting: boolean // 如果是自身更新则为 true
): Promise<Set<ModuleNode> | undefined> {
// 如果为 true,表示接收模块自身的热更新
mod.isSelfAccepting = isSelfAccepting
// 获取该模块之前导入集合
const prevImports = mod.importedModules
// 创建新的 Set
const nextImports = (mod.importedModules = new Set())
let noLongerImported: Set<ModuleNode> | undefined
// update import graph
// 遍历 importedModules
for (const imported of importedModules) {
// 如果 imported 是字符串则为依赖模块创建/查找 ModuleNode 实例
const dep =
typeof imported === 'string'
? await this.ensureEntryFromUrl(imported)
: imported
// 将当前模块的 ModuleNode 实例添加到依赖模块对应的 ModuleNode 实例的 importers 上
dep.importers.add(mod)
// 将这个依赖模块对应的 ModuleNode 实例添加到 nextImports 中
nextImports.add(dep)
}
prevImports.forEach((dep) => {
// 如果 nextImports 中没有这个 dep
// 说明这个 dep 对应的模块没在当前模块中导入
// 所以将 mod 从 dep 的 dep.importers 中删除
if (!nextImports.has(dep)) {
dep.importers.delete(mod)
if (!dep.importers.size) {
// 如果没有模块导入 dep 对应的模块,则收集到 noLongerImported 中
;(noLongerImported || (noLongerImported = new Set())).add(
dep
)
}
}
})
// 将 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)
: accepted
deps.add(dep)
}
// 将当前模块导入过,现在没有任何模块导入的文件集合返回
return noLongerImported
}
总结
Vite 为每个模块创建一个ModuleNode
对象,对象内包含模块间的引用关系以及模块信息。模块信息包含绝对路径、转换后的代码、接收的热更新模块等。