vitejs預(yù)構(gòu)建理解及流程解析
引言
vite
在官網(wǎng)介紹中,第一條就提到的特性就是自己的本地冷啟動(dòng)極快。這主要是得益于它在本地服務(wù)啟動(dòng)的時(shí)候做了預(yù)構(gòu)建。出于好奇,抽時(shí)間了解了下vite
在預(yù)構(gòu)建部分的主要實(shí)現(xiàn)思路,分享出來(lái)供大家參考。
為啥要預(yù)構(gòu)建
簡(jiǎn)單來(lái)講就是為了提高本地開(kāi)發(fā)服務(wù)器的冷啟動(dòng)速度。按照vite
的說(shuō)法,當(dāng)冷啟動(dòng)開(kāi)發(fā)服務(wù)器時(shí),基于打包器的方式啟動(dòng)必須優(yōu)先抓取并構(gòu)建你的整個(gè)應(yīng)用,然后才能提供服務(wù)。隨著應(yīng)用規(guī)模的增大,打包速度顯著下降,本地服務(wù)器的啟動(dòng)速度也跟著變慢。
為了加快本地開(kāi)發(fā)服務(wù)器的啟動(dòng)速度,vite
引入了預(yù)構(gòu)建機(jī)制。在預(yù)構(gòu)建工具的選擇上,vite
選擇了 esbuild
。esbuild
使用 Go
編寫(xiě),比以 JavaScript
編寫(xiě)的打包器構(gòu)建速度快 10-100 倍,有了預(yù)構(gòu)建,再利用瀏覽器的esm
方式按需加載業(yè)務(wù)代碼,動(dòng)態(tài)實(shí)時(shí)進(jìn)行構(gòu)建,結(jié)合緩存機(jī)制,大大提升了服務(wù)器的啟動(dòng)速度。
預(yù)構(gòu)建的流程
1. 查找依賴(lài)
如果是首次啟動(dòng)本地服務(wù),那么vite
會(huì)自動(dòng)抓取源代碼,從代碼中找到需要預(yù)構(gòu)建的依賴(lài),最終對(duì)外返回類(lèi)似下面的一個(gè)deps
對(duì)象:
{ vue: '/path/to/your/project/node_modules/vue/dist/vue.runtime.esm-bundler.js', 'element-plus': '/path/to/your/project/node_modules/element-plus/es/index.mjs', 'vue-router': '/path/to/your/project/node_modules/vue-router/dist/vue-router.esm-bundler.js' }
具體實(shí)現(xiàn)就是,調(diào)用esbuild
的build
api,以index.html
作為查找入口(entryPoints
),將所有的來(lái)自node_modules
以及在配置文件的optimizeDeps.include
選項(xiàng)中指定的模塊找出來(lái)。
//...省略其他代碼 if (explicitEntryPatterns) { entries = await globEntries(explicitEntryPatterns, config) } else if (buildInput) { const resolvePath = (p: string) => path.resolve(config.root, p) if (typeof buildInput === 'string') { entries = [resolvePath(buildInput)] } else if (Array.isArray(buildInput)) { entries = buildInput.map(resolvePath) } else if (isObject(buildInput)) { entries = Object.values(buildInput).map(resolvePath) } else { throw new Error('invalid rollupOptions.input value.') } } else { // 重點(diǎn)看這里:使用html文件作為查找入口 entries = await globEntries('**/*.html', config) } //...省略其他代碼 build.onResolve( { // avoid matching windows volume filter: /^[\w@][^:]/ }, async ({ path: id, importer }) => { const resolved = await resolve(id, importer) if (resolved) { // 來(lái)自node_modules和在include中指定的模塊 if (resolved.includes('node_modules') || include?.includes(id)) { // dependency or forced included, externalize and stop crawling if (isOptimizable(resolved)) { // 重點(diǎn)看這里:將符合預(yù)構(gòu)建條件的依賴(lài)記錄下來(lái),depImports就是對(duì)外導(dǎo)出的需要預(yù)構(gòu)建的依賴(lài)對(duì)象 depImports[id] = resolved } return externalUnlessEntry({ path: id }) } else if (isScannable(resolved)) { const namespace = htmlTypesRE.test(resolved) ? 'html' : undefined // linked package, keep crawling return { path: path.resolve(resolved), namespace } } else { return externalUnlessEntry({ path: id }) } } else { missing[id] = normalizePath(importer) } } )
但是熟悉esbuild
的小伙伴可能知道,esbuild
默認(rèn)支持的入口文件類(lèi)型有js
、ts
、jsx
、css
、json
、base64
、dataurl
、binary
、file
(.png等),并不包括html
。
vite
是如何做到將index.html
作為打包入口的呢?原因是vite
自己實(shí)現(xiàn)了一個(gè)esbuild
插件esbuildScanPlugin
,來(lái)處理.vue
和.html
這種類(lèi)型的文件。
具體做法是讀取html
的內(nèi)容,然后將里面的script
提取到一個(gè)esm
格式的js
模塊。
// 對(duì)于html類(lèi)型(.VUE/.HTML/.svelte等)的文件,提取文件里的script內(nèi)容。html types: extract script contents ----------------------------------- build.onResolve({ filter: htmlTypesRE }, async ({ path, importer }) => { const resolved = await resolve(path, importer) if (!resolved) return // It is possible for the scanner to scan html types in node_modules. // If we can optimize this html type, skip it so it's handled by the // bare import resolve, and recorded as optimization dep. if (resolved.includes('node_modules') && isOptimizable(resolved)) return return { path: resolved, namespace: 'html' } }) // 配合build.onResolve,對(duì)于類(lèi)html文件,提取其中的script,作為一個(gè)js模塊extract scripts inside HTML-like files and treat it as a js module build.onLoad( { filter: htmlTypesRE, namespace: 'html' }, async ({ path }) => { let raw = fs.readFileSync(path, 'utf-8') // Avoid matching the content of the comment raw = raw.replace(commentRE, '<!---->') const isHtml = path.endsWith('.html') const regex = isHtml ? scriptModuleRE : scriptRE regex.lastIndex = 0 // js 的內(nèi)容被處理成了一個(gè)虛擬模塊 let js = '' let scriptId = 0 let match: RegExpExecArray | null while ((match = regex.exec(raw))) { const [, openTag, content] = match const typeMatch = openTag.match(typeRE) const type = typeMatch && (typeMatch[1] || typeMatch[2] || typeMatch[3]) const langMatch = openTag.match(langRE) const lang = langMatch && (langMatch[1] || langMatch[2] || langMatch[3]) // skip type="application/ld+json" and other non-JS types if ( type && !( type.includes('javascript') || type.includes('ecmascript') || type === 'module' ) ) { continue } // 默認(rèn)的js文件的loader是js,其他對(duì)于ts、tsx jsx有對(duì)應(yīng)的同名loader let loader: Loader = 'js' if (lang === 'ts' || lang === 'tsx' || lang === 'jsx') { loader = lang } const srcMatch = openTag.match(srcRE) // 對(duì)于<script src='path/to/some.js'>引入的js,將它轉(zhuǎn)換為import 'path/to/some.js'的代碼 if (srcMatch) { const src = srcMatch[1] || srcMatch[2] || srcMatch[3] js += `import ${JSON.stringify(src)}\n` } else if (content.trim()) { // The reason why virtual modules are needed: // 1. There can be module scripts (`<script context="module">` in Svelte and `<script>` in Vue) // or local scripts (`<script>` in Svelte and `<script setup>` in Vue) // 2. There can be multiple module scripts in html // We need to handle these separately in case variable names are reused between them // append imports in TS to prevent esbuild from removing them // since they may be used in the template const contents = content + (loader.startsWith('ts') ? extractImportPaths(content) : '') // 將提取出來(lái)的script腳本,存在以xx.vue?id=1為key的script對(duì)象中script={'xx.vue?id=1': 'js contents'} const key = `${path}?id=${scriptId++}` if (contents.includes('import.meta.glob')) { scripts[key] = { // transformGlob already transforms to js loader: 'js', contents: await transformGlob( contents, path, config.root, loader, resolve, config.logger ) } } else { scripts[key] = { loader, contents } } const virtualModulePath = JSON.stringify( virtualModulePrefix + key ) const contextMatch = openTag.match(contextRE) const context = contextMatch && (contextMatch[1] || contextMatch[2] || contextMatch[3]) // Especially for Svelte files, exports in <script context="module"> means module exports, // exports in <script> means component props. To avoid having two same export name from the // star exports, we need to ignore exports in <script> if (path.endsWith('.svelte') && context !== 'module') { js += `import ${virtualModulePath}\n` } else { // e.g. export * from 'virtual-module:xx.vue?id=1' js += `export * from ${virtualModulePath}\n` } } } // This will trigger incorrectly if `export default` is contained // anywhere in a string. Svelte and Astro files can't have // `export default` as code so we know if it's encountered it's a // false positive (e.g. contained in a string) if (!path.endsWith('.vue') || !js.includes('export default')) { js += '\nexport default {}' } return { loader: 'js', contents: js } } )
由上文我們可知,來(lái)自node_modules
中的模塊依賴(lài)是需要預(yù)構(gòu)建的。
例如import ElementPlus from 'element-plus'。
因?yàn)樵跒g覽器環(huán)境下,是不支持這種裸模塊引用的(bare import)。
另一方面,如果不進(jìn)行構(gòu)建,瀏覽器面對(duì)由成百上千的子模塊組成的依賴(lài),依靠原生esm
的加載機(jī)制,每個(gè)的依賴(lài)的import
都將產(chǎn)生一次http
請(qǐng)求。面對(duì)大量的請(qǐng)求,瀏覽器是吃不消的。
因此客觀上需要對(duì)裸模塊引入進(jìn)行打包,并處理成瀏覽器環(huán)境下支持的相對(duì)路徑或路徑的導(dǎo)入方式。
例如:import ElementPlus from '/path/to/.vite/element-plus/es/index.mjs'。
2. 對(duì)查找到的依賴(lài)進(jìn)行構(gòu)建
在上一步,已經(jīng)得到了需要預(yù)構(gòu)建的依賴(lài)列表?,F(xiàn)在需要把他們作為esbuild
的entryPoints
打包就行了。
//使用esbuild打包,入口文件即為第一步中抓取到的需要預(yù)構(gòu)建的依賴(lài) import { build } from 'esbuild' // ...省略其他代碼 const result = await build({ absWorkingDir: process.cwd(), // flatIdDeps即為第一步中所得到的需要預(yù)構(gòu)建的依賴(lài)對(duì)象 entryPoints: Object.keys(flatIdDeps), bundle: true, format: 'esm', target: config.build.target || undefined, external: config.optimizeDeps?.exclude, logLevel: 'error', splitting: true, sourcemap: true, // outdir指定打包產(chǎn)物輸出目錄,processingCacheDir這里并不是.vite,而是存放構(gòu)建產(chǎn)物的臨時(shí)目錄 outdir: processingCacheDir, ignoreAnnotations: true, metafile: true, define, plugins: [ ...plugins, esbuildDepPlugin(flatIdDeps, flatIdToExports, config, ssr) ], ...esbuildOptions }) // 寫(xiě)入_metadata文件,并替換緩存文件。Write metadata file, delete `deps` folder and rename the new `processing` folder to `deps` in sync commitProcessingDepsCacheSync()
vite
并沒(méi)有將esbuild
的outdir
(構(gòu)建產(chǎn)物的輸出目錄)直接配置為.vite
目錄,而是先將構(gòu)建產(chǎn)物存放到了一個(gè)臨時(shí)目錄。當(dāng)構(gòu)建完成后,才將原來(lái)舊的.vite
(如果有的話(huà))刪除。然后再將臨時(shí)目錄重命名為.vite
。這樣做主要是為了避免在程序運(yùn)行過(guò)程中發(fā)生了錯(cuò)誤,導(dǎo)致緩存不可用。
function commitProcessingDepsCacheSync() { // Rewire the file paths from the temporal processing dir to the final deps cache dir const dataPath = path.join(processingCacheDir, '_metadata.json') writeFile(dataPath, stringifyOptimizedDepsMetadata(metadata)) // Processing is done, we can now replace the depsCacheDir with processingCacheDir // 依賴(lài)處理完成后,使用依賴(lài)緩存目錄替換處理中的依賴(lài)緩存目錄 if (fs.existsSync(depsCacheDir)) { const rmSync = fs.rmSync ?? fs.rmdirSync // TODO: Remove after support for Node 12 is dropped rmSync(depsCacheDir, { recursive: true }) } fs.renameSync(processingCacheDir, depsCacheDir) } }
以上就是預(yù)構(gòu)建的主要處理流程。
緩存與預(yù)構(gòu)建
vite
冷啟動(dòng)之所以快,除了esbuild
本身構(gòu)建速度夠快外,也與vite
做了必要的緩存機(jī)制密不可分。
vite
在預(yù)構(gòu)建時(shí),除了生成預(yù)構(gòu)建的js
文件外,還會(huì)創(chuàng)建一個(gè)_metadata.json
文件,其結(jié)構(gòu)大致如下:
{ "hash": "22135fca", "browserHash": "632454bc", "optimized": { "vue": { "file": "/path/to/your/project/node_modules/.vite/vue.js", "src": "/path/to/your/project/node_modules/vue/dist/vue.runtime.esm-bundler.js", "needsInterop": false }, "element-plus": { "file": "/path/to/your/project/node_modules/.vite/element-plus.js", "src": "/path/to/your/project/node_modules/element-plus/es/index.mjs", "needsInterop": false }, "vue-router": { "file": "/path/to/your/project/node_modules/.vite/vue-router.js", "src": "/path/to/your/project/node_modules/vue-router/dist/vue-router.esm-bundler.js", "needsInterop": false } } }
hash
是緩存的主要標(biāo)識(shí),由vite
的配置文件和項(xiàng)目依賴(lài)決定(依賴(lài)的信息取自package-lock.json
、yarn.lock
、pnpm-lock.yaml
)。 所以如果用戶(hù)修改了vite.config.js
或依賴(lài)發(fā)生了變化(依賴(lài)的添加刪除更新會(huì)導(dǎo)致lock文件變化)都會(huì)令hash
發(fā)生變化,緩存也就失效了。這時(shí),vite
需要重新進(jìn)行預(yù)構(gòu)建。當(dāng)然如果手動(dòng)刪除了.vite
緩存目錄,也會(huì)重新構(gòu)建。
// 基于配置文件+依賴(lài)信息生成hash const lockfileFormats = ['package-lock.json', 'yarn.lock', 'pnpm-lock.yaml'] function getDepHash(root: string, config: ResolvedConfig): string { let content = lookupFile(root, lockfileFormats) || '' // also take config into account // only a subset of config options that can affect dep optimization content += JSON.stringify( { mode: config.mode, root: config.root, define: config.define, resolve: config.resolve, buildTarget: config.build.target, assetsInclude: config.assetsInclude, plugins: config.plugins.map((p) => p.name), optimizeDeps: { include: config.optimizeDeps?.include, exclude: config.optimizeDeps?.exclude, esbuildOptions: { ...config.optimizeDeps?.esbuildOptions, plugins: config.optimizeDeps?.esbuildOptions?.plugins?.map( (p) => p.name ) } } }, (_, value) => { if (typeof value === 'function' || value instanceof RegExp) { return value.toString() } return value } ) return createHash('sha256').update(content).digest('hex').substring(0, 8) }
在vite
啟動(dòng)時(shí)首先檢查hash
的值,如果當(dāng)前的hash
值與_metadata.json
中的hash
值相同,說(shuō)明項(xiàng)目的依賴(lài)沒(méi)有變化,無(wú)需重復(fù)構(gòu)建了,直接使用緩存即可。
// 計(jì)算當(dāng)前的hash const mainHash = getDepHash(root, config) const metadata: DepOptimizationMetadata = { hash: mainHash, browserHash: mainHash, optimized: {}, discovered: {}, processing: processing.promise } let prevData: DepOptimizationMetadata | undefined try { const prevDataPath = path.join(depsCacheDir, '_metadata.json') prevData = parseOptimizedDepsMetadata( fs.readFileSync(prevDataPath, 'utf-8'), depsCacheDir, processing.promise ) } catch (e) { } // hash is consistent, no need to re-bundle // 比較緩存的hash與當(dāng)前hash if (prevData && prevData.hash === metadata.hash) { log('Hash is consistent. Skipping. Use --force to override.') return { metadata: prevData, run: () => (processing.resolve(), processing.promise) } }
總結(jié)
以上就是vite
預(yù)構(gòu)建的主要處理邏輯,總結(jié)起來(lái)就是先查找需要預(yù)構(gòu)建的依賴(lài),然后將這些依賴(lài)作為entryPoints
進(jìn)行構(gòu)建,構(gòu)建完成后更新緩存。vite
在啟動(dòng)時(shí)為提升速度,會(huì)檢查緩存是否有效,有效的話(huà)就可以跳過(guò)預(yù)構(gòu)建環(huán)節(jié),緩存是否有效的判定是對(duì)比緩存中的hash
值與當(dāng)前的hash
值是否相同。由于hash
的生成算法是基于vite
配置文件和項(xiàng)目依賴(lài)的,所以配置文件和依賴(lài)的的變化都會(huì)導(dǎo)致hash
發(fā)生變化,從而重新進(jìn)行預(yù)構(gòu)建。
更多關(guān)于vitejs預(yù)構(gòu)建流程的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!,希望大家以后多多支持腳本之家!
相關(guān)文章
vue通過(guò)watch對(duì)input做字?jǐn)?shù)限定的方法
本篇文章主要介紹了vue通過(guò)watch對(duì)input做字?jǐn)?shù)限定的方法,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-07-07vue如何實(shí)現(xiàn)在線(xiàn)編輯excel
這篇文章主要介紹了vue如何實(shí)現(xiàn)在線(xiàn)編輯excel問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-10-10Vue項(xiàng)目結(jié)合Vue-layer實(shí)現(xiàn)彈框式編輯功能(實(shí)例代碼)
這篇文章主要介紹了Vue項(xiàng)目中結(jié)合Vue-layer實(shí)現(xiàn)彈框式編輯功能,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-03-03vue中關(guān)于template報(bào)錯(cuò)等問(wèn)題的解決
這篇文章主要介紹了vue中關(guān)于template報(bào)錯(cuò)等問(wèn)題的解決,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-04-04