由上文我们可知,来自node_modules
中的模块依赖是需要预构建的。例如import ElementPlus from 'element-plus'
。因为在浏览器环境下,是不支持这种裸模块引用的(bare import)。另一方面,如果不进行构建,浏览器面对由成百上千的子模块组成的依赖,依靠原生esm
的加载机制,每个的依赖的import
都将产生一次http
请求。面对大量的请求,浏览器是吃不消的。因此客观上需要对裸模块引入进行打包,并处理成浏览器环境下支持的相对路径或路径的导入方式。例如:import ElementPlus from '/path/to/.vite/element-plus/es/index.mjs'
。
2. 对查找到的依赖进行构建
在上一步,已经得到了需要预构建的依赖列表。现在需要把他们作为esbuild
的entryPoints
打包就行了。
//使用esbuild打包,入口文件即为第一步中抓取到的需要预构建的依赖 import { build } from 'esbuild' // ...省略其他代码 const result = await build({ absWorkingDir: process.cwd(), // flatIdDeps即为第一步中所得到的需要预构建的依赖对象 entryPoints: Object.keys(flatIdDeps), bundle: true, format: 'esm', target: config.build.target || undefined, external: config.optimizeDeps?.exclude, logLevel: 'error', splitting: true, sourcemap: true, // outdir指定打包产物输出目录,processingCacheDir这里并不是.vite,而是存放构建产物的临时目录 outdir: processingCacheDir, ignoreAnnotations: true, metafile: true, define, plugins: [ ...plugins, esbuildDepPlugin(flatIdDeps, flatIdToExports, config, ssr) ], ...esbuildOptions }) // 写入_metadata文件,并替换缓存文件。Write metadata file, delete `deps` folder and rename the new `processing` folder to `deps` in sync commitProcessingDepsCacheSync()
vite
并没有将esbuild
的outdir
(构建产物的输出目录)直接配置为.vite
目录,而是先将构建产物存放到了一个临时目录。当构建完成后,才将原来旧的.vite
(如果有的话)删除。然后再将临时目录重命名为.vite
。这样做主要是为了避免在程序运行过程中发生了错误,导致缓存不可用。
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 // 依赖处理完成后,使用依赖缓存目录替换处理中的依赖缓存目录 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) } }
以上就是预构建的主要处理流程。
缓存与预构建
vite
冷启动之所以快,除了esbuild
本身构建速度够快外,也与vite
做了必要的缓存机制密不可分。vite
在预构建时,除了生成预构建的js
文件外,还会创建一个_metadata.json
文件,其结构大致如下:
{ "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
是缓存的主要标识,由vite
的配置文件和项目依赖决定(依赖的信息取自package-lock.json
、yarn.lock
、pnpm-lock.yaml
)。 所以如果用户修改了vite.config.js
或依赖发生了变化(依赖的添加删除更新会导致lock文件变化)都会令hash
发生变化,缓存也就失效了。这时,vite
需要重新进行预构建。当然如果手动删除了.vite
缓存目录,也会重新构建。
// 基于配置文件+依赖信息生成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
启动时首先检查hash
的值,如果当前的hash
值与_metadata.json
中的hash
值相同,说明项目的依赖没有变化,无需重复构建了,直接使用缓存即可。
// 计算当前的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与当前hash if (prevData && prevData.hash === metadata.hash) { log('Hash is consistent. Skipping. Use --force to override.') return { metadata: prevData, run: () => (processing.resolve(), processing.promise) } }
总结
以上就是vite
预构建的主要处理逻辑,总结起来就是先查找需要预构建的依赖,然后将这些依赖作为entryPoints
进行构建,构建完成后更新缓存。vite
在启动时为提升速度,会检查缓存是否有效,有效的话就可以跳过预构建环节,缓存是否有效的判定是对比缓存中的hash
值与当前的hash
值是否相同。由于hash
的生成算法是基于vite
配置文件和项目依赖的,所以配置文件和依赖的的变化都会导致hash
发生变化,从而重新进行预构建。