五千字剖析 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 鉤子中,實現複雜的外掛行為

最後

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