欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

一文弄懂Vite 配置文件

 更新時(shí)間:2024年08月31日 16:11:18   作者:程序員小英  
Vite?構(gòu)建環(huán)境分為開發(fā)環(huán)境和生產(chǎn)環(huán)境,不同環(huán)境會(huì)有不同的構(gòu)建策略,本文就來介紹下Vite 配置文件,具有一定的參考價(jià)值,感興趣的可以了解一下

我們知道,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)文章

最新評(píng)論