Vite是如何对我们写的(vite.config.x)进行解析?

Vite配置解析是怎么做的?

  • 本文为笔者学习 Vite 源码时的一些笔记,如有错误,请指出✊

  • 也就是 怎么解析 我们写的 vite.config.ts等的vite配置文件

  • 这一步是由 vite配置解析的resolveConfig函数来做的

  • export async function resolveConfig(
    inlineConfig: InlineConfig,
    command: 'build' | 'serve',
    defaultMode = 'development',
    defaultNodeEnv = 'development',
    ): Promise<ResolvedConfig>
    export async function resolveConfig(
      inlineConfig: InlineConfig,
      command: 'build' | 'serve',
      defaultMode = 'development',
      defaultNodeEnv = 'development',
    ): Promise<ResolvedConfig> 
    export async function resolveConfig( inlineConfig: InlineConfig, command: 'build' | 'serve', defaultMode = 'development', defaultNodeEnv = 'development', ): Promise<ResolvedConfig>

1. 加载配置文件

  • 大概思路是首先加载,解析配置文件,然后 合并命令行的配置

  • let { configFile } = config // config 是 resolveConfig 的参数 inlineConfig
    if (configFile !== false) {
    // 默认会走到这里 除非显示指定conFile为false
    const loadResult = await loadConfigFromFile(
    configEnv,
    configFile,
    config.root,
    config.logLevel,
    )
    if (loadResult) {
    // 解析配置后 应该与命令行的配置合并
    config = mergeConfig(loadResult.config, config)
    configFile = loadResult.path
    /*
    * 因为配置文件代码可能会有第三方库的依赖,所以当第三方库依赖的代码更改时,Vite * 可以通过 HMR 处理逻辑中记录的configFileDependencies检测到更改,再重启 * DevServer ,来保证当前生效的配置永远是最新的
    */
    configFileDependencies = loadResult.dependencies
    }
    }
    let { configFile } = config // config 是 resolveConfig 的参数 inlineConfig
    if (configFile !== false) {
        // 默认会走到这里 除非显示指定conFile为false
        const loadResult = await loadConfigFromFile(
          configEnv,
          configFile,
          config.root,
          config.logLevel,
        )
        if (loadResult) {
          // 解析配置后 应该与命令行的配置合并
          config = mergeConfig(loadResult.config, config)
          
          configFile = loadResult.path
          
          /* 
           * 因为配置文件代码可能会有第三方库的依赖,所以当第三方库依赖的代码更改时,Vite        * 可以通过 HMR 处理逻辑中记录的configFileDependencies检测到更改,再重启          *  DevServer ,来保证当前生效的配置永远是最新的 
           */
          configFileDependencies = loadResult.dependencies
        }
      }
    let { configFile } = config // config 是 resolveConfig 的参数 inlineConfig if (configFile !== false) { // 默认会走到这里 除非显示指定conFile为false const loadResult = await loadConfigFromFile( configEnv, configFile, config.root, config.logLevel, ) if (loadResult) { // 解析配置后 应该与命令行的配置合并 config = mergeConfig(loadResult.config, config) configFile = loadResult.path /* * 因为配置文件代码可能会有第三方库的依赖,所以当第三方库依赖的代码更改时,Vite * 可以通过 HMR 处理逻辑中记录的configFileDependencies检测到更改,再重启 * DevServer ,来保证当前生效的配置永远是最新的 */ configFileDependencies = loadResult.dependencies } }
  • loadConfigFromFile函数这里先不做详细介绍,他的主要作用是加载,解析配置文件

2. 解析用户插件

  • 这一步主要干了2件事:

根据apply参数,剔除不生效的插件, 给插件排好顺序

  • 有些插件只在开发阶段生效,或者说只在生产环境生效,我们可以通过 apply: 'serve' 或 'build' 来指定它们,同时也可以将apply配置为一个函数,来自定义插件生效的条件
  • 因为插件执行时机不一样,所以需要排序,顺便合并插件的配置
// user config may provide an alternative mode. But --mode has a higher priority
// 优先级为 命令行 > 配置文件声明 > 默认
mode = inlineConfig.mode || config.mode || mode
configEnv.mode = mode
const filterPlugin = (p: Plugin) => {
if (!p) {
return false
} else if (!p.apply) {
// 没有显示声明apply,默认都执行
return true
} else if (typeof p.apply === 'function') {
// 如果为函数的话 则执行这个函数 用函数来定义apply的话可以自定义插件生效时机
return p.apply({ ...config, mode }, configEnv)
} else {
return p.apply === command
}
}
......
// resolve plugins
const rawUserPlugins = (
(await asyncFlatten(config.plugins || [])) as Plugin[]
).filter(filterPlugin)
// 这里干了两件事 排序 + 过滤
const [prePlugins, normalPlugins, postPlugins] =
sortUserPlugins(rawUserPlugins)
// user config may provide an alternative mode. But --mode has a higher priority
// 优先级为 命令行 > 配置文件声明 > 默认
  mode = inlineConfig.mode || config.mode || mode
  configEnv.mode = mode

  const filterPlugin = (p: Plugin) => {
    if (!p) {
      return false
    } else if (!p.apply) {
      // 没有显示声明apply,默认都执行
      return true
    } else if (typeof p.apply === 'function') {
      // 如果为函数的话 则执行这个函数 用函数来定义apply的话可以自定义插件生效时机
      return p.apply({ ...config, mode }, configEnv)
    } else {
      return p.apply === command
    }
  }
  
  ......
  
  // resolve plugins
  const rawUserPlugins = (
    (await asyncFlatten(config.plugins || [])) as Plugin[]
  ).filter(filterPlugin)

// 这里干了两件事 排序 + 过滤
  const [prePlugins, normalPlugins, postPlugins] =
    sortUserPlugins(rawUserPlugins) 
// user config may provide an alternative mode. But --mode has a higher priority // 优先级为 命令行 > 配置文件声明 > 默认 mode = inlineConfig.mode || config.mode || mode configEnv.mode = mode const filterPlugin = (p: Plugin) => { if (!p) { return false } else if (!p.apply) { // 没有显示声明apply,默认都执行 return true } else if (typeof p.apply === 'function') { // 如果为函数的话 则执行这个函数 用函数来定义apply的话可以自定义插件生效时机 return p.apply({ ...config, mode }, configEnv) } else { return p.apply === command } } ...... // resolve plugins const rawUserPlugins = ( (await asyncFlatten(config.plugins || [])) as Plugin[] ).filter(filterPlugin) // 这里干了两件事 排序 + 过滤 const [prePlugins, normalPlugins, postPlugins] = sortUserPlugins(rawUserPlugins)

调用 插件的 config 钩子,进行配置合并

// run config hooks
// 这一步操作由runConfigHook这个函数内部实现
const userPlugins = [...prePlugins, ...normalPlugins, ...postPlugins]
config = await runConfigHook(config, userPlugins, configEnv)
 // run config hooks
  // 这一步操作由runConfigHook这个函数内部实现
  const userPlugins = [...prePlugins, ...normalPlugins, ...postPlugins]
  config = await runConfigHook(config, userPlugins, configEnv)
// run config hooks // 这一步操作由runConfigHook这个函数内部实现 const userPlugins = [...prePlugins, ...normalPlugins, ...postPlugins] config = await runConfigHook(config, userPlugins, configEnv)

解析root参数,alias参数

  • 如果在配置文件内没有指定的话,默认root解析的是 process.cwd()

  • 解析alias时,需要加上一些内置的 alias 规则,如@vite/env@vite/client这种直接重定向到 Vite 内部的模块

  • // resolve root
    const resolvedRoot = normalizePath(
    config.root ? path.resolve(config.root) : process.cwd(),
    )
    // 内置alias规则
    const clientAlias = [
    {
    find: /^\/?@vite\/env/,
    replacement: path.posix.join(FS_PREFIX, normalizePath(ENV_ENTRY)),
    },
    {
    find: /^\/?@vite\/client/,
    replacement: path.posix.join(FS_PREFIX, normalizePath(CLIENT_ENTRY)),
    },
    ]
    // resolve alias with internal client alias
    const resolvedAlias = normalizeAlias(
    mergeAlias(clientAlias, config.resolve?.alias || []),
    )
      // resolve root
      const resolvedRoot = normalizePath(
        config.root ? path.resolve(config.root) : process.cwd(),
      )
      
    
      // 内置alias规则
      const clientAlias = [
        {
          find: /^\/?@vite\/env/,
          replacement: path.posix.join(FS_PREFIX, normalizePath(ENV_ENTRY)),
        },
        {
          find: /^\/?@vite\/client/,
          replacement: path.posix.join(FS_PREFIX, normalizePath(CLIENT_ENTRY)),
        },
      ]
      
      // resolve alias with internal client alias
      const resolvedAlias = normalizeAlias(
        mergeAlias(clientAlias, config.resolve?.alias || []),
      )
    // resolve root const resolvedRoot = normalizePath( config.root ? path.resolve(config.root) : process.cwd(), ) // 内置alias规则 const clientAlias = [ { find: /^\/?@vite\/env/, replacement: path.posix.join(FS_PREFIX, normalizePath(ENV_ENTRY)), }, { find: /^\/?@vite\/client/, replacement: path.posix.join(FS_PREFIX, normalizePath(CLIENT_ENTRY)), }, ] // resolve alias with internal client alias const resolvedAlias = normalizeAlias( mergeAlias(clientAlias, config.resolve?.alias || []), )

3. 加载环境变量

  • 没有指定envDir的话,默认扫描process.cwd()目录下的.env文件

  • loadEnv函数会去扫描 process.env.env文件,解析出 env 对象,这个对象的属性最终会被挂载到import.meta.env 这个全局对象上

  • // load .env files
    const envDir = config.envDir
    ? normalizePath(path.resolve(resolvedRoot, config.envDir))
    : resolvedRoot
    /*
    * loadEnv的具体步骤(详细代码在src/node/env.ts文件):
    * 1. 遍历 process.env 的属性,拿到指定前缀开头的属性(默认指定为VITE_),并挂载
    * 在 env 对象上
    * 2. 遍历 .env 文件,解析文件,然后往 env 对象挂载那些以指定前缀开头的属性。遍历的
    * 文件先后顺序如下:
    * .env.${mode}.local
    * .env.${mode}
    * .env.local
    * .env
    */
    const userEnv =
    inlineConfig.envFile !== false &&
    loadEnv(mode, envDir, resolveEnvPrefix(config))
     // load .env files
      const envDir = config.envDir
        ? normalizePath(path.resolve(resolvedRoot, config.envDir))
        : resolvedRoot
      
    
      /*
       * loadEnv的具体步骤(详细代码在src/node/env.ts文件):
       * 1. 遍历 process.env 的属性,拿到指定前缀开头的属性(默认指定为VITE_),并挂载
       * 在 env 对象上
       * 2. 遍历 .env 文件,解析文件,然后往 env 对象挂载那些以指定前缀开头的属性。遍历的
       * 文件先后顺序如下:
       *  .env.${mode}.local
         *  .env.${mode}
         *  .env.local
         *  .env
       */
      const userEnv =
        inlineConfig.envFile !== false &&
        loadEnv(mode, envDir, resolveEnvPrefix(config))
    // load .env files const envDir = config.envDir ? normalizePath(path.resolve(resolvedRoot, config.envDir)) : resolvedRoot /* * loadEnv的具体步骤(详细代码在src/node/env.ts文件): * 1. 遍历 process.env 的属性,拿到指定前缀开头的属性(默认指定为VITE_),并挂载 * 在 env 对象上 * 2. 遍历 .env 文件,解析文件,然后往 env 对象挂载那些以指定前缀开头的属性。遍历的 * 文件先后顺序如下: * .env.${mode}.local * .env.${mode} * .env.local * .env */ const userEnv = inlineConfig.envFile !== false && loadEnv(mode, envDir, resolveEnvPrefix(config))
  • 特殊情况: 如果在加载过程中遇到 NODE_ENV 属性,则挂到 process.env.VITE_USER_NODE_ENV,Vite 会优先通过这个属性来决定是否走生产环境的构建

  • 其他一些附带操作

    • /*
      * 解析资源公共路径 base
      * 关键在于 resolvebaseUrl 函数,里面的细节主要有:
      * 空字符或者 ./ 在开发阶段特殊处理,全部重写为/
      * .开头的路径,自动重写为 /
      * 以http(s)://开头的路径,在开发环境下重写为对应的 pathname
      * 确保路径开头和结尾都是/
      */
      // During dev, we ignore relative base and fallback to '/'
      // For the SSR build, relative base isn't possible by means
      // of import.meta.url.
      const resolvedBase = relativeBaseShortcut
      ? !isBuild || config.build?.ssr
      ? '/'
      : './'
      : resolveBaseUrl(config.base, isBuild, logger) ?? '/'
      // 解析生产环境的构建配置
      const resolvedBuildOptions = resolveBuildOptions(
      config.build,
      logger,
      resolvedRoot,
      )
      // 对cacheDir的解析,这个路径相对于在 Vite 预编译时写入依赖产物的路径
      // resolve cache directory
      const pkgDir = findNearestPackageData(resolvedRoot, packageCache)?.dir
      /*
      * 当显示指定cacheDir时,cache directory为配置文件中指定的位置
      * 否则 判断 pkgDir 是否存在
      * 存在的话 指定为 pkgDir下的 node_modules/.vite
      * 不存在 则为 root 位置下的 .vite
      */
      const cacheDir = normalizePath(
      config.cacheDir
      ? path.resolve(resolvedRoot, config.cacheDir)
      : pkgDir
      ? path.join(pkgDir, `node_modules/.vite`)
      : path.join(resolvedRoot, `.vite`),
      )
      // 处理用户配置的assetsInclude,将其转换为一个过滤器函数:
      // Vite 在最终整理所有配置阶段,会将用户传入的 assetsInclude 和内置的规则合并
      // 这个配置决定是否让 Vite 将对应的后缀名视为静态资源文件(asset)来处理
      const assetsFilter =
      config.assetsInclude &&
      (!Array.isArray(config.assetsInclude) || config.assetsInclude.length)
      ? createFilter(config.assetsInclude)
      : () => false
      // 最终所有配置会被合并为这个对象
      const resolvedConfig: ResolvedConfig = {
      ......
      assetsInclude(file: string) {
      return DEFAULT_ASSETS_RE.test(file) || assetsFilter(file)
      },
      ......
      }
      /*
       * 解析资源公共路径 base
       * 关键在于 resolvebaseUrl 函数,里面的细节主要有:
       * 空字符或者 ./ 在开发阶段特殊处理,全部重写为/
       * .开头的路径,自动重写为 /
       * 以http(s)://开头的路径,在开发环境下重写为对应的 pathname
       * 确保路径开头和结尾都是/ 
      */
      // During dev, we ignore relative base and fallback to '/'
        // For the SSR build, relative base isn't possible by means
        // of import.meta.url.
        const resolvedBase = relativeBaseShortcut
          ? !isBuild || config.build?.ssr
            ? '/'
            : './'
          : resolveBaseUrl(config.base, isBuild, logger) ?? '/'
      
        // 解析生产环境的构建配置
        const resolvedBuildOptions = resolveBuildOptions(
          config.build,
          logger,
          resolvedRoot,
        )
        
        // 对cacheDir的解析,这个路径相对于在 Vite 预编译时写入依赖产物的路径
         // resolve cache directory
        const pkgDir = findNearestPackageData(resolvedRoot, packageCache)?.dir
        /*
        * 当显示指定cacheDir时,cache directory为配置文件中指定的位置
        * 否则 判断 pkgDir 是否存在 
        *   存在的话 指定为 pkgDir下的 node_modules/.vite
        *   不存在 则为 root 位置下的 .vite
        */
        const cacheDir = normalizePath(
          config.cacheDir
            ? path.resolve(resolvedRoot, config.cacheDir)
            : pkgDir
            ? path.join(pkgDir, `node_modules/.vite`)
            : path.join(resolvedRoot, `.vite`),
        )
        
        // 处理用户配置的assetsInclude,将其转换为一个过滤器函数:
        // Vite 在最终整理所有配置阶段,会将用户传入的 assetsInclude 和内置的规则合并
        // 这个配置决定是否让 Vite 将对应的后缀名视为静态资源文件(asset)来处理
          const assetsFilter =
          config.assetsInclude &&
          (!Array.isArray(config.assetsInclude) || config.assetsInclude.length)
            ? createFilter(config.assetsInclude)
            : () => false
       
          // 最终所有配置会被合并为这个对象
          const resolvedConfig: ResolvedConfig = {
            ......
           
                assetsInclude(file: string) {
                  return DEFAULT_ASSETS_RE.test(file) || assetsFilter(file)
                },
            ......
          }
      /* * 解析资源公共路径 base * 关键在于 resolvebaseUrl 函数,里面的细节主要有: * 空字符或者 ./ 在开发阶段特殊处理,全部重写为/ * .开头的路径,自动重写为 / * 以http(s)://开头的路径,在开发环境下重写为对应的 pathname * 确保路径开头和结尾都是/ */ // During dev, we ignore relative base and fallback to '/' // For the SSR build, relative base isn't possible by means // of import.meta.url. const resolvedBase = relativeBaseShortcut ? !isBuild || config.build?.ssr ? '/' : './' : resolveBaseUrl(config.base, isBuild, logger) ?? '/' // 解析生产环境的构建配置 const resolvedBuildOptions = resolveBuildOptions( config.build, logger, resolvedRoot, ) // 对cacheDir的解析,这个路径相对于在 Vite 预编译时写入依赖产物的路径 // resolve cache directory const pkgDir = findNearestPackageData(resolvedRoot, packageCache)?.dir /* * 当显示指定cacheDir时,cache directory为配置文件中指定的位置 * 否则 判断 pkgDir 是否存在 * 存在的话 指定为 pkgDir下的 node_modules/.vite * 不存在 则为 root 位置下的 .vite */ const cacheDir = normalizePath( config.cacheDir ? path.resolve(resolvedRoot, config.cacheDir) : pkgDir ? path.join(pkgDir, `node_modules/.vite`) : path.join(resolvedRoot, `.vite`), ) // 处理用户配置的assetsInclude,将其转换为一个过滤器函数: // Vite 在最终整理所有配置阶段,会将用户传入的 assetsInclude 和内置的规则合并 // 这个配置决定是否让 Vite 将对应的后缀名视为静态资源文件(asset)来处理 const assetsFilter = config.assetsInclude && (!Array.isArray(config.assetsInclude) || config.assetsInclude.length) ? createFilter(config.assetsInclude) : () => false // 最终所有配置会被合并为这个对象 const resolvedConfig: ResolvedConfig = { ...... assetsInclude(file: string) { return DEFAULT_ASSETS_RE.test(file) || assetsFilter(file) }, ...... }

4. 定义路径解析器工厂

主流程

  • 这里所说的路径解析器,是指调用插件容器进行路径解析的函数

  • // create an internal resolver to be used in special scenarios, e.g.
    // optimizer & handling css @imports
    const createResolver: ResolvedConfig['createResolver'] = (options) => {
    let aliasContainer: PluginContainer | undefined
    let resolverContainer: PluginContainer | undefined
    // 返回了一个函数 这个函数就是路径解析器
    return async (id, importer, aliasOnly, ssr) => {
    let container: PluginContainer
    if (aliasOnly) {
    // 新建 aliasPlugin
    container =
    aliasContainer ||
    (aliasContainer = await createPluginContainer({
    ...resolved,
    plugins: [aliasPlugin({ entries: resolved.resolve.alias })],
    }))
    } else {
    // 新建 resolvePlugin
    container =
    resolverContainer ||
    (resolverContainer = await createPluginContainer({
    ...resolved,
    plugins: [
    aliasPlugin({ entries: resolved.resolve.alias }),
    resolvePlugin({
    ...resolved.resolve,
    root: resolvedRoot,
    isProduction,
    isBuild: command === 'build',
    ssrConfig: resolved.ssr,
    asSrc: true,
    preferRelative: false,
    tryIndex: true,
    ...options,
    idOnly: true,
    }),
    ],
    }))
    }
    return (
    await container.resolveId(id, importer, {
    ssr,
    scan: options?.scan,
    })
    )?.id
    }
    }
    // 这里有 aliasContainer 和 resolverContainer 两个工具对象,它们都含有 resolveId 这个专门解析路径的方法,可以被 Vite 调用来获取解析结果
    // container 的类型是 PluginContainer 这个我们后续在插件机制那块会讲到
    // create an internal resolver to be used in special scenarios, e.g.
      // optimizer & handling css @imports
      const createResolver: ResolvedConfig['createResolver'] = (options) => {
        let aliasContainer: PluginContainer | undefined
        let resolverContainer: PluginContainer | undefined
        // 返回了一个函数 这个函数就是路径解析器
        return async (id, importer, aliasOnly, ssr) => {
          let container: PluginContainer
          if (aliasOnly) {
            // 新建 aliasPlugin
            container =
              aliasContainer ||
              (aliasContainer = await createPluginContainer({
                ...resolved,
                plugins: [aliasPlugin({ entries: resolved.resolve.alias })],
              }))
          } else {
              // 新建 resolvePlugin
            container =
              resolverContainer ||
              (resolverContainer = await createPluginContainer({
                ...resolved,
                plugins: [
                  aliasPlugin({ entries: resolved.resolve.alias }),
                  resolvePlugin({
                    ...resolved.resolve,
                    root: resolvedRoot,
                    isProduction,
                    isBuild: command === 'build',
                    ssrConfig: resolved.ssr,
                    asSrc: true,
                    preferRelative: false,
                    tryIndex: true,
                    ...options,
                    idOnly: true,
                  }),
                ],
              }))
          }
          return (
            await container.resolveId(id, importer, {
              ssr,
              scan: options?.scan,
            })
          )?.id
        }
      }
    // 这里有 aliasContainer 和 resolverContainer 两个工具对象,它们都含有 resolveId 这个专门解析路径的方法,可以被 Vite 调用来获取解析结果
    // container 的类型是 PluginContainer 这个我们后续在插件机制那块会讲到
    // create an internal resolver to be used in special scenarios, e.g. // optimizer & handling css @imports const createResolver: ResolvedConfig['createResolver'] = (options) => { let aliasContainer: PluginContainer | undefined let resolverContainer: PluginContainer | undefined // 返回了一个函数 这个函数就是路径解析器 return async (id, importer, aliasOnly, ssr) => { let container: PluginContainer if (aliasOnly) { // 新建 aliasPlugin container = aliasContainer || (aliasContainer = await createPluginContainer({ ...resolved, plugins: [aliasPlugin({ entries: resolved.resolve.alias })], })) } else { // 新建 resolvePlugin container = resolverContainer || (resolverContainer = await createPluginContainer({ ...resolved, plugins: [ aliasPlugin({ entries: resolved.resolve.alias }), resolvePlugin({ ...resolved.resolve, root: resolvedRoot, isProduction, isBuild: command === 'build', ssrConfig: resolved.ssr, asSrc: true, preferRelative: false, tryIndex: true, ...options, idOnly: true, }), ], })) } return ( await container.resolveId(id, importer, { ssr, scan: options?.scan, }) )?.id } } // 这里有 aliasContainer 和 resolverContainer 两个工具对象,它们都含有 resolveId 这个专门解析路径的方法,可以被 Vite 调用来获取解析结果 // container 的类型是 PluginContainer 这个我们后续在插件机制那块会讲到
  • 这个解析器 未来会用于依赖预构建过程

    const resolve = config.createResolver()
    // 调用以拿到 react 路径
    rseolve('react', undefined, undefined, false)
    const resolve = config.createResolver()
    // 调用以拿到 react 路径
    rseolve('react', undefined, undefined, false)
    const resolve = config.createResolver() // 调用以拿到 react 路径 rseolve('react', undefined, undefined, false)

解析 public 参数

// 顺带解析了 public 参数 -> 静态资源目录
const { publicDir } = config
const resolvedPublicDir =
publicDir !== false && publicDir !== ''
? path.resolve(
resolvedRoot,
typeof publicDir === 'string' ? publicDir : 'public',
)
: ''
// 顺带解析了 public 参数 -> 静态资源目录
const { publicDir } = config
  const resolvedPublicDir =
    publicDir !== false && publicDir !== ''
      ? path.resolve(
          resolvedRoot,
          typeof publicDir === 'string' ? publicDir : 'public',
        )
      : ''
// 顺带解析了 public 参数 -> 静态资源目录 const { publicDir } = config const resolvedPublicDir = publicDir !== false && publicDir !== '' ? path.resolve( resolvedRoot, typeof publicDir === 'string' ? publicDir : 'public', ) : ''

最终阶段

  • 对上面所有解析结果进行合并

  • // 上述的解析 只列举了几个 详细的所有配置解析 可以自行查看源码
    const resolvedConfig: ResolvedConfig = {
    configFile: configFile ? normalizePath(configFile) : undefined,
    configFileDependencies: configFileDependencies.map((name) =>
    normalizePath(path.resolve(name)),
    ),
    inlineConfig,
    root: resolvedRoot,
    base: resolvedBase.endsWith('/') ? resolvedBase : resolvedBase + '/',
    rawBase: resolvedBase,
    resolve: resolveOptions,
    publicDir: resolvedPublicDir,
    cacheDir,
    command,
    mode,
    ssr,
    isWorker: false,
    mainConfig: null,
    isProduction,
    plugins: userPlugins,
    esbuild:
    config.esbuild === false
    ? false
    : {
    jsxDev: !isProduction,
    ...config.esbuild,
    },
    server,
    build: resolvedBuildOptions,
    preview: resolvePreviewOptions(config.preview, server),
    envDir,
    env: {
    ...userEnv,
    BASE_URL,
    MODE: mode,
    DEV: !isProduction,
    PROD: isProduction,
    },
    assetsInclude(file: string) {
    return DEFAULT_ASSETS_RE.test(file) || assetsFilter(file)
    },
    logger,
    packageCache,
    createResolver,
    optimizeDeps: {
    disabled: 'build',
    ...optimizeDeps,
    esbuildOptions: {
    preserveSymlinks: resolveOptions.preserveSymlinks,
    ...optimizeDeps.esbuildOptions,
    },
    },
    worker: resolvedWorkerOptions,
    appType: config.appType ?? (middlewareMode === 'ssr' ? 'custom' : 'spa'),
    experimental: {
    importGlobRestoreExtension: false,
    hmrPartialAccept: false,
    ...config.experimental,
    },
    getSortedPlugins: undefined!,
    getSortedPluginHooks: undefined!,
    }
    const resolved: ResolvedConfig = {
    ...config,
    ...resolvedConfig,
    }
    // 上述的解析 只列举了几个 详细的所有配置解析 可以自行查看源码
    const resolvedConfig: ResolvedConfig = {
        configFile: configFile ? normalizePath(configFile) : undefined,
        configFileDependencies: configFileDependencies.map((name) =>
          normalizePath(path.resolve(name)),
        ),
        inlineConfig,
        root: resolvedRoot,
        base: resolvedBase.endsWith('/') ? resolvedBase : resolvedBase + '/',
        rawBase: resolvedBase,
        resolve: resolveOptions,
        publicDir: resolvedPublicDir,
        cacheDir,
        command,
        mode,
        ssr,
        isWorker: false,
        mainConfig: null,
        isProduction,
        plugins: userPlugins,
        esbuild:
          config.esbuild === false
            ? false
            : {
                jsxDev: !isProduction,
                ...config.esbuild,
              },
        server,
        build: resolvedBuildOptions,
        preview: resolvePreviewOptions(config.preview, server),
        envDir,
        env: {
          ...userEnv,
          BASE_URL,
          MODE: mode,
          DEV: !isProduction,
          PROD: isProduction,
        },
        assetsInclude(file: string) {
          return DEFAULT_ASSETS_RE.test(file) || assetsFilter(file)
        },
        logger,
        packageCache,
        createResolver,
        optimizeDeps: {
          disabled: 'build',
          ...optimizeDeps,
          esbuildOptions: {
            preserveSymlinks: resolveOptions.preserveSymlinks,
            ...optimizeDeps.esbuildOptions,
          },
        },
        worker: resolvedWorkerOptions,
        appType: config.appType ?? (middlewareMode === 'ssr' ? 'custom' : 'spa'),
        experimental: {
          importGlobRestoreExtension: false,
          hmrPartialAccept: false,
          ...config.experimental,
        },
        getSortedPlugins: undefined!,
        getSortedPluginHooks: undefined!,
      }
    
      const resolved: ResolvedConfig = {
        ...config,
        ...resolvedConfig,
      }
    // 上述的解析 只列举了几个 详细的所有配置解析 可以自行查看源码 const resolvedConfig: ResolvedConfig = { configFile: configFile ? normalizePath(configFile) : undefined, configFileDependencies: configFileDependencies.map((name) => normalizePath(path.resolve(name)), ), inlineConfig, root: resolvedRoot, base: resolvedBase.endsWith('/') ? resolvedBase : resolvedBase + '/', rawBase: resolvedBase, resolve: resolveOptions, publicDir: resolvedPublicDir, cacheDir, command, mode, ssr, isWorker: false, mainConfig: null, isProduction, plugins: userPlugins, esbuild: config.esbuild === false ? false : { jsxDev: !isProduction, ...config.esbuild, }, server, build: resolvedBuildOptions, preview: resolvePreviewOptions(config.preview, server), envDir, env: { ...userEnv, BASE_URL, MODE: mode, DEV: !isProduction, PROD: isProduction, }, assetsInclude(file: string) { return DEFAULT_ASSETS_RE.test(file) || assetsFilter(file) }, logger, packageCache, createResolver, optimizeDeps: { disabled: 'build', ...optimizeDeps, esbuildOptions: { preserveSymlinks: resolveOptions.preserveSymlinks, ...optimizeDeps.esbuildOptions, }, }, worker: resolvedWorkerOptions, appType: config.appType ?? (middlewareMode === 'ssr' ? 'custom' : 'spa'), experimental: { importGlobRestoreExtension: false, hmrPartialAccept: false, ...config.experimental, }, getSortedPlugins: undefined!, getSortedPluginHooks: undefined!, } const resolved: ResolvedConfig = { ...config, ...resolvedConfig, }

5. 生成插件流水线

  • // 先生成完整插件列表传给resolve.plugins
    // 细节都在 resolvePlugins 函数内部 后续会详细研究这个函数
    ;(resolved.plugins as Plugin[]) = await resolvePlugins(
    resolved,
    prePlugins,
    normalPlugins,
    postPlugins,
    )
    ......
    // call configResolved hooks
    // 调用每个插件的 configResolved 钩子函数
    await Promise.all([
    ...resolved
    .getSortedPluginHooks('configResolved')
    .map((hook) => hook(resolved)),
    ...resolvedConfig.worker
    .getSortedPluginHooks('configResolved')
    .map((hook) => hook(workerResolved)),
    ])
    ......
    // 先生成完整插件列表传给resolve.plugins
    // 细节都在  resolvePlugins 函数内部 后续会详细研究这个函数
    ;(resolved.plugins as Plugin[]) = await resolvePlugins(
        resolved,
        prePlugins,
        normalPlugins,
        postPlugins,
      )
    
    ......
    
      // call configResolved hooks
    // 调用每个插件的 configResolved 钩子函数
      await Promise.all([
        ...resolved
          .getSortedPluginHooks('configResolved')
          .map((hook) => hook(resolved)),
        ...resolvedConfig.worker
          .getSortedPluginHooks('configResolved')
          .map((hook) => hook(workerResolved)),
      ])
    
    ......
    // 先生成完整插件列表传给resolve.plugins // 细节都在 resolvePlugins 函数内部 后续会详细研究这个函数 ;(resolved.plugins as Plugin[]) = await resolvePlugins( resolved, prePlugins, normalPlugins, postPlugins, ) ...... // call configResolved hooks // 调用每个插件的 configResolved 钩子函数 await Promise.all([ ...resolved .getSortedPluginHooks('configResolved') .map((hook) => hook(resolved)), ...resolvedConfig.worker .getSortedPluginHooks('configResolved') .map((hook) => hook(workerResolved)), ]) ......
  • 最后 这个 resolvedConfig函数会 返回 最终的 配置结果 -> resolved

加载配置文件中 的关键函数 loadConfigFromFile

  • // 定义部分 接受四个参数
    export async function loadConfigFromFile(
    configEnv: ConfigEnv,
    configFile?: string,
    configRoot: string = process.cwd(),
    logLevel?: LogLevel,
    ): Promise<{
    path: string
    config: UserConfig
    dependencies: string[]
    } | null>
    // 定义部分 接受四个参数
    export async function loadConfigFromFile(
      configEnv: ConfigEnv,
      configFile?: string,
      configRoot: string = process.cwd(),
      logLevel?: LogLevel,
    ): Promise<{
      path: string
      config: UserConfig
      dependencies: string[]
    } | null> 
    // 定义部分 接受四个参数 export async function loadConfigFromFile( configEnv: ConfigEnv, configFile?: string, configRoot: string = process.cwd(), logLevel?: LogLevel, ): Promise<{ path: string config: UserConfig dependencies: string[] } | null>

主要思路

  • 既然是 加载配置文件,那么就需要处理 不同的配置文件类型,主要有以下四种

    • TS + ESM
    • TS + CJS
    • JS + ESM
    • JS + CJS
  • 所以,要做的就首先识别 配置文件的类型,然后根据不同的类型,进行解析

1. 寻找配置文件路径

  • // node/contants.ts
    export const DEFAULT_CONFIG_FILES = [
    'vite.config.js',
    'vite.config.mjs',
    'vite.config.ts',
    'vite.config.cjs',
    'vite.config.mts',
    'vite.config.cts',
    ]
    // node/config.ts
    let resolvedPath: string | undefined
    // configfile 就是 传入的参数 也就是 在命令行启动 vite 的时候指定的参数
    if (configFile) {
    // explicit config path is always resolved from cwd
    // configFile 存在的话 则用这个路径来 resolve
    resolvedPath = path.resolve(configFile)
    } else {
    // implicit config file loaded from inline root (if present)
    // otherwise from cwd
    // 否则的话 从默认的 跟路径 process.cwd() 来resolve
    for (const filename of DEFAULT_CONFIG_FILES) {
    const filePath = path.resolve(configRoot, filename)
    if (!fs.existsSync(filePath)) continue
    resolvedPath = filePath
    break
    }
    }
    // 这不到 则返回 null ,同时,给出提示
    if (!resolvedPath) {
    debug?.('no config file found.')
    return null
    }
    // node/contants.ts
    export const DEFAULT_CONFIG_FILES = [
      'vite.config.js',
      'vite.config.mjs',
      'vite.config.ts',
      'vite.config.cjs',
      'vite.config.mts',
      'vite.config.cts',
    ]
    
    // node/config.ts
    let resolvedPath: string | undefined
    // configfile 就是 传入的参数 也就是 在命令行启动 vite 的时候指定的参数
      if (configFile) {
        // explicit config path is always resolved from cwd
        // configFile 存在的话 则用这个路径来 resolve
        resolvedPath = path.resolve(configFile)
      } else {
        // implicit config file loaded from inline root (if present)
        // otherwise from cwd
        // 否则的话 从默认的 跟路径 process.cwd() 来resolve
        for (const filename of DEFAULT_CONFIG_FILES) {
          const filePath = path.resolve(configRoot, filename)
          if (!fs.existsSync(filePath)) continue
    
          resolvedPath = filePath
          break
        }
      }
    
    // 这不到 则返回 null ,同时,给出提示
      if (!resolvedPath) {
        debug?.('no config file found.')
        return null
      }
    // node/contants.ts export const DEFAULT_CONFIG_FILES = [ 'vite.config.js', 'vite.config.mjs', 'vite.config.ts', 'vite.config.cjs', 'vite.config.mts', 'vite.config.cts', ] // node/config.ts let resolvedPath: string | undefined // configfile 就是 传入的参数 也就是 在命令行启动 vite 的时候指定的参数 if (configFile) { // explicit config path is always resolved from cwd // configFile 存在的话 则用这个路径来 resolve resolvedPath = path.resolve(configFile) } else { // implicit config file loaded from inline root (if present) // otherwise from cwd // 否则的话 从默认的 跟路径 process.cwd() 来resolve for (const filename of DEFAULT_CONFIG_FILES) { const filePath = path.resolve(configRoot, filename) if (!fs.existsSync(filePath)) continue resolvedPath = filePath break } } // 这不到 则返回 null ,同时,给出提示 if (!resolvedPath) { debug?.('no config file found.') return null }

2. 识别配置文件的类别

  • let isESM = false
    // vite 首先会 检查 这个跟路径的命名,是否包含 mjs , cjs 的后缀,
    // 如果有的话,会修改isESM 的标识
    if (/\.m[jt]s$/.test(resolvedPath)) {
    isESM = true
    } else if (/\.c[jt]s$/.test(resolvedPath)) {
    isESM = false
    } else {
    // check package.json for type: "module" and set `isESM` to true
    // 没有的话 会查看 package.json 文件,
    // 如果有 type: "module"则打上 isESM 的标识
    try {
    const pkg = lookupFile(configRoot, ['package.json'])
    isESM =
    !!pkg && JSON.parse(fs.readFileSync(pkg, 'utf-8')).type === 'module'
    } catch (e) {}
    }
    let isESM = false
    // vite 首先会 检查 这个跟路径的命名,是否包含 mjs , cjs 的后缀,
    // 如果有的话,会修改isESM 的标识
      if (/\.m[jt]s$/.test(resolvedPath)) {
        isESM = true
      } else if (/\.c[jt]s$/.test(resolvedPath)) {
        isESM = false
      } else {
        // check package.json for type: "module" and set `isESM` to true
        // 没有的话 会查看 package.json 文件,
        // 如果有 type: "module"则打上 isESM 的标识
        try {
          const pkg = lookupFile(configRoot, ['package.json'])
          isESM =
            !!pkg && JSON.parse(fs.readFileSync(pkg, 'utf-8')).type === 'module'
        } catch (e) {}
      }
    let isESM = false // vite 首先会 检查 这个跟路径的命名,是否包含 mjs , cjs 的后缀, // 如果有的话,会修改isESM 的标识 if (/\.m[jt]s$/.test(resolvedPath)) { isESM = true } else if (/\.c[jt]s$/.test(resolvedPath)) { isESM = false } else { // check package.json for type: "module" and set `isESM` to true // 没有的话 会查看 package.json 文件, // 如果有 type: "module"则打上 isESM 的标识 try { const pkg = lookupFile(configRoot, ['package.json']) isESM = !!pkg && JSON.parse(fs.readFileSync(pkg, 'utf-8')).type === 'module' } catch (e) {} }

3. 利用 esbuild 打包,解析 配置文件

  • try {
    // 首先 用 esbuild 将配置文件 编译,打包为为 js 文件 (因为 可能为 ts 格式 所以需要先转一下)
    const bundled = await bundleConfigFile(resolvedPath, isESM)
    // 解析 打包后的配置文件 这个函数 详细信息在下面,
    // 主要就是 分为 esm cjs 格式去做不同的解析
    const userConfig = await loadConfigFromBundledFile(
    resolvedPath,
    bundled.code,
    isESM,
    )
    debug?.(`bundled config file loaded in ${getTime()}`)
    // 读取 配置文件后, 处理 是函数的情况
    const config = await (typeof userConfig === 'function'
    ? userConfig(configEnv)
    : userConfig)
    if (!isObject(config)) {
    throw new Error(`config must export or return an object.`)
    }
    // 接下来返回最终的配置信息
    return {
    path: normalizePath(resolvedPath),
    config,
    // esbuild 打包过程中收集的依赖信息
    dependencies: bundled.dependencies,
    }
    } catch (e) {
    createLogger(logLevel).error(
    colors.red(`failed to load config from ${resolvedPath}`),
    { error: e },
    )
    throw e
    }
    ......
    // loadConfigFromBundledFile 函数
    // 创建 require 函数 用于 下面的 cjs 格式配置文件处理
    // 这个 createRequire 方法 来自于 node:module
    const _require = createRequire(import.meta.url)
    async function loadConfigFromBundledFile(
    fileName: string,
    bundledCode: string,
    isESM: boolean,
    ): Promise<UserConfigExport> {
    // for esm, before we can register loaders without requiring users to run node
    // with --experimental-loader themselves, we have to do a hack here:
    // write it to disk, load it with native Node ESM, then delete the file.
    // 如果是 ESM格式,Vite 会将编译后的 js 代码写入临时文件,通过 Node 原生 ESM Import 来读取这个临时的内容,以获取到配置内容,再直接删掉临时文件
    if (isESM) {
    // import 路径结果要加上时间戳 query,是因为
    // 为了让 dev server 重启后仍然读取最新的配置,避免缓存
    const fileBase = `${fileName}.timestamp-${Date.now()}-${Math.random()
    .toString(16)
    .slice(2)}`
    const fileNameTmp = `${fileBase}.mjs`
    const fileUrl = `${pathToFileURL(fileBase)}.mjs`
    await fsp.writeFile(fileNameTmp, bundledCode)
    try {
    // 通过 Node 原生 ESM Import 来读取这个临时的内容,以获取到配置内容
    return (await dynamicImport(fileUrl)).default
    } finally {
    // 最后直接 删掉临时文件
    fs.unlink(fileNameTmp, () => {}) // Ignore errors
    }
    }
    // for cjs, we can register a custom loader via `_require.extensions`
    // 如果是 cjs 格式,那么主要的思路是
    // 通过拦截原生 require.extensions 的加载函数来实现对 bundle 后配置代码的加载
    else {
    // 默认加载器
    const extension = path.extname(fileName)
    // We don't use fsp.realpath() here because it has the same behaviour as
    // fs.realpath.native. On some Windows systems, it returns uppercase volume
    // letters (e.g. "C:\") while the Node.js loader uses lowercase volume letters.
    // See https://github.com/vitejs/vite/issues/12923
    // 拿到 promisifyed 过的真实的文件名字
    const realFileName = await promisifiedRealpath(fileName)
    // 默认 拦截原生 require 对于 js 文件的加载
    const loaderExt = extension in _require.extensions ? extension : '.js'
    // 先保存 一份 原来的 加载器 -> loader
    const defaultLoader = _require.extensions[loaderExt]!
    // 这里 进行 拦截,重写
    _require.extensions[loaderExt] = (module: NodeModule, filename: string) => {
    // 如果加载的文件 是 该配置文件 则 调用 module._compile 方法进行编译
    if (filename === realFileName) {
    ;(module as NodeModuleWithCompile)._compile(bundledCode, filename)
    } else {
    defaultLoader(module, filename)
    }
    }
    // clear cache in case of server restart
    delete _require.cache[_require.resolve(fileName)]
    // 编译后 再 进行一次手动的 require 即可拿到配置对象
    const raw = _require(fileName)
    // 恢复原生的加载方法
    _require.extensions[loaderExt] = defaultLoader
    return raw.__esModule ? raw.default : raw
    }
    }
    // node/utils.ts
    // 这里 注释已经给的很明显了 在非 jest 下 dynamicImport 返回的是
    // new Function('file', 'return import(file)')
    // @ts-expect-error jest only exists when running Jest
    export const usingDynamicImport = typeof jest === 'undefined'
    /**
    * Dynamically import files. It will make sure it's not being compiled away by TS/Rollup.
    *
    * As a temporary workaround for Jest's lack of stable ESM support, we fallback to require
    * if we're in a Jest environment.
    * See https://github.com/vitejs/vite/pull/5197#issuecomment-938054077
    *
    * @param file File path to import.
    */
    // 为什么不直接 import, 而是要用 new Function 包裹?
    // 这是为了避免打包工具处理这段代码,比如 Rollup 和 TSC,类似的手段还有 eval
    export const dynamicImport = usingDynamicImport
    ? new Function('file', 'return import(file)')
    : _require
    try {
      // 首先 用 esbuild 将配置文件 编译,打包为为 js 文件 (因为 可能为 ts 格式 所以需要先转一下)
        const bundled = await bundleConfigFile(resolvedPath, isESM)
        // 解析 打包后的配置文件 这个函数 详细信息在下面,
        // 主要就是 分为 esm cjs 格式去做不同的解析
        const userConfig = await loadConfigFromBundledFile(
          resolvedPath,
          bundled.code,
          isESM,
        )
        debug?.(`bundled config file loaded in ${getTime()}`)
    
      // 读取 配置文件后, 处理 是函数的情况 
        const config = await (typeof userConfig === 'function'
          ? userConfig(configEnv)
          : userConfig)
        if (!isObject(config)) {
          throw new Error(`config must export or return an object.`)
        }
      // 接下来返回最终的配置信息
        return {
          path: normalizePath(resolvedPath),
          config,
          // esbuild 打包过程中收集的依赖信息
          dependencies: bundled.dependencies,
        }
      } catch (e) {
        createLogger(logLevel).error(
          colors.red(`failed to load config from ${resolvedPath}`),
          { error: e },
        )
        throw e
      }
    
    ......
    // loadConfigFromBundledFile 函数
    
    // 创建 require 函数 用于 下面的 cjs 格式配置文件处理
    // 这个 createRequire 方法 来自于 node:module
    const _require = createRequire(import.meta.url)
    async function loadConfigFromBundledFile(
      fileName: string,
      bundledCode: string,
      isESM: boolean,
    ): Promise<UserConfigExport> {
      // for esm, before we can register loaders without requiring users to run node
      // with --experimental-loader themselves, we have to do a hack here:
      // write it to disk, load it with native Node ESM, then delete the file.
      // 如果是 ESM格式,Vite 会将编译后的 js 代码写入临时文件,通过 Node 原生 ESM Import 来读取这个临时的内容,以获取到配置内容,再直接删掉临时文件
        if (isESM) {
        // import 路径结果要加上时间戳 query,是因为
        // 为了让 dev server 重启后仍然读取最新的配置,避免缓存
        const fileBase = `${fileName}.timestamp-${Date.now()}-${Math.random()
          .toString(16)
          .slice(2)}`
        const fileNameTmp = `${fileBase}.mjs`
        const fileUrl = `${pathToFileURL(fileBase)}.mjs`
        await fsp.writeFile(fileNameTmp, bundledCode)
        try {
          // 通过 Node 原生 ESM Import 来读取这个临时的内容,以获取到配置内容
          return (await dynamicImport(fileUrl)).default
        } finally {
          // 最后直接 删掉临时文件
          fs.unlink(fileNameTmp, () => {}) // Ignore errors
        }
      }
        
      // for cjs, we can register a custom loader via `_require.extensions`
        // 如果是 cjs 格式,那么主要的思路是
        // 通过拦截原生 require.extensions 的加载函数来实现对 bundle 后配置代码的加载
      else {
        // 默认加载器
        const extension = path.extname(fileName)
        // We don't use fsp.realpath() here because it has the same behaviour as
        // fs.realpath.native. On some Windows systems, it returns uppercase volume
        // letters (e.g. "C:\") while the Node.js loader uses lowercase volume letters.
        // See https://github.com/vitejs/vite/issues/12923
        
        // 拿到 promisifyed 过的真实的文件名字
        const realFileName = await promisifiedRealpath(fileName)
        // 默认 拦截原生 require 对于 js 文件的加载
        const loaderExt = extension in _require.extensions ? extension : '.js'
        
        // 先保存 一份 原来的 加载器 -> loader
        const defaultLoader = _require.extensions[loaderExt]!
              
        // 这里 进行 拦截,重写
        _require.extensions[loaderExt] = (module: NodeModule, filename: string) => {
          // 如果加载的文件 是 该配置文件 则 调用 module._compile 方法进行编译
          if (filename === realFileName) {
            ;(module as NodeModuleWithCompile)._compile(bundledCode, filename)
          } else {
            defaultLoader(module, filename)
          }
        }
        // clear cache in case of server restart
        delete _require.cache[_require.resolve(fileName)]
        // 编译后 再 进行一次手动的 require 即可拿到配置对象
        const raw = _require(fileName)
        // 恢复原生的加载方法
        _require.extensions[loaderExt] = defaultLoader
        return raw.__esModule ? raw.default : raw
      }
    }
    
    
    
    
    // node/utils.ts
    // 这里 注释已经给的很明显了 在非 jest 下 dynamicImport 返回的是 
    // new Function('file', 'return import(file)')
    
    // @ts-expect-error jest only exists when running Jest
    export const usingDynamicImport = typeof jest === 'undefined'
    
    /**
     * Dynamically import files. It will make sure it's not being compiled away by TS/Rollup.
     *
     * As a temporary workaround for Jest's lack of stable ESM support, we fallback to require
     * if we're in a Jest environment.
     * See https://github.com/vitejs/vite/pull/5197#issuecomment-938054077
     *
     * @param file File path to import.
     */
    // 为什么不直接 import, 而是要用 new Function 包裹?
    // 这是为了避免打包工具处理这段代码,比如 Rollup 和 TSC,类似的手段还有 eval
    export const dynamicImport = usingDynamicImport
      ? new Function('file', 'return import(file)')
      : _require
    try { // 首先 用 esbuild 将配置文件 编译,打包为为 js 文件 (因为 可能为 ts 格式 所以需要先转一下) const bundled = await bundleConfigFile(resolvedPath, isESM) // 解析 打包后的配置文件 这个函数 详细信息在下面, // 主要就是 分为 esm cjs 格式去做不同的解析 const userConfig = await loadConfigFromBundledFile( resolvedPath, bundled.code, isESM, ) debug?.(`bundled config file loaded in ${getTime()}`) // 读取 配置文件后, 处理 是函数的情况 const config = await (typeof userConfig === 'function' ? userConfig(configEnv) : userConfig) if (!isObject(config)) { throw new Error(`config must export or return an object.`) } // 接下来返回最终的配置信息 return { path: normalizePath(resolvedPath), config, // esbuild 打包过程中收集的依赖信息 dependencies: bundled.dependencies, } } catch (e) { createLogger(logLevel).error( colors.red(`failed to load config from ${resolvedPath}`), { error: e }, ) throw e } ...... // loadConfigFromBundledFile 函数 // 创建 require 函数 用于 下面的 cjs 格式配置文件处理 // 这个 createRequire 方法 来自于 node:module const _require = createRequire(import.meta.url) async function loadConfigFromBundledFile( fileName: string, bundledCode: string, isESM: boolean, ): Promise<UserConfigExport> { // for esm, before we can register loaders without requiring users to run node // with --experimental-loader themselves, we have to do a hack here: // write it to disk, load it with native Node ESM, then delete the file. // 如果是 ESM格式,Vite 会将编译后的 js 代码写入临时文件,通过 Node 原生 ESM Import 来读取这个临时的内容,以获取到配置内容,再直接删掉临时文件 if (isESM) { // import 路径结果要加上时间戳 query,是因为 // 为了让 dev server 重启后仍然读取最新的配置,避免缓存 const fileBase = `${fileName}.timestamp-${Date.now()}-${Math.random() .toString(16) .slice(2)}` const fileNameTmp = `${fileBase}.mjs` const fileUrl = `${pathToFileURL(fileBase)}.mjs` await fsp.writeFile(fileNameTmp, bundledCode) try { // 通过 Node 原生 ESM Import 来读取这个临时的内容,以获取到配置内容 return (await dynamicImport(fileUrl)).default } finally { // 最后直接 删掉临时文件 fs.unlink(fileNameTmp, () => {}) // Ignore errors } } // for cjs, we can register a custom loader via `_require.extensions` // 如果是 cjs 格式,那么主要的思路是 // 通过拦截原生 require.extensions 的加载函数来实现对 bundle 后配置代码的加载 else { // 默认加载器 const extension = path.extname(fileName) // We don't use fsp.realpath() here because it has the same behaviour as // fs.realpath.native. On some Windows systems, it returns uppercase volume // letters (e.g. "C:\") while the Node.js loader uses lowercase volume letters. // See https://github.com/vitejs/vite/issues/12923 // 拿到 promisifyed 过的真实的文件名字 const realFileName = await promisifiedRealpath(fileName) // 默认 拦截原生 require 对于 js 文件的加载 const loaderExt = extension in _require.extensions ? extension : '.js' // 先保存 一份 原来的 加载器 -> loader const defaultLoader = _require.extensions[loaderExt]! // 这里 进行 拦截,重写 _require.extensions[loaderExt] = (module: NodeModule, filename: string) => { // 如果加载的文件 是 该配置文件 则 调用 module._compile 方法进行编译 if (filename === realFileName) { ;(module as NodeModuleWithCompile)._compile(bundledCode, filename) } else { defaultLoader(module, filename) } } // clear cache in case of server restart delete _require.cache[_require.resolve(fileName)] // 编译后 再 进行一次手动的 require 即可拿到配置对象 const raw = _require(fileName) // 恢复原生的加载方法 _require.extensions[loaderExt] = defaultLoader return raw.__esModule ? raw.default : raw } } // node/utils.ts // 这里 注释已经给的很明显了 在非 jest 下 dynamicImport 返回的是 // new Function('file', 'return import(file)') // @ts-expect-error jest only exists when running Jest export const usingDynamicImport = typeof jest === 'undefined' /** * Dynamically import files. It will make sure it's not being compiled away by TS/Rollup. * * As a temporary workaround for Jest's lack of stable ESM support, we fallback to require * if we're in a Jest environment. * See https://github.com/vitejs/vite/pull/5197#issuecomment-938054077 * * @param file File path to import. */ // 为什么不直接 import, 而是要用 new Function 包裹? // 这是为了避免打包工具处理这段代码,比如 Rollup 和 TSC,类似的手段还有 eval export const dynamicImport = usingDynamicImport ? new Function('file', 'return import(file)') : _require
  • 在处理 ESM类型的配置文件时,采用的是将bundle(打包编译)后的 js 代码写入临时文件,通过 Node 原生 ESM Import 来读取这个临时的内容,以获取到配置内容,再直接删掉临时文件

    • 这种先编译配置文件,再将产物写入临时目录,最后加载临时目录产物的做法,也是 AOT (Ahead Of Time)编译技术的一种具体实现
  • 在处理 CJS类型的配置文件时, 采用的是拦截原生 require.extensions 的加载函数来实现对 bundle(打包编译) 后的 js 代码的加载

    • 这种运行时加载 JS配置的方式,也叫做 JIT(即时编译),这种方式和 AOT 最大的区别在于不会将内存中计算出来的 js 代码写入磁盘再加载,而是通过拦截 Node.js 原生 require.extension 方法实现即时加载

总结

  • 主要梳理了 Vite 配置解析的整体流程加载配置文件的方法

  • Vite 配置文件解析的逻辑由 resolveConfig 函数统一实现

    • 经历了加载配置文件、解析用户插件、加载环境变量、创建路径解析器工厂和生成插件流水线这几个主要的流程
  • 加载配置文件的过程中,Vite 需要处理四种类型的配置文件((TS, JS)-(ESM, CJS)

    • 首先先 用 esbuildTS代码 打包编译为 JS代码
    • 其中对于 ESMCJS 两种格式文件,分别采用了AOTJIT两种编译技术实现了配置加载
  • 学习链接

© 版权声明
THE END
喜欢就支持一下吧
点赞0

Warning: mysqli_query(): (HY000/3): Error writing file '/tmp/MYhhZYpu' (Errcode: 28 - No space left on device) in /www/wwwroot/583.cn/wp-includes/class-wpdb.php on line 2345
admin的头像-五八三
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

图形验证码
取消
昵称代码图片