# 模块转换

拦截请求

在开发阶段,我们的代码不会被打包成bundle,那么当浏览器请求tstsxscssjsonsvg等模块时如何返回浏览器支持的结果呢?

response-type

可以看到,这些模块返回的Content-Type都为application/javascript,他们返回给浏览器作为js文件执行。

// src/node/server/index.ts  main transform middleware 
middlewares.use(transformMiddleware(server))

得益于Vite的中间件机制,在开发阶段中,所有模块请求经过transformMiddleware中间件,这个中间件对模块内容进行转换,返回最终浏览器兼容的结果

# transformMiddleware中间件

// src/node/server/middlewares/transform
export function transformMiddleware(
  server: ViteDevServer
): Connect.NextHandleFunction {
  //...
  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(   //send = res.end
      req,
      res,
      result.code,
      type,
      result.etag,
      isDep ? 'max-age=31536000,immutable' : 'no-cache',
      result.map
    )
    //...
}

自定义中间件transformMiddleware中,主要逻辑集中在transformRequest,它定义了模块的转换流程,返回模块内容的转换结果code以及是否需要缓存etag

# transformRequest

export async function transformRequest(
  url: string,
  { config, pluginContainer, moduleGraph, watcher }: ViteDevServer,
  options: TransformOptions = {}
): Promise<TransformResult | null> {
  // check cache
  const module = await moduleGraph.getModuleByUrl(url)
  const cached =
    module && (ssr ? module.ssrTransformResult : module.transformResult)
  if (cached) {
    isDebug && debugCache(`[memory] ${prettyUrl}`)
    return cached
  }
  // resolve
  const id = (await pluginContainer.resolveId(url))?.id || url
  const file = cleanUrl(id)

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

  // load
  const loadStart = isDebug ? Date.now() : 0
  const loadResult = await pluginContainer.load(id, ssr)
  if (loadResult == null) {
    if (options.ssr || isFileAccessAllowed(file, config.server.fsServe)) {
      try {
        code = await fs.readFile(file, 'utf-8')
        isDebug && debugLoad(`${timeFrom(loadStart)} [fs] ${prettyUrl}`)
      } catch (e) {
        if (e.code !== 'ENOENT') {
          throw e
        }
      }
    }
    if (code) {
      try {
        map = (
          convertSourceMap.fromSource(code) ||
          convertSourceMap.fromMapFileSource(code, path.dirname(file))
        )?.toObject()
      } catch (e) {
        logger.warn(`Failed to load source map for ${url}.`, {
          timestamp: true
        })
      }
    }
  } else {
    isDebug && debugLoad(`${timeFrom(loadStart)} [plugin] ${prettyUrl}`)
    if (typeof loadResult === 'object') {
      code = loadResult.code
      map = loadResult.map
    } else {
      code = loadResult
    }
  }
  // transform
  const transformStart = isDebug ? Date.now() : 0
  const transformResult = await pluginContainer.transform(code, id, map, ssr)
  if (
    transformResult == null ||
    (typeof transformResult === 'object' && transformResult.code == null)
  ) {
    // no transform applied, keep code as-is
    isDebug &&
      debugTransform(
        timeFrom(transformStart) + chalk.dim(` [skipped] ${prettyUrl}`)
      )
  } else {
    isDebug && debugTransform(`${timeFrom(transformStart)} ${prettyUrl}`)
    code = transformResult.code!
    map = transformResult.map
  }
  return (mod.transformResult = {
    code,
    map,
    etag: getEtag(code, { weak: true })
  } as TransformResult)
}

transformRequest主要分为四个步骤,分别为check cacheresolveloadtransform

check cache用来检查缓存,如果该模块已经被转换过,直接返回缓存结果。

resolve通过pluginContainer的resolveId解析模块路径

load用来加载模块的内容,将resolve返回的id传入pluginContainer的load,读取模块的内容,一般没有自定义插件返回null的情况下,会接着通过fs直接读取模块内容

# transform

transform会转换模块的内容,tstsxscss等文件也是在这一步转换成浏览器可以执行的js文件,这一步调用了pluginContainer的transform,这里主要分析一下转换用到的几个插件。

# vite:esbuild

TS transform

//before transform
export interface Ha {
  a:string
}
const a:Ha = {
  a:'2'
};
export {
  a
};
console.log('aaa',a);
//after transform
const a = {
   a: "2"
};
export { a };
console.log("aaa", a);

TSX transform

//before transform 
import React from 'react'
import ReactDOM from 'react-dom'
import './index.css'
import App from './App'
import {a} from './afafa';
import './b.js';
import './a.json';
ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
  document.getElementById('root')
)
//after transform
import React from "react";
import ReactDOM from "react-dom";
import "./index.css";
import App from "./App";
import {a} from "./afafa";
import "./b.js";
import "./a.json";
console.log("a", a);
ReactDOM.render(/* @__PURE__ */ React.createElement(React.StrictMode, null, /* @__PURE__ */ React.createElement(App, null)), document.getElementById("root"));

上面两个简单的文件转换的例子,我们可以看到,ts文件的interface消失了,tsx中的tsx标签被转换为了React.createElement,这些都是符合预期的,通过vite:esbuild插件, 它们均被被转换为了ts和tsx均被转换为了js文件。

const result = await transform(code, {
  loader: "tsx",
  sourcemap: true,
  sourcefile: "/Users/admin/Desktop/vite-react-ts/src/main.tsx",
});

vite:esbuild会对tsxjsxts模块编译,其中主要是通过esbuildtransform API (opens new window)输出转换后的结果。 然而,现在仍然有个问题,代码里引用的reactreact-dom这种bare name,以及./App这种相对引用在浏览器里都是无法识别的,因此还需要对这些import进行转换。

# vite:import-analysis

import-analysis中,import statement得以转换。WIP