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

語言: CN / TW / HK

這篇文章,主要是分析一下,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

對應的代碼結構如下,我們只保留主幹部分,先忽略細節

```typescript 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 就可以了,實現如下:

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

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

這樣的確能夠讀取到配置內容,但這樣做是有缺點的:

  • 使用 require 加載配置文件,無法兼容 ES6 import 語法
  • vite 還支持 ts 語法的配置文件,require 無法處理 ts 文件

要解決以上兩個問題,複雜度好像就高了那麼一點點了。

那麼 vite 是如何實現多種模塊規範,支持 js 和 ts 配置文件的呢?

答案是:將配置文件進行編譯,編譯成 ES6 module,然後 import 引入

下面來看看整個大的處理過程:

  1. 確定配置文件的格式
  2. 是否為 ESM
  3. 是否為 TS
  4. 加載配置文件
  5. 返回配置文件信息

函數的大致流程如下(具體細節會在後面講):

```typescript 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 這兩個變量,後面會根據這兩個變量,執行不同的代碼

```typescript // 沿着運行目錄往上查找,找到最近的 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 變量

```typescript // 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 函數的實現如下:

typescript 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 的構建結果

typescript 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 插件,有什麼用?

typescript { 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 之後,它的代碼就不會被打包工具打包到我們的代碼中,仍然作為外部依賴被引入。

假設有如下代碼

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

當 vue 被 external 之後,vue 不會被打包到產物代碼中,仍然是如下代碼

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

如果沒有 external,則不再 import vue 模塊,而是將代碼直接寫到輸出產物的代碼中

typescript function createApp(){ // createApp 函數的源碼 } console.log(createApp)

為什麼需要使用 external 標記?

因為配置熱更新,只需要監聽本地配置文件及本地依賴的更改,不需要監聽 npm 包的改變

我們來看看一個真實的例子:

下面是一個 vite.config.ts 的代碼:

```typescript // 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 打包結果),有以下的執行結果:

typescript { 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 模塊

typescript // 如果還沒有 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 格式

typescript 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 方法如下(節選部分代碼):

typescript Module._extensions['.js'] = function (module, filename) { var content = fs.readFileSync(filename, 'utf8') module._compile(stripBOM(content), filename) }

  1. 讀取文件,獲取文件的內容字符串
  2. 執行 compile 方法

_compile 函數核心步驟如下:

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

假設引入的代碼如下:

typescript module.exports.foo = 'bar'

執行了 compiledWrapper 方法,相當於執行以下代碼

```typescript ;(function (exports, require, module, __filename, __dirname) { // 執行 module._compile 方法中傳入的代碼 // 相當於執行了 module.exports.foo = 'bar' // module.exports 就已經有了 foo 屬性,相當於已經導入模塊成功了

// 返回 exports 對象 }) ```

最後返回整個 exports 對象,這時就 require 就基本完成了,因為模塊的變量已經寫到了 exports。

如何重寫 require 的導入行為?

我們來看看 loadConfigFromBundledFile 的實現如下:

```typescript async function loadConfigFromBundledFile( fileName: string, bundledCode: string ): Promise { 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 代碼寫到磁盤

解析插件

```typescript 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,最後將這三組插件組合,就是一組調整好順序的用户插件了

```typescript 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 的行為,就是一種擴展性的體現

typescript 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 鈎子,讓插件能夠修改用户的配置,提高框架的可擴展性

讀取環境變文件

```typescript 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 函數的實現如下:

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

// 將 prefixes 轉換成數組,例如 'VITE_' 會轉換成 ['VITE_'], ['VITE_'] 則不變 prefixes = arraify(prefixes) const env: Record = {}

// 要讀取的環境變量文件 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 函數的行為

env VITE_TEST_2=123 VITE_TEST_3=VITE_TEST_3_${VITE_TEST_2}

該文件,經過 dotenv 處理後,會是如下的結構:

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

dotenv 不支持動態字符串格式,因此要用 dotenv-expand 處理,處理的結果如下:

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

image-20220601193059562

合成 ResolvedConfig

這一小節不會細講,因為 ResolvedConfig 的屬性,在解析過程都是用不上的,它是給 vite 的其他流程使用的

下面代碼不需要細看,只需要知道,我們之前處理了一些配置,然後將這些配置組合成 ResolvedConfig,然後作為 resolveConfig 的返回,即可

typescript 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 鈎子

typescript // 調用 configResolved 鈎子 await Promise.all(userPlugins.map((p) => p.configResolved?.(resolved)))

總結

過完了一遍 vite 的配置解析的流程,我們用下圖再總結一下

image-20220601195734804

另外,我們還對 vite 的擴展性,做了一些分析。

在配置解析過程中,vite 通過插件鈎子,提供了擴展性,這個體現在:

  • 第三方插件,能夠通過鈎子,在 vite 的運行過程中,與 vite 進行通訊
  • 第三方插件,能夠通過 config 鈎子,對 vite 的配置進行二次修改,修改最終的解析配置,從而可以改變 vite 的行為。
  • 第三方插件,能夠通過 configResolved 鈎子,獲取到 vite 最終解析出來的配置並保存起來,這使插件能夠根據 vite 配置,在其他 vite 鈎子中,實現複雜的插件行為

最後

如果這篇文章對您有所幫助,請幫忙點個贊👍,您的鼓勵是我創作路上的最大的動力。