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

Vite配置文件如何加載深入剖析

 更新時間:2023年11月01日 10:06:44   作者:xiangzhihong  
我們知道,Vite?構建環(huán)境分為開發(fā)環(huán)境和生產(chǎn)環(huán)境,不同環(huán)境會有不同的構建策略,但不管是哪種環(huán)境,Vite?都會首先解析用戶配置,那接下來,本文就來與大家分析配置解析過程中?Vite?到底做了什么?即Vite是如何加載配置文件的

一、流程梳理

我們先來梳理整體的流程,Vite 中的配置解析由 resolveConfig 函數(shù)來實現(xiàn),你可以對照源碼一起學習。

1.1 加載配置文件

進行一些必要的變量聲明后,我們進入到解析配置邏輯中,配置文件的源碼如下:

// 這里的 config 是命令行指定的配置,如 vite --configFile=xxx
let { configFile } = config
if (configFile !== false) {
  // 默認都會走到下面加載配置文件的邏輯,除非你手動指定 configFile 為 false
  const loadResult = await loadConfigFromFile(
    configEnv,
    configFile,
    config.root,
    config.logLevel
  )
  if (loadResult) {
    // 解析配置文件的內容后,和命令行配置合并
    config = mergeConfig(loadResult.config, config)
    configFile = loadResult.path
    configFileDependencies = loadResult.dependencies
  }
}

第一步是解析配置文件的內容,然后與命令行配置合并。值得注意的是,后面有一個記錄configFileDependencies的操作。因為配置文件代碼可能會有第三方庫的依賴,所以當?shù)谌綆煲蕾嚨拇a更改時,Vite 可以通過 HMR 處理邏輯中記錄的configFileDependencies檢測到更改,再重啟 DevServer ,來保證當前生效的配置永遠是最新的。

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[]
// 對用戶插件進行排序
const [prePlugins, normalPlugins, postPlugins] =
  sortUserPlugins(rawUserPlugins)

接著,Vite 會拿到這些過濾且排序完成的插件,依次調用插件 config 鉤子,進行配置合并。

// 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ù),默認取 process.cwd()的結果。

// resolve root
const resolvedRoot = normalizePath(
  config.root ? path.resolve(config.root) : process.cwd()
)

緊接著處理 alias ,這里需要加上一些內置的 alias 規(guī)則,如@vite/env、@vite/client這種直接重定向到 Vite 內部的模塊。

// 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 的屬性,拿到指定前綴開頭的屬性(默認指定為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)境的構建。

接下來,是對資源公共路徑即base URL的處理,邏輯集中在 resolveBaseUrl 函數(shù)當中:

// 解析 base url
const BASE_URL = resolveBaseUrl(config.base, command === 'build', logger)
// 解析生產(chǎn)環(huán)境構建配置
const resolvedBuildOptions = resolveBuildOptions(config.build)

resolveBaseUrl里面有這些處理規(guī)則需要注意:

  • 空字符或者 ./ 在開發(fā)階段特殊處理,全部重寫為/
  • .開頭的路徑,自動重寫為 /
  • 以http(s)://開頭的路徑,在開發(fā)環(huán)境下重寫為對應的 pathname
  • 確保路徑開頭和結尾都是/

當然,還有對cacheDir的解析,這個路徑相對于在 Vite 預編譯時寫入依賴產(chǎn)物的路徑:

// resolve cache directory
const pkgPath = lookupFile(resolvedRoot, [`package.json`], true /* pathOnly */)
// 默認為 node_module/.vite
const cacheDir = config.cacheDir
  ? path.resolve(resolvedRoot, config.cacheDir)
  : pkgPath && path.join(path.dirname(pkgPath), `node_modules/.vite`)

緊接著處理用戶配置的assetsInclude,將其轉換為一個過濾器函數(shù):

const assetsFilter = config.assetsInclude
  ? createFilter(config.assetsInclude)
  : () => false

然后,Vite 后面會將用戶傳入的 assetsInclude 和內置的規(guī)則合并:

assetsInclude(file: string) {
  return DEFAULT_ASSETS_RE.test(file) || assetsFilter(file)
}

這個配置決定是否讓 Vite 將對應的后綴名視為靜態(tài)資源文件(asset)來處理。

1.4 路徑解析器

這里所說的路徑解析器,是指調用插件容器進行路徑解析的函數(shù),代碼結構如下所示:

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
  }
}

并且,這個解析器未來會在依賴預構建的時候用上,具體用法如下:

const resolve = config.createResolver()
// 調用以拿到 react 路徑
rseolve('react', undefined, undefined, false)

這里有aliasContainer和resolverContainer兩個工具對象,它們都含有resolveId這個專門解析路徑的方法,可以被 Vite 調用來獲取解析結果,本質都是PluginContainer。

接著,會順便處理一個 public 目錄,也就是 Vite 作為靜態(tài)資源服務的目錄:

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,而后調用每個插件的 configResolved 鉤子函數(shù)。其中 resolvePlugins 內部細節(jié)比較多,插件數(shù)量比較龐大,我們暫時不去深究具體實現(xiàn),編譯流水線這一小節(jié)再來詳細介紹。

至此,所有核心配置都生成完畢。不過,后面 Vite 還會處理一些邊界情況,在用戶配置不合理的時候,給用戶對應的提示。比如:用戶直接使用alias時,Vite 會提示使用resolve.alias。

最后,resolveConfig 函數(shù)會返回 resolved 對象,也就是最后的配置集合,那么配置解析服務到底也就結束了。

二、加載配置文件詳解

首先,我們來看一下加載配置文件(loadConfigFromFile)的實現(xiàn):

const loadResult = await loadConfigFromFile(/*省略傳參*/)

這里的邏輯稍微有點復雜,很難梳理清楚,所以我們不妨借助剛才梳理的配置解析流程,深入loadConfigFromFile 的細節(jié)中,研究下 Vite 對于配置文件加載的實現(xiàn)思路。

接下來,我們來分析下需要處理的配置文件類型,根據(jù)文件后綴和模塊格式可以分為下面這幾類:

  • TS + ESM 格式
  • TS + CommonJS 格式
  • JS + ESM 格式
  • JS + CommonJS 格式

2.1 識別配置文件的類別

首先,Vite 會檢查項目的 package.json文件,如果有type: "module"則打上 isESM 的標識:

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 也會給當前配置文件打上isESM和isTS的標識,方便后續(xù)的解析。

2.2 根據(jù)類別解析配置

2.2.1 ESM 格式

對于 ESM 格式配置的處理代碼如下:

let userConfig: UserConfigExport | undefined
if (isESM) {
  const fileUrl = require('url').pathToFileURL(resolvedPath)
  // 首先對代碼進行打包
  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 來讀取這個臨時的內容,以獲取到配置內容,再直接刪掉臨時文件:

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)編譯技術的一種具體實現(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 路徑結果要加上時間戳 query?這其實是為了讓 dev server 重啟后仍然讀取最新的配置,避免緩存。

2.2.2 CommonJS 格式

對于 CommonJS 格式的配置文件,Vite 集中進行了解析:

// 對于 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 后配置代碼的加載,代碼如下:

// 默認加載器
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 內部也是先讀取文件內容,然后編譯該模塊。當代碼中調用module._compile 相當于手動編譯一個模塊,該方法在 Node 內部的實現(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)
}

在調用完 module._compile 編譯完配置代碼后,進行一次手動的 require,即可拿到配置對象:

const raw = require(fileName)
const config = raw.__esModule ? raw.default : raw
// 恢復原生的加載方法
require.extensions[extension] = defaultLoader
// 返回配置
return config

這種運行時加載 TS 配置的方式,也叫做 JIT(即時編譯),這種方式和 AOT 最大的區(qū)別在于不會將內存中計算出來的 js 代碼寫入磁盤再加載,而是通過攔截 Node.js 原生 require.extension 方法實現(xiàn)即時加載。

至此,配置文件的內容已經(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
}

三、總結

下面我們來總結一下Vite 配置解析的整體流程和加載配置文件的方法:

首先,Vite 配置文件解析的邏輯由 resolveConfig 函數(shù)統(tǒng)一實現(xiàn),其中經(jīng)歷了加載配置文件、解析用戶插件、加載環(huán)境變量、創(chuàng)建路徑解析器工廠和生成插件流水線這幾個主要的流程。

其次,在加載配置文件的過程中,Vite 需要處理四種類型的配置文件,其中對于 ESM 和 CommonJS 兩種格式的 TS 文件,分別采用了AOT和JIT兩種編譯技術實現(xiàn)了配置加載。

以上就是Vite配置文件如何加載深入剖析的詳細內容,更多關于Vite加載配置文件的資料請關注腳本之家其它相關文章!

相關文章

  • vue項目中如何調用多個不同的ip接口

    vue項目中如何調用多個不同的ip接口

    這篇文章主要介紹了vue項目中如何調用多個不同的ip接口,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教
    2022-08-08
  • Vue中使用debugger在瀏覽器中不起作用的問題及解決

    Vue中使用debugger在瀏覽器中不起作用的問題及解決

    這篇文章主要介紹了Vue中使用debugger在瀏覽器中不起作用的問題及解決,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教
    2024-06-06
  • vuex入門最詳細整理

    vuex入門最詳細整理

    在本篇文章里小編給大家分享的是關于vuex入門最詳細整理的相關內容,需要的朋友們參考下。
    2020-03-03
  • Vue3中結合ElementPlus實現(xiàn)彈窗的封裝方式

    Vue3中結合ElementPlus實現(xiàn)彈窗的封裝方式

    這篇文章主要介紹了Vue3中結合ElementPlus實現(xiàn)彈窗的封裝方式,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教
    2024-01-01
  • Vue單文件組件的如何使用方式介紹

    Vue單文件組件的如何使用方式介紹

    本篇文章主要介紹了Vue單文件組件的如何使用方式介紹,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧
    2017-07-07
  • vue中輸入框事件的使用及數(shù)值校驗方式

    vue中輸入框事件的使用及數(shù)值校驗方式

    這篇文章主要介紹了vue中輸入框事件的使用及數(shù)值校驗方式,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教
    2022-08-08
  • vue3 el-select懶加載以及自定義指令方式

    vue3 el-select懶加載以及自定義指令方式

    這篇文章主要介紹了vue3 el-select懶加載以及自定義指令方式,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教
    2024-04-04
  • vue-star評星組件開發(fā)實例

    vue-star評星組件開發(fā)實例

    下面小編就為大家分享一篇vue-star評星組件開發(fā)實例,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧
    2018-03-03
  • Vue中掛載全局的方法詳解

    Vue中掛載全局的方法詳解

    有時候,頻繁調用的函數(shù),我們需要把它掛載在全局的vue原型上,方便調用,這篇文章主要為大家詳細介紹了Vue中掛載全局的具體操作,需要的可以參考下
    2024-03-03
  • Vue3監(jiān)聽store中數(shù)據(jù)變化的三種方式

    Vue3監(jiān)聽store中數(shù)據(jù)變化的三種方式

    這篇文章給大家介紹了Vue3監(jiān)聽store中數(shù)據(jù)變化的三種方法,使用watch和storeToRefs函數(shù),使用計算屬性computed和使用watchEffect函數(shù)這三種方法,文中通過代碼講解非常詳細,需要的朋友可以參考下
    2024-01-01

最新評論