Site Overlay

Vite源码浅析

Vite源码浅析

Vite应用的入口文件引入type='module'的script标签

<script type='module' src='/src/main.js' />

浏览器开始原生支持模块功能,也就是说在main.js中可以用ES模块的语句,import或者export,浏览器根据main.jsimport依赖发起http请求依赖的模块。

为什么import语句需要转化?

在开发模式中看到main.js中的import Vue from ‘vue’;被转化为

什么是裸模块?

import { a } from './a.js';
import { createApp } from 'vue';

裸模块:没有路径标识
浏览器不支持裸模块的导入,在执行时会报错,被拦截。所以Vite花了大量代码将裸模块的路径转化为绝对或相对路径。

Vite如何实现路径转化?

export async function createServer(
  inlineConfig: InlineConfig = {}
): Promise<ViteDevServer> {
  const config = await resolveConfig(inlineConfig, 'serve', 'development')
  const root = config.root
  const serverConfig = config.server
  const httpsOptions = await resolveHttpsConfig(config)
  let { middlewareMode } = serverConfig
  if (middlewareMode === true) {
    middlewareMode = 'ssr'
  }


  const middlewares = connect() as Connect.Server

  ...

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

  return server
}

transformMiddleware中间件:对拦截到请求文件,将其内容转换成浏览器能识别代码

export function transformMiddleware(
  server: ViteDevServer
): Connect.NextHandleFunction {
      if (
        isJSRequest(url) ||
        isImportRequest(url) ||
        isCSSRequest(url) ||
        isHTMLProxy(url)
      ) {

        ...

        // resolve, load and transform using the plugin container
        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,
            result.etag,
            // allow browser to cache npm deps!
            isDep ? 'max-age=31536000,immutable' : 'no-cache',
            result.map
          )
        }
      }
    } catch (e) {
      return next(e)
    }

    next()
  }
}
export async function transformRequest(
  url: string,
  server: ViteDevServer,
  options: TransformOptions = {}
): Promise<TransformResult | null> {
  const { config, pluginContainer, moduleGraph, watcher } = server

  ...

  // transform
  const transformStart = isDebug ? Date.now() : 0
  const transformResult = await pluginContainer.transform(code, id, map, ssr)

  ...
}

pluginContainer.transform() 通过es-module-lexer插件解析文件获取所有的import语句,循环,通过一系列插件用来解析出裸导入的真实路径。

vite配置

  1. server.port & server.strictPort
    server.port 默认端口为3000,如果端口被占用,vite会自动尝试下一个可用的端口,这是基于server.strictPort为false的情况下
export async function httpServerStart(
  httpServer: HttpServer,
  serverOptions: {
    port: number
    strictPort: boolean | undefined
    host: string | undefined
    logger: Logger
  }
): Promise<number> {
  return new Promise((resolve, reject) => {
    let { port, strictPort, host, logger } = serverOptions

    const onError = (e: Error & { code?: string }) => {
      if (e.code === 'EADDRINUSE') { // 只处理端口被占用的错误
        if (strictPort) {
          httpServer.removeListener('error', onError)
          reject(new Error(`Port {port} is already in use`))
        } else {
          logger.info(`Port{port} is in use, trying another one...`)
          httpServer.listen(++port, host)
        }
      } else {
        httpServer.removeListener('error', onError)
        reject(e)
      }
    }

    httpServer.on('error', onError)

    httpServer.listen(port, host, () => {
      httpServer.removeListener('error', onError)
      resolve(port)
    })
  })
}
  1. server.open
import open from 'open'

export function openBrowser(
  url: string,
  opt: string | true,
  logger: Logger
): boolean {
  // The browser executable to open.
  // See https://github.com/sindresorhus/open#app for documentation.
  const browser = typeof opt === 'string' ? opt : process.env.BROWSER || ''
  if (browser.toLowerCase().endsWith('.js')) {
    return executeNodeScript(browser, url, logger)
  } else if (browser.toLowerCase() !== 'none') {
    return startBrowserProcess(browser, url)
  }
  return false
}

function startBrowserProcess(browser: string | undefined, url: string) {
  // If we're on OS X, the user hasn't specifically
  // requested a different browser, we can try opening
  // Chrome with AppleScript. This lets us reuse an
  // existing tab when possible instead of creating a new one.
  const shouldTryOpenChromeWithAppleScript =
    process.platform === 'darwin' && (browser === '' || browser === OSX_CHROME)

  if (shouldTryOpenChromeWithAppleScript) {
    try {
      // Try our best to reuse existing tab
      // on OS X Google Chrome with AppleScript
      execSync('ps cax | grep "Google Chrome"')
      execSync('osascript openChrome.applescript "' + encodeURI(url) + '"', {
        cwd: path.dirname(require.resolve('vite/bin/openChrome.applescript')),
        stdio: 'ignore'
      })
      return true
    } catch (err) {
      // Ignore errors
    }
  }

  // Another special case: on OS X, check if BROWSER has been set to "open".
  // In this case, instead of passing the string `open` to `open` function (which won't work),
  // just ignore it (thus ensuring the intended behavior, i.e. opening the system browser):
  // https://github.com/facebook/create-react-app/pull/1690#issuecomment-283518768
  if (process.platform === 'darwin' && browser === 'open') {
    browser = undefined
  }

  // Fallback to open
  // (It will always open new tab)
  try {
    const options: open.Options = browser ? { app: { name: browser } } : {}
    open(url, options).catch(() => {}) // Prevent `unhandledRejection` error.
    return true
  } catch (err) {
    return false
  }
}

参考资料
1. https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Guide/Modules
2. https://cn.vitejs.dev/config/#server-port
3. https://juejin.cn/post/6965675731661783076#heading-6