五千字剖析 vite 是如何对配置文件进行解析的

3,603 阅读16分钟

这篇文章,主要是分析一下,vite 是如何解析它的配置的,我们定义的 vite.config.ts 配置文件,最终会被转换成什么样子,被 vite 的整个执行过程中使用。

学习完 vite 的配置解析,大家能够:

  • 配置解析、框架/库的扩展性有一定的理解,
  • 有能力自己实现一套自己的框架/库配置解析器
  • 能模仿 vite ,实现一套简单的插件机制

概念约定

在讲文章之前,先来说说,vite 的配置是什么,怎么分类

vite 的配置分为 InlineConfigUserConfigResolvedConfig

  • InlineConfig:命令行中执行 vite 命令时,传入的配置。如:vite dev --force
  • UserConfig:用户侧的配置对象,写在 vite 的配置文件中。
  • ResolvedConfig:vite 解析后的配置,vite 的整个运行流程都会被用到该配置。

它们的关系如下:

image-20220529214819223

由于该文章主要讲配置解析,不关心配置解析完成之后,要怎么被使用

因此,我们其实也不必关心 ResolveConfig 的具体结构是什么,该怎么用,我们可以把重点放在读取配置、合并 + 解析这两个部分

流程图

image-20220529212805579

对应的代码结构如下,我们只保留主干部分,先忽略细节

function resolveConfig(
  inlineConfig: InlineConfig,
  command: 'build' | 'serve',
  defaultMode = 'development'
){
      
    // 1. 读取配置文件
    let config = inlineConfig
    const loadResult = await loadConfigFromFile()
    config = mergeConfig(loadResult.config, config)
 
    // 2. 解析插件
    
    // 3. 读取环境变量文件
    
    // 4. 合成 ResolvedConfig
    const resolved: ResolvedConfig = {
        // 省略 ResolveConfig 的属性
        // ...
    }
    
    return resolved
}

接下来我们会从这几个部分讲解:

  1. 读取配置文件
  2. 解析插件
  3. 读取环境变量文件
  4. 合成 ResolvedConfig

读取配置文件

目标:在 vite 运行过程中,获取配置文件中定义的对象。

先来思考一个问题,如果是自己手写,我们该如何实现读取配置文件的能力?

有的小伙伴可能会说,我直接用 require 就可以了,实现如下:

// vite.config.js
module.exports = {
    // 配置内容
}

// 读取配置
const config = require('./config.js')

这样的确能够读取到配置内容,但这样做是有缺点的:

  • 使用 require 加载配置文件,无法兼容 ES6 import 语法
  • vite 还支持 ts 语法的配置文件,require 无法处理 ts 文件

要解决以上两个问题,复杂度好像就高了那么一点点了。

那么 vite 是如何实现多种模块规范,支持 js 和 ts 配置文件的呢?

答案是:将配置文件进行编译,编译成 ES6 module,然后 import 引入

下面来看看整个大的处理过程:

  1. 确定配置文件的格式
    • 是否为 ESM
    • 是否为 TS
  2. 加载配置文件
  3. 返回配置文件信息

函数的大致流程如下(具体细节会在后面讲):

export async function loadConfigFromFile(
  configEnv: ConfigEnv,			// 'build' | 'serve'
  configFile?: string,			// 指定的配置文件
  configRoot: string = process.cwd(),
) {
  let resolvedPath: string | undefined		// 配置文件的真实路径
  let isTS = false		// 标记配置文件是否为 ts
  let isESM = false		// 标记配置文件是否文 ESM
  let dependencies: string[] = []	// 配置文件的依赖
  
  // 1. 确定配置文件的格式
  
  
  // 2. 加载配置文件,根据不同的格式,有不同的加载方法
  if(isESM){
  	if(isTS){
        userConfig = // 加载 TS 配置文件
    }else{
        userConfig = // 加载普通的 ESM 配置文件
    }
  }
        
  if(!userConfig){
      userConfig = // 加载普通的 CJS 格式的配置文件
  }
  
  // 如果配置是函数,则调用,其返回值作为配置
  const config = await (typeof userConfig === 'function'
    ? userConfig(configEnv)
    : userConfig)

  // 3. 返回配置文件信息
  return {
    path: normalizePath(resolvedPath),
    config,
    dependencies
  }    
}

确定配置文件的格式

尝试各种后缀的配置文件,来确定配置文件的格式。

输出就是 isESMisTS 这两个变量,后面会根据这两个变量,执行不同的代码

// 沿着运行目录往上查找,找到最近的 package.json,确定是否为 ESM
try {
    const pkg = lookupFile(configRoot, ['package.json'])
    if (pkg && JSON.parse(pkg).type === 'module') {
        isESM = true
    }
} catch (e) {}


// 有指定配置文件
if (configFile) {
    resolvedPath = path.resolve(configFile)
    // 根据后缀判断是否为 ts
    isTS = configFile.endsWith('.ts')

    // 根据后缀判断是否为 ESM
    if (configFile.endsWith('.mjs')) {
        isESM = true
    }
} else {
    // 没有指定配置文件

    // 尝试使用 vite.config.js
    const jsconfigFile = path.resolve(configRoot, 'vite.config.js')
    if (fs.existsSync(jsconfigFile)) {
        resolvedPath = jsconfigFile
    }

    // 尝试使用 vite.config.mjs
    if (!resolvedPath) {
        const mjsconfigFile = path.resolve(configRoot, 'vite.config.mjs')
        if (fs.existsSync(mjsconfigFile)) {
            resolvedPath = mjsconfigFile
            isESM = true
        }
    }

    // 尝试使用 vite.config.ts
    if (!resolvedPath) {
        const tsconfigFile = path.resolve(configRoot, 'vite.config.ts')
        if (fs.existsSync(tsconfigFile)) {
            resolvedPath = tsconfigFile
            isTS = true
        }
    }

    // 尝试使用 vite.config.cjs
    if (!resolvedPath) {
        const cjsConfigFile = path.resolve(configRoot, 'vite.config.cjs')
        if (fs.existsSync(cjsConfigFile)) {
            resolvedPath = cjsConfigFile
            isESM = false
        }
    }
}

这里没什么好的办法,就是一个个文件看它存不存在

加载 ESM 模块

esm 的处理如下,最终是设置 userConfigdependencies 变量

// ESM 处理
if (isESM) {

    // 生成配置文件的 url,例如 file:///foo/bar
    const fileUrl = require('url').pathToFileURL(resolvedPath)

    // 对配置文件进行打包,输出 code 代码文本和 dependencies 该文件的依赖
    // 后面会解析具体是怎么实现的,当前只需要知道输入输出即可
    const bundled = await bundleConfigFile(resolvedPath, true)

    dependencies = bundled.dependencies

    // ts 文件处理
    if (isTS) {

        // 将编译的 code 文本,写到本地文件
        // 用 import 引用
        // 再删除文件
        fs.writeFileSync(resolvedPath + '.js', bundled.code)
        userConfig = (await dynamicImport(`${fileUrl}.js?t=${Date.now()}`))
            .default
        fs.unlinkSync(resolvedPath + '.js')

    } else {
        // 直接 import 引入配置文件
        // 因为配置文件格式本身就是 ESM,可以直接 import
        // 在之前进行打包,是因为要获取 dependencies
        userConfig = (await dynamicImport(`${fileUrl}?t=${Date.now()}`)).default
    }
}

ESM 可以直接通过动态 import 函数,引入配置文件

dynamicImport 函数的实现如下:

export const dynamicImport = new Function('file', 'return import(file)')

实际上,就是用 await import(package),引入一个 es module

引入 ESM,直接使用动态 import 就行了,为什么要封装成 dynamicImport ?

用 new Function 实现的动态 import,在构建打包 vite 源码时,不会被 Rollup 打包到 vite 的构建产物中。

为什么不能一起打包?

  • 配置文件,不属于 vite 源码的一部分,不是 vite 源码的依赖,不能打包到 vite 源码
  • 配置文件在 vite 源码打包过程中,并不存在
  • 配置文件是在 vite 实际运行中,才被动态引入的

这里还要区分 vite 源码打包过程和 vite 打包项目的过程:

  • vite 源码打包:打包产物是 vite 这个工具的代码
  • vite 项目打包:打包产物是项目的代码,该过程才会有 vite 配置文件

打包配置文件

使用 esbuild API,对配置文件进行打包。目的是转换 TS 语法和获取参与打包的本地文件依赖

  • TS 语法转换,这个打包一下,就变成 js
  • 获取参与打包的本地文件依赖,可以从打包结果的 meta 数据中拿到。用于配置的热更新,参与打包的文件依赖改变,需要自动重启

下面是打包对的过程,用得是 esbuild

esbuild 参数比较多,其实这部分不需要过多关注,我们要理解以下两点即可:

  • 理解插件的作用
  • 理解 esbuild 的构建结果
async function bundleConfigFile(
  fileName: string,
  isESM = false
): Promise<{ code: string; dependencies: string[] }> {
  const result = await build({
    absWorkingDir: process.cwd(),
    entryPoints: [fileName],
    outfile: 'out.js',
    write: false,
    platform: 'node',
    bundle: true,
    // 编译输出的格式
    format: isESM ? 'esm' : 'cjs',
    sourcemap: 'inline',
    metafile: true,
    plugins: [
      // 对裸模块,进行 external 处理,即不打包到 bundle
      {
        name: 'externalize-deps',
        setup(build) {
          build.onResolve({ filter: /.*/ }, (args) => {
            const id = args.path
            if (id[0] !== '.' && !path.isAbsolute(id)) {
              return {
                external: true
              }
            }
          })
        }
      },
	  // 省略其他插件
    ]
  })
  const { text } = result.outputFiles[0]
  return {
    code: text,
    dependencies: result.metafile ? Object.keys(result.metafile.inputs) : []
  }
}

我们来看看,里面写了一个 esbuild 插件,有什么用?

{
    name: 'externalize-deps',
    setup(build) {
        // 当引入另外一个模块时,如果匹配 filter 的正则表达式,则执行后面定义的回调
        build.onResolve({ filter: /.*/ }, (args) => {
            // 获取引入模块的路径
            const id = args.path
            // 如果不是 . 开头的路径/模块,且不是绝对路径,则设置为 external
            if (id[0] !== '.' && !path.isAbsolute(id)) {
                return {
                    external: true
                }
            }
        })
    }
},

filter 为 /.*/,就是匹配所有引入的模块,当 import 一个模块时(本地模块,npm 模块),就会执行该回调

回调函数的作用是:对所有裸模块,进行 external 标记

什么是裸模块?

英文为 bare import。没有任何路径的模块,例如我们使用的 vue 时,是直接 import { createApp } from "vue",vue 就是没有任何路径的模块。

相反,我们通过相对路径和绝对路径,引入的模块,就不是裸模块。

通常 npm 第三方依赖用裸模块的方式引入,本地模块用相对路径和绝对路径

什么是 external 标记?

一个模块被设置为 external 之后,它的代码就不会被打包工具打包到我们的代码中,仍然作为外部依赖被引入。

假设有如下代码

import { createApp } from "vue" 
console.log(createApp)

当 vue 被 external 之后,vue 不会被打包到产物代码中,仍然是如下代码

import { createApp } from "vue" 
console.log(createApp)

如果没有 external,则不再 import vue 模块,而是将代码直接写到输出产物的代码中

function createApp(){
    // createApp 函数的源码
}
console.log(createApp)

为什么需要使用 external 标记?

因为配置热更新,只需要监听本地配置文件及本地依赖的更改,不需要监听 npm 包的改变

我们来看看一个真实的例子:

下面是一个 vite.config.ts 的代码:

// vite.config.ts
import { defineConfig, splitVendorChunkPlugin } from 'vite'
import vuePlugin from '@vitejs/plugin-vue'
import { vueI18nPlugin } from './CustomBlockPlugin'

export default defineConfig({
  plugins: [
    vuePlugin({
      reactivityTransform: true
    }),
    splitVendorChunkPlugin(),
    vueI18nPlugin
  ],
  // 省略其他配置
})

经过 bundleConfigFile 函数的处理(并非 esbuild 的执行结果,bundleConfigFile 函数只取了部分的 esbuild 打包结果),有以下的执行结果:

{
    code: '打包后的 js 代码文本',
    dependencies: ["CustomBlockPlugin.ts", "vite.config.ts"]
}

dependencies 是参与打包的文件(依赖),取值为 Object.keys(result.metafile.inputs),裸模块(第三方模块)并没有被打包进来

因此,一般情况下,dependencies 只有本地写的配置文件及本地依赖。

image-20220529225432252

dependencies 有什么用?

dependencies 用于热更新,当配置被修改时,vite 会重新加载配置,重启 dev Server

因此,当我们修改 vite 配置文件时,它会自动读取配置,重启 server,这一点比 webpack 是更优的

理解了 dependencies 的作用之后,我们才能理解,要external 裸模块,最重要的原因,是不需要对第三方依赖进行热更新的监听

加载 cjs 模块

// 如果还没有 UserConfig,就当做 cjs 处理。js/cjs 后缀的配置文件
if (!userConfig) {
    // 打包配置文件,获取 code 和 dependencies
    const bundled = await bundleConfigFile(resolvedPath)
    dependencies = bundled.dependencies
    // 用 require 引入配置文件
    userConfig = await loadConfigFromBundledFile(resolvedPath, bundled.code)
}

js/cjs 同样需要对配置文件进行构建,主要目的还是获取到 dependencies,用于配置热更新。

js/cjs 能不能通过 require 引入?

理论上,直接用 require 直接引入配置文件即可。

js 文件,可以使用 cjs 和 import 语法的其中一种,这取决于package.jsontype 字段的值是否为module

如果 package.json 没有声明 type: modulenode require js 文件时,也只能使用 cjs 语法,开发者编写 js 文件时必须使用 cjs。

但实际情况,我们更多时候是配合打包工具一起开发的

image-20220603151922662

我们在写 vue/react 等项目时,往往是没有在 package.json 声明 type: module,但仍然可以使用 import 语法,这是因为我们写的页面代码,会经过打包工具编译打包

因此用户很有可能,在 vite.config.js 中,并没有遵守使用 cjs 的这一规则,使用了 import 语法,这时候直接 require 就会报错(因为运行时的 js 会被打包,但 vite.config.js 并没有在运行时引入 )。

如果要兼容这一情况,就需要手动将配置文件,编译成 cjs 语法

因此,vite.config.js 配置文件由于可能使用 ES6 module ,也需要进行编译

bundleConfigFile 函数,会将配置文件,编译成 cjs 格式

async function bundleConfigFile(
  fileName: string,
  isESM = false
): Promise<{ code: string; dependencies: string[] }> {
  const result = await build({
    // 省略其他配置
    // 编译输出的格式,默认是 cjs
    format: isESM ? 'esm' : 'cjs',
    plugins: [
	  // 省略插件
    ]
  })
  const { text } = result.outputFiles[0]
  return {
    code: text,
    dependencies: result.metafile ? Object.keys(result.metafile.inputs) : []
  }
}

编译好的代码,可以直接 require 了吗?

可以,但需要先写入到文件系统,然后再通过文件路径 require

vite 使用了一种更加简单的方法,临时重写 node require 的行为,直接使用内存中编译好的代码字符串

在讲 loadConfigFromBundledFile 函数之前,我们先来大概看看,node require 做了什么?

require 文件时,会根据文件后缀,执行不同的 extensions 回调方法,其中 js 方法如下(节选部分代码):

Module._extensions['.js'] = function (module, filename) {
  var content = fs.readFileSync(filename, 'utf8')
  module._compile(stripBOM(content), filename)
}
  1. 读取文件,获取文件的内容字符串
  2. 执行 compile 方法

_compile 函数核心步骤如下:

Module.prototype._compile = function (content, filename) {
  var self = this
  var args = [self.exports, require, self, filename, dirname]
  return compiledWrapper.apply(self.exports, args)
}

假设引入的代码如下:

module.exports.foo = 'bar'

执行了 compiledWrapper 方法,相当于执行以下代码

;(function (exports, require, module, __filename, __dirname) {
  // 执行 module._compile 方法中传入的代码
  // 相当于执行了 module.exports.foo = 'bar' 
  // module.exports 就已经有了 foo 属性,相当于已经导入模块成功了
    
    
  // 返回 exports 对象
})

最后返回整个 exports 对象,这时就 require 就基本完成了,因为模块的变量已经写到了 exports。

如何重写 require 的导入行为?

我们来看看 loadConfigFromBundledFile 的实现如下:

async function loadConfigFromBundledFile(
  fileName: string,
  bundledCode: string
): Promise<UserConfig> {
  const extension = path.extname(fileName)
  
  // 保存老的 require 行为
  const defaultLoader = require.extensions[extension]!
  
  // 临时重写当前配置文件后缀的 require 行为
  require.extensions[extension] = (module: NodeModule, filename: string) => {
    // 只处理配置文件
    if (filename === fileName) {
      // 直接调用 compile,传入编译好的代码
      ;(module as NodeModuleWithCompile)._compile(bundledCode, filename)
    } else {
      defaultLoader(module, filename)
    }
  }
  // 清除缓存
  delete require.cache[require.resolve(fileName)]
  const raw = require(fileName)
  const config = raw.__esModule ? raw.default : raw
  require.extensions[extension] = defaultLoader
  return config
}

重写 require 行为,核心思路是,不从文件系统中读取模块,直接调用 compile 传入编译好的代码即可

__esModule 有什么用?

如果 vite.config.js 之前是 ES6 module,使用了 export default,现在编译成 cjs,那么 __esModule 属性就为 true

__esModule 属性,是编译器写进去的。

更多细节可以查看这篇文章

在 vite 运行过程中编译 TS 配置文件,这种方式叫 JIT(Just-in-time,即时编译),与 AOT(Ahead Of Time,预先编译)不同的是,JIT 不会将内存中编译好的 js 代码写到磁盘

解析插件

function resolveConfig(
  inlineConfig: InlineConfig,
  command: 'build' | 'serve',
  defaultMode = 'development'
){
      
    // 读取配置文件
 
    // 解析插件
    // 过滤掉不使用的插件
    const rawUserPlugins = (config.plugins || []).flat(Infinity).filter((p) => {
      if (!p) {
        return false
      } else if (!p.apply) {
        return true
      } else if (typeof p.apply === 'function') {
        return p.apply({ ...config, mode }, configEnv)
      } else {
        return p.apply === command
      }
    }) as Plugin[]

    // 将用户插件分类
    const [prePlugins, normalPlugins, postPlugins] = sortUserPlugins(rawUserPlugins)

    // 重新组合用户插件的顺序
    const userPlugins = [...prePlugins, ...normalPlugins, ...postPlugins]
    // 执行插件 config 钩子,并将返回的配置,与原配置合并
    for (const p of userPlugins) {
        if (p.config) {
            const res = await p.config(config, configEnv)
            if (res) {
                config = mergeConfig(config, res)
            }
        }
    }
    
    // 读取环境变量文件
    
    // 合成 ResolveConfig
    
    return resolved
}

插件顺序

由于 vite 的插件有一套简单的顺序控制机制,因此需要对用户传入的插件,进行顺序的调整,调整规则如下:

  • 带有 enforce: 'pre' 的用户插件
  • 没有设置 enforce 的用户插件
  • 带有 enforce: 'post' 的用户插件

sortUserPlugins 函数,就是为了实现用户插件的分类,分成 prenormalpost,最后将这三组插件组合,就是一组调整好顺序的用户插件了

export function sortUserPlugins(
  plugins: (Plugin | Plugin[])[] | undefined
): [Plugin[], Plugin[], Plugin[]] {
  const prePlugins: Plugin[] = []
  const postPlugins: Plugin[] = []
  const normalPlugins: Plugin[] = []

  if (plugins) {
    plugins.flat().forEach((p) => {
      if (p.enforce === 'pre') prePlugins.push(p)
      else if (p.enforce === 'post') postPlugins.push(p)
      else normalPlugins.push(p)
    })
  }

  return [prePlugins, normalPlugins, postPlugins]
}

config 钩子

执行插件内部定义的 config 钩子。

config 钩子的作用,是让插件能够修改 vite 的用户配置,这种通过外部插件,修改了 vite 的配置,从而改变 vite 的行为,就是一种扩展性的体现

for (const p of userPlugins) {
    if (p.config) {
        const res = await p.config(config, configEnv)
        if (res) {
            config = mergeConfig(config, res)
        }
    }
}

可以通过两种方式,修改用户配置:

  1. 给 config 钩子设置返回值,返回的配置,会跟用户配置进行合并(推荐)
  2. 直接修改 config 钩子的入参 config 对象(在 mergeConfig 不能达到预期效果时使用)

当我们想要实现一套可扩展性框架的时候,我们也可以通过插件机制,通过 config 钩子,让插件能够修改用户的配置,提高框架的可扩展性

读取环境变文件

function resolveConfig(
  inlineConfig: InlineConfig,
  command: 'build' | 'serve',
  defaultMode = 'development'
){
      
    // 读取配置文件
 
    // 解析插件
    
    // 读取环境变量文件
    const envDir = config.envDir
      ? normalizePath(path.resolve(resolvedRoot, config.envDir))
      : resolvedRoot
    const userEnv =
      inlineConfig.envFile !== false &&
      loadEnv(mode, envDir, 'VITE_')
    
    // 合成 ResolveConfig
    
    return resolved
}

这部分比较简单,从配置中读取 envDir,配置文件所在的目录,然后调用 loadEnv 去读取环境变量

loadEnv 函数的实现如下:

export function loadEnv(
  mode: string,
  envDir: string,
  prefixes: string | string[] = 'VITE_'
): Record<string, string> {

  // 将 prefixes 转换成数组,例如 'VITE_' 会转换成 ['VITE_'], ['VITE_'] 则不变
  prefixes = arraify(prefixes)
  const env: Record<string, string> = {}
  
  // 要读取的环境变量文件
  const envFiles = [
    /** mode local file */ `.env.${mode}.local`,
    /** mode file */ `.env.${mode}`,
    /** local file */ `.env.local`,
    /** default file */ `.env`
  ]
  
  // 遍历 process.env 的环境变量
  // 优先从 process.env 中读取 prefix 开头的环境变量
  for (const key in process.env) {
    if (
      prefixes.some((prefix) => key.startsWith(prefix)) &&
      env[key] === undefined
    ) {
      env[key] = process.env[key] as string
    }
  }

  for (const file of envFiles) {
    // 找到最近的 file,找不到就往父目录找,直到找到位置或根目录也没有
    const path = lookupFile(envDir, [file], { pathOnly: true, rootDir: envDir })
    if (path) {
      // 用 dotenv 解析环境变量文件
      const parsed = dotenv.parse(fs.readFileSync(path))

      // 使环境变量,可以使用动态字符串格式
      dotenvExpand({
        parsed,
        // 防止写入到 process.env
        ignoreProcessEnv: true
      } as any)

      // 只有以 prefix 开头的环境变量,才会暴露给页面
      // 如果 env[key] 有值,则证明已经从 process.env 环境变量中读取过了,优先使用
      for (const [key, value] of Object.entries(parsed)) {
        if (
          prefixes.some((prefix) => key.startsWith(prefix)) &&
          env[key] === undefined
        ) {
          env[key] = value
        }
      }
    }
  }
    
  // 返回 prefix 开头的环境变量
  return env
}

loadEnv 用 dotenv 包读取环境变量,用 dotenv-expand 包扩展环境变量的语法,使其能支持动态字符串格式

.env 文件来说明,loadEnv 函数的行为

VITE_TEST_2=123
VITE_TEST_3=VITE_TEST_3_${VITE_TEST_2}

该文件,经过 dotenv 处理后,会是如下的结构:

{
    VITE_TEST_2: "123",
    VITE_TEST_3: "VITE_TEST_3_${VITE_TEST_2}"
}

dotenv 不支持动态字符串格式,因此要用 dotenv-expand 处理,处理的结果如下:

{
    VITE_TEST_2: "123",
    VITE_TEST_3: "VITE_TEST_3_123"
}

image-20220601193059562

合成 ResolvedConfig

这一小节不会细讲,因为 ResolvedConfig 的属性,在解析过程都是用不上的,它是给 vite 的其他流程使用的

下面代码不需要细看,只需要知道,我们之前处理了一些配置,然后将这些配置组合成 ResolvedConfig,然后作为 resolveConfig 的返回,即可

const resolved: ResolvedConfig = {
    ...config,
    configFile: configFile ? normalizePath(configFile) : undefined,
    configFileDependencies: configFileDependencies.map((name) =>
        normalizePath(path.resolve(name))
    ),
    inlineConfig,
    root: resolvedRoot,
    base: BASE_URL,
    resolve: resolveOptions,
    publicDir: resolvedPublicDir,
    cacheDir,
    command,
    mode,
    isWorker: false,
    isProduction,
    plugins: userPlugins,
    server,
    build: resolvedBuildOptions,
    preview: resolvePreviewOptions(config.preview, server),
    env: {
        ...userEnv,
        BASE_URL,
        MODE: mode,
        DEV: !isProduction,
        PROD: isProduction
    },
    assetsInclude(file: string) {
        return DEFAULT_ASSETS_RE.test(file) || assetsFilter(file)
    },
    logger,
    packageCache: new Map(),
    createResolver,
    optimizeDeps: {
        ...optimizeDeps,
        esbuildOptions: {
            keepNames: optimizeDeps.keepNames,
            preserveSymlinks: config.resolve?.preserveSymlinks,
            ...optimizeDeps.esbuildOptions
        }
    },
    worker: resolvedWorkerOptions
}

ResolvedConfig 对象被创建之后,还会执行插件的 configResolved 钩子

// 调用 configResolved 钩子
await Promise.all(userPlugins.map((p) => p.configResolved?.(resolved)))

总结

过完了一遍 vite 的配置解析的流程,我们用下图再总结一下

image-20220601195734804

另外,我们还对 vite 的扩展性,做了一些分析。

在配置解析过程中,vite 通过插件钩子,提供了扩展性,这个体现在:

  • 第三方插件,能够通过钩子,在 vite 的运行过程中,与 vite 进行通讯
  • 第三方插件,能够通过 config 钩子,对 vite 的配置进行二次修改,修改最终的解析配置,从而可以改变 vite 的行为。
  • 第三方插件,能够通过 configResolved 钩子,获取到 vite 最终解析出来的配置并保存起来,这使插件能够根据 vite 配置,在其他 vite 钩子中,实现复杂的插件行为

最后

如果这篇文章对您有所帮助,请帮忙点个赞👍,您的鼓励是我创作路上的最大的动力。