# HTML中间件

indexHtmlMiddleware中间件匹配html请求,返回转义处理后的html

export function indexHtmlMiddleware(
  server: ViteDevServer
): Connect.NextHandleFunction {
  return async function viteIndexHtmlMiddleware(req, res, next) {
    const url = req.url && cleanUrl(req.url)
    if (url?.endsWith('.html') && req.headers['sec-fetch-dest'] !== 'script') {
      const filename = getHtmlFilename(url, server)
      if (fs.existsSync(filename)) {
        try {
          let html = fs.readFileSync(filename, 'utf-8')
          html = await server.transformIndexHtml(url, html, req.originalUrl)
          return send(req, res, html, 'html')
        } catch (e) {
          return next(e)
        }
      }
    }
    next()
  }
}
export async function traverseHtml(
  html: string,
  filePath: string,
  visitor: NodeTransform
): Promise<void> {
  const { parse, transform } = await import('@vue/compiler-dom')
  // @vue/compiler-core doesn't like lowercase doctypes
  html = html.replace(/<!doctype\s/i, '<!DOCTYPE ')
  const ast = parse(html, { comments: true })
  transform(ast, {
    nodeTransforms: [visitor]
  })
}

traverseHtml中,引用'@vue/compiler-dom'parse解析html ast,ast例如下图所示。visitor函数中定义了ast节点的转换规则,transform最终会应用这些规则 html-ast

const devHtmlHook: IndexHtmlTransformHook = async (
  html,
  { path: htmlPath, server, originalUrl }
) => {
  const config = server?.config!
  const base = config.base || '/'
  const s = new MagicString(html)
  let scriptModuleIndex = -1
  await traverseHtml(html, htmlPath, (node) => {
    if (node.type !== NodeTypes.ELEMENT) {
      return
    }
    // script tags
    if (node.tag === 'script') {
      const { src, isModule } = getScriptInfo(node)
      if (isModule) {
        scriptModuleIndex++
      }
      if (src) {
        processNodeUrl(src, s, config, htmlPath, originalUrl)
      } else if (isModule) {
        s.overwrite(
          node.loc.start.offset,
          node.loc.end.offset,
          `<script type="module" src="${
            config.base + htmlPath.slice(1)
          }?html-proxy&index=${scriptModuleIndex}.js"></script>`
        )
      }
    }
  })
  html = s.toString()
  return {
    html,
    tags: [
      {
        tag: 'script',
        attrs: {
          type: 'module',
          src: path.posix.join(base, CLIENT_PUBLIC_PATH)
        },
        injectTo: 'head-prepend'
      }
    ]
  }
}

devHtmlHook里使用了magic-string,通过overwrite(start,end,content)这样简易的方法操作ast,start和end为ast某个部分起始结尾坐标,content为要替换的字符串内容。从根结点开始,逐层级遍历。对于非引用别的模块的esmodule script标签,修改其内容为代理模块,比如<script type="module">import log from './src/b.ts';log();</script>会被修改为<script type="module" src="/index.html?html-proxy&index=1.js">这个代理模块,最后返回了head-prepend这个tag用injectToHead插入html。

function injectToHead(
  html: string,
  tags: HtmlTagDescriptor[],
  prepend = false
) {
  const tagsHtml = serializeTags(tags)
  if (prepend) {
    // inject after head or doctype
    for (const re of headPrependInjectRE) {
      if (re.test(html)) {
        return html.replace(re, `$&\n${tagsHtml}`)
      }
    }
  } else {
    // inject before head close
    if (headInjectRE.test(html)) {
      return html.replace(headInjectRE, `${tagsHtml}\n$&`)
    }
  }
  // if no <head> tag is present, just prepend
  return tagsHtml + `\n` + html
}

injectToHead中通过正则匹配,将<script type="module" src="/@vite/client"></script>插入到了head的最前面,这个脚本很重要,包含了热更新等功能. 我们也可以开发自定义插件,定义transformIndexHtml (opens new window)来处理html内容