Vite2+客户端请求源码分析

上篇文章来分析下vite启动完服务器之后客户端请求的流程,浏览器请求localhost:3000/时匹配到了D:\workspace\vue3\vite-2.7.2\packages\vite\src\node\server\middlewares\spaFallback.ts中的viteSpaFallbackMiddleware这个中间件,中间件方法如下:

代码1
D:\workspace\vue3\vite-2.7.2\packages\vite\src\node\server\middlewares\spaFallback.ts


export function spaFallbackMiddleware(
  root: string
): Connect.NextHandleFunction {
    const historySpaFallbackMiddleware = history({
    logger: createDebugger('vite:spa-fallback'),
    // support /dir/ without explicit index.html
    rewrites: [
      {
        from: /\/$/,
        to({ parsedUrl }: any) {
          const rewritten =
            decodeURIComponent(parsedUrl.pathname) + 'index.html'

          if (fs.existsSync(path.join(root, rewritten))) {
            return rewritten
          } else {
            return `/index.html`
          }
        }
      }
    ]
  })
  return function viteSpaFallbackMiddleware(req, res, next) {
    return historySpaFallbackMiddleware(req, res, next)
  }
}
经过服务端重写将/index.html的内容返回给浏览器。URL重写与重定向的区别:

URL重定向是客户端操作,指示客户端访问另一个地址的资源。这需要往返服务器。 客户端对资源发出新请求时,返回客户端的重定向URL会出现在浏览器地址栏。

URL重写是服务器端操作,提供来自不同资源地址的资源。重写URL不需要往返服务器。 重写的URL不会返回客户端,也不会出现在浏览器地址栏。例如/resource 重写到 /different-resource 时,客户端会请求 /resource ,并且服务器会在内部提取 /different-resource 处的资源。

然后匹配到D:\workspace\vue3\vite-2.7.2\packages\vite\src\node\server\middlewares\indexHtml.ts中的indexHtmlMiddleware中间件

代码2
D:\workspace\vue3\vite-2.7.2\packages\vite\src\node\server\middlewares\indexHtml.ts

export function indexHtmlMiddleware(
  server: ViteDevServer
): Connect.NextHandleFunction {
  // Keep the named function. The name is visible in debug logs via `DEBUG=connect:dispatcher ...`
  return async function viteIndexHtmlMiddleware(req, res, next) {
    if (res.writableEnded) {
      return next()
    }
    //url='/index.html'
    const url = req.url && cleanUrl(req.url)
    // spa-fallback always redirects to /index.html
    if (url?.endsWith('.html') && req.headers['sec-fetch-dest'] !== 'script') {
      //filename='D:\\workspace\\vue3\\vitelearn\\index.html'
      const filename = getHtmlFilename(url, server)
      if (fs.existsSync(filename)) {
        try {
          //读取index.html文件的内容
          let html = fs.readFileSync(filename, 'utf-8')
          html = await server.transformIndexHtml(url, html, req.originalUrl)
          return send(req, res, html, 'html', {
            headers: server.config.server.headers
          })
        } catch (e) {
          return next(e)
        }
      }
    }
    next()
  }
}

来看下server.transformIndexHtml方法,transformIndexHtml方法是在createServer时赋值的,方法如下:

代码3
D:\workspace\vue3\vite-2.7.2\packages\vite\src\node\server\index.ts



export async function createServer(
  inlineConfig: InlineConfig = {}
): Promise<ViteDevServer> {
    ...省略代码
    server.transformIndexHtml = createDevHtmlTransformFn(server)
    ...省略代码
}

所以代码2中调用server.transformIndexHtml,其实就是调用的createDevHtmlTransformFn的return处的方法,来看下createDevHtmlTransformFn的return处的方法

代码4
D:\workspace\vue3\vite-2.7.2\packages\vite\src\node\server\middlewares\indexHtml.ts



export function createDevHtmlTransformFn(
  server: ViteDevServer
): (url: string, html: string, originalUrl: string) => Promise<string> {
  const [preHooks, postHooks] = resolveHtmlTransforms(server.config.plugins)

  //url='/index.html' html='index.html文件的内容' originalUrl='/'
  return (url: string, html: string, originalUrl: string): Promise<string> => {
    return applyHtmlTransforms(html, [...preHooks, devHtmlHook, ...postHooks], {
      path: url,
      filename: getHtmlFilename(url, server),
      server,
      originalUrl
    })
  }
}

来看下applyHtmlTransforms方法

代码5
D:\workspace\vue3\vite-2.7.2\packages\vite\src\node\plugins\html.ts


//html='index.html文件的内容'
async function applyHtmlTransforms(html, hooks, ctx) {
    const headTags = [];
    const headPrependTags = [];
    const bodyTags = [];
    const bodyPrependTags = [];
    for (const hook of hooks) {
        const res = await hook(html, ctx);
        if (!res) {
            continue;
        }
        if (typeof res === 'string') {
            html = res;
        }
        else {
            let tags;
            if (Array.isArray(res)) {
                tags = res;
            }
            else {
                html = res.html || html;
                tags = res.tags;
            }
            for (const tag of tags) {
                if (tag.injectTo === 'body') {
                    bodyTags.push(tag);
                }
                else if (tag.injectTo === 'body-prepend') {
                    bodyPrependTags.push(tag);
                }
                else if (tag.injectTo === 'head') {
                    headTags.push(tag);
                }
                else {
                    headPrependTags.push(tag);
                }
            }
        }
    }
    // inject tags
    if (headPrependTags.length) {
        html = injectToHead(html, headPrependTags, true);
    }
    if (headTags.length) {
        html = injectToHead(html, headTags);
    }
    if (bodyPrependTags.length) {
        html = injectToBody(html, bodyPrependTags, true);
    }
    if (bodyTags.length) {
        html = injectToBody(html, bodyTags);
    }
    return html;
}

然后循环调用前置插件、devHtmlHook插件、后置插件的函数,因为前置插件、后置插件都为空数组,所以只剩了devHtmlHook插件,来看下devHtmlHook插件的实现:

代码6
D:\workspace\vue3\vite-2.7.2\packages\vite\src\node\server\middlewares\indexHtml.ts


//html='index.html文件的内容'
//htmlPath='/index.html'
//originalUrl='/'
const devHtmlHook: IndexHtmlTransformHook = async (
  html,
  { path: htmlPath, server, originalUrl }
) => {
  ...省略代码

  await traverseHtml(html, htmlPath, (node) => {
        ...省略代码
  })

  html = s.toString()

  return {
    html,
    tags: [
      {
        tag: 'script',
        attrs: {
          type: 'module',
          //src='/@vite/client'
          src: path.posix.join(base, CLIENT_PUBLIC_PATH)
        },
        injectTo: 'head-prepend'
      }
    ]
  }
}

代码6中traverseHtml其实就是@vue/compiler-dom中的parse方法对index.html的文件内容进行抽象语法树分析,然后调用@vue/compiler-dom中的transform方法对抽象语法树进行遍历。再回到代码5处,调用injectToHead方法将<script type="module" src="/@vite/client">插入到index.html文件的<head>标签中,然后将index.html文件的内容返回给浏览器。通过ESM的模块化规范,浏览器会自动向服务器发起获取/src/main.ts和/@vite/client的GET请求,当浏览器发起/@vite/client的GET请求时会匹配到transformMiddleware这个中间件,transformMiddleware返回了viteTransformMiddleware方法,来看下viteTransformMiddleware方法

代码7
D:\workspace\vue3\vite-2.7.2\packages\vite\src\node\server\middlewares\transform.ts


export function transformMiddleware(
  server: ViteDevServer
): Connect.NextHandleFunction {
    ...省略代码
    //url='/@vite/client'
    return async function viteTransformMiddleware(req, res, next) {
        ...省略代码
        if (
            //匹配到了isJSRequest方法
            isJSRequest(url) ||
            isImportRequest(url) ||
            isCSSRequest(url) ||
            isHTMLProxy(url)
          ) {
            ...省略代码
            //判断能不能让客户端直接读取缓存,因为是首次请求/@vite/client所以肯定没有缓存存在
            const ifNoneMatch = req.headers['if-none-match']
            if (
                ifNoneMatch &&
                (await moduleGraph.getModuleByUrl(url))?.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')
            })
            ...省略代码
        }
    ...省略代码  
    }
}

来看下transformRequest方法

代码8
D:\workspace\vue3\vite-2.7.2\packages\vite\src\node\server\transformRequest.ts



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)
    server._pendingRequests.set(cacheKey, request)
    const done = () => server._pendingRequests.delete(cacheKey)
    request.then(done, done)
  }
  return request
}

transformRequest方法主要判断如果是在请求中的请求那么不进行doTransform,不在请求中的请求才进行doTransform,doTransform实现如下:

代码9
D:\workspace\vue3\vite-2.7.2\packages\vite\src\node\server\transformRequest.ts


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='/@vite/client'
  //ssr=false
  const module = await server.moduleGraph.getModuleByUrl(url, ssr)
  ...省略代码
}

来看下getModuleByUrl方法

代码10
D:\workspace\vue3\vite-2.7.2\packages\vite\src\node\server\moduleGraph.ts


export class ModuleGraph {
    ...省略代码
    async getModuleByUrl(
        rawUrl: string,
        ssr?: boolean
      ): Promise<ModuleNode | undefined> {
        const [url] = await this.resolveUrl(rawUrl, ssr)
        return this.urlToModuleMap.get(url)
    }
    ...省略代码
}

看下this.resolveUrl方法

代码11
D:\workspace\vue3\vite-2.7.2\packages\vite\src\node\server\moduleGraph.ts


export class ModuleGraph {
    ...省略代码
    //url='/@vite/client'
    async resolveUrl(url: string): Promise<ResolvedUrl> {
        url = removeImportQuery(removeTimestampQuery(url))
        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]
    }
    ...省略代码
}

再看下this.resolveId方法,resolveId是在createServer时实例化ModuleGraph类时传进来的,具体如下:

D:\workspace\vue3\vite-2.7.2\packages\vite\src\node\server\index.ts

export async function createServer(
  inlineConfig: InlineConfig = {}
): Promise<ViteDevServer> {
  ...省略代码
  const moduleGraph: ModuleGraph = new ModuleGraph((url, ssr) =>
    container.resolveId(url, undefined, { ssr })
  )
  ...省略代码 
}

 所以代码11处的this.resolveId其实调用的是container.resolveId方法,container.resolveId方法实现如下:

代码12
D:\workspace\vue3\vite-2.7.2\packages\vite\src\node\server\pluginContainer.ts


class TransformContext extends Context {
    ...省略代码
    //rawId='/@vite/client' 
    //importer='D:\\workspace\\vue3\\vitelearn\\index.html'
    //options=undefined
    async resolveId(rawId, importer = join(root, 'index.html'), options) {
            ...省略代码
            const ctx = new Context();
            ...省略代码
            for (const plugin of plugins) {
                ...省略代码
                const result = await plugin.resolveId.call(ctx, rawId, importer, { ssr });
                if (!result)
                    continue;
                if (typeof result === 'string') {
                    id = result;
                }
                else {
                    id = result.id;
                    Object.assign(partial, result);
                }
                ...省略代码
                break;
            }
            ...省略代码
            if (id) {
                partial.id = isExternalUrl(id) ? id : normalizePath$4(id);
                return partial;
            }
            else {
                return null;
            }
    }
    ...省略代码
}

 循环调用每个插件的resolveId方法,plugins结构如图:

先匹配到了name:alias插件,alias插件实现如下:

代码13
D:\workspace\vue3\vite-2.7.2\node_modules\.pnpm\@rollup+plugin-alias@3.1.8_rollup@2.59.0\node_modules\@rollup\plugin-alias\dist\index.es.js的alias方法


function alias(options = {}) {
    ...省略代码
    return {
        name:'alias',
        ...省略代码
        //importee='/@vite/client'
        //importer='D:\\workspace\\vue3\\vitelearn\\index.html'
        //resolveOptions={ssr:undefined}
        resolveId(importee, importer, resolveOptions) {
            ...省略代码
            //updatedId='D:\\workspace\\vue3\\vite-2.7.2\\packages\\vite\\dist\\client\\client.mjs'
            const updatedId = normalizeId(importeeId.replace(matchedEntry.find, matchedEntry.replacement));
            ...省略代码
            return this.resolve(updatedId, importer, Object.assign({ skipSelf: true }, resolveOptions)).then((resolved) => {
                let finalResult = resolved;
                if (!finalResult) {
                    finalResult = { id: updatedId };
                }
                return finalResult;
            });
        }
    }
}

 然后调用了this.resolve方法,this此时指向的是D:\workspace\vue3\vite-2.7.2\packages\vite\src\node\server\pluginContainer.ts的Context对象,来看下Context的resovle方法

代码14
D:\workspace\vue3\vite-2.7.2\packages\vite\src\node\server\pluginContainer.ts


class Context implements PluginContext {
    ...省略代码
      //id='D:\\workspace\\vue3\\vitelearn\\node_modules\\vite\\packages\\vite\\dist\\client\\client.mjs'
    //importer='D:\\workspace\\vue3\\vitelearn\\index.html'
    //options={skipSelf:true,ssr:false}
    async resolve(
      id: string,
      importer?: string,
      options?: { skipSelf?: boolean }
    ) {
      let skip: Set<Plugin> | undefined
      if (options?.skipSelf && this._activePlugin) {
        skip = new Set(this._resolveSkips)
        skip.add(this._activePlugin)
      }
      let out = await container.resolveId(id, importer, { skip, ssr: this.ssr })
      if (typeof out === 'string') out = { id: out }
      return out as ResolvedId | null
    }
    ...省略代码
}

然后看下container.resolveId方法

代码15
D:\workspace\vue3\vite-2.7.2\packages\vite\src\node\server\pluginContainer.ts


class TransformContext extends Context {
    ...省略代码
        //rawId='D:\\workspace\\vue3\\vite-2.7.2\\packages\\vite\\dist\\client\\client.mjs' 
    //importer='D:\\workspace\\vue3\\vitelearn\\index.html'
    //options={skip:Set对象,ssr:false}
    async resolveId(rawId, importer = join(root, 'index.html'), options) {
            ...省略代码
            const ctx = new Context();
            ...省略代码
            for (const plugin of plugins) {
                ...省略代码
                const result = await plugin.resolveId.call(ctx, rawId, importer, { ssr });
                if (!result)
                    continue;
                if (typeof result === 'string') {
                    id = result;
                }
                else {
                    id = result.id;
                    Object.assign(partial, result);
                }
                ...省略代码
                break;
            }
            ...省略代码
            if (id) {
                partial.id = isExternalUrl(id) ? id : normalizePath$4(id);
                return partial;
            }
            else {
                return null;
            }
    }
    ...省略代码
}

 参数options如图:

 循环调用每个插件的resovleId方法,匹配到了resolvePlugin插件

代码16
D:\workspace\vue3\vite-2.7.2\packages\vite\src\node\plugins\resolve.ts


export function resolvePlugin(baseOptions: InternalResolveOptions): Plugin {
    ...省略代码
    return {
        name:'vite:resolve',
        ...省略代码
        //id='D:\\workspace\\vue3\\vite-2.7.2\\packages\\vite\\dist\\client\\client.mjs'
        //importer='D:\\workspace\\vue3\\vitelearn\\index.html'
        //resolveOpts={ssr:false}
        resolveId(id, importer, resolveOpts) {
            ...省略代码
            // absolute fs paths
            if (path.isAbsolute(id) && (res = tryFsResolve(id, options))) {
                isDebug && debug(`[fs] ${colors.cyan(id)} -> ${colors.dim(res)}`)
                return res
            }
            ...省略代码
        }
    }
}

 也就是代码10返回了

D:\workspace\vue3\vite-2.7.2\packages\vite\src\node\server\moduleGraph.ts

export class ModuleGraph {
    ...省略代码
    async getModuleByUrl(
        rawUrl: string,
        ssr?: boolean
    ): Promise<ModuleNode | undefined> {
        //url='/@vite/client.mjs'
        const [url] = await this.resolveUrl(rawUrl, ssr)
        //urlToModuleMap此时为空,所以返回undefined,只在调用ensureEntryFromUrl时才会写入内容
        return this.urlToModuleMap.get(url)
    }
    ...省略代码
}

 回到代码9,继续往下执行

代码17
D:\workspace\vue3\vite-2.7.2\packages\vite\src\node\server\transformRequest.ts

async function doTransform(
  url: string,
  server: ViteDevServer,
  options: TransformOptions
) {
    ...省略代码
    //module=undefined
    const module = await server.moduleGraph.getModuleByUrl(url, ssr)
    ...省略代码
    //url='/@vite/client'
    const id = (await pluginContainer.resolveId(url, undefined, { ssr }))?.id || url
    const file = cleanUrl(id)
    ...省略代码

}

 调用pluginContainer.resolveId方法

代码18
D:\workspace\vue3\vite-2.7.2\packages\vite\src\node\server\pluginContainer.ts


class TransformContext extends Context {
    ...省略代码
    //rawId='/@vite/client'
    //importer='D:\\workspace\\vue3\\vitelearn\\index.html'
    //options=undefined
    async resolveId(rawId, importer = join(root, 'index.html'), options) {
          ...省略代码
          const ctx = new Context()
          ...省略代码
          for (const plugin of plugins) {
                ...省略代码
                const result = await plugin.resolveId.call(
                      ctx as any,
                      rawId,
                      importer,
                      { ssr }
                )
                if (!result) continue

                if (typeof result === 'string') {
                      id = result
                } else {
                      id = result.id
                      Object.assign(partial, result)
                }

                ...省略代码
                break
           }

           ...省略代码
           if (id) {
                partial.id = isExternalUrl(id) ? id : normalizePath(id)
                return partial as PartialResolvedId
           } else {
                return null
           }
    }
    ...省略代码
}

 重复以上的步骤,先匹配上D:\workspace\vue3\vite-2.7.2\node_modules\.pnpm\@rollup+plugin-alias@3.1.8_rollup@2.59.0\node_modules\@rollup\plugin-alias\dist\index.es.js的alias插件,变更/@vite/client为D:\\workspace\\vue3\\vite-2.7.2\\packages\\vite\\dist\\client\\client.mjs,然后匹配上D:\workspace\vue3\vite-2.7.2\packages\vite\src\node\plugins\resolve.ts的resolvePlugin的resolveId,最终代码17处id='D:/workspace/vue3/vite-2.7.2/packages/vite/dist/client/client.mjs',代码17继续往下执行

代码19
node_modules\vite\src\node\server\transformRequest.ts


async function doTransform(
  url: string,
  server: ViteDevServer,
  options: TransformOptions
) {
    ...省略代码
    //id=''
    const id =
    (await pluginContainer.resolveId(url, undefined, { ssr }))?.id || url
    ...省略代码
    const loadResult = await pluginContainer.load(id, { ssr })
    ...省略代码
}

看下pluginContainer的load方法

代码20
D:\workspace\vue3\vite-2.7.2\packages\vite\src\node\server\pluginContainer.ts



const container: PluginContainer = {
    ...省略代码
     //id='D:/workspace/vue3/vite-2.7.2/packages/vite/dist/client/client.mjs'
    //options={ssr:false}
    async load(id, options) {
          const ssr = options?.ssr
          const ctx = new Context()
          ctx.ssr = !!ssr
          for (const plugin of plugins) {
            if (!plugin.load) continue
            ctx._activePlugin = plugin
            const result = await plugin.load.call(ctx as any, id, { ssr })
            if (result != null) {
              if (isObject(result)) {
                updateModuleInfo(id, result)
              }
              return result
          }
      }
      return null
    },
    ...省略代码
}

 没有匹配到任何一个插件的load规则,所以代码19处loadResult=null,代码19继续往下执行,看下后续代码

代码21
D:\workspace\vue3\vite-2.7.2\packages\vite\src\node\server\transformRequest.ts




async function doTransform(
  url: string,
  server: ViteDevServer,
  options: TransformOptions
) {
    ...省略代码
    //id='D:/workspace/vue3/vite-2.7.2/packages/vite/dist/client/client.mjs'
    const loadResult = await pluginContainer.load(id, { ssr })
    if (loadResult == null) {
       ...省略代码
       if (options.ssr || isFileServingAllowed(file, server)) {
          try {
            //读取client.mjs文件的内容
            code = await fs.readFile(file, 'utf-8')
            isDebug && debugLoad(`${timeFrom(loadStart)} [fs] ${prettyUrl}`)
          } catch (e) {
            if (e.code !== 'ENOENT') {
            throw e
          }
         }
       }
    }
    ...省略代码
    //url='/@vite/client'
    //ssr=false
    const mod = await moduleGraph.ensureEntryFromUrl(url, ssr);
    ...省略代码
}

来看下 moduleGraph.ensureEntryFromUrl方法

代码22
D:\workspace\vue3\vite-2.7.2\packages\vite\src\node\server\moduleGraph.ts



export class ModuleGraph {
    ...省略代码
    //rawUrl='/@vite/client'
    //ssr=false
    async ensureEntryFromUrl(rawUrl: string): Promise<ModuleNode> {
        const [url, resolvedId, meta] = await this.resolveUrl(rawUrl, ssr)
        let mod = this.urlToModuleMap.get(url)
        if (!mod) {
          mod = new ModuleNode(url)
          if (meta) mod.meta = meta
          this.urlToModuleMap.set(url, mod)
          mod.id = resolvedId
          this.idToModuleMap.set(resolvedId, mod)
          const file = (mod.file = cleanUrl(resolvedId))
          let fileMappedModules = this.fileToModulesMap.get(file)
          if (!fileMappedModules) {
            fileMappedModules = new Set()
            this.fileToModulesMap.set(file, fileMappedModules)
          }
          fileMappedModules.add(mod)
        }
        return mod
    }
    ...省略代码
}

看下this.resolveUrl方法

代码23
D:\workspace\vue3\vite-2.7.2\packages\vite\src\node\server\moduleGraph.ts


export class ModuleGraph {
    ...省略代码
    //url='/@vite/client'
    async resolveUrl(url: string, ssr?: boolean): Promise<ResolvedUrl> {
        url = removeImportQuery(removeTimestampQuery(url))
        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]
    }
    ...省略代码
}

resolveId在createServer时创建ModuleGraph实例传递进来的,如下:

export async function createServer(
  inlineConfig: InlineConfig = {}
): Promise<ViteDevServer> {
    ...省略代码
    const moduleGraph: ModuleGraph = new ModuleGraph((url, ssr) =>
        container.resolveId(url, undefined, { ssr })
    )
    ...省略代码
}

所以代码23处的this.resolveId就是调用container.resolveId方法,看下container的resolveId方法

代码24
D:\workspace\vue3\vite-2.7.2\packages\vite\src\node\server\pluginContainer.ts


class TransformContext extends Context {
    ...省略代码
    //rawId='/@vite/client'
    //importer='D:\\workspace\\vue3\\vitelearn\\index.html'
    //options=undefined
    async resolveId(rawId, importer = join(root, 'index.html'), options) {
      ...省略代码
      const ctx = new Context()
      ...省略代码
      for (const plugin of plugins) {
        ...省略代码
        const result = await plugin.resolveId.call(
          ctx as any,
          rawId,
          importer,
          { ssr }
        )
        if (!result) continue

        if (typeof result === 'string') {
          id = result
        } else {
          id = result.id
          Object.assign(partial, result)
        }

        ...省略代码
        break
      }
      if (id) {
        partial.id = isExternalUrl(id) ? id : normalizePath(id)
        return partial as PartialResolvedId
      } else {
        return null
      }
    }
    ...省略代码
}

 经过与以上同样的步骤,最终代码24返回

return {
    id:'D:/workspace/vue3/vite-2.7.2/packages/vite/dist/client/client.mjs'
}

在代码代码22处的ensureEntryFromUrl方法将rawUrl与resolveId以及File缓存起来,这样下次直接从缓存中读取,D:\workspace\vue3\vite-2.7.2\packages\vite\src\node\server\moduleGraph.ts的ensureEntryFromUrl方法中字段含义:

urlToModuleMap:Map<string, ModuleNode>:key是rawUrl,value是ModuleNode对象

idToModuleMap:Map<string, ModuleNode>:key是rawUrl经过resolve之后的路径,也就是绝对路径(路径后面可能会带有参数),value是ModuleNode对象

fileToModulesMap:Map<string, Set<ModuleNode>>:key是rawUrl经过resolve之后的路径,也就是绝对路径(路径后面不带任何参数)

代码21继续往下执行

代码25
D:\workspace\vue3\vite-2.7.2\packages\vite\src\node\server\transformRequest.ts

async function doTransform(
  url: string,
  server: ViteDevServer,
  options: TransformOptions
) {
   ...省略代码
   const mod = await moduleGraph.ensureEntryFromUrl(url, ssr)
   ensureWatchedFile(watcher, mod.file, root)
   const transformStart = isDebug ? performance.now() : 0
   //code='D:/workspace/vue3/vite-2.7.2/packages/vite/dist/client/client.mjs文件的内容'
   //id='D:/workspace/vue3/vite-2.7.2/packages/vite/dist/client/client.mjs'
   const transformResult = await pluginContainer.transform(code, id, {
     inMap: map,
     ssr
   })
   ...省略代码
}

 来看下pluginContainer.transform方法

代码26
D:\workspace\vue3\vite-2.7.2\packages\vite\src\node\server\pluginContainer.ts


class TransformContext extends Context {
    ...省略代码
    //code='D:/workspace/vue3/vite-2.7.2/packages/vite/dist/client/client.mjs文件的内容'
    //id='D:/workspace/vue3/vite-2.7.2/packages/vite/dist/client/client.mjs'
    async transform(code, id, options) {
      const inMap = options?.inMap
      const ssr = options?.ssr
      const ctx = new TransformContext(id, code, inMap as SourceMap)
      ctx.ssr = !!ssr
      for (const plugin of plugins) {
        if (!plugin.transform) continue
        ctx._activePlugin = plugin
        ctx._activeId = id
        ctx._activeCode = code
        const start = isDebug ? performance.now() : 0
        let result: TransformResult | string | undefined
        try {
          result = await plugin.transform.call(ctx as any, code, id, { ssr })
        } catch (e) {
          ctx.error(e)
        }
        if (!result) continue
        isDebug &&
          debugPluginTransform(
            timeFrom(start),
            plugin.name,
            prettifyUrl(id, root)
          )
        if (isObject(result)) {
          if (result.code !== undefined) {
            code = result.code
            if (result.map) {
              ctx.sourcemapChain.push(result.map)
            }
          }
          updateModuleInfo(id, result)
        } else {
          code = result
        }
      }
      return {
        code,
        map: ctx._getCombinedSourcemap()
      }
    }
    ...省略代码
}

 循环调用每个插件的transform方法,并将this指向为D:\workspace\vue3\vite-2.7.2\packages\vite\src\node\server\pluginContainer.ts的TransformContext对象,最终匹配到了clientInjectionsPlugin插件

代码27
D:\workspace\vue3\vite-2.7.2\packages\vite\src\node\plugins\clientInjections.ts


export function clientInjectionsPlugin(config: ResolvedConfig): Plugin {
    return {
        name: 'vite:client-inject',
        //id='D:/workspace/vue3/vite-2.7.2/packages/vite/dist/client/client.mjs'
        transform(code, id) {
            if (id === normalizedClientEntry || id === normalizedEnvEntry) {
                ...省略代码
                return code
                      .replace(`__MODE__`, JSON.stringify(config.mode))
                      .replace(`__BASE__`, JSON.stringify(config.base))
                      .replace(`__DEFINES__`, serializeDefine(config.define || {}))
                      .replace(`__HMR_PROTOCOL__`, JSON.stringify(protocol))
                      .replace(`__HMR_HOSTNAME__`, JSON.stringify(host))
                      .replace(`__HMR_PORT__`, JSON.stringify(port))
                      .replace(`__HMR_TIMEOUT__`, JSON.stringify(timeout))
                      .replace(`__HMR_ENABLE_OVERLAY__`, JSON.stringify(overlay))
            } else if (code.includes('process.env.NODE_ENV')) {
                ...省略代码
            }
        }
    }
}

主要是对 D:/workspace/vue3/vite-2.7.2/packages/vite/dist/client/client.mjs文件中的一些值做替换,例如__MODE__替换为config.mode等。然后又匹配上了importAnalysisPlugin插件

代码28
D:\workspace\vue3\vite-2.7.2\packages\vite\src\node\plugins\importAnalysis.ts


export function importAnalysisPlugin(config: ResolvedConfig): Plugin {
    ...省略代码
    return {
        name: 'vite:import-analysis',
        ...省略代码
        //source='经过上个插件处理过之后的文件内容'
        //importer='D:/workspace/vue3/vite-2.7.2/packages/vite/dist/client/client.mjs'
        //options={ssr:false}
        async transform(source, importer, options) {
            ...省略代码
            try {
                //调用es-module-lexer的parseImports分析client.mjs的依赖
                imports = parseImports(source)[0]
            } catch (e: any) {
                ...省略代码
            }
            ...省略代码
            //读取缓存中的ModuleNode对象
            const importerModule = moduleGraph.getModuleById(importer);
        }
    }
}

 imports结构如图:

调用了moduleGraph的getModuleById方法

代码29
D:\workspace\vue3\vite-2.7.2\packages\vite\src\node\server\moduleGraph.ts


export class ModuleGraph {
    ...省略代码
    //id='D:/workspace/vue3/vitelearn/node_modules/vite/dist/client/client.mjs'
    getModuleById(id: string): ModuleNode | undefined {
        return this.idToModuleMap.get(removeTimestampQuery(id))
    }
    ...省略代码
}

 this.idToModuleMap在代码22处的ensureEntryFromUrl方法已经缓存了起来,所以这里直接根据id取出ModuleNode对象返回,回到代码28,继续往下执行

代码30
D:\workspace\vue3\vite-2.7.2\packages\vite\src\node\plugins\importAnalysis.ts


export function importAnalysisPlugin(config: ResolvedConfig): Plugin {
    ...省略代码
    return {
        name: 'vite:import-analysis',
        async transform(source, importer, options) {
            ...省略代码
            const importerModule = moduleGraph.getModuleById(importer)!
            ...省略代码
            for (let index = 0; index < imports.length; index++) {
                ...省略代码
                //rawUrl='@vite/env'
                const rawUrl = source.slice(start, end)
                ...省略代码
                //specifier='@vite/env'
                if (specifier) {
                    ...省略代码
                    const [normalizedUrl, resolvedId] = await normalizeUrl(specifier,start)
                    ...省略代码
                }
            }
        },
        ...省略代码
    }
}

 来看下normalizeUrl方法

代码31
D:\workspace\vue3\vite-2.7.2\packages\vite\src\node\plugins\importAnalysis.ts


export function importAnalysisPlugin(config: ResolvedConfig): Plugin {
    ...省略代码
    return {
        name: 'vite:import-analysis',
        ...省略代码
        async transform(source, importer, options) {
            ...省略代码
            //url='@vite/env'
            const normalizeUrl = async (url: string,pos: number): Promise<[string, string]> => {
                ...省略代码
                //importerFile='D:/workspace/vue3/vitelearn/node_modules/vite/dist/client/client.mjs'
                const resolved = await this.resolve(url, importerFile)
                ...省略代码
            }
            ...省略代码
        }
    }
}

看下this.resolve方法,this此时指向的是D:\workspace\vue3\vite-2.7.2\packages\vite\src\node\server\pluginContainer.ts的TransformContext对象,TransformContext继承自D:\workspace\vue3\vite-2.7.2\packages\vite\src\node\server\pluginContainer.ts的Context对象,看下Context的resolve方法

代码32
D:\workspace\vue3\vite-2.7.2\packages\vite\src\node\server\pluginContainer.ts



class Context implements PluginContext {
    ...省略代码
    //id='@vite/env'
    //importer='D:/workspace/vue3/vite-2.7.2/packages/vite/dist/client/client.mjs'
    //options=undefined
    async resolve(id: string, importer?: string, options?: { skipSelf?: boolean }) {
        let skip: Set<Plugin> | undefined
          if (options?.skipSelf && this._activePlugin) {
            skip = new Set(this._resolveSkips)
            skip.add(this._activePlugin)
          }
          let out = await container.resolveId(id, importer, { skip, ssr: this.ssr })
          if (typeof out === 'string') out = { id: out }
          return out as ResolvedId | null
    }
    ...省略代码
}

container.resolveId方法就与/@vite/client解析过程类似了,匹配上了D:\workspace\vue3\vite-2.7.2\node_modules\.pnpm\@rollup+plugin-alias@3.1.8_rollup@2.59.0\node_modules\@rollup\plugin-alias\dist\index.es.js插件的resolveId方法,将@vite/env转为D:\\workspace\\vue3\\vite-2.7.2\\packages\\vite\\dist\\client\\env.mjs,然后D:\\workspace\\vue3\\vite-2.7.2\\packages\\vite\\dist\\client\\env.mjs又匹配上了D:\workspace\vue3\vite-2.7.2\packages\vite\src\node\plugins\resolve.ts的resolvePlugin插件的resolveId方法,esolvePlugin插件的resolveId方法最后返回D:/workspace/vue3/vite-2.7.2/packages/vite/dist/client/env.mjs,回到代码31处,代码31处返回了D:/workspace/vue3/vite-2.7.2/packages/vite/dist/client/env.mjs,代码31处继续往下执行

代码33
D:\workspace\vue3\vite-2.7.2\packages\vite\src\node\plugins\importAnalysis.ts


const normalizeUrl = async (url: string, pos: number): Promise<[string, string]> => {
    ...省略代码
    //resolved={id:'D:/workspace/vue3/vite-2.7.2/packages/vite/dist/client/env.mjs'}
    const resolved = await this.resolve(url, importerFile)
    ...省略代码
    //root='D:/workspace/vue3/vitelearn'
    if (resolved.id.startsWith(root + '/')) {
          // in root: infer short absolute path from root
          ...省略代码
    } else if (fs.existsSync(cleanUrl(resolved.id))) {
          // exists but out of root: rewrite to absolute /@fs/ paths
          //url='/@fs/D:/workspace/vue3/vite-2.7.2/packages/vite/dist/client/env.mjs'
          url = path.posix.join(FS_PREFIX + resolved.id)
    } else {
          ...省略代码
    }
    ...省略代码
    if (!ssr) {
        ...省略代码
        try {
            const depModule = await moduleGraph.ensureEntryFromUrl(url)
            if (depModule.lastHMRTimestamp > 0) {
              url = injectQuery(url, `t=${depModule.lastHMRTimestamp}`)
            }
        } catch (e:any){
            ...省略代码
        }
        url = base + url.replace(/^\//, '')
    }
    return [url, resolved.id]
}

来看下moduleGraph.ensureEntryFromUrl方法

代码34
D:\workspace\vue3\vite-2.7.2\packages\vite\src\node\server\moduleGraph.ts




export class ModuleGraph {
    ...省略代码
    //rawUrl='/@fs/D:/workspace/vue3/vite-2.7.2/packages/vite/dist/client/env.mjs'
    async ensureEntryFromUrl(rawUrl: string): Promise<ModuleNode> {
        const [url, resolvedId, meta] = await this.resolveUrl(rawUrl)
        let mod = this.urlToModuleMap.get(url)
        if (!mod) {
          mod = new ModuleNode(url)
          if (meta) mod.meta = meta
          this.urlToModuleMap.set(url, mod)
          mod.id = resolvedId
          this.idToModuleMap.set(resolvedId, mod)
          const file = (mod.file = cleanUrl(resolvedId))
          let fileMappedModules = this.fileToModulesMap.get(file)
          if (!fileMappedModules) {
            fileMappedModules = new Set()
            this.fileToModulesMap.set(file, fileMappedModules)
          }
          fileMappedModules.add(mod)
        }
        return mod
    }
    ...省略代码
}

this.resolveUrl调用流程就与以上一样了,不重复分析了,最终匹配到了resolvePlugin插件的resolveId

代码35
D:\workspace\vue3\vite-2.7.2\packages\vite\src\node\plugins\resolve.ts



export function resolvePlugin(baseOptions: InternalResolveOptions): Plugin {
    ...省略代码
    return {
        name: 'vite:resolve',
        ...省略代码
        //id='/@fs/D:/workspace/vue3/vite-2.7.2/packages/vite/dist/client/env.mjs'
        //importer='D:\\workspace\\vue3\\vitelearn\\index.html'
        resolveId(id, importer, resolveOpts) {
            ...省略代码
            //asSrc=true
            if (asSrc && id.startsWith(FS_PREFIX)) {
                //fsPath='D:/workspace/vue3/vite-2.7.2/packages/vite/dist/client/env.mjs'
                const fsPath = fsPathFromId(id)
                //res='D:/workspace/vue3/vite-2.7.2/packages/vite/dist/client/env.mjs'
                res = tryFsResolve(fsPath, options)
                isDebug && debug(`[@fs] ${chalk.cyan(id)} -> ${chalk.dim(res)}`)
                // always return here even if res doesn't exist since /@fs/ is explicit
                // if the file doesn't exist it should be a 404
                return res || fsPath
            }
            ...省略代码
        }
    }
}

 回到代码33,代码继续往下执行

代码36
D:\workspace\vue3\vite-2.7.2\packages\vite\src\node\plugins\importAnalysis.ts


const normalizeUrl = async (url: string, pos: number): Promise<[string, string]> => {
    ...省略代码
    //resolved={id:'D:/workspace/vue3/vite-2.7.2/packages/vite/dist/client/env.mjs'}
    const resolved = await this.resolve(url, importerFile)
    ...省略代码
    //root='D:/workspace/vue3/vitelearn'
    if (resolved.id.startsWith(root + '/')) {
          // in root: infer short absolute path from root
          ...省略代码
    } else if (fs.existsSync(cleanUrl(resolved.id))) {
          // exists but out of root: rewrite to absolute /@fs/ paths
          //url='/@fs/D:/workspace/vue3/vite-2.7.2/packages/vite/dist/client/env.mjs'
          url = path.posix.join(FS_PREFIX + resolved.id)
    } else {
          ...省略代码
    }
    ...省略代码
    if (!ssr) {
        ...省略代码
        try {
            const depModule = await moduleGraph.ensureEntryFromUrl(url)
            if (depModule.lastHMRTimestamp > 0) {
              url = injectQuery(url, `t=${depModule.lastHMRTimestamp}`)
            }
        } catch (e:any){
            ...省略代码
        }
        //base='/'
        //url='/@fs/D:/workspace/vue3/vite-2.7.2/packages/vite/dist/client/env.mjs'
        url = base + url.replace(/^\//, '')
    }
    //resolve.id='D:/workspace/vue3/vite-2.7.2/packages/vite/dist/client/env.mjs'
    return [url, resolved.id]
}

 回到代码30,代码继续往下执行

代码37
D:\workspace\vue3\vite-2.7.2\packages\vite\src\node\plugins\importAnalysis.ts


export function importAnalysisPlugin(config: ResolvedConfig): Plugin {
    ...省略代码
    return {
        name: 'vite:import-analysis',
        async transform(source, importer, options) {
            ...省略代码
            const importerModule = moduleGraph.getModuleById(importer)!
            ...省略代码
            for (let index = 0; index < imports.length; index++) {
                ...省略代码
                //rawUrl='@vite/env'
                const rawUrl = source.slice(start, end)
                ...省略代码
                //specifier='@vite/env'
                if (specifier) {
                    ...省略代码
                    //normalizedUrl='/@fs/D:/workspace/vue3/vite-2.7.2/packages/vite/dist/client/env.mjs'
                    //resolvedId='D:/workspace/vue3/vite-2.7.2/packages/vite/dist/client/env.mjs'
                    const [normalizedUrl, resolvedId] = await normalizeUrl(specifier,start)
                    ...省略代码
                    if (url !== specifier) {
                        if (resolvedId.endsWith(`&es-interop`)) {
                            ...省略代码
                        } else {
                            //对D:\workspace\vue3\vitelearn\node_modules\vite\dist\client\client.mjs
                            //源文件中的import '@vite/env';进行改写,
                            //改为import '/@fs/D:/workspace/vue3/vite-2.7.2/packages/vite/dist/client/env.mjs'
                            str().overwrite(start, end, isDynamicImport ? `'${url}'` : url)
                        }
                    }
                    ...省略代码
                }
            }
            ...省略代码
            //importer='D:/workspace/vue3/vite-2.7.2/packages/vite/dist/client/client.mjs'
            if (!isCSSRequest(importer)) {
                ...省略代码
            }
            //staticImportedUrls=Set<>("/@fs/D:/workspace/vue3/vite-2.7.2/packages/vite/dist/client/env.mjs")
            if (staticImportedUrls.size) {
                staticImportedUrls.forEach((url) => {
                  //异步的将每个依赖都进行transformRequest加载对应依赖的源码
                  transformRequest(unwrapId(removeImportQuery(url)), server, { ssr })
                })
            }
            if (s) {
                //返回经过改写的代码给浏览器
                return s.toString()
            } else {
                return source
            }
        }
    }
}

异步的对D:\workspace\vue3\vitelearn\node_modules\vite\dist\client\client.mjs文件中引入的依赖进行transformRequest,将经过改写的D:\workspace\vue3\vitelearn\node_modules\vite\dist\client\client.mjs的文件的内容同步的返回给浏览器。 分析过程与url='/@vite/client'的过程一样,只是此时url='/@fs/D:/workspace/vue3/vite-2.7.2/packages/vite/dist/client/env.mjs',匹配到D:\workspace\vue3\vite-2.7.2\packages\vite\src\node\plugins\resolve.ts插件的resolveId方法,返回url='/@fs/D:/workspace/vue3/vite-2.7.2/packages/vite/dist/client/env.mjs'的路径为:D:/workspace/vue3/vite-2.7.2/packages/vite/dist/client/env.mjs,然后调用pluginContainer的load方法加载,没有匹配到任何插件的load方法,然后用node的fs读取D:/workspace/vue3/vite-2.7.2/packages/vite/dist/client/env.mjs文件的源码,然后去transform env.mjs文件的内容,进行transform时匹配到了D:\workspace\vue3\vite-2.7.2\packages\vite\src\node\plugins\clientInjections.ts的clientInjectionsPlugin插件的transform方法,对env.mjs文件的内容进行一些变量的替换,然后又匹配到D:\workspace\vue3\vite-2.7.2\packages\vite\src\node\plugins\importAnalysis.ts的importAnalysisPlugin插件的transform对env.mjs文件进行依赖分析,发下env.mjs文件并没有依赖任何文件,至此index.html去请求/@vite/client,以及/@vite/client所依赖的依赖都解析结束,index.html请求/@vite/client的同时还请求了/src/main.ts,来看下请求/src/main.ts的流程,transform时匹配到了D:\workspace\vue3\vite-2.7.2\packages\vite\src\node\plugins\esbuild.ts的esbuildPlugin插件的transform方法,对/src/main.ts进行esbuild得到其源码跟sourcemap,然后匹配到D:\workspace\vue3\vite-2.7.2\packages\vite\src\node\plugins\importAnalysis.ts的importAnalysisPlugin插件的transform方法,对/src/main.ts进行依赖分析,发现/src/main.ts文件依赖了vue与./App.vue两个文件,D:\workspace\vue3\vite-2.7.2\packages\vite\src\node\plugins\importAnalysis.ts的importAnalysisPlugin插件的transform方法对/src/main.ts的内容进行修改,改为

import { createApp } from "/node_modules/.vite/vue.js?v=c86b8604";
import App from "/src/App.vue";
createApp(App).mount("#app");

返回给浏览器,然后在异步的解析/src/main.ts依赖的依赖,解析过程与以上就一样了。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值