Vite配置文件如何加載深入剖析
一、流程梳理
我們先來梳理整體的流程,Vite 中的配置解析由 resolveConfig 函數(shù)來實現(xiàn),你可以對照源碼一起學(xué)習(xí)。
1.1 加載配置文件
進(jìn)行一些必要的變量聲明后,我們進(jìn)入到解析配置邏輯中,配置文件的源碼如下:
// 這里的 config 是命令行指定的配置,如 vite --configFile=xxx
let { configFile } = config
if (configFile !== false) {
// 默認(rèn)都會走到下面加載配置文件的邏輯,除非你手動指定 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)容,然后與命令行配置合并。值得注意的是,后面有一個記錄configFileDependencies的操作。因為配置文件代碼可能會有第三方庫的依賴,所以當(dāng)?shù)谌綆煲蕾嚨拇a更改時,Vite 可以通過 HMR 處理邏輯中記錄的configFileDependencies檢測到更改,再重啟 DevServer ,來保證當(dāng)前生效的配置永遠(yuǎn)是最新的。
1.2 解析用戶插件
第二個重點環(huán)節(jié)是 解析用戶插件。首先,我們通過 apply 參數(shù) 過濾出需要生效的用戶插件。為什么這么做呢?因為有些插件只在開發(fā)階段生效,或者說只在生產(chǎn)環(huán)境生效,我們可以通過 apply: 'serve' 或 'build' 來指定它們,同時也可以將apply配置為一個函數(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 為一個函數(shù)的情況
return p.apply({ ...config, mode }, configEnv)
} else {
return p.apply === command
}
}) as Plugin[]
// 對用戶插件進(jìn)行排序
const [prePlugins, normalPlugins, postPlugins] =
sortUserPlugins(rawUserPlugins)接著,Vite 會拿到這些過濾且排序完成的插件,依次調(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ù),大家有興趣可以閱讀一下實現(xiàn)
config = mergeConfig(config, res)
}
}
}然后,解析項目的根目錄即 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
}1.3 加載環(huán)境變量
加載環(huán)境變量的實現(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 其實就是掃描 process.env 與 .env文件,解析出 env 對象,值得注意的是,這個對象的屬性最終會被掛載到import.meta.env 這個全局對象上。解析 env 對象的實現(xiàn)思路如下:
- 遍歷 process.env 的屬性,拿到指定前綴開頭的屬性(默認(rèn)指定為VITE_),并掛載 env 對象上
- 遍歷 .env 文件,解析文件,然后往 env 對象掛載那些以指定前綴開頭的屬性。遍歷的文件先后順序如下(下面的 mode 開發(fā)階段為 development,生產(chǎn)環(huán)境為production)
特殊情況下,如果中途遇到 NODE_ENV 屬性,則掛到 process.env.VITE_USER_NODE_ENV,Vite 會優(yōu)先通過這個屬性來決定是否走生產(chǎn)環(huán)境的構(gòu)建。
接下來,是對資源公共路徑即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ā)階段特殊處理,全部重寫為/
- .開頭的路徑,自動重寫為 /
- 以http(s)://開頭的路徑,在開發(fā)環(huán)境下重寫為對應(yīng)的 pathname
- 確保路徑開頭和結(jié)尾都是/
當(dāng)然,還有對cacheDir的解析,這個路徑相對于在 Vite 預(yù)編譯時寫入依賴產(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)換為一個過濾器函數(shù):
const assetsFilter = config.assetsInclude ? createFilter(config.assetsInclude) : () => false
然后,Vite 后面會將用戶傳入的 assetsInclude 和內(nèi)置的規(guī)則合并:
assetsInclude(file: string) {
return DEFAULT_ASSETS_RE.test(file) || assetsFilter(file)
}這個配置決定是否讓 Vite 將對應(yīng)的后綴名視為靜態(tài)資源文件(asset)來處理。
1.4 路徑解析器
這里所說的路徑解析器,是指調(diào)用插件容器進(jìn)行路徑解析的函數(shù),代碼結(jié)構(gòu)如下所示:
const createResolver: ResolvedConfig['createResolver'] = (options) => {
let aliasContainer: PluginContainer | undefined
let resolverContainer: PluginContainer | undefined
// 返回的函數(shù)可以理解為一個解析器
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
}
}并且,這個解析器未來會在依賴預(yù)構(gòu)建的時候用上,具體用法如下:
const resolve = config.createResolver()
// 調(diào)用以拿到 react 路徑
rseolve('react', undefined, undefined, false)這里有aliasContainer和resolverContainer兩個工具對象,它們都含有resolveId這個專門解析路徑的方法,可以被 Vite 調(diào)用來獲取解析結(jié)果,本質(zhì)都是PluginContainer。
接著,會順便處理一個 public 目錄,也就是 Vite 作為靜態(tài)資源服務(wù)的目錄:
const { publicDir } = config
const resolvedPublicDir =
publicDir !== false && publicDir !== ''
? path.resolve(
resolvedRoot,
typeof publicDir === 'string' ? publicDir : 'public'
)
: ''至此,配置已經(jīng)基本上解析完成,最后通過 resolved 對象來整理一下:
const resolved: ResolvedConfig = {
...config,
configFile: configFile ? normalizePath(configFile) : undefined,
configFileDependencies,
inlineConfig,
root: resolvedRoot,
base: BASE_URL
... //其他配置
}1.5 生成插件流水線
生成插件流水線的代碼如下:
(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)用每個插件的 configResolved 鉤子函數(shù)。其中 resolvePlugins 內(nèi)部細(xì)節(jié)比較多,插件數(shù)量比較龐大,我們暫時不去深究具體實現(xiàn),編譯流水線這一小節(jié)再來詳細(xì)介紹。
至此,所有核心配置都生成完畢。不過,后面 Vite 還會處理一些邊界情況,在用戶配置不合理的時候,給用戶對應(yīng)的提示。比如:用戶直接使用alias時,Vite 會提示使用resolve.alias。
最后,resolveConfig 函數(shù)會返回 resolved 對象,也就是最后的配置集合,那么配置解析服務(wù)到底也就結(jié)束了。
二、加載配置文件詳解
首先,我們來看一下加載配置文件(loadConfigFromFile)的實現(xiàn):
const loadResult = await loadConfigFromFile(/*省略傳參*/)
這里的邏輯稍微有點復(fù)雜,很難梳理清楚,所以我們不妨借助剛才梳理的配置解析流程,深入loadConfigFromFile 的細(xì)節(jié)中,研究下 Vite 對于配置文件加載的實現(xiàn)思路。
接下來,我們來分析下需要處理的配置文件類型,根據(jù)文件后綴和模塊格式可以分為下面這幾類:
- TS + ESM 格式
- TS + CommonJS 格式
- JS + ESM 格式
- JS + CommonJS 格式
2.1 識別配置文件的類別
首先,Vite 會檢查項目的 package.json文件,如果有type: "module"則打上 isESM 的標(biāo)識:
try {
const pkg = lookupFile(configRoot, ['package.json'])
if (pkg && JSON.parse(pkg).type === 'module') {
isMjs = true
}
} catch (e) {
}然后,Vite 會尋找配置文件路徑,代碼簡化后如下:
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 {
// 從項目根目錄尋找配置文件路徑,尋找順序:
// - 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
}
}
}在尋找路徑的同時, Vite 也會給當(dāng)前配置文件打上isESM和isTS的標(biāo)識,方便后續(xù)的解析。
2.2 根據(jù)類別解析配置
2.2.1 ESM 格式
對于 ESM 格式配置的處理代碼如下:
let userConfig: UserConfigExport | undefined
if (isESM) {
const fileUrl = require('url').pathToFileURL(resolvedPath)
// 首先對代碼進(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
對于 TS 配置文件來說,Vite 會將編譯后的 js 代碼寫入臨時文件,通過 Node 原生 ESM Import 來讀取這個臨時的內(nèi)容,以獲取到配置內(nèi)容,再直接刪掉臨時文件:
fs.writeFileSync(resolvedPath + '.js', bundled.code)
userConfig = (await dynamicImport(`${fileUrl}.js?t=${Date.now()}`)).default
fs.unlinkSync(resolvedPath + '.js')以上這種先編譯配置文件,再將產(chǎn)物寫入臨時目錄,最后加載臨時目錄產(chǎn)物的做法,也是 AOT (Ahead Of Time)編譯技術(shù)的一種具體實現(xiàn)。
而對于 JS 配置文件來說,Vite 會直接通過 Node 原生 ESM Import 來讀取,也是使用 dynamicImport 函數(shù)的邏輯,dynamicImport 的實現(xiàn)如下:
export const dynamicImport = new Function('file', 'return import(file)')你可能會問,為什么要用 new Function 包裹?這是為了避免打包工具處理這段代碼,比如 Rollup 和 TSC,類似的手段還有 eval。你可能還會問,為什么 import 路徑結(jié)果要加上時間戳 query?這其實是為了讓 dev server 重啟后仍然讀取最新的配置,避免緩存。
2.2.2 CommonJS 格式
對于 CommonJS 格式的配置文件,Vite 集中進(jìn)行了解析:
// 對于 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ù)來實現(xiàn)對 bundle 后配置代碼的加載,代碼如下:
// 默認(rèn)加載器
const defaultLoader = require.extensions[extension]!
// 攔截原生 require 對于`.js`或者`.ts`的加載
require.extensions[extension] = (module: NodeModule, filename: string) => {
// 針對 vite 配置文件的加載特殊處理
if (filename === fileName) {
;(module as NodeModuleWithCompile)._compile(bundledCode, filename)
} else {
defaultLoader(module, filename)
}
}而原生 require 對于 js 文件的加載代碼如下所示。
Module._extensions['.js'] = function (module, filename) {
var content = fs.readFileSync(filename, 'utf8')
module._compile(stripBOM(content), filename)
}事實上,Node.js 內(nèi)部也是先讀取文件內(nèi)容,然后編譯該模塊。當(dāng)代碼中調(diào)用module._compile 相當(dāng)于手動編譯一個模塊,該方法在 Node 內(nèi)部的實現(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)行一次手動的 require,即可拿到配置對象:
const raw = require(fileName) const config = raw.__esModule ? raw.default : raw // 恢復(fù)原生的加載方法 require.extensions[extension] = defaultLoader // 返回配置 return config
這種運行時加載 TS 配置的方式,也叫做 JIT(即時編譯),這種方式和 AOT 最大的區(qū)別在于不會將內(nèi)存中計算出來的 js 代碼寫入磁盤再加載,而是通過攔截 Node.js 原生 require.extension 方法實現(xiàn)即時加載。
至此,配置文件的內(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)一實現(xiàn),其中經(jīng)歷了加載配置文件、解析用戶插件、加載環(huán)境變量、創(chuàng)建路徑解析器工廠和生成插件流水線這幾個主要的流程。
其次,在加載配置文件的過程中,Vite 需要處理四種類型的配置文件,其中對于 ESM 和 CommonJS 兩種格式的 TS 文件,分別采用了AOT和JIT兩種編譯技術(shù)實現(xiàn)了配置加載。
以上就是Vite配置文件如何加載深入剖析的詳細(xì)內(nèi)容,更多關(guān)于Vite加載配置文件的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Vue3中結(jié)合ElementPlus實現(xiàn)彈窗的封裝方式
這篇文章主要介紹了Vue3中結(jié)合ElementPlus實現(xiàn)彈窗的封裝方式,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2024-01-01
Vue3監(jiān)聽store中數(shù)據(jù)變化的三種方式
這篇文章給大家介紹了Vue3監(jiān)聽store中數(shù)據(jù)變化的三種方法,使用watch和storeToRefs函數(shù),使用計算屬性computed和使用watchEffect函數(shù)這三種方法,文中通過代碼講解非常詳細(xì),需要的朋友可以參考下2024-01-01

