本文使用vite 5.2.8版本
依赖预构建的入口是initDepsOptimizer
函数,由initServer
触发。
但在触发之前,通过isDepsOptimizerEnabled
来判断,是否需要进行依赖预构建。
而isDepsOptimizerEnabled
的逻辑与文档保持一致。
如果你想完全禁用优化器,可以设置
optimizeDeps.noDiscovery: true
来禁止自动发现依赖项,并保持optimizeDeps.include
未定义或为空。
在代码中也针对noDiscovery
和optimizeDeps.include
进行了判断,并进行取反,如果不符合禁止依赖预构建的条件,那么就启用依赖预构建。
typescript
export function isDepsOptimizerEnabled(
config: ResolvedConfig,
ssr: boolean,
): boolean {
// 获取预构建配置
const optimizeDeps = getDepOptimizationConfig(config, ssr)
// 如果不符合禁止预构建的条件
return !(optimizeDeps.noDiscovery && !optimizeDeps.include?.length)
}
而initDepsOptimizer
实际是启用了一个单例的依赖预构建,如果当前的配置已经存在一个对应的预构建对象,那么就使用缓存,否则就创建一个。
typescript
export function getDepsOptimizer(
config: ResolvedConfig,
ssr?: boolean,
): DepsOptimizer | undefined {
// 根据config从 WeakMap 获取预构建对象
return (ssr ? devSsrDepsOptimizerMap : depsOptimizerMap).get(config)
}
export async function initDepsOptimizer(
config: ResolvedConfig,
server: ViteDevServer,
): Promise<void> {
// 如果本地缓存 (WeakMap)获取不到预构建对象,使用createDepsOptimizer创建一个新的
if (!getDepsOptimizer(config, false)) {
await createDepsOptimizer(config, server)
}
}
因此,我们的目光来到了createDepsOptimizer
这个函数,我们分段理解这个函数。
createDepsOptimizer
首先,使用loadCachedDepOptimizationMetadata
,通过配置中的config.cacheDir
,并且拼接上deps
和_metadata.json
,来获取本地存储的_metadata.json
。
一般情况下, config.cacheDir
会被给予默认值node_modules/.vite
,因此_metadata.json
的默认地址是node_modules/.vite/deps/_metadata.json
。
typescript
const cachedMetadata = await loadCachedDepOptimizationMetadata(config, ssr)
如果cachedMetadata
是空的,也就是loadCachedDepOptimizationMetadata
获取不到硬盘上存储的_metadata.json
,那么就使用initDepsOptimizerMetadata
,来初始化一个metadata
数据。
typescript
let metadata =
cachedMetadata || initDepsOptimizerMetadata(config, ssr, sessionTimestamp)
initDepsOptimizerMetadata
仅仅是初始化metadata
,并不负责将它存储在硬盘上。
我们看看initDepsOptimizerMetadata
初始化了什么数据。
typescript
export function initDepsOptimizerMetadata(
config: ResolvedConfig,
ssr: boolean,
timestamp?: string,
): DepOptimizationMetadata {
const { lockfileHash, configHash, hash } = getDepHash(config, ssr)
return {
hash,
lockfileHash,
configHash,
browserHash: getOptimizedBrowserHash(hash, {}, timestamp),
optimized: {},
chunks: {},
discovered: {},
depInfoList: [],
}
}
lockfileHash
:枚举常见包管理工具的lock
文件,优先使用npm_config_user_agent
中定义的包,否则依照以下优先级:package-lock.json
>yarn.lock
>pnpm-lock.yaml
>bun.lockb
从项目文件或者父目录中寻找,计算找到文件的hash
。configHash
:从config
摘取了几个关键配置组成了JSON
,这个JSON
的hash
。hash
:lockfileHash
和configHash
拼接到一起的字符串,计算出来的hash
。browserHash
:上面的hash
和空JSON
字符串以及时间戳拼接在一起的字符串,计算出来的hash
。optimized
:每个预构建依赖的对照集合。depInfoList
:依赖列表。chunks
:chunk
集合。discovered
:新发现的依赖。
因此,变量metadata
在经过以上逻辑后,总会有值的。
然后,连同预构建配置项,一起包装成预构建对象,并存入本地缓存中。
typescript
const depsOptimizer: DepsOptimizer = {
metadata, // 上文读取的metadata
registerMissingImport, // 发现新的依赖进行预构建
run: () => debouncedProcessing(0), // 立即预构建
isOptimizedDepFile: createIsOptimizedDepFile(config), //是否是经过预构建的文件
isOptimizedDepUrl: createIsOptimizedDepUrl(config),//是否是经过预构建的
getOptimizedDepId: (depInfo: OptimizedDepInfo) =>//预构建产物拼入hash
`${depInfo.file}?v=${depInfo.browserHash}`,
close, //结束预构建
options, //预构建配置项
}
depsOptimizerMap.set(config, depsOptimizer)
接着,会判断如果本地硬盘没有找到_metadata.json
,那么首先会处理optimizeDeps.include
,通过跑一遍插件流水线resolveId
钩子,计算出将它们的相关信息,然后推入metadata
的discovered
和depInfoList
字段。
这里需要注意的是,这些信息中的file
字段,是这个依赖被预构建之后的地址,因为预构建还没开始,因此此时此刻,硬盘上并没有对应地址。
如果使用optimizeDeps.noDiscovery: true
来禁止自动发现依赖项,那么调用runOptimizer
,将只处理metadata
中,optimized
和discovered
记录的依赖。
如果没有禁止自动发现依赖,那么就使用discoverProjectDependencies
进行依赖扫描。
scanImports
虽然discoverProjectDependencies
开启了依赖扫描,但实际核心函数在scanImports
之中,discoverProjectDependencies
是针对scanImports
的进一步包装。
在scanImports
中,首先定义了两个空对象,收集的依赖deps
和找不到的依赖missing
,这俩相当重要!它们是依赖收集的主要容器。
我们知道,依赖扫描实际上是依靠esbuild
实现的,所以,之后调用computeEntries
计算出入口文件。在computeEntries
中,进行了以下逻辑。
-
初始化
entries
数组。 -
检查配置中是否存在明确的入口(
optimizeDeps.entries
)。- 如果存在,使用
globEntries
根据这些模式解析匹配的文件路径,并将结果存储在entries
数组中。
- 如果存在,使用
-
如果不存在明确的入口模式,检查配置中是否存在
build.rollupOptions.input
。-
如果存在,则根据
rollupOptions.input
的类型进行处理:- 如果是字符串,则将其解析为绝对路径,并将其添加到
entries
数组中。 - 如果是数组,则对数组中的每个路径执行相同的操作。
- 如果是对象,则对对象的每个值(路径)执行相同的操作。
- 如果
rollupOptions.input
不是字符串、数组或对象,则抛出错误。
- 如果是字符串,则将其解析为绝对路径,并将其添加到
-
-
如果既没有明确的入口模式也没有
rollupOptions.input
,则默认使用**/*.html
作为入口模式,并使用globEntries
函数解析匹配的文件路径。 -
这还没完事,还需要对确定的入口文件路径进行过滤:
- 排除不支持的入口文件类型和虚拟文件。
- 排除不存在的文件。
-
返回
entries
数组。
如果computeEntries
计算出来的入口数组有值,那么使用prepareEsbuildScanner
函数处理。
在prepareEsbuildScanner
中,定义了esbuild
的扫描插件esbuildScanPlugin
。
typescript
const plugin = esbuildScanPlugin(config, container, deps, missing, entries)
注意,这个插件把插件流水线container
、上文中把相当重要的deps
和missing
以及算出来的入口数组entries
传入了。
扫描插件往deps
和missing
写入数据,因为内存不变的原因,是可以不用通过返回值就可以拿到的。
我们待会看这个插件逻辑。
在构造完扫描插件后,还会尝试取optimizeDeps.esbuildOptions
的数据,并把其中的插件选项剥离出来。
typescript
const { plugins = [], ...esbuildOptions } = config.optimizeDeps?.esbuildOptions ?? {}
然后针对esbuild
的tsconfigRaw
进行兼容处理。
最后调用esbuild.context
并使prepareEsbuildScanner
返回它的上下文对象。
typescript
// prepareEsbuildScanner
return await esbuild.context({
absWorkingDir: process.cwd(),
write: false,
stdin: {
contents: entries.map((e) => `import ${JSON.stringify(e)}`).join('\n'),
loader: 'js',
},
bundle: true,
format: 'esm',
logLevel: 'silent',
plugins: [...plugins, plugin],
...esbuildOptions,
tsconfigRaw,
})
然后在scanImports
触发这个上下文对象的rebuild
函数,让esbuild
进行真正的构建。当构建结束的时候,返回已经被填充好的、并且排好序的deps
对象和missing
对象。
typescript
const result = esbuildContext
.then((context) => {
return context
.rebuild()
.then(() => {
return {
deps: orderedDependencies(deps),
missing,
}
})
})
而这个result
其实也作为scanImports
的返回值返回给了discoverProjectDependencies
。
之前提到过,discoverProjectDependencies
实际上是scanImports
的包装,因此,它将result
再次包装处理:
- 如果
missing
是空对象,那么就抛出错误------找不到对应的依赖。 - 只返回
deps
。也就是收集的依赖。
好了,我们现在通过esbuild
得到了扫描到的依赖对象,接下来跟处理optimizeDeps.include
一样,把它们对应的信息推入depInfoList
和discovered
之中。
同样的,这些信息中的file
字段,是这个依赖被预构建之后的地址,因为预构建还没开始,因此此时此刻,硬盘上并没有对应地址。
最后把已知依赖组合起来,传入runOptimizeDeps
。
runOptimizeDeps
首先runOptimizeDeps
会往node_modules/.vite/
创建一个deps_temp_[hash]
的文件夹。
之所以不直接创建或者修改deps
文件夹,是因为deps
文件可能是存在的,并且此时还没进行依赖预构建,而是仅仅完成了依赖收集,因此如果依赖预构建出现了错误,就没有回退余地了。所以创立了临时目录,当预构建成功结束后,临时目录转正。
然后再次通过initDepsOptimizerMetadata
计算出一份新的metadata
模板。与之前相同,不同的是本次发现了依赖,因此browserHash
不能以空对象作为基准计算hash
了,而是前面组合的已知依赖。
然后使用prepareEsbuildOptimizerRun
进行依赖预构建。
prepareEsbuildOptimizerRun
预构建部分逻辑很核心,但比较简单。
首先,跟prepareEsbuildScanner
类似,尝试取optimizeDeps.esbuildOptions
的数据,并把其中的插件剥离出来。
然后,它会处理传入的依赖,也就是上文的出来的已知依赖组合,将其扁平化为 flatIdDeps
对象,并提取每个依赖项的导出信息,将其存储在 idToExports
对象中。
接着,它准备一些构建配置,包括定义一些全局变量、选择平台、指定外部模块、设置插件等。
typescript
const context = await esbuild.context({
absWorkingDir: process.cwd(),
// 入口,上文扁平的flatIdDeps
entryPoints: Object.keys(flatIdDeps),
bundle: true,
format: 'esm',
// 构建输出的 JavaScript 版本
target: ESBUILD_MODULES_TARGET,
// 是否生成源映射文件
sourcemap: true,
// 输出目录
outdir: processingCacheDir,
// 是否生成元数据文件,用于后续分析依赖关系
metafile: true,
// 构建插件,用于在构建过程中对代码进行处理
plugins,
//略
})
我们省略了一些配置,剩下的配置需要我们重点关照:
entryPoints
: 将每一个依赖作为入口文件。format
: 转为esm
。outdir
:输出目录,还记得我们之前新建了临时目录吗?就是那个临时目录。metafile
:元数据,我们之后要用。
最后,它使用这些配置调用 esbuild.context
方法,生成一个新的构建上下文,并返回该上下文以及导出信息对象 idToExports
。当然,这个是esbuild.context
方法,因此并没有实际构建。
因此在runOptimizeDeps
中,调用完prepareEsbuildOptimizerRun
后,会调用返回的esbuild
上下文的rebuild
方法,触发构建,构建结束后,临时目录中已经有已知依赖的esm
产物了。
注意:是已知依赖,不是所有依赖。以上逻辑是vite启动时候的逻辑。当项目运行中,新增依赖,页面也引用了,但没有访问对应页面,那么这个依赖就是未知依赖。
当构建结束后,我们可以从rebuild
的then
方法获取metafile
。我们使用metafile.outputs
,根据已知依赖,来填充前文刚建立起来的metadata.optimized
。
这里需要注意的是,虽然两次都是调用了esbuild
。
但第一次的入口是项目入口,所遍历出来的依赖是项目直接引用的依赖。
第二次入口是这些依赖,因此构建出来的产物不仅仅有这些依赖的esm
格式,还有它们本身的依赖也会被打包进chunk
里面。
同时,如果是异步依赖,会自动在名称后面拼入hash
。
因此,根据metafile.outputs
,不存在已知依赖列表的产物,都会被填充到metadata.chunks
里面------包括上面的异步依赖------它们被拼入一个hash
字符串,并且依赖扫描不会收集异步依赖。
最后构建出一个对象successfulResult
,作为预构建调用链的返回值,填充到runOptimizeDeps
的返回值的result
属性上。
这个successfulResult
对象暴露三个属性。
- 之前构建的
metadata
。 - 取消函数
cancel
。 - 更新
deps
文件的commit
函数。
最重要的就是commit
。但我们稍后再讲。
最后runOptimizeDeps
的返回值挂载到createDepsOptimizer
的optimizationResult
属性上。
意味着,只要能访问到optimizationResult
,那么就可以通过await optimizationResult.result
获取上文定义的successfulResult
。从而可以更新deps
或者获取填充的metadata
。
但到目前为止,预构建已经结束了。
我们整理一下现状:
metadata
有两份,一个是createDepsOptimizer
初始化的,另一个是runOptimizeDeps
返回值,挂载到optimizationResult
上的。- 两个
metadata
已经填充完毕了,但都只在内存中,并没有写入硬盘。 createDepsOptimizer
的metadata
数据比较基础,依赖最多收集到了依赖扫描到的依赖。discovered
是有值的。optimizationResult
的metadata
数据比较全,discovered
没有值。- 依赖预构建产物已经生成了,但放入的是临时文件夹。
- 创建、更新
deps
文件的函数commit
和metadata
挂载到了optimizationResult
属性上。
那么什么时候才会将metadata
填充到_metadata.json
中呢?以及什么时候预构建产物所在文件夹才会转正呢?
onCrawlEnd
onCrawlEnd
函数用于处理静态文件爬取结束后的依赖优化行为。主要负责处理临时文件转正和写入metadata
的操作。
我们来看看它的逻辑。
首先它会使用await
取出optimizationResult
的内容。
然后清空optimizationResult
。
typescript
const afterScanResult = optimizationResult.result
optimizationResult = undefined
const result = await afterScanResult
如果createDepsOptimizer
中的metadata.discovered
所记录的依赖都能在optimizationResult
的metadata
的optimized
找到。
那么就可以进行写入_metadata.json
和临时文件转正了。
如果optimized
比discovered
少,说明存在遗漏依赖,或者虽然一一对应,但数据属性不一致。
那么optimizationResult
中的数据作废,删除临时文件夹。并且将optimized
中多了的依赖添加到discovered
中,并且立刻重新开始一轮运行时的预构建。
注意,这里是将
optimized
比discovered
多的依赖添加到discovered
,确实存在这种情况,比如optimized
的依赖是【A,B,D】而discovered
是【A,B,C,E】,那么就会把D添加到discovered
。
最后执行runOptimizer(result)
执行依赖预构建收尾,我们待会看看runOptimizer
做了什么。
我们先看看onCrawlEnd
是如何被调用的。
在createDepsOptimizer
中,将onCrawlEnd
使用server._onCrawlEnd
推入到了onCrawlEndCallbacks
这个数组中。
这个数组最后是被谁消费呢?答案是setupOnCrawlEnd
。
setupOnCrawlEnd
内部定义了一个逻辑,确保onCrawlEndCallbacks
中的回调函数只会执行一次,它返回一个函数,叫做crawlEndFinder
。
这些不重要,重要的是crawlEndFinder
最终被_registerRequestProcessing
包装了下,并被doTransform
调用。
doTransform
是不是很眼熟,没错,就是模块依赖图的核心逻辑。
typescript
const result = loadAndTransform(...)
if (!ssr) {
const depsOptimizer = getDepsOptimizer(config, ssr)
if (!depsOptimizer?.isOptimizedDepFile(id)) {
server._registerRequestProcessing(id, () => result)
}
}
如果当前文件不是需要预构建的文件,那么就会调用_registerRequestProcessing
。从而最终调用runOptimizer(result)
。
registerMissingImport
我们上文提到了运行时的预构建。什么意思?
比如现在项目完全启动起来,并且浏览器已经渲染出目标页面。当前情况已经超出了我们上文分析过的任何逻辑,是一个运行时的环境。
那么通过新增依赖包,并在页面进行引用,此时新增的包同样需要进行预构建。
那么需要一个对应的逻辑处理这种情况,那么就需要运行时的预构建。
当然,显而易见,增加一个运行时是不太划算的,那么目前vite
有什么可以针对新引入的包做出反应的功能吗?
答案就是钩子。
更加具体点就是vite:resolve
插件的resolveId
钩子。
这个钩子如果发现引入的模块是一个并没有被预构建的钩子,那么就会调用registerMissingImport
方法。
这个方法我们之前提到过,是一个专门注册未预构建依赖的方法。
这个方法会检测新发现的模块在不在待预构建的列表里面,也就是在不在discovered
里面,如果在,就返回待预构建的信息。
否则就将此模块添加到discovered
里面,并调用一次runOptimizer()
,同时在控制台打印new dependencies found: xxx
。
runOptimizer
好了,现在两个方法最后都调用runOptimizer
。不同的是,onCrawlEnd
会传入optimizationResult.result
,而registerMissingImport
不会传入任何参数。
- 如果传入参数的话,那么入参会赋值给
processingResult
。 - 否则,就会整理已知依赖,然后调用
runOptimizeDeps
,把它的返回值赋值给optimizationResult
,并且把optimizationResult.result
赋值给processingResult
。换句话说,又走了遍初始化metadata
-创建临时文件夹-将已知依赖构建输出到临时文件夹这个流程。
之后,会调用processingResult.commit()
,并在调用结束后,将createDepsOptimizer
中的metadata.discovered
所记录的依赖赋值给optimizationResult
的metadata
的discovered
。
也就是说,runOptimizer
起到了收束的作用,让不同时态的预构建逻辑,收束为调用commit
,也就是更新deps
。
commit
我们看看commit
的逻辑,首先,会往临时目录下写入_metadata.json
。
typescript
fs.writeFileSync(
dataPath,
stringifyDepsOptimizerMetadata(metadata, depsCacheDir),
)
好了,现在临时目录什么都有了:预构建产物、_metadata.json
。只差转正临门一脚了。
接着会判断现在当下有没有.vite/deps
目录。如果有,将它重命名为一个临时文件。
然后将之前创建的临时文件夹重命名为.vite/deps
,完成转正。
最后删除之前已经被重命名临时文件的原.vite/deps
。
为什么这么做呢?
这也做是为了最大程度地减少.vite/deps
处于不一致状态的时间。因为重命名-重命名(然后在后台删除旧文件夹)比删除-重命名操作快。
同时我们也从侧面看出,如果进行依赖预构建,一直是针对所有已知依赖的预构建------即使仅仅新增了一个依赖包。
看到这里,我们应该了解依赖预构建的流程了,但是,构建是结束了,那么怎么将请求的包,指向构建结束后的产物呢?
当然还是vite:resolve
插件的resolveId
钩子。
当它第一次处理依赖的时候,就会执行tryOptimizedResolve
函数。
tryOptimizedResolve
在tryOptimizedResolve
中,会使用await
等待依赖扫描结束。
然后获取createDepsOptimizer
的metadata
。查询当前依赖是否存在optimized
、discovered
、chunks
之中,如果存在,那么就返回metadata
记录的元数据。
然后拿这个元数据去调用预构建对象的getOptimizedDepId
。
getOptimizedDepId
我们一开始就提到了,代码很简单,就是拿元数据的file
拼上一个hash
然后返回。
typescript
getOptimizedDepId: (depInfo: OptimizedDepInfo) => `${depInfo.file}?v=${depInfo.browserHash}`
而file
,正是预构建产物所在的deps
目录。
可能有人问了,这是当前依赖可以在预构建metadata
中找到,如果找不到呢?
如果找不到,就会尝试从package.json
的browser
拿地址,当然,如果还拿不到,就会调用tryNodeResolve
方法。
tryNodeResolve
方法可能大家不熟,它里面有个情况就是发现当前包是一个新依赖,就会调用registerMissingImport
------发现新依赖,并添加到预构建列表。
也就是tryNodeResolve
是registerMissingImport
前置逻辑。
当然,上面的逻辑并非重新请求(刷新页面)都会调用一次,而是项目启动后,只会调用一次。
因为处理完当前依赖后,之后依赖都被指向预构建的地址。
准确来说,会被vite:import-analysis
插件的transform
钩子中的interopNamedImports
方法,将原来的依赖地址,使用MagicString
替换为预构建产物的地址。
MagicString
是一个用于处理字符串的JavaScript
库。它可以让你在字符串中进行插入、删除、替换等操作。可以看这篇文章。
替换完毕后,这个预构建产物就如同普通模块一样,在之后的逻辑中,被记录在模块依赖图里面。
之后再次刷新页面,由于地址已经被重写,并且模块依赖图存在对应数据,因此直接从模块依赖图拿数据,返回给浏览器。
好了,以上是预构建的所有逻辑,但我们还有个坑没有填,扫描插件是什么样的?
esbuildScanPlugin
还得提一下那两个比较重要的对象deps
和missing
,这两个分别记录收集到的依赖和找不到的依赖。
一开始,会定义以下规则:
data
请求不处理。http
、https
开头的模块不处理。worker
不做处理。- 剔除
virtual-module
前缀,Svelte
的<script>
和Vue
的<script setup>
会被增加这个虚拟前缀。 - 如果是
.html
,.vue
,.svelte
,.astro
, 或.imba
结尾的文件,并且可被插件流水线的resolveId
解析,那么返回解析后的链接,并归类到html
类别。 css
等样式文件,json
与wasm
和其它静态文件,如果不被指定为入口文件,那么就不处理。
对于bare imports
,会进行以下操作
- 首先会判断扫描到的依赖是否是被
optimizeDeps.exclude
排除的,并且不是@vite/client
和@vite/env
。 - 如果是的话,就
return
,不过return
的时候还会判断是不是入口模块,如果不是入口模块,那么就返回忽略标记,让esbuild
遇到相同的路径的时候进行忽略。 - 如果已经记录到
deps
里面,就return
,同样的,如果不是入口模块,那么就返回忽略标记,让esbuild
遇到相同的路径的时候进行忽略。 - 然后进入插件流水线的
resolveId
进行解析,如果解析不成功,那么就放到missing
里面。【这里收集到了missing
依赖】 - 如果解析成功还会判断解析出来的是否是绝对路径。如果不是绝对路径,或者是虚拟模块,那么就
return
,并根据是否是入口模块打上忽略标记。 - 如果是
node_modules
模块,或者记录在optimizeDeps.include
,那么就将此模块收集到deps
中。并根据是否是入口模块打上忽略标记。【这里收集到了deps
依赖】 - 如果不是
node_modules
模块,会按需归为html
类别。
那些归为html
类别的文件,会进行以下操作
- 首先会读取源码,然后正则匹配所有的
script
标签。 - 针对每个匹配,会进行如下处理。
- 如果是
.html
结尾的文件,并且当前匹配的type
不是module
,那么跳过。 - 如果
script
的type
不是javaScript
或ecmascript
或module
,则跳过。 - 确定
loader
,如果是typeScript
或tsx
,则使用对应的loader
,否则默认使用javaScript
的loader
。 - 如果标签包含
src
属性,则将其作为模块导入。否则,如果内容不为空,则处理内容。 - 如果是
typescript
的话,那么需要把导入的模块再次全量引入,这样可以防止esbuild
优化代码的时候把它们删除,比如在vue3
的setup
中,使用import { ModuleA } from './modules'
,然后在<template>
使用ModuleA
,但esbuild
不认<template>
,会认为ModuleA
是无效引入,从而删除,因此这里会在内容后面拼接\nimport './modules'
,从而避免esbuild
删除。 - 根据内容是否包含
import.meta.glob
来确定是否需要转换glob
导入路径,并将内容存储到scripts
对象中,以供后续加载时使用。 - 生成
virtual-module
路径------还记得上文中剔除的virtual-module
前缀吗,在这里添加的。 - 根据文件类型和上下文属性生成导出语句。进行引入或者重导出。
- 对于非
Vue
文件或者不包含默认导出的文件,在末尾添加默认导出。 - 最后返回
loader
和处理后的内容。
- 如果是
这样就可以让上面的bare imports
逻辑捕获刚刚生成的内容。从而解析html
类别文件中的依赖。
结束
至此,Vite
开发环境下的源码已经分析了大致脉络,但需要学习的东西还有很多,因此我们依然在路上。