ModuleGraph revealed by Vite technology

Get into the habit of writing together! This is the 5th day of my participation in the "Nuggets Daily New Plan · April Update Challenge", click to view the details of the event .

Hello everyone, I'm Xiaoyu, a code farmer. In the previous section, we learned that vite dev uses resolveConfig to obtain and merge the configuration, process the plug-in sequence and execute the config and configResolved hooks, and finally learned the configuration processing of alias and env.

From the perspective of the whole process, after parsing the configuration, we will create a server (createServer) and initialize a file listener (watcher). These two processes were briefly described in CreateServer in Vue Technology Revealing , and the details are relatively few, so it is not necessary to It will be discussed in a separate section. So then new ModuleGraphwe .

By convention, we still use the vite vanilla template to construct the smallest DEMO:

// index.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="favicon.svg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Vite App</title>
  </head>
  <body>
    <div id="app"></div>
    <script type="module" src="/main.js"></script>
  </body>
</html>
复制代码
// main.js
import './style.css'
import { sayHello, name } from './src/foo'

document.querySelector('#app').innerHTML = `
  <h1>${sayHello(name)}</h1>
  <a href="https://vitejs.dev/guide/features.html" target="_blank">Documentation</a>
`

// ./src/foo.js
export * from './baz'
export const name = 'module graph'

// ./src/baz.js
export const sayHello = (msg) => {
  return `Hello, ${msg}`
}
复制代码
// style.css
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
复制代码

The above code, the root directory index.html loads main.js. main.js refers to the style file of style.css, and style.css introduces common.css through @import; main.js also refers to foo.js, and .src/foo introduces sayHello, name; in the file ./src/ The sayHello method is exported in foo.js via export * from './baz'. The relationship between the files is shown in the following figure:

ModuleGraph & ModuleNode

When createServer , an instance of the module graph is created:

// 初始化模块图
  const moduleGraph: ModuleGraph = new ModuleGraph((url, ssr) =>
    container.resolveId(url, undefined, { ssr })
  )
复制代码

Create an instance through new ModuleGraph, now let's understand the ModuleGraph definition:

export class ModuleGraph {
  // url 和模块的映射
  urlToModuleMap = new Map<string, ModuleNode>()
  // id 和模块的映射
  idToModuleMap = new Map<string, ModuleNode>()
  // 文件和模块的映射,一个文件对应多个模块,比如 SFC 就对应多个模块
  fileToModulesMap = new Map<string, Set<ModuleNode>>()
  // /@fs 的模块
  safeModulesPath = new Set<string>()

  constructor(
    // 内部的 resolvceId 是通过构造函数传进来的,指向的是插件容器的 resolveId 方法
    private resolveId: (
      url: string,
      ssr: boolean
    ) => Promise<PartialResolvedId | null>
  ) {}

  /**
   * 通过url获取模块
   */
  async getModuleByUrl(
    rawUrl: string,
    ssr?: boolean
  ): Promise<ModuleNode | undefined> {
    const [url] = await this.resolveUrl(rawUrl, ssr)
    return this.urlToModuleMap.get(url)
  }

  /**
   * 通过 id 获取模块
   */
  getModuleById(id: string): ModuleNode | undefined {
    return this.idToModuleMap.get(removeTimestampQuery(id))
  }

  /**
   * 通过文件获取模块
   */
  getModulesByFile(file: string): Set<ModuleNode> | undefined {
    return this.fileToModulesMap.get(file)
  }

  /**
   * 文件修改的事件
   */
  onFileChange(file: string): void {
    const mods = this.getModulesByFile(file)
    if (mods) {
      const seen = new Set<ModuleNode>()
      mods.forEach((mod) => {
        this.invalidateModule(mod, seen)
      })
    }
  }

  /**
   * 使指定模块失效
   */
  invalidateModule(mod: ModuleNode, seen: Set<ModuleNode> = new Set()): void {
    mod.info = undefined
    mod.transformResult = null
    mod.ssrTransformResult = null
    invalidateSSRModule(mod, seen)
  }

  /**
   * 使全部模块失效
   */
  invalidateAll(): void {
    const seen = new Set<ModuleNode>()
    this.idToModuleMap.forEach((mod) => {
      this.invalidateModule(mod, seen)
    })
  }

  /**
   * 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,
    importedModules: Set<string | ModuleNode>,
    acceptedModules: Set<string | ModuleNode>,
    isSelfAccepting: boolean,
    ssr?: boolean
  ): Promise<Set<ModuleNode> | undefined> {
    // ...
  }

  /**
   * 根据 url 生成模块
   */
  async ensureEntryFromUrl(rawUrl: string, ssr?: boolean): Promise<ModuleNode> {
    const [url, resolvedId, meta] = await this.resolveUrl(rawUrl, ssr)
    // 根据 url 获取模块
    let mod = this.urlToModuleMap.get(url)
    if (!mod) {
      // 实例化一个模块节点
      mod = new ModuleNode(url)
      // 设置模块节点元信息
      if (meta) mod.meta = meta
      // 存入 url 跟模块的 map 中
      this.urlToModuleMap.set(url, mod)
      // id 就是 import 进来的路径
      mod.id = resolvedId
      // 存入 id 跟模块的 map 中
      this.idToModuleMap.set(resolvedId, mod)
      // 设置节点的 file 信息
      const file = (mod.file = cleanUrl(resolvedId))
      // 处理 file 跟模块的关系
      let fileMappedModules = this.fileToModulesMap.get(file)
      if (!fileMappedModules) {
        fileMappedModules = new Set()
        this.fileToModulesMap.set(file, fileMappedModules)
      }
      fileMappedModules.add(mod)
    }
    return mod
  }
 
  /** 
   * 根据引入生成file,比如 css 常用的 import,在 css 代码里面没有 url
   * 但是这种也属于模块图中的节点
   */
  createFileOnlyEntry(file: string): ModuleNode {
    file = normalizePath(file)
    // 获取文件对应的模块
    let fileMappedModules = this.fileToModulesMap.get(file)
    if (!fileMappedModules) {
      fileMappedModules = new Set()
      this.fileToModulesMap.set(file, fileMappedModules)
    }

    // 已经有对应的对应当前的 file
    const url = `${FS_PREFIX}${file}`
    for (const m of fileMappedModules) {
      if (m.url === url || m.id === file) {
        return m
      }
    }

    // 没有模块对应文件,通过url创建模块实例,然后加入到fileMappedModules
    const mod = new ModuleNode(url)
    mod.file = file
    fileMappedModules.add(mod)
    return mod
  }

  /**
   * 解析url,做两件事:
   * 1. 移除 HMR 的时间戳
   * 2. 处理文件后缀,保证文件名一致时(后缀即使不一样)也能够映射到同一个模块
   */
  async resolveUrl(url: string, ssr?: boolean): Promise<ResolvedUrl> {
    // 移除url中的时间戳参数
    url = removeImportQuery(removeTimestampQuery(url))
    // 使用 pluginContainer.resolveId 去解析 url
    const resolved = await this.resolveId(url, !!ssr)
    const resolvedId = resolved?.id || url
    // 获取 id 的扩展
    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]
  }
}
复制代码

ModuleGraph defines 4 properties:

  1. urlToModuleMap: The mapping between url and module;
  2. idToModuleMap:id 跟模块的映射;
  3. fileToModulesMap:文件跟模块的映射,注意这里的 Modules 是复数,说明一个文件可以对应多个模块;
  4. safeModulesPath:/@fs 模块集合;@fs 模块具体指代哪些模块呢?这里先留一个悬念;

并提供了一系列获取、更新这些属性的方法:

  1. getModuleByUrlgetModuleByIdgetModulesByFile 分别是通过 url、id、file 获取模块的方法;

  2. onFileChangeinvalidateAllinvalidateModule 文件改变时的响应函数以及清除模块方法;

  3. updateModuleInfo 更新模块时触发,这部分代码在热更新时才会涉及,先不看这部分;

  4. resolveUrl 用于解析网址,createFileOnlyEntry 对于一些 import 会生成 file 和 模块节点,比如 css 中的 @import,对于 @import 的内容也需要热更功能;

  5. 最后还有一个在构造函数参数中传入的函数 resolveId:

    // 初始化模块图谱
      const moduleGraph: ModuleGraph = new ModuleGraph((url, ssr) =>
        container.resolveId(url, undefined, { ssr })
      )
      // 创建插件容器
      const container = await createPluginContainer(config, moduleGraph, watcher)
    复制代码

​ 可以看到,resolveId 指向的是插件容器的 resolveId 方法;

上面多次提到“模块”,到底什么才是模块呢?这就要看到 ModuleNode 的定义:

/**
 * 模块节点类
 */
export class ModuleNode {
  /**
   * Public served url path, starts with /
   * 模块url -> 公共服务的 url,以 / 开头
   */
  url: string
  /**
   * Resolved file system path + query
   * 模块id -> 解析的文件系统路径+查询
   */
  id: string | null = null
  // 文件路径
  file: string | null = null
  // 模块类型,脚本还是样式
  type: 'js' | 'css'
  // 模块信息,引用 rollup 的 ModuleInfo
  info?: ModuleInfo
  // 模块元信息
  meta?: Record<string, any>
  // 引用者,代表哪些模块引用了这个模块,也叫前置依赖
  importers = new Set<ModuleNode>()
  // 依赖模块,当前模块依赖引入了哪些模块,也叫后置依赖
  importedModules = new Set<ModuleNode>()
  // 当前模块热更“接受”的模块
  acceptedHmrDeps = new Set<ModuleNode>()
  // 是否自我“接受”
  isSelfAccepting = false
  // 转换结果
  transformResult: TransformResult | null = null
  ssrTransformResult: TransformResult | null = null
  ssrModule: Record<string, any> | null = null
  // 模块最后的热更时间
  lastHMRTimestamp = 0
	// 构造函数
  constructor(url: string) {
    this.url = url
    this.type = isDirectCSSRequest(url) ? 'css' : 'js'
  }
}
复制代码

小结

当 Vite 解析完全部配置后,就会去创建模块图实例,这节我们知道了模块图类有 4 个属性,分别是 url、id、file 和 /@fs 与对应模块的关系;还有约 10 个方法,用来对 4 个属性的信息进行增删改查;我们也了解到了图中的每一个节点都是 ModuleNode 的实例,每一个节点都有大量的属性,具体可以见上述 ModuleNode 的定义。

明白了 ModuleGraph 和 ModuleNode 的定义,接下来我们分析一下,ModuleGraph 是如何将 ModuleNode 关联起来的?从本文的例子入手,index.html 只加载了 main.js 模块,Vite server 会如何去处理这个文件呢?我们接着探索。

模块图是怎么加载的?

将我们本节案例在浏览器中跑起来,然后进入 F12 模式,打开网络(network) 面板:

简单分析一下,client 是 vite 自身的客户端脚本,我们先略过。

从 main.js 开始,我们不难注意到的点:根据瀑布关系,main.js 加载并编译完成之后,才去加载 style.css 和 foo.js;foo.js 加载编译完成之后再去加载 baz.js;这种管理跟我们开头的模块文件依赖关系是一致的。这时我们就可以聚焦在 main.js 身上了,回到 createServer 主流程中,在浏览器请求 main.js 这个资源时,会发生什么事情?

// 接收传入配置创建服务
export async function createServer(
  inlineConfig: InlineConfig = {}
): Promise<ViteDevServer> {
	// ...
  // 初始化模块图谱
  const moduleGraph: ModuleGraph = new ModuleGraph((url, ssr) =>
    container.resolveId(url, undefined, { ssr })
  )
  // 创建插件容器
  const container = await createPluginContainer(config, moduleGraph, watcher)

  // 用字面量的形式初始化 server 配置
  const server: ViteDevServer = {
    // ...
  }
  })

	// ...
  // 一系列内部的中间件...
  middlewares.use(transformMiddleware(server))

 	// ... 

  return server
}
复制代码

当浏览器发起资源请求时,会经过一系列中间件,其中 transformMiddleware 就是关键:

/**
 * 文件转换中间件
 * @param {ViteDevServer} server http服务
 * @returns {Connect.NextHandleFunction} 中间件
 */
export function transformMiddleware(
  server: ViteDevServer
): Connect.NextHandleFunction {
  const {
    config: { root, logger, cacheDir },
    moduleGraph
  } = server
	
  // ...
  return async function viteTransformMiddleware(req, res, next) {
    // 如果请求不是GET、url在忽略列表中,直接到下一个中间件
    if (req.method !== 'GET' || knownIgnoreList.has(req.url!)) {
      return next()
    }
		// ...

    try {
      // 省略sourcemap、publicDir的逻辑处理...
      
      // 如果是js、import查询、css、html-proxy
      if (
        isJSRequest(url) ||
        isImportRequest(url) ||
        isCSSRequest(url) ||
        isHTMLProxy(url)
      ) {
        // 去掉 import 的查询参数
        url = removeImportQuery(url)
        // 去除有效的 id 前缀。这是由 importAnalysis 插件在解析的不是有效浏览器导入说明符的 Id 之前添加的
        url = unwrapId(url)

        // 区分 css 请求和导入
        if (
          isCSSRequest(url) &&
          !isDirectRequest(url) &&
          req.headers.accept?.includes('text/css')
        ) {
          url = injectQuery(url, 'direct')
        }

        // 二次加载,利用 etag 做协商缓存
        const ifNoneMatch = req.headers['if-none-match']
        if (
          ifNoneMatch &&
          (await moduleGraph.getModuleByUrl(url, false))?.transformResult
            ?.etag === ifNoneMatch
        ) {
          isDebug && debugCache(`[304] ${prettifyUrl(url, root)}`)
          res.statusCode = 304
          return res.end()
        }

        // 使用插件容器解析、接在和转换
        const result = await transformRequest(url, server, {
          html: req.headers.accept?.includes('text/html')
        })
        if (result) {
          const type = isDirectCSSRequest(url) ? 'css' : 'js'
          const isDep =
            DEP_VERSION_RE.test(url) ||
            (cacheDirPrefix && url.startsWith(cacheDirPrefix))

          // 输出结果
          return send(req, res, result.code, type, {
            etag: result.etag,
            // allow browser to cache npm deps!
            cacheControl: isDep ? 'max-age=31536000,immutable' : 'no-cache',
            headers: server.config.server.headers,
            map: result.map
          })
        }
      }
    } catch (e) {
      return next(e)
    }

    next()
  }
}
复制代码

transformMiddleware(server) 传入 server 对象,利用闭包特性返回中间件函数——viteTransformMiddleware 的整体流程如下图所示:

当浏览器有一个请求到 Vite server 时,会经过 transform 中间件。中间件内部先依据是否以 .map 后缀判断 sourcemap 请求,是的话直接将 transformResult 对应的 map 返回。然后检查公共目录与根目录的位置关系,如果一个请求 url 以公共路径打头,就会触发如下的告警:

然后会对 url 做以下处理:移除 import 参数、移除 /@id 前缀(这玩意是在 importAnalysis 插件中添加,后面单独分析)、如果是 css import 的话,会加入 direct 参数。接着缓存如果命中,直接返回,否则就会进入 transformRequest:

export function transformRequest(
  url: string,
  server: ViteDevServer,
  options: TransformOptions = {}
): Promise<TransformResult | null> {
  const cacheKey = (options.ssr ? 'ssr:' : options.html ? 'html:' : '') + url
  // 判断请求队列中是否存在当前的请求
  let request = server._pendingRequests.get(cacheKey)
  // 如果不存在就解析、编译模块
  if (!request) {
    // 模块解析、转换
    request = doTransform(url, server, options)
    // 将当前请求存到 _pendingRequests 中
    server._pendingRequests.set(cacheKey, request)
    // 请求完成后就从 _pendingRequests 删除
    const done = () => server._pendingRequests.delete(cacheKey)
    // 请求完成之后在 _pendingRequests 删除 
    request.then(done, done)
  }
  return request
}
复制代码

进入 transformRequest,先生成 cacheKey,如果是 ssr 或 html,就在 url 前面加上对应的前缀。通过 server._pendingRequests 判断当前 url 是否还在请求中,如果存在就直接返回 _pendingRequests 中的请求;不存在就调用 doTransform 做转换,并将请求缓存到 _pendingRequests 中,最后请求完成后(不管请求成功还是失败)都会从 _pendingRequests 删除。接下来就来分析 doTransform 具体做了什么事:

/**
 * 解析转换
 * @param {string} url 请求进来的路径
 * @param {ViteDevServer} server http服务
 * @param {TransformOptions} options 转换配置,{ html: false }
 */
async function doTransform(
  url: string,
  server: ViteDevServer,
  options: TransformOptions
) {
  // 移除时间戳的查询参数
  url = removeTimestampQuery(url)
  const { config, pluginContainer, moduleGraph, watcher } = server
  const { root, logger } = config
  const prettyUrl = isDebug ? prettifyUrl(url, root) : ''
  const ssr = !!options.ssr

  // 根据 url 获取模块
  const module = await server.moduleGraph.getModuleByUrl(url, ssr)

  // dev 下缓存解析转换后的结果
  const cached =
    module && (ssr ? module.ssrTransformResult : module.transformResult)
  if (cached) {
    return cached
  }

  // 拿到模块对应的绝对路径 /Users/yjcjour/Documents/code/vite/examples/vite-learning/main.js
  const id =
    (await pluginContainer.resolveId(url, undefined, { ssr }))?.id || url
  
  // 净化id,去除hash和query参数
  const file = cleanUrl(id)

  let code: string | null = null
  let map: SourceDescription['map'] = null

  // load
  const loadStart = isDebug ? performance.now() : 0
  // 执行插件的 load 钩子
  const loadResult = await pluginContainer.load(id, { ssr })
  // 执行 load 后返回 null
  if (loadResult == null) {
   	// ...
    if (options.ssr || isFileServingAllowed(file, server)) {
      try {
        code = await fs.readFile(file, 'utf-8')
      } catch (e) {
        
      }
    }
    // 省略 sourcemap 代码...
  } else {
    // 如果 load 返回的结果不是为空,并且返回的是一个对象,code 和 map
    // 也有可能返回的是一个字符串,也就是 code
    if (isObject(loadResult)) {
      code = loadResult.code
      map = loadResult.map
    } else {
      code = loadResult
    }
  }

  // 确保模块在模块图中正常加载
  const mod = await moduleGraph.ensureEntryFromUrl(url, ssr)
  // 确保模块文件被文件监听器监听
  ensureWatchedFile(watcher, mod.file, root)

  // 核心核心核心!调用转换钩子
  const transformResult = await pluginContainer.transform(code, id, {
    inMap: map,
    ssr
  })
 
	// 省略 debug、sourcemap、ssr 的逻辑
  // 返回转换结果
  return (mod.transformResult = {
    code,
    map,
    etag: getEtag(code, { weak: true })
  } as TransformResult)
}
复制代码

doTransform 删减 sourcemap、ssr、debug 模式下的代码,整个流程就比较清晰了:

我们就以初次加载 main.js 的过程执行一遍上述流程:

  1. 初次加载 main.js 时,server._pendingRequests 中不存在这个请求,所以进入 doTransform;

  2. 浏览器中加载 main.js 的请求是 http://localhost:3000/main.js,所以 doTransform 的 url 参数是 /main.js。

    const module = await server.moduleGraph.getModuleByUrl(url, ssr)
    复制代码

    初次加载时,moduleGraph 中没有 url /main.js对应的模块。所以这里查找的结果是 undefined。

  3. 接着会通过插件容器去解析 url

    const id =
        (await pluginContainer.resolveId(url, undefined, { ssr }))?.id || url
    复制代码

    插件容器的 API 我们留到下一章去分析;这里我们只需要知道经过 pluginContainer.resolveId 转换后的 id 是文件的绝对路径(在我本地的结果是 Users/xxx/vite/examples/module-gtaph/main.js );

  4. 获取到 id 后,执行 pluginContainer.load 钩子:

    let code: string | null = null
    let map: SourceDescription['map'] = null
    // 执行插件的 load 钩子
    const loadResult = await pluginContainer.load(id, { ssr })
    if (loadResult == null) {
      if (options.ssr || isFileServingAllowed(file, server)) {
        try {
          code = await fs.readFile(file, 'utf-8')
        } catch (e) {
        }
      }
    	// ...
    } else {
      // ...
      if (isObject(loadResult)) {
        code = loadResult.code
        map = loadResult.map
      } else {
        code = loadResult
      }
    }
    // ...
    复制代码

    对于例子而言,因为我们没有自定义插件,所行全部内置插件的 load 钩子后,loadResult 最终结果是 null。这里 isFileServingAllowed(file, server) 会返回 ture,将 fs.readFile 读取 main.js 文件的内容存到 code 变量;

  5. 通过将 url 传入 moduleGraph.ensureEntryFromUrl 函数:

     const mod = await moduleGraph.ensureEntryFromUrl(url, ssr)
    复制代码

    在 ModuleGraph 类中我们已经了解 ensureEntryFromUrl 是将 url 解析后创建 ModuleNode 实例,并存到 urlToModuleMap、idToModuleMap、fileToModulesMap,最后返回的 mod 信息如下:

  6. 将 main.js 模块的 file 文件添加到文件监听实例中,达到后面修改 main.js 就会触发更新的效果;

  7. 将第4步拿到的 code 调用全部插件的 transform 钩子:

    const transformResult = await pluginContainer.transform(code, id, {
      inMap: map,
      ssr
    })
    复制代码

    最终生成转换后的结果 transformResult。可以看到,上述所有步骤都是在处理 /main.js 这个 url 和对应的模块

    那么 style.css 、foo.js 是怎么添加到 moduleGraph 中的呢?答案就是通过内置插件 vite:import-analysis ,在该插件的 transform 钩子中,会进行 import 的静态分析,如果有引用其他资源,那么也会添加到 moduleGraph 中。importAnalysis 源码会放到插件篇单独展开分析。但是我们可以看下 /main.js 经过 transform 钩子之后 moduleGraph 的结果:

    从上图可以看到,main.js 依赖的 style.css 和 foo.js 已经被添加到 moduleGraph 中。不仅如此,对于彼此之间的依赖关系也已经形成,我们展开 main.js 和 style.css 两个模块看看:

main.js 模块通过 importedModules 关联了两个子模块(style.css、foo.js)的信息,style 模块通过 importers 关联了父模块(main.js)的信息。

  1. if (
        transformResult == null ||
        (isObject(transformResult) && transformResult.code == null)
      ) {
        // ...
      } else {
        code = transformResult.code!
        map = transformResult.map
      }
    
    	// ...
    
      if (ssr) {
        // ...
      } else {
        return (mod.transformResult = {
          code,
          map,
          etag: getEtag(code, { weak: true })
        } as TransformResult)
      }
    复制代码

    main.js 处理之后 code 是文件内容,map 为 null。最后将转换后的结果存入模块的 transformResult 属性,并使用 etag 根据 code 结果生成文件 etag 信息,整个 doTransform 过程就就结束了。

  2. doTransform 结束后最后回到 transformMiddleware 中,拿到转换后的结果:

    // 使用插件容器解析、接在和转换
    const result = await transformRequest(url, server, {
      html: req.headers.accept?.includes('text/html')
    })
    if (result) {
      // 判断是脚本还是样式类型
      const type = isDirectCSSRequest(url) ? 'css' : 'js'
      const isDep =
            DEP_VERSION_RE.test(url) ||
            (cacheDirPrefix && url.startsWith(cacheDirPrefix))
    
      return send(req, res, result.code, type, {
        etag: result.etag,
        // allow browser to cache npm deps!
        cacheControl: isDep ? 'max-age=31536000,immutable' : 'no-cache',
        headers: server.config.server.headers,
        map: result.map
      })
    }
    复制代码

    对于 /main.js 而言,type 是脚本类型,最终通过 send 返回到浏览器。

  3. 浏览器拿到 main.js 的响应后,解析过程中遇到 import 就会接着发起资源请求,就又会进入 transformMiddleware 重复上述流程,一层一层地完成整个应用的资源加载,这里就利用浏览器支持 ESM 的设计。

总结

本文我们先学习了 ModuleGraph 类,了解到它的 4 个属性和 10 个方法;然后学习了 ModuleNode 类,知道 ModuleGraph 中的每一个节点都是 ModuleNode 的实例,以及每个节点上的属性。

在 dev 服务器启动完,我们在浏览器输入地址后:

浏览器就会向服务端请求 main.js,服务器通过中间件 transformMiddleware 拿到请求 url,通过解析(id、url)的处理,生成模块实例并被文件监听实例监听变化,然后调用插件的 load 和 transform 钩子完成完成代码转换。完成文件内容转换之后,将其响应给浏览器。浏览器解析转换后的 main.js,就会遇到 import ,从而继续加载资源……就这样,完成了整个 moduleGraph 的加载。

In this article, we have encountered pluginContainer (plug-in container) many times in key processes, such as:

  • Module url resolution (resolveUrl) is handled by pluginContainer.resolveId;
  • Loading the module calls pluginContainer.load;
  • Transcode generation calls pluginContainer.transform.

So what exactly is pluginContainer? In the next article, we will get to know it - the plug-in container .

Guess you like

Origin juejin.im/post/7082922754164391972
Recommended