一文弄懂Vite 配置文件
我們知道,Vite 構(gòu)建環(huán)境分為開發(fā)環(huán)境和生產(chǎn)環(huán)境,不同環(huán)境會(huì)有不同的構(gòu)建策略,但不管是哪種環(huán)境,Vite 都會(huì)首先解析用戶配置。那接下來,我就與你分析配置解析過程中 Vite 到底做了什么?即Vite是如何加載配置文件的。
流程梳理
我們先來梳理整體的流程,Vite 中的配置解析由 resolveConfig 函數(shù)來實(shí)現(xiàn),你可以對(duì)照源碼一起學(xué)習(xí)。
加載配置文件
進(jìn)行一些必要的變量聲明后,我們進(jìn)入到解析配置邏輯中,配置文件的源碼如下:
// 這里的 config 是命令行指定的配置,如 vite --configFile=xxx let { configFile } = config if (configFile !== false) { // 默認(rèn)都會(huì)走到下面加載配置文件的邏輯,除非你手動(dòng)指定 configFile 為 false const loadResult = await loadConfigFromFile( configEnv, configFile, config.root, config.logLevel ) if (loadResult) { // 解析配置文件的內(nèi)容后,和命令行配置合并 config = mergeConfig(loadResult.config, config) configFile = loadResult.path configFileDependencies = loadResult.dependencies } }
第一步是解析配置文件的內(nèi)容,然后與命令行配置合并。值得注意的是,后面有一個(gè)記錄 configFileDependencies 的操作。因?yàn)榕渲梦募a可能會(huì)有第三方庫(kù)的依賴,所以當(dāng)?shù)谌綆?kù)依賴的代碼更改時(shí),Vite 可以通過 HMR 處理邏輯中記錄的 configFileDependencies 檢測(cè)到更改,再重啟 DevServer ,來保證當(dāng)前生效的配置永遠(yuǎn)是最新的。
解析用戶插件
第二個(gè)重點(diǎn)環(huán)節(jié)是 解析用戶插件。首先,我們通過 apply 參數(shù) 過濾出需要生效的用戶插件。為什么這么做呢?因?yàn)橛行┎寮辉陂_發(fā)階段生效,或者說只在生產(chǎn)環(huán)境生效,我們可以通過 apply: 'serve' 或 'build' 來指定它們,同時(shí)也可以將 apply 配置為一個(gè)函數(shù),來自定義插件生效的條件。解析代碼如下:
// resolve plugins const rawUserPlugins = (config.plugins || []).flat().filter((p) => { if (!p) { return false } else if (!p.apply) { return true } else if (typeof p.apply === 'function') { // apply 為一個(gè)函數(shù)的情況 return p.apply({ ...config, mode }, configEnv) } else { return p.apply === command } }) as Plugin[] // 對(duì)用戶插件進(jìn)行排序 const [prePlugins, normalPlugins, postPlugins] = sortUserPlugins(rawUserPlugins)
接著,Vite 會(huì)拿到這些過濾且排序完成的插件,依次調(diào)用插件 config 鉤子,進(jìn)行配置合并。
// run config hooks const userPlugins = [...prePlugins, ...normalPlugins, ...postPlugins] for (const p of userPlugins) { if (p.config) { const res = await p.config(config, configEnv) if (res) { // mergeConfig 為具體的配置合并函數(shù),大家有興趣可以閱讀一下實(shí)現(xiàn) config = mergeConfig(config, res) } } }
然后,解析項(xiàng)目的根目錄即 root 參數(shù),默認(rèn)取 process.cwd()的結(jié)果。
// resolve root const resolvedRoot = normalizePath( config.root ? path.resolve(config.root) : process.cwd() )
緊接著處理 alias ,這里需要加上一些內(nèi)置的 alias 規(guī)則,如 @vite/env、@vite/client 這種直接重定向到 Vite 內(nèi)部的模塊。
// resolve alias with internal client alias const resolvedAlias = mergeAlias( clientAlias, config.resolve?.alias || config.alias || [] ) const resolveOptions: ResolvedConfig['resolve'] = { dedupe: config.dedupe, ...config.resolve, alias: resolvedAlias }
加載環(huán)境變量
加載環(huán)境變量的實(shí)現(xiàn)代碼如下:
// load .env files const envDir = config.envDir ? normalizePath(path.resolve(resolvedRoot, config.envDir)) : resolvedRoot const userEnv = inlineConfig.envFile !== false && loadEnv(mode, envDir, resolveEnvPrefix(config))
loadEnv 其實(shí)就是掃描 process.env 與 .env文件,解析出 env 對(duì)象,值得注意的是,這個(gè)對(duì)象的屬性最終會(huì)被掛載到 import.meta.env 這個(gè)全局對(duì)象上。解析 env 對(duì)象的實(shí)現(xiàn)思路如下:
遍歷 process.env 的屬性,拿到指定前綴開頭的屬性(默認(rèn)指定為VITE_),并掛載 env 對(duì)象上
遍歷 .env 文件,解析文件,然后往 env 對(duì)象掛載那些以指定前綴開頭的屬性。遍歷的文件先后順序如下(下面的 mode 開發(fā)階段為 development,生產(chǎn)環(huán)境為production)
特殊情況下,如果中途遇到 NODE_ENV 屬性,則掛到 process.env.VITE_USER_NODE_ENV,Vite 會(huì)優(yōu)先通過這個(gè)屬性來決定是否走生產(chǎn)環(huán)境的構(gòu)建。
接下來,是對(duì)資源公共路徑即 base URL 的處理,邏輯集中在 resolveBaseUrl 函數(shù)當(dāng)中:
// 解析 base url const BASE_URL = resolveBaseUrl(config.base, command === 'build', logger) // 解析生產(chǎn)環(huán)境構(gòu)建配置 const resolvedBuildOptions = resolveBuildOptions(config.build)
resolveBaseUrl 里面有這些處理規(guī)則需要注意:
空字符或者 ./ 在開發(fā)階段特殊處理,全部重寫為 /
.開頭的路徑,自動(dòng)重寫為 /
以 http(s):// 開頭的路徑,在開發(fā)環(huán)境下重寫為對(duì)應(yīng)的 pathname
確保路徑開頭和結(jié)尾都是 /
當(dāng)然,還有對(duì) cacheDir 的解析,這個(gè)路徑相對(duì)于在 Vite 預(yù)編譯時(shí)寫入依賴產(chǎn)物的路徑:
// resolve cache directory const pkgPath = lookupFile(resolvedRoot, [`package.json`], true /* pathOnly */) // 默認(rèn)為 node_module/.vite const cacheDir = config.cacheDir ? path.resolve(resolvedRoot, config.cacheDir) : pkgPath && path.join(path.dirname(pkgPath), `node_modules/.vite`)
緊接著處理用戶配置的 assetsInclude,將其轉(zhuǎn)換為一個(gè)過濾器函數(shù):
const assetsFilter = config.assetsInclude ? createFilter(config.assetsInclude) : () => false
然后,Vite 后面會(huì)將用戶傳入的 assetsInclude 和內(nèi)置的規(guī)則合并:
assetsInclude(file: string) { return DEFAULT_ASSETS_RE.test(file) || assetsFilter(file) }
這個(gè)配置決定是否讓 Vite 將對(duì)應(yīng)的后綴名視為靜態(tài)資源文件(asset)來處理。
路徑解析器
這里所說的路徑解析器,是指調(diào)用插件容器進(jìn)行路徑解析的函數(shù),代碼結(jié)構(gòu)如下所示:
const createResolver: ResolvedConfig['createResolver'] = (options) => { let aliasContainer: PluginContainer | undefined let resolverContainer: PluginContainer | undefined // 返回的函數(shù)可以理解為一個(gè)解析器 return async (id, importer, aliasOnly, ssr) => { let container: PluginContainer if (aliasOnly) { container = aliasContainer || // 新建 aliasContainer } else { container = resolverContainer || // 新建 resolveContainer } return (await container.resolveId(id, importer, undefined, ssr))?.id } }
并且,這個(gè)解析器未來會(huì)在依賴預(yù)構(gòu)建的時(shí)候用上,具體用法如下:
const resolve = config.createResolver() // 調(diào)用以拿到 react 路徑 rseolve('react', undefined, undefined, false)
這里有 aliasContainer 和 resolverContainer 兩個(gè)工具對(duì)象,它們都含有 resolveId 這個(gè)專門解析路徑的方法,可以被 Vite 調(diào)用來獲取解析結(jié)果,本質(zhì)都是 PluginContainer。
接著,會(huì)順便處理一個(gè) public 目錄,也就是 Vite 作為靜態(tài)資源服務(wù)的目錄:
const { publicDir } = config const resolvedPublicDir = publicDir !== false && publicDir !== '' ? path.resolve( resolvedRoot, typeof publicDir === 'string' ? publicDir : 'public' ) : ''
至此,配置已經(jīng)基本上解析完成,最后通過 resolved 對(duì)象來整理一下:
const resolved: ResolvedConfig = { ...config, configFile: configFile ? normalizePath(configFile) : undefined, configFileDependencies, inlineConfig, root: resolvedRoot, base: BASE_URL ... //其他配置 }
生成插件流水線
生成插件流水線的代碼如下:
;(resolved.plugins as Plugin[]) = await resolvePlugins( resolved, prePlugins, normalPlugins, postPlugins ) // call configResolved hooks await Promise.all(userPlugins.map((p) => p.configResolved?.(resolved)))
先生成完整插件列表傳給 resolve.plugins,而后調(diào)用每個(gè)插件的 configResolved 鉤子函數(shù)。其中 resolvePlugins 內(nèi)部細(xì)節(jié)比較多,插件數(shù)量比較龐大,我們暫時(shí)不去深究具體實(shí)現(xiàn),編譯流水線這一小節(jié)再來詳細(xì)介紹。
至此,所有核心配置都生成完畢。不過,后面 Vite 還會(huì)處理一些邊界情況,在用戶配置不合理的時(shí)候,給用戶對(duì)應(yīng)的提示。比如:用戶直接使用 alias 時(shí),Vite 會(huì)提示使用 resolve.alias。
最后,resolveConfig 函數(shù)會(huì)返回 resolved 對(duì)象,也就是最后的配置集合,那么配置解析服務(wù)到底也就結(jié)束了。
加載配置文件詳解
首先,我們來看一下加載配置文件 (loadConfigFromFile) 的實(shí)現(xiàn):
const loadResult = await loadConfigFromFile(/*省略傳參*/)
這里的邏輯稍微有點(diǎn)復(fù)雜,很難梳理清楚,所以我們不妨借助剛才梳理的配置解析流程,深入 loadConfigFromFile 的細(xì)節(jié)中,研究下 Vite 對(duì)于配置文件加載的實(shí)現(xiàn)思路。
接下來,我們來分析下需要處理的配置文件類型,根據(jù)文件后綴和模塊格式可以分為下面這幾類:
TS + ESM 格式
TS + CommonJS 格式
JS + ESM 格式
JS + CommonJS 格式
識(shí)別配置文件的類別
首先,Vite 會(huì)檢查項(xiàng)目的 package.json 文件,如果有 type: "module" 則打上 isESM 的標(biāo)識(shí):
try { const pkg = lookupFile(configRoot, ['package.json']) if (pkg && JSON.parse(pkg).type === 'module') { isMjs = true } } catch (e) { }
然后,Vite 會(huì)尋找配置文件路徑,代碼簡(jiǎn)化后如下:
let isTS = false let isESM = false let dependencies: string[] = [] // 如果命令行有指定配置文件路徑 if (configFile) { resolvedPath = path.resolve(configFile) // 根據(jù)后綴判斷是否為 ts 或者 esm,打上 flag isTS = configFile.endsWith('.ts') if (configFile.endsWith('.mjs')) { isESM = true } } else { // 從項(xiàng)目根目錄尋找配置文件路徑,尋找順序: // - vite.config.js // - vite.config.mjs // - vite.config.ts // - vite.config.cjs const jsconfigFile = path.resolve(configRoot, 'vite.config.js') if (fs.existsSync(jsconfigFile)) { resolvedPath = jsconfigFile } if (!resolvedPath) { const mjsconfigFile = path.resolve(configRoot, 'vite.config.mjs') if (fs.existsSync(mjsconfigFile)) { resolvedPath = mjsconfigFile isESM = true } } if (!resolvedPath) { const tsconfigFile = path.resolve(configRoot, 'vite.config.ts') if (fs.existsSync(tsconfigFile)) { resolvedPath = tsconfigFile isTS = true } } if (!resolvedPath) { const cjsConfigFile = path.resolve(configRoot, 'vite.config.cjs') if (fs.existsSync(cjsConfigFile)) { resolvedPath = cjsConfigFile isESM = false } } }
在尋找路徑的同時(shí), Vite 也會(huì)給當(dāng)前配置文件打上 isESM 和 isTS 的標(biāo)識(shí),方便后續(xù)的解析。
根據(jù)類別解析配置
ESM 格式
對(duì)于 ESM 格式配置的處理代碼如下:
let userConfig: UserConfigExport | undefined if (isESM) { const fileUrl = require('url').pathToFileURL(resolvedPath) // 首先對(duì)代碼進(jìn)行打包 const bundled = await bundleConfigFile(resolvedPath, true) dependencies = bundled.dependencies // TS + ESM if (isTS) { fs.writeFileSync(resolvedPath + '.js', bundled.code) userConfig = (await dynamicImport(`${fileUrl}.js?t=${Date.now()}`)) .default fs.unlinkSync(resolvedPath + '.js') debug(`TS + native esm config loaded in ${getTime()}`, fileUrl) } // JS + ESM else { userConfig = (await dynamicImport(`${fileUrl}?t=${Date.now()}`)).default debug(`native esm config loaded in ${getTime()}`, fileUrl) } }
可以看到,首先通過 Esbuild 將配置文件編譯打包成 js 代碼:
const bundled = await bundleConfigFile(resolvedPath, true) // 記錄依賴 dependencies = bundled.dependencies
對(duì)于 TS 配置文件來說,Vite 會(huì)將編譯后的 js 代碼寫入臨時(shí)文件,通過 Node 原生 ESM Import 來讀取這個(gè)臨時(shí)的內(nèi)容,以獲取到配置內(nèi)容,再直接刪掉臨時(shí)文件:
fs.writeFileSync(resolvedPath + '.js', bundled.code) userConfig = (await dynamicImport(`${fileUrl}.js?t=${Date.now()}`)).default fs.unlinkSync(resolvedPath + '.js')
以上這種先編譯配置文件,再將產(chǎn)物寫入臨時(shí)目錄,最后加載臨時(shí)目錄產(chǎn)物的做法,也是 AOT (Ahead Of Time)編譯技術(shù)的一種具體實(shí)現(xiàn)。
而對(duì)于 JS 配置文件來說,Vite 會(huì)直接通過 Node 原生 ESM Import 來讀取,也是使用 dynamicImport 函數(shù)的邏輯,dynamicImport 的實(shí)現(xiàn)如下:
export const dynamicImport = new Function('file', 'return import(file)')
你可能會(huì)問,為什么要用 new Function 包裹?這是為了避免打包工具處理這段代碼,比如 Rollup 和 TSC,類似的手段還有 eval。你可能還會(huì)問,為什么 import 路徑結(jié)果要加上時(shí)間戳 query?這其實(shí)是為了讓 dev server 重啟后仍然讀取最新的配置,避免緩存。
CommonJS 格式
對(duì)于 CommonJS 格式的配置文件,Vite 集中進(jìn)行了解析:
// 對(duì)于 js/ts 均生效 // 使用 esbuild 將配置文件編譯成 commonjs 格式的 bundle 文件 const bundled = await bundleConfigFile(resolvedPath) dependencies = bundled.dependencies // 加載編譯后的 bundle 代碼 userConfig = await loadConfigFromBundledFile(resolvedPath, bundled.code)
bundleConfigFile 函數(shù)的主要功能是通過 Esbuild 將配置文件打包,拿到打包后的 bundle 代碼以及配置文件的依賴 (dependencies)。而接下來的事情就是考慮如何加載 bundle 代碼了,這也是 loadConfigFromBundledFile 要做的事情。
async function loadConfigFromBundledFile( fileName: string, bundledCode: string ): Promise<UserConfig> { const extension = path.extname(fileName) const defaultLoader = require.extensions[extension]! require.extensions[extension] = (module: NodeModule, filename: string) => { if (filename === fileName) { ;(module as NodeModuleWithCompile)._compile(bundledCode, filename) } else { defaultLoader(module, filename) } } // 清除 require 緩存 delete require.cache[require.resolve(fileName)] const raw = require(fileName) const config = raw.__esModule ? raw.default : raw require.extensions[extension] = defaultLoader return config }
loadConfigFromBundledFile 大體完成的是通過攔截原生 require.extensions 的加載函數(shù)來實(shí)現(xiàn)對(duì) bundle 后配置代碼的加載,代碼如下:
// 默認(rèn)加載器 const defaultLoader = require.extensions[extension]! // 攔截原生 require 對(duì)于`.js`或者`.ts`的加載 require.extensions[extension] = (module: NodeModule, filename: string) => { // 針對(duì) vite 配置文件的加載特殊處理 if (filename === fileName) { ;(module as NodeModuleWithCompile)._compile(bundledCode, filename) } else { defaultLoader(module, filename) } }
而原生 require 對(duì)于 js 文件的加載代碼如下所示。
Module._extensions['.js'] = function (module, filename) { var content = fs.readFileSync(filename, 'utf8') module._compile(stripBOM(content), filename) }
事實(shí)上,Node.js 內(nèi)部也是先讀取文件內(nèi)容,然后編譯該模塊。當(dāng)代碼中調(diào)用module._compile 相當(dāng)于手動(dòng)編譯一個(gè)模塊,該方法在 Node 內(nèi)部的實(shí)現(xiàn)如下:
Module.prototype._compile = function (content, filename) { var self = this var args = [self.exports, require, self, filename, dirname] return compiledWrapper.apply(self.exports, args) }
在調(diào)用完 module._compile 編譯完配置代碼后,進(jìn)行一次手動(dòng)的 require,即可拿到配置對(duì)象:
const raw = require(fileName) const config = raw.__esModule ? raw.default : raw // 恢復(fù)原生的加載方法 require.extensions[extension] = defaultLoader // 返回配置 return config
這種運(yùn)行時(shí)加載 TS 配置的方式,也叫做 JIT (即時(shí)編譯),這種方式和 AOT 最大的區(qū)別在于不會(huì)將內(nèi)存中計(jì)算出來的 js 代碼寫入磁盤再加載,而是通過攔截 Node.js 原生 require.extension 方法實(shí)現(xiàn)即時(shí)加載。
至此,配置文件的內(nèi)容已經(jīng)讀取完成,等后處理完成再返回即可:
// 處理是函數(shù)的情況 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 }
總結(jié)
下面我們來總結(jié)一下 Vite 配置解析的整體流程和加載配置文件的方法:
首先,Vite 配置文件解析的邏輯由 resolveConfig 函數(shù)統(tǒng)一實(shí)現(xiàn),其中經(jīng)歷了加載配置文件、解析用戶插件、加載環(huán)境變量、創(chuàng)建路徑解析器工廠和生成插件流水線這幾個(gè)主要的流程。
其次,在加載配置文件的過程中,Vite 需要處理四種類型的配置文件,其中對(duì)于 ESM 和 CommonJS 兩種格式的 TS 文件,分別采用了AOT和JIT兩種編譯技術(shù)實(shí)現(xiàn)了配置加載。
到此這篇關(guān)于一文弄懂Vite 配置文件的文章就介紹到這了,更多相關(guān)Vite 配置文件內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
第一次使用webstrom簡(jiǎn)單創(chuàng)建vue項(xiàng)目的一些報(bào)錯(cuò)實(shí)戰(zhàn)記錄
在使用webstorm新建vue項(xiàng)目時(shí)常會(huì)遇到一些報(bào)錯(cuò),特別是新手第一次運(yùn)行項(xiàng)目,這篇文章主要給大家介紹了關(guān)于第一次使用webstrom簡(jiǎn)單創(chuàng)建vue項(xiàng)目的一些報(bào)錯(cuò)實(shí)戰(zhàn)記錄,需要的朋友可以參考下2023-02-02Vue 如何使用props、emit實(shí)現(xiàn)自定義雙向綁定的實(shí)現(xiàn)
這篇文章主要介紹了Vue 如何使用props、emit實(shí)現(xiàn)自定義雙向綁定的實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-06-06關(guān)于el-scrollbar滾動(dòng)條初始化不顯示的問題及解決
這篇文章主要介紹了關(guān)于el-scrollbar滾動(dòng)條初始化不顯示的問題及解決方案,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-08-08使用axios請(qǐng)求時(shí),發(fā)送formData請(qǐng)求的示例
今天小編就為大家分享一篇使用axios請(qǐng)求時(shí),發(fā)送formData請(qǐng)求的示例,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2019-10-10Vue3使用Univer Docs創(chuàng)建在線編輯Excel的示例代碼
本文介紹了如何在Vue3項(xiàng)目中集成UniverDocs,一個(gè)基于Luckysheet的企業(yè)文檔與數(shù)據(jù)協(xié)同解決方案,指導(dǎo)了從安裝到在頁(yè)面中使用的步驟,以及注意事項(xiàng),如數(shù)據(jù)格式轉(zhuǎn)換和二次開發(fā)的靈活性,需要的朋友可以參考下2025-04-04vue-router的beforeRouteUpdate不觸發(fā)問題
這篇文章主要介紹了vue-router的beforeRouteUpdate不觸發(fā)問題及解決方案,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-04-04