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