前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >模块之间的依赖关系是一个图

模块之间的依赖关系是一个图

作者头像
码农小余
发布2022-06-16 16:48:05
1.9K0
发布2022-06-16 16:48:05
举报
文章被收录于专栏:码农小余

大家好,我是码农小余。

解析配置时,Vite 做了这些事 一文中,我们知道了 vite dev 时通过 resolveConfig 去获取并合并配置,处理插件顺序和执行 config 、configResolved 钩子,最后还学习了 alias 以及 env 的配置处理。

从全流程上看,我们在解析完配置后,就会创建服务器(createServer)、初始化文件监听器(watcher),这两个过程在 敲下 vite 命令后,server 做了哪些事? 简述过,细节部分比较少,所以不会用单独的篇幅去展开讲。接着就会通过 new ModuleGraph 去创建模块图。

按照惯例,我们依然用 vite vanilla 模板[1]构造最小 DEMO:

代码语言:javascript
复制
// 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>
代码语言:javascript
复制
// 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}`
}
代码语言:javascript
复制
// 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;
}

上述代码,根目录 index.html 加载 main.js。main.js 引用了 style.css 的样式文件,style.css 通过 @import 引入 common.css;main.js 同时还引用了 foo.js,.src/foo 引入了 sayHello、name;在文件 ./src/foo.js 中通过 export * from './baz' 导出了 sayHello 方法。文件之间的关系就如下图所示:

ModuleGraph & ModuleNode

createServer[2] 时,会创建模块图的实例:

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

通过 new ModuleGraph 创建实例,现在我们就去了解 ModuleGraph 定义:

代码语言:javascript
复制
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 定义了 4 个属性:

  1. urlToModuleMap:url 跟模块的映射;
  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:
代码语言:javascript
复制
// 初始化模块图谱
  const moduleGraph: ModuleGraph = new ModuleGraph((url, ssr) =>
    container.resolveId(url, undefined, { ssr })
  )
  // 创建插件容器
  const container = await createPluginContainer(config, moduleGraph, watcher)

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

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

代码语言:javascript
复制
/**
 * 模块节点类
 */
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 这个资源时,会发生什么事情?

代码语言:javascript
复制
// 接收传入配置创建服务
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 就是关键:

代码语言:javascript
复制
/**
 * 文件转换中间件
 * @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:

代码语言:javascript
复制
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 具体做了什么事:

代码语言:javascript
复制
/**
 * 解析转换
 * @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。
代码语言:javascript
复制
const module = await server.moduleGraph.getModuleByUrl(url, ssr)

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

  1. 接着会通过插件容器去解析 url
代码语言:javascript
复制
const id =
    (await pluginContainer.resolveId(url, undefined, { ssr }))?.id || url

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

  1. 获取到 id 后,执行 pluginContainer.load 钩子:
代码语言:javascript
复制
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 变量;

  1. 通过将 url 传入 moduleGraph.ensureEntryFromUrl 函数:
代码语言:javascript
复制
 const mod = await moduleGraph.ensureEntryFromUrl(url, ssr)

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

  1. 将 main.js 模块的 file 文件添加到文件监听实例中,达到后面修改 main.js 就会触发更新的效果;
  2. 将第4步拿到的 code 调用全部插件的 transform 钩子:
代码语言:javascript
复制
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)的信息。

代码语言:javascript
复制
 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[3] 根据 code 结果生成文件 etag 信息,整个 doTransform 过程就就结束了。

  1. doTransform 结束后最后回到 transformMiddleware 中,拿到转换后的结果:
代码语言:javascript
复制
 // 使用插件容器解析、接在和转换
 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 返回到浏览器。

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

总结

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

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

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

在本文中,我们多次在关键流程上遇到了 pluginContainer(插件容器),比如:

  • 模块 url 解析(resolveUrl)通过 pluginContainer.resolveId 处理;
  • 加载模块调用了 pluginContainer.load;
  • 代码转换生成调用了 pluginContainer.transform。

那么 pluginContainer 到底是什么?下一篇我们就去认识它——插件容器

参考资料

[1]

vite vanilla 模板: https://github.com/Jouryjc/vite/blob/main/examples/module-graph/package.json

[2]

createServer: ./create-server

[3]

etag: https://www.npmjs.com/package/etag

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2022-04-18,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 码农小余 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • ModuleGraph & ModuleNode
    • 小结
    • 模块图是怎么加载的?
    • 总结
      • 参考资料
      相关产品与服务
      容器服务
      腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档