小白也能读懂的vite源码系列——vite 中间件处理(二)

895 阅读17分钟

前言

上一章我们讲到了vite 开发模式下,启动到打开浏览器的过程,要想编写的代码能够正常的访问,vite 期间做了很多事情,本章主要讲解vite中间件,下面我们来介绍vite 中比较重要的 几个中间件。

1. 目录

下面这段代码为 createServer 过程中,vite 所使用的一些中间件,vite 使用的 connect 来作为开发

const middlewares = connect() as Connect.Server


// Internal middlewares ------------------------------------------------------

  // request timer
  if (process.env.DEBUG) {
    middlewares.use(timeMiddleware(root))
  }

  // cors (enabled by default)
  const { cors } = serverConfig
  if (cors !== false) {
    middlewares.use(corsMiddleware(typeof cors === 'boolean' ? {} : cors))
  }

  middlewares.use(cachedTransformMiddleware(server))

  // proxy
  const { proxy } = serverConfig
  if (proxy) {
    const middlewareServer =
      (isObject(middlewareMode) ? middlewareMode.server : null) || httpServer
    middlewares.use(proxyMiddleware(middlewareServer, proxy, config))
  }

  // base
  if (config.base !== '/') {
    middlewares.use(baseMiddleware(config.rawBase, !!middlewareMode))
  }

  // open in editor support
  middlewares.use('/__open-in-editor', launchEditorMiddleware())

  // ping request handler
  // Keep the named function. The name is visible in debug logs via `DEBUG=connect:dispatcher ...`
  middlewares.use(function viteHMRPingMiddleware(req, res, next) {
    if (req.headers['accept'] === 'text/x-vite-ping') {
      res.writeHead(204).end()
    } else {
      next()
    }
  })

  // serve static files under /public
  // this applies before the transform middleware so that these files are served
  // as-is without transforms.
  if (publicDir) {
    middlewares.use(servePublicMiddleware(server, publicFiles))
  }

  // main transform middleware
  middlewares.use(transformMiddleware(server))

  // serve static files
  middlewares.use(serveRawFsMiddleware(server))
  middlewares.use(serveStaticMiddleware(server))

  // html fallback
  if (config.appType === 'spa' || config.appType === 'mpa') {
    middlewares.use(
      htmlFallbackMiddleware(
        root,
        config.appType === 'spa',
        getFsUtils(config),
      ),
    )
  }

  // run post config hooks
  // This is applied before the html middleware so that user middleware can
  // serve custom content instead of index.html.
  postHooks.forEach((fn) => fn && fn())

  if (config.appType === 'spa' || config.appType === 'mpa') {
    // transform index.html
    middlewares.use(indexHtmlMiddleware(root, server))

    // handle 404s
    middlewares.use(notFoundMiddleware())
  }

  // error handler
  middlewares.use(errorMiddleware(server, !!middlewareMode))

vite 用到了以下中间件:

  • timeMiddleware
  • corsMiddleware
  • cachedTransformMiddleware
  • proxyMiddleware
  • baseMiddleware
  • launchEditorMiddleware
  • viteHMRPingMiddleware
  • servePublicMiddleware
  • transformMiddleware
  • serveRawFsMiddleware
  • serveStaticMiddleware
  • htmlFallbackMiddleware
  • indexHtmlMiddleware
  • notFoundMiddleware

cachedTransformMiddleware 缓存转换中间件,处理缓存的转换,以提高性能,这里先不讲,放到预构建依赖那一章讲解

本文主要讲解:

  • servePublicMiddleware 静态文件服务
  • transformMiddleware 文件转换中间件
  • serveRawFsMiddleware 静态文件服务
  • serveStaticMiddleware 静态文件服务
  • htmlFallbackMiddleware  html 回退中间件
  • indexHtmlMiddleware index.html 转换中间件

2. connect

connect 是一个基于 Node.js 的中间件框架,常用于创建 HTTP 服务器。Vite 使用 connect 作为其开发服务器的基础,用来处理请求和响应

connect 的功能和作用:

  1. 中间件机制:connect 提供了一种机制,可以将一系列中间件函数按顺序组合起来处理 HTTP 请求。每个中间件函数可以处理请求、响应或者将控制权交给下一个中间件。
  2. 简洁的 API:connect 提供了一组简洁的 API,用于添加、删除和处理中间件。
  3. 兼容性强:connect 与 express 等流行的 Node.js 框架兼容,可以与多种中间件库一起使用。

3. servePublicMiddleware

这个用于处理 Vite 开发服务器上公共目录的中间件

出现在vite/src/node/server/index.ts 中的_createServer中

  // 在 /public 目录下提供静态文件服务
  // 这适用于转换中间件之前,这样这些文件就可以按原样提供而不需要转换。
  if (publicDir) {
    middlewares.use(servePublicMiddleware(server, publicFiles));
  }

这里使用sirv来提供静态文件服务。源码位置server/middlewares/static.ts

export function servePublicMiddleware(
  server: ViteDevServer,
  publicFiles?: Set<string>
): Connect.NextHandleFunction {
  const dir = server.config.publicDir;
  const serve = sirv(
    dir,
    sirvOptions({
      getHeaders: () => server.config.server.headers,
    })
  );

  // 用于将请求的 URL 转换为文件路径
  const toFilePath = (url: string) => {
    let filePath = cleanUrl(url);
    // 如果 URL 包含百分号编码(%),尝试解码它
    if (filePath.indexOf("%") !== -1) {
      try {
        filePath = decodeURI(filePath);
      } catch (err) {
        /* malform uri */
      }
    }
    return normalizePath(filePath);
  };

  return function viteServePublicMiddleware(req, res, next) {
    // 这是实际的中间件函数,它被返回并用于处理请求
    /**
     * 为了避免' existsSync '对每个请求的性能影响,我们检查内存中已知的公共文件集。
     * 该集合在重启时更新。也要跳过导入请求和内部请求' /@fs/ /@vite-client '等…
     */
    if (
      // 检查 publicFiles 集合是否存在,并且请求的文件路径是否在该集合中。如果不在集合中,则调用 next() 跳过此中间件
      (publicFiles && !publicFiles.has(toFilePath(req.url!))) ||
      // 检查请求是否为导入请求(isImportRequest)
      isImportRequest(req.url!) ||
      // 是否为内部请求
      isInternalRequest(req.url!)
    ) {
      // 如果是,则调用 next() 跳过此中间件
      return next();
    }

    // 调用 serve 函数处理请求,提供 publicDir 目录中的静态文件
    serve(req, res, next);
  };
}

sirv 是一个用于提供静态文件服务器的库,支持多种配置和优化,适用于开发和生产环境

  • 高性能sirv 经过优化,可以快速响应对静态文件的请求。
  • 易于使用:只需要几行代码就可以启动一个静态文件服务器。
  • 支持缓存控制:可以配置缓存策略,以提高静态资源的加载速度。
  • gzip 和 brotli 压缩:支持对静态文件进行压缩,以减少传输的文件大小。

4. serveRawFsMiddleware

这个用于处理特定路径前缀的请求,并从文件系统的根目录提供这些文件。 这在某些情况下很有用,比如在链接的 monorepo 项目中,需要访问根目录之外的文件

import /@fs/..../package/vite/dist/client/env.mjs 这种导入请求就是在这里做的处理

export function serveRawFsMiddleware(
  server: ViteDevServer
): Connect.NextHandleFunction {
  // sirv 是一个静态文件服务器中间件
  //  "/" 表示从根目录开始提供服务
  const serveFromRoot = sirv(
    "/",
    sirvOptions({ getHeaders: () => server.config.server.headers })
  );

  // 保留命名函数。这个名字可以通过“debug =connect:dispatcher…”在调试日志中看到。
  // 返回中间件函数
  return function viteServeRawFsMiddleware(req, res, next) {
    // 这是实际的中间件函数,处理传入的请求

    // 解析请求的 URL,将双斜杠替换为单斜杠,避免路径错误
    const url = new URL(req.url!.replace(/^\/{2,}/, "/"), "http://example.com");
    /**
     * 在某些情况下(例如链接的单节点),根目录之外的文件将引用同样不在服务根目录中的资产。
     * 在这种情况下,路径被重写为' /@fs/ '前缀路径,并且必须通过基于fs根的搜索来提供。
     */

    // 检查 URL 路径是否以 FS_PREFIX 开头,这是一个特殊前缀,用于标识需要从文件系统根目录提供的文件
    if (url.pathname.startsWith(FS_PREFIX)) {
      // 解码 URL 路径
      const pathname = decodeURI(url.pathname);
      //限制' fs.allow '之外的文件

      // 确保文件的路径在允许的范围内,如果不允许访问,则终止请求处理
      if (
        !ensureServingAccess(
          slash(path.resolve(fsPathFromId(pathname))),
          server,
          res,
          next
        )
      ) {
        return;
      }

      // 去掉前缀 FS_PREFIX
      let newPathname = pathname.slice(FS_PREFIX.length);
      // 处理 Windows 系统的路径,将驱动器号去掉,如 C: 或 D:  将 C:\path\to\file 变为 \path\to\file
      if (isWindows) newPathname = newPathname.replace(/^[A-Z]:/i, "");

      // 更新 URL 路径
      url.pathname = encodeURI(newPathname);
      /**
       * 更新请求路径,使其仅包含路径和查询参数部分,而不包含协议和主机名
       *
       * url.href 返回整个 URL 字符串。例如,http://example.com/path/to/file
       * url.origin 返回 URL 的协议和主机名部分。例如,http://example.com
       *
       * 通过slice 提取子字符串,起始位置为 url.origin.length,即从主机名之后的部分开始,
       * 结果字符串为 /path/to/file,即仅包含路径和查询参数部分
       *
       * 假设初始 URL 为 http://example.com/@fs/C:/path to/file:
       * newPathname = '/path to/file',去除了驱动器号
       * url.pathname = encodeURI('/path to/file') 结果为 /path%20to/file,空格被编码为 %20
       * 最终请求 URL 为 /@fs/path%20to/file,确保路径被正确编码和理解
       */
      req.url = url.href.slice(url.origin.length); //以便后续中间件能够正确处理请求
      // 提供文件
      serveFromRoot(req, res, next);
    } else {
      // 如果 URL 路径不以 FS_PREFIX 开头,直接调用 next 处理下一个中间件
      next();
    }

    // 这个中间件的作用是处理带有特定前缀的请求,通过文件系统根目录提供相应的文件
  };
}

5. serveStaticMiddleware

这个用于在 Vite 开发服务器中提供静态文件服务,并对请求路径进行了一些处理和检查

export function serveStaticMiddleware(
  server: ViteDevServer
): Connect.NextHandleFunction {
  const dir = server.config.root;
  // 提供静态文件服务
  const serve = sirv(
    dir,
    sirvOptions({
      getHeaders: () => server.config.server.headers,
    })
  );

  // 保留命名函数。这个名字可以通过“debug =connect:dispatcher…”在调试日志中看到。
  return function viteServeStaticMiddleware(req, res, next) {
    // 这是实际的中间件函数,处理传入的请求

    /**
     * 只有当它不是HTML请求或以' / '结尾时才提供文件,
     * 这样HTML请求可以通过我们的HTML中间件进行特殊处理,也可以跳过内部请求' /@fs/ /@vite-client '等
     *
     * HTML 请求通常需要经过特殊处理(例如注入脚本),因此需要跳过静态文件处理,交给专门处理 HTML 请求的中间件
     * 而以 / 结尾的请求通常表示目录请求,需要进一步处理或重定向
     * 内部请求是 Vite 自身使用的模块或功能,不应通过静态文件中间件处理,应该交给其他专门的中间件或处理逻辑
     */

    // 去掉请求 URL 中的查询参数和哈希部分
    const cleanedUrl = cleanUrl(req.url!);
    if (
      cleanedUrl[cleanedUrl.length - 1] === "/" ||
      path.extname(cleanedUrl) === ".html" ||
      isInternalRequest(req.url!)
    ) {
      // 如果 URL 以 / 结尾,或者是一个 .html 文件,或者是内部请求,
      // 则直接跳过静态文件处理,调用 next() 进入下一个中间件
      return next();
    }

    // 将请求 URL 转换为标准 URL 对象,并解码路径名
    const url = new URL(req.url!.replace(/^/{2,}/, "/"), "http://example.com");
    const pathname = decodeURI(url.pathname);

    // 对静态请求也应用别名
    let redirectedPathname: string | undefined;

    // 根据 Vite 配置中的别名规则,对请求路径进行替换,支持字符串匹配和正则表达式匹配
    for (const { find, replacement } of server.config.resolve.alias) {
      const matches =
        typeof find === "string"
          ? pathname.startsWith(find)
          : find.test(pathname);
      if (matches) {
        redirectedPathname = pathname.replace(find, replacement);
        break;
      }
    }

    if (redirectedPathname) {
      // 检查它是否以 dir 结尾的斜杠形式开头 dir 是根目录
      if (redirectedPathname.startsWith(withTrailingSlash(dir))) {
        // 截取为不包含 dir 的路径
        redirectedPathname = redirectedPathname.slice(dir.length);
      }
    }

    // 解析完成的路径名
    const resolvedPathname = redirectedPathname || pathname;

    // 构建文件路径:将 dir 和去掉开头斜杠的 resolvedPathname 组合成完整的文件路径
    let fileUrl = path.resolve(dir, removeLeadingSlash(resolvedPathname));

    // 处理路径结尾斜杠:
    if (
      resolvedPathname[resolvedPathname.length - 1] === "/" &&
      fileUrl[fileUrl.length - 1] !== "/"
    ) {
      // 如果请求的路径以斜杠结尾,并且构建的 fileUrl 不以斜杠结尾,则给构建的路径添加斜杠
      fileUrl = withTrailingSlash(fileUrl);
    }

    // 检查访问权限:检查请求的 fileUrl 是否可以被访问。如果无法访问,则返回
    if (!ensureServingAccess(fileUrl, server, res, next)) {
      return;
    }

    // 更新请求对象 req 的 url 属性为经过编码后 redirectedPathname
    if (redirectedPathname) {
      url.pathname = encodeURI(redirectedPathname);
      req.url = url.href.slice(url.origin.length);
    }

    // 最终调用 serve 函数来处理静态文件的请求
    serve(req, res, next);
  };
}

6. htmlFallbackMiddleware

这个用于处理 Vite 项目中的 HTML 文件请求,其主要功能是在处理不同路径请求时,检查对应的 HTML 文件是否存在,并进行相应的路径重写。这个中间件很重要,经过这个中间件处理后,后一个中间件 indexHtmlMiddleware才能去处理根目录下面的index.html

// html 回退,为单页应用(SPA)或多页应用(MPA)提供 HTML 回退功能
  if (config.appType === "spa" || config.appType === "mpa") {
    middlewares.use(
      htmlFallbackMiddleware(root, config.appType === "spa", getFsUtils(config))
    );
  }
export function htmlFallbackMiddleware(
  root: string,
  spaFallback: boolean,
  fsUtils: FsUtils = commonFsUtils
): Connect.NextHandleFunction {
  // Keep the named function. The name is visible in debug logs via `DEBUG=connect:dispatcher ...`
  return function viteHtmlFallbackMiddleware(req, res, next) {
    if (
      // 该中间件只处理 GET 和 HEAD 请求,其他方法的请求会直接调用 next() 跳过此中间件。
      (req.method !== "GET" && req.method !== "HEAD") ||
      // 请求路径为 /favicon.ico 的请求会直接跳过此中间件
      req.url === "/favicon.ico" ||
      // 该中间件要求 Accept 头部包含 text/html 或 */*,否则请求会被跳过。
      !(
        req.headers.accept === undefined || // equivalent to `Accept: */*`
        req.headers.accept === "" || // equivalent to `Accept: */*`
        req.headers.accept.includes("text/html") ||
        req.headers.accept.includes("*/*")
      )
    ) {
      return next();
    }

    // 对请求路径进行清理和解码,以便后续处理
    const url = cleanUrl(req.url!);
    const pathname = decodeURIComponent(url);

    // .html文件不被serveStaticMiddleware处理,所以我们需要检查该文件是否存在
    if (pathname.endsWith(".html")) {
      const filePath = path.join(root, pathname);
      if (fsUtils.existsSync(filePath)) {
        // 存在则重写请求路径
        debug?.(`Rewriting ${req.method} ${req.url} to ${url}`);
        req.url = url;
        return next();
      }
    }
    // 尾斜杠应该检查是否有回退index.html
    else if (pathname[pathname.length - 1] === "/") {
      // 末尾为 "/" 的,统一添加 index.html
      const filePath = path.join(root, pathname, "index.html");
      if (fsUtils.existsSync(filePath)) {
        // 存在的话,重写路径,这里访问"/" 目录的时候,会走到这里,从而添加index.html
        // 被后面的中间件去处理
        const newUrl = url + "index.html";
        debug?.(`Rewriting ${req.method} ${req.url} to ${newUrl}`);
        req.url = newUrl;
        return next();
      }
    }
    // 非尾随斜杠应该检查是否有回退 .html
    else {
      // 非尾随斜杠的会添加.html 后缀,文件存在后重写后,调用next() 交给下一个中间件
      const filePath = path.join(root, pathname + ".html");
      if (fsUtils.existsSync(filePath)) {
        const newUrl = url + ".html";
        debug?.(`Rewriting ${req.method} ${req.url} to ${newUrl}`);
        req.url = newUrl;
        return next();
      }
    }

    // 以上的情况都不满足,启用单页面应用的回退处理,将所有未匹配的路径重写为 /index.html
    if (spaFallback) {
      debug?.(`Rewriting ${req.method} ${req.url} to /index.html`);
      req.url = "/index.html";
    }

    next();
  };
}

debug调试过程

当vite 启动完成,打开浏览器后,会访问根路径“/”,会进入到这个中间件,我们来看这个中间件的处理流程

image.png

我们可以看到,目前请求访问的是 /,这里会清理url,保证干净的url,以便后续的处理

//匹配 URL 中以 ? 或 # 开头的字符序列,包括 ? 或 # 本身及其后面的任意字符
const postfixRE = /[?#].*$/;
//用于清理 URL,移除其末尾的查询字符串和哈希部分。
export function cleanUrl(url: string): string {
  return url.replace(postfixRE, "");
}

image.png

因为不是.html 结尾,所以会走到这一步,执行这一块的逻辑

image.png

经过这一块的逻辑处理后,我们会发现req.url 变成了 /index.html,这样的话,就会去访问项目根目录下面的index.html,现在知道为什么vite需要把index.html 放在根目录下面了吧,因为经过这个中间件的处理,会把这个html文件返回给浏览器,但是在返回给浏览器之前,还需要经过一个中间件的处理,那就是indexHtmlMiddleware 这个中间件

7. indexHtmlMiddleware

这个用于在 Vite 开发服务器或预览服务器上处理请求,特别是处理 .html 文件的请求。

//对于单页应用(SPA)或多页应用(MPA),提供 index.html 的转换和 404 错误处理。
  if (config.appType === "spa" || config.appType === "mpa") {
    // transform index.html
    middlewares.use(indexHtmlMiddleware(root, server));
    // handle 404s
    middlewares.use(notFoundMiddleware());
  }
export function indexHtmlMiddleware(
  root: string,
  server: ViteDevServer | PreviewServer
): Connect.NextHandleFunction {
  // 判断服务器是否处于开发模式
  const isDev = isDevServer(server);
  // 获取文件系统工具,主要用于检查文件是否存在
  const fsUtils = getFsUtils(server.config);

  // 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) {
      // 如果响应已经结束,则直接调用 next 传递给下一个中间件
      return next();
    }

    const url = req.url && cleanUrl(req.url);

    // htmlFallbackMiddleware appends '.html' to URLs
    // 这个中间件很关键,添加完 html 后缀后,可以进入到下面的处理,从而解析根目录下的index.html 并发送给客户端
    if (url?.endsWith(".html") && req.headers["sec-fetch-dest"] !== "script") {
      let filePath: string;

      // 确定文件路径
      if (isDev && url.startsWith(FS_PREFIX)) {
        // 如果是FS_PREFIX开头的,则调用fsPathFromId 去除前缀
        filePath = decodeURIComponent(fsPathFromId(url));
      } else {
        // 将 URL 解码并与 root 拼接来确定文件路径
        filePath = path.join(root, decodeURIComponent(url));
      }

      // 检查文件是否存在并读取内容
      if (fsUtils.existsSync(filePath)) {
        // 根据模式的不同,选择的不同的请求头
        const headers = isDev
          ? server.config.server.headers
          : server.config.preview.headers;

        try {
          // 读取文件内容
          let html = await fsp.readFile(filePath, "utf-8");
          if (isDev) {
            // 开发模式下 调用transformIndexHtml 转换html 内容
            html = await server.transformIndexHtml(url, html, req.originalUrl);
          }

          // 通过 send 函数将 HTML 内容发送给客户端
          return send(req, res, html, "html", { headers });
        } catch (e) {
          // 出错的话,调用next 传递给下一个中间件
          return next(e);
        }
      }
    }
    next();
  };
}

debug 调试

经过html 回退中间件处理后,req.url 变成了 /index.html,同时也来到了这个中间件处理,专门用于处理index.html image.png

可以看到req.url = /index.html image.png

进去下面逻辑进行处理,橙色框圈出来的地方很重要!在开发模式下,对html进行转换,然后返回给浏览器 image.png

// 将 URL 解码并与 root 拼接来确定文件路径
filePath = path.join(root, decodeURIComponent(url));

这样就将filePath 拼接为项目根目录下面的index.html

image.png

我们来看看 server.transformIndexHtml 这个函数到底做了什么,这个是vite 实例身上的一个方法。可以看到内部其实调用了devHtmlTransformFn,我们接着看

transformIndexHtml(url, html, originalUrl) {
    return devHtmlTransformFn(server, url, html, originalUrl);
},

在创建server 的时候初始化,再来看createDevHtmlTransformFn image.png

createDevHtmlTransformFn

在 Vite 中用于创建一个开发环境下的 HTML 转换函数,这个函数会根据配置的插件顺序和一些内置的转换钩子,对 HTML 内容进行一系列的处理和修改

export function createDevHtmlTransformFn(
  config: ResolvedConfig
): (
  server: ViteDevServer,
  url: string,
  html: string,
  originalUrl?: string
) => Promise<string> {
  // 解析 HTML 转换钩子
  const [preHooks, normalHooks, postHooks] = resolveHtmlTransforms(
    config.plugins,
    config.logger
  );

  // 定义一系列用于转换 HTML 的钩子
  const transformHooks = [
    // 在 HTML 中导入 map 之前的预处理钩子
    preImportMapHook(config),
    // 注入 CSP (Content Security Policy) nonce 元标签的钩子
    injectCspNonceMetaTagHook(config),
    // 用户插件的预处理钩子
    ...preHooks,
    // 处理环境变量相关的钩子
    htmlEnvHook(config),
    // 开发环境下的 HTML 处理钩子
    devHtmlHook,
    // 用户插件的正常处理钩子
    ...normalHooks,
    // 用户插件的后处理钩子
    ...postHooks,
    // 注入 nonce 属性标签的钩子
    injectNonceAttributeTagHook(config),
    // 在 HTML 中导入 map 之后的后处理钩子
    postImportMapHook(),
  ];

  // 返回转换函数
  return (
    server: ViteDevServer,
    url: string,
    html: string,
    originalUrl?: string
  ): Promise<string> => {
    // 应用所有的转换钩子
    return applyHtmlTransforms(html, transformHooks, {
      path: url,
      filename: getHtmlFilename(url, server),
      server,
      originalUrl,
    });
  };
}

applyHtmlTransforms

用于应用一系列 HTML 转换钩子来处理 HTML 内容,将html文件处理完成之后返回

export async function applyHtmlTransforms(
  html: string,
  hooks: IndexHtmlTransformHook[],
  ctx: IndexHtmlTransformContext
): Promise<string> {
  // 循环遍历每个钩子并应用转换
  for (const hook of hooks) {
    const res = await hook(html, ctx);
    if (!res) {
      continue;
    }
    if (typeof res === "string") {
      // 如果钩子返回一个字符串,将其作为新的 html
      html = res;
    } else {
      // 如果钩子返回的是一个对象或数组,进行进一步处理
      let tags: HtmlTagDescriptor[];
      if (Array.isArray(res)) {
        // 如果返回值是数组,则认为它是包含标签的数组
        tags = res;
      } else {
        // 如果返回值是对象,提取 html 和 tags 属
        html = res.html || html;
        tags = res.tags;
      }

      // 分类标签并注入到相应位置
      let headTags: HtmlTagDescriptor[] | undefined;
      let headPrependTags: HtmlTagDescriptor[] | undefined;
      let bodyTags: HtmlTagDescriptor[] | undefined;
      let bodyPrependTags: HtmlTagDescriptor[] | undefined;

      for (const tag of tags) {
        // 根据 injectTo 属性将标签分类存储到不同的数组中
        switch (tag.injectTo) {
          case "body":
            (bodyTags ??= []).push(tag);
            break;
          case "body-prepend":
            (bodyPrependTags ??= []).push(tag);
            break;
          case "head":
            (headTags ??= []).push(tag);
            break;
          default:
            (headPrependTags ??= []).push(tag);
        }
      }

      // 用于检查和处理需要插入到 <head> 中的标签
      headTagInsertCheck(
        [...(headTags || []), ...(headPrependTags || [])],
        ctx
      );

      // 用于将标签插入到 HTML 的相应位置
      if (headPrependTags) html = injectToHead(html, headPrependTags, true);
      if (headTags) html = injectToHead(html, headTags);
      if (bodyPrependTags) html = injectToBody(html, bodyPrependTags, true);
      if (bodyTags) html = injectToBody(html, bodyTags);
    }
  }

  // 通过这个函数,Vite 可以灵活地处理 HTML 文件,注入脚本、样式等资源,
  // 以实现插件系统和各种自定义功能。

  return html;
}

经过这个钩子处理后,页面上能够正常的显现出index.html 文件的内容

image.png

但是我们发现红色框框里面的东西并没有生效,这是为什么呢?

眼尖的同学发现了index.html 多了一些东西 , <script type="module" src="/@vite/client"></script>,这是注入的热更新代码,是在devHtmlHook 这个钩子中执行的,还记得indexHtmlMiddleware 这个中间件的代码吗

image.png

就是通过这里注入的 image.png

这个以后讲热更新时候会再次提到,我们发现index.html 中引入了 /src/main.ts,所以就会有一个get请求,去请求相应的资源到浏览器 image.png

这里我们可以是这样导入的vue,这样浏览器是不认识的,所以会报错

image.png

这里会有两个错误,网站图标的错误请忽略,浏览器需要我们提供相对路径或者绝对路径,而不是像import { createApp} from vue 这种裸导入

image.png

上面的错误同样是因为注入了热更新的代码

image.png

要解决这个错误,涉及到几个方面:

  1. 预构建依赖
  2. 导入替换(importAnalysis 这个插件来完成的)

还记得我们本章需要讲解的几个中间件吗,这里还剩一个没有提到,就是用来处理这种的中间件

8. transformMiddleware

主要的文件转换中间件,这个中间件很重要,访问的文件都是通过这个中间件去做转换。 比如 /src/main.ts 中涉及到 import {createApp} from 'vue',就是通过这个中间件调用 transform 方法做转换,实际的转换发生在 importAnaysis 这个插件里面做的转换

之前的报错,都是通过这个中间件来做处理的

image.png

image.png

这里最主要的步骤就是通过调用 transformRequest 来对文件进行处理,并将处理完成的文件返回给浏览器

image.png

这里需要预构建依赖,通过 initDepsOptimizer 来完成,在创建vite 实例种的 initServer 方法种

image.png

还需要通过导入分析插件来完成代码的替换,resolvePlugins 这个函数在创建vite 实例的时候执行 image.png

导入分析插件,源码位置vite/src/node/plugins/importAnalysis.ts

image.png

主要就是在 transform 这个钩子做的处理,这里给大家看一下

image.png

这里需要的前置知识很多,所以这里中间件不作过多讲解,等完成后续的 预构建依赖和导入分析插件后,再来完成。给大家看看完成这些之后的代码会变成什么样子

import { createApp } from 'vue'

// 会变成
import { createApp } from '/node_modules/.vite/deps/vue.js?v=6cd8be50'

image.png

这样浏览器就能够正常寻找到文件,/node_modules/.vite/deps 就是存在依赖的地方,这个目录可以通过传入配置项更改,这里做了缓存。后面拼接?v=6cd8be50 是为了能够获取到最新的依赖版本 image.png

import '@vite/env'
// 变成
import "/@fs/C:/Users/.../packages/vite/dist/client/env.mjs";

image.png

可以发现这里的路径很奇怪,以@fs 开头的,还记得开头的中间件吗,没错就是 serverRawFsMiddleware,它就是专门用来处理@fs 开头的请求

结语

看到这里相信你对vite也有一个大概的了解,后面的文章,我们会慢慢揭开vite的面纱

源码系列文章: