vite源码分析(2):vite依赖预构建功能分析本文旨在分析vite中的依赖预构建功能,并附加相关功能的代码地址和官网
本文旨在分析vite中的依赖预构建功能,并附加相关功能的代码地址和官网地址。
依赖预构建主要流程
-
在开启server之前进行依赖预构建。
-
读取用户的package-lock.json,yarn.lock,pnpm-lock.yaml,生成depHash。
-
读取上次文件缓存的预构建文件信息,如果有,则将获取到的hash和上一步的depHash进行比较,一样则返回,否则重新构建。没有缓存或设置force参数,则重新构建。
-
利用esbuild,对项目文件进行扫描,获取到项目依赖。
-
利用esbuild,将项目依赖的模块化方式转化成es module方式。
-
将转换的模块存入cacheDir(默认是node_module/.vite)。
-
前端请求资源时,判断请求资源是否为依赖(即bare import),如果是则替换为缓存文件路径,加载相应文件。
-
启动服务后,每当引入新的依赖,则重新进行依赖构建。执行2,3,4,5过程。
源码分析
构建开始代码入口
如果不是中间件模式,则在server启动前,首先执行plugin.buildStart钩子函数,再执行构建函数。否则直接执行,此处的container是一个plugin的集合体,按运行顺序依次执行相关钩子函数。
if (!middlewareMode && httpServer) {
const listen = httpServer.listen.bind(httpServer)
// 重写listen,确保server启动之前执行。
httpServer.listen = (async (port, ...args) => {
try {
// container plugin集合体
await container.buildStart({})// plugin.buildStart
await runOptimize() // 预构建
} catch (e) {}
return listen(port, ...args)
})
} else {
await container.buildStart({})
await runOptimize()
}
runOptimize函数
_isRunningOptimizer添加构建状态,optimizeDeps函数返回构建过程3,4,5步中返回的预构建信息,_registerMissingImport返回一个预构建函数可以随时进行预构建,当运行的服务中有新的依赖引入时重新构建,同时_isRunningOptimizer状态可以有效避免构建时的数据请求。
const runOptimize = async () => {
if (config.cacheDir) {
server._isRunningOptimizer = true
try {
server._optimizeDepsMetadata = await optimizeDeps(config)
} finally {
server._isRunningOptimizer = false
}
server._registerMissingImport = createMissingImporterRegisterFn(server)
}
}
optimizeDeps函数
optimizeDeps是主要的函数,做以下事情
-
获取前次预构建信息,对此次信息进行比较,决定是否重新构建。
-
扫描源码,或根据参数,获取依赖。
-
利用
es-module-lexer
扁平化嵌套的源码依赖。 -
解析用户依赖优化配置,调用esbuild构建文件,并存入cacheDir。
-
存放此次构建信息并返回
function optimizeDeps( config, force=config.server.force,asCommand=false,newDeps?) { // ... const dataPath = path.join(cacheDir, '_metadata.json') // 生成此次构建hash const mainHash = getDepHash(root, config) const data: DepOptimizationMetadata = { hash: mainHash, browserHash: mainHash, optimized: {} } // 用户的force参数决定是否每次都重新构建 if (!force) { let prevData try { // 加载上次构建信息 prevData = JSON.parse(fs.readFileSync(dataPath, 'utf-8')) } catch (e) {} // 前后比对hash,相同则直接返回 if (prevData && prevData.hash === data.hash) { return prevData } } // ... // newDeps参数是在服务启动后加入依赖时传入的依赖信息。 let deps if (!newDeps) { // 借助esbuild扫描源码,获取依赖 ;({ deps, missing } = await scanImports(config)) } else { deps = newDeps missing = {} } // ... const include = config.optimizeDeps?.include if (include) { // ...加入用户指定的include } // 扁平化依赖 await init for (const id in deps) { flatIdDeps[id]=//... // ... } // ... // ...加入用户指定的esbuildOptions const { plugins = [], ...esbuildOptions } = config.optimizeDeps?.esbuildOptions ?? {} // 调用esbuild.build打包文件 const result = await build({ // ... entryPoints: Object.keys(flatIdDeps),// 入口 format: 'esm',// 打包成esm模式 external: config.optimizeDeps?.exclude,// 剔除exclude文件 outdir: cacheDir,// 输出地址 // ... }) // 重新写入_metadata.json for (const id in deps) { const entry = deps[id] data.optimized[id] = { file: normalizePath(path.resolve(cacheDir, flattenId(id) + '.js')), src: entry, } } writeFile(dataPath, JSON.stringify(data, null, 2)) return data }
前端获取依赖时替换成缓存的依赖
过程:访问有引入依赖的文件时,匹配依赖名称,返回cacheDir下的依赖路径。
- 解析config时在plugins中引入preAliasPlugin插件
- 匹配依赖名字,返回添加缓存路径的名字
plugin.resolveId的作用是,如果返回一个值,则会替换源码中的依赖,否则将名字传递给下一个插件处理,这里当匹配到依赖名字后,通过返回tryOptimizedResolve函数修改源码中的依赖名字。此时打开浏览器devtool,可以看到文件里的react路径变成了/node_modules/.vite/react.js?v=7db446d6
const bareImportRE = /^[\w@](?!.*:\/\/)/ // 匹配依赖
function preAliasPlugin() {
let server: ViteDevServer
return {
name: 'vite:pre-alias',
configureServer(_server) {
server = _server
},
resolveId(id, _, __, ssr) {
// 判断是依赖,则添加缓存路径
if (!ssr && bareImportRE.test(id)) {
return tryOptimizedResolve(id, server)
}
}
}
}
function tryOptimizedResolve(
id: string,
server: ViteDevServer
): string | undefined {
const cacheDir = server.config.cacheDir
const depData = server._optimizeDepsMetadata // 依赖预构建中生成的构建信息
if (cacheDir && depData) {
const isOptimized = depData.optimized[id] // 查找是否存在依赖
if (isOptimized) {
return ( // 返回新的依赖路径
isOptimized.file +
`?v=${depData.browserHash}${
isOptimized.needsInterop ? `&es-interop` : ``
}`
)
}
}
}
运行服务时检测新增依赖重新构建
此处的代码太散乱,大致流程是:请求新依赖资源后,躲过了preAliasPlugin的匹配,依赖名称传递到resolvePlugin插件中,判断引入依赖的文件是否也为依赖,如果是则重新构建。
转载自:https://juejin.cn/post/6967257397991571463