Vite 源码(四)Vite 的 Module Graph

1,717 阅读4分钟

每种构建工具都会有一个 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 个属性urlToModuleMapidToModuleMapfileToModulesMapsafeModulesPath

接下来分别看下每个方法的作用

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中,键是去掉queryhash的文件绝对路径;值是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对象,对象内包含模块间的引用关系以及模块信息。模块信息包含绝对路径、转换后的代码、接收的热更新模块等。