vite 预编译实现
直入正題,前段時(shí)間, vite 做了一個(gè)優(yōu)化 – 依賴預(yù)編譯。本文就來(lái)逐步分析預(yù)編譯的邏輯和代碼實(shí)現(xiàn)。
那什么是依賴預(yù)編譯呢?這一過(guò)程簡(jiǎn)而言之,就是在 DevServer 啟動(dòng)前對(duì)須編譯的依賴,進(jìn)行預(yù)先編譯,而后在模塊使用導(dǎo)入(import)時(shí),會(huì)直接引用預(yù)編譯過(guò)的依賴。
我們先來(lái)看張圖,梳理一下整體的預(yù)編譯邏輯。在 DevServer 啟動(dòng)前,在模塊使用導(dǎo)入(import)時(shí),vite 會(huì)解析該依賴是否有緩存,如果存在,則判斷緩存是否失效,若失效則加入預(yù)編譯列表;如未失效則利用 node_modules/.vite 目錄下對(duì)應(yīng)編譯后的依賴;如果不存在,為首次預(yù)編譯,則加入預(yù)編譯列表中,等所有預(yù)編譯依賴收集完成后進(jìn)行預(yù)編譯。
接下來(lái)我們分塊梳理。
1. createServer
首先,vite 會(huì)創(chuàng)建一個(gè)本地開(kāi)發(fā)服務(wù)器,這個(gè)過(guò)程由 createServer 函數(shù)完成。
監(jiān)聽(tīng)端口,執(zhí)行其他服務(wù)之前,會(huì)執(zhí)行 optimizeDeps 方法,即優(yōu)化依賴。vite 將這部分優(yōu)化叫做依賴預(yù)打包 Dependency Pre-Bundling,這么做的理由有兩個(gè):一是將非 ES module轉(zhuǎn)化為可被瀏覽器導(dǎo)入的 ESM;二是將 ESM 依賴的多個(gè)內(nèi)部模塊轉(zhuǎn)化為一個(gè)模塊,以減少瀏覽器請(qǐng)求從而提升頁(yè)面加載速度。
createServer 方法中包含初了始化配置,HMR,預(yù)打包 等功能。我們重點(diǎn)關(guān)注預(yù)打包代碼。
createServer 函數(shù):
export async function createServer(inlineConfig: inlineConfig = {} ): Promise<ViteDevServer> {... if (!middlewareMode && httpServer) {// 重寫(xiě) DevServer 的 listen,保證在 DevServer 啟動(dòng)前進(jìn)行依賴預(yù)編譯const listen = httpServer.listen.bind(httpServer)httpServer.listen = (async (port: number, ...args: any[]) => {try {...// 依賴預(yù)編譯await runOptimize()} ...}) as any...} else {await runOptimize()}... }createServer 代碼里可以看到,在服務(wù)器啟動(dòng)前,會(huì)先調(diào)用 runOptimize 函數(shù),來(lái)處理依賴預(yù)編譯相關(guān)的邏輯。
2. runOptimize
一起看看 runOptimize 函數(shù):
const runOptimize = async () => { // config.optimzizeCacheDir 指的是 node_modules/.vite 文件下的內(nèi)容,用來(lái)存放預(yù)編譯的文件if (config.optimizeCacheDir) {...try {// 進(jìn)行依賴預(yù)編譯server._optimizeDepsMetadata = await optimizeDeps(config)}...//注冊(cè)依賴預(yù)編譯server._registerMissingImport = createMissingImpoterRegisterFn(server)} }通過(guò)代碼知道,runOptimize 函數(shù)主要做兩件事:
執(zhí)行依賴的預(yù)編譯方法
注冊(cè)新依賴的預(yù)編譯
2.1 依賴預(yù)編譯
runOptimize 告訴我們,執(zhí)行預(yù)編譯的核心函數(shù)由 optimizeDeps 方法完成。
optimizeDeps 的實(shí)現(xiàn)在第三章具體分析,這里先整體表述 optimizeDeps 邏輯。 optimizeDeps 會(huì)根據(jù)配置文件 vite.config.js 的 optimizeDeps 對(duì)象內(nèi)容和 package.json 的 dependencies 進(jìn)行第一次預(yù)編譯;對(duì)于沒(méi)有配置的依賴,vite 會(huì)先解析 AST 語(yǔ)法樹(shù)里面使用到的依賴,再將該依賴進(jìn)行預(yù)編譯。
預(yù)編譯結(jié)束后,在 node_moduels/.vite 文件下生成一份 _metadata.json 對(duì)象文件,主要用來(lái)存儲(chǔ)預(yù)編譯依賴的詳細(xì)信息。如下圖:
里面每個(gè)屬性的含義:
- hash 獲取該文件此時(shí) hash,主要利用文件簽名以及 config 屬性是否改變來(lái)判斷,是否須要從新編譯;
- browserHash 由 hash 和在運(yùn)行時(shí)發(fā)現(xiàn)的額定的依賴生成的,主要用于優(yōu)化請(qǐng)求數(shù)量,避免太多的請(qǐng)求影響性能;
- optimized 包含每個(gè)進(jìn)行過(guò)預(yù)編譯的依賴,其對(duì)應(yīng)的屬性會(huì)描述依賴源文件路徑 src 和編譯后所在路徑 file;
- needsInterop 主要用于在 vite 進(jìn)行依賴性導(dǎo)入分析,它會(huì)重寫(xiě)需要預(yù)編譯且為 commonJS 的依賴。例如:
2.2 注冊(cè)依賴預(yù)編譯
runOptimize 告訴我們,注冊(cè)依賴預(yù)編譯調(diào)用 createMissingImporterRegisterFn 函數(shù)實(shí)現(xiàn),主要是注冊(cè)新的依賴預(yù)編譯。
createMissingImporterRegisterFn 函數(shù):
//在觸發(fā)前等待新依賴項(xiàng)的請(qǐng)求數(shù)量 export function createMissingImporterRegisterFn(server: ViteDevServer){...async function rerun(){...try{server._isRunningOptimizer = true;server._optimizeDepsMetadata = null;const newData = (server._optimizeDepsMetadata = await optimizeDeps(server.config,true,false,newDeps ))}...}return function registerMissingImport(id:string, resolved: string){...handle = setTimeout(rerun,100);...}... }它會(huì)返回一個(gè)函數(shù),函數(shù)內(nèi)部調(diào)用 optimizeDeps 函數(shù)進(jìn)行預(yù)編譯。與第一次預(yù)編譯不同的是,新預(yù)編譯會(huì)傳入一個(gè) newDeps,即新的需要預(yù)編譯的依賴。
通過(guò)對(duì) runOptimize 里執(zhí)行依賴的預(yù)編譯方法和注冊(cè)依賴預(yù)編譯代碼的梳理,看到均由 optimizeDeps 函數(shù)來(lái)實(shí)現(xiàn)依賴預(yù)編譯。接下來(lái),劃重點(diǎn) optimizeDeps。
3. optimizeDeps
optimizeDeps 是預(yù)編譯的核心內(nèi)容,由于內(nèi)部邏輯比較復(fù)雜,我們拆分為三大步,依賴是否失效 -> 收集依賴 -> esbuild 打包。具體的邏輯如下圖所示。
3.1 依賴是否生效
在代碼里依賴失效與否,主要通過(guò)文件內(nèi)容對(duì)應(yīng)的 hash 值來(lái)判斷,以便于判斷依賴是否失效以及依賴發(fā)生變化時(shí),能夠重新編譯,應(yīng)用最新的編譯文件。
第一步,需要讀取緩存的文件信息。
3.1.1 讀取 hash
每次編譯都需要讀取該依賴的當(dāng)前文件信息,調(diào)用 getDepHash 方法,拿到對(duì)應(yīng)的 hash 值。
具體代碼如下:
第二步,判斷 hash 值是否失效。
3.1.2 對(duì)比 hash
對(duì)比當(dāng)前文件的 hash 和 _metadata.json文件的 hash 是否一致,如果一致,則緩存未失效,直接返回上次依賴緩存的信息,optimizeDeps 方法也至此結(jié)束;如果不一致,則緩存失效,需要重新進(jìn)行預(yù)編譯。
具體代碼如下:
第三步,緩存失效或不存在,需要重新預(yù)編譯。
3.1.3 緩存失效或不存在
如果緩存失效,則刪除緩存文件夾即 node_modules/.vite ;還有一種情況,緩存文件不存在,即第一次進(jìn)行預(yù)編譯,需新建緩存文件夾。
先來(lái)看判斷緩存是否失效代碼:
當(dāng)然了,更新緩存后,需要及時(shí)地更新 hash。
//更新 browser hash data.browserHash = createHash('sha256').update(data.hash + JSON.stringify(deps)).digest('hex').substr(0, 8)3.2 收集依賴
在上述判斷緩存失效后,就需要收集依賴。主要為兩大類依賴,編譯依賴和指定依賴。
3.2.1 收集編譯依賴
依賴收集情況分為兩種:首次預(yù)編譯和后續(xù)更新依賴。這兩者的區(qū)別在于,后續(xù)更新會(huì)傳入一個(gè) newDep 來(lái)表示需預(yù)編譯模塊。代碼如下:
let deps: Record<string, string>, missing: Record<string, string> if (!newDeps) {// 首次預(yù)編譯;({ missing,deps } = await scanImports(config)) } else {// 后續(xù)更新依賴// 直接將需要更新的依賴賦給 deps,此時(shí)不存在 missing 依賴deps = newDepsmissing = {} }通過(guò)代碼知道,如果是第一次預(yù)編譯,則會(huì)調(diào)用 scanImports 函數(shù)來(lái)找出需要預(yù)編譯的依賴 deps 和 missing。
missing 為引入但不能成功解析的模塊,即在 node_modules 中沒(méi)找到的依賴;deps 是一個(gè)對(duì)象,主要用來(lái)存儲(chǔ)模塊路徑,結(jié)構(gòu)如下:
{lodash:'/Users/user/Documents/user/code/vite/vite-project/node_modules/lodash/lodash.js' }3.1.2 收集指定依賴
預(yù)編譯的依賴除了 import 引入也會(huì)由 vite.config.js 的 optimizeDeps 選項(xiàng)指定以來(lái)。所以在處理完 import 的依賴后,需要處理 optimizeDeps 配置的依賴。
此時(shí),會(huì)遍歷、從 dependencies 獲取到的 deps,判斷 optimizeDeps.iclude(數(shù)組)所指定的依賴是否存在,若存在就省去此次制定編譯;若不存在,則加入強(qiáng)制執(zhí)行編譯依賴中。
// 拿到 vite.config.js 的 optimizeDeps const include = config.optimizeDeps?.includeif (include) {// 解析依賴const resolve = config.createResolver({ asSrc: false })for (const id of include) {// 制定依賴是否存在 deps 中if (!deps[id]) {const entry = await resolve(id)if (entry) {deps[id] = entry} else {throw new Error(`Failed to resolve force included dependency: ${chalk.cyan(id)}`)}}}}3.3 ESbuild 打包
在確認(rèn)需要預(yù)構(gòu)建的依賴后,就到了最后一步,使用 esbuild 對(duì)依賴進(jìn)行編譯打包。代碼如下:
const esbuildService = await ensureService() await esbuildService.build({entryPoints: Object.keys(flatIdDeps),bundle: true,format: 'esm',... })ensureService 函數(shù)是 vite 外部封裝的 util,ensureService 實(shí)質(zhì)是創(chuàng)立一個(gè) esbuild 的 service,應(yīng)用 service.build 函數(shù)來(lái)實(shí)現(xiàn)編譯過(guò)程。
flatIdDeps 參數(shù)是一個(gè)對(duì)象,它是由上述的 deps 收集好的依賴創(chuàng)立,它的作用是為 esbuild 進(jìn)行編譯的時(shí)候提供多路口,flatIdDeps 對(duì)象:
{lodash-es:'/Users/user/Documents/FE/demos/vite2.0-demo/node_modules/lodash-es/lodash.js' }至此,我們分析了 vite 的預(yù)編譯邏輯和代碼實(shí)現(xiàn)。vite 通過(guò)對(duì)依賴進(jìn)行預(yù)編譯和預(yù)編譯緩存,防止重復(fù)預(yù)編譯,可以減少不必要的等待項(xiàng)目重啟或模塊更新時(shí)間,從而縮短冷啟動(dòng),使得開(kāi)發(fā)人員擁有更良好的開(kāi)發(fā)體驗(yàn),加快開(kāi)發(fā)進(jìn)度。
總結(jié)
以上是生活随笔為你收集整理的vite 预编译实现的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: Spring中的DataSource
- 下一篇: 职业化“实训”网管培训班招生简章