You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
前面,我们也提及了如果启动 Vite 时使用了 --force Option,则会强制重新进行依赖预构建。所以,当不是 --force 场景时,则会进行比较新旧依赖的 Hash 值的过程:
// 默认为 falseif(!force){letprevDatatry{// 获取到此时缓存(本地磁盘)中构建的文件信息prevData=JSON.parse(fs.readFileSync(dataPath,'utf-8'))}catch(e){}// 对比此时的 if(prevData&&prevData.hash===data.hash){log('Hash is consistent. Skipping. Use --force to override.')returnprevData}}
constinclude=config.optimizeDeps?.includeif(include){constresolve=config.createResolver({asSrc: false})for(constidofinclude){if(!deps[id]){constentry=awaitresolve(id)if(entry){deps[id]=entry}else{thrownewError(`Failed to resolve force included dependency: ${chalk.cyan(id)}`)}}}}
前言
前段时间,Vite 做了一个优化依赖预构建(Dependency Pre-Bundling)。简而言之,它指的是 Vite 会在 DevServer 启动前对需要预构建的依赖进行构建,然后在分析模块的导入(import)时会动态地应用构建过的依赖。
这么一说,我想大家可能立马会抛出一个疑问:Vite 不是 No Bundle 吗?确实 Vite 是 No Bundle,但是依赖预构建并不是意味着 Vite 要走向 Bundle,我们不要急着下定义,因为它的存在必然是有着其实际的价值。
那么,今天本文将会围绕以下 3 点来和大家一起从疑问点出发,深入浅出一番 Vite 的依赖预构建过程:
什么是依赖预构建
依赖预构建的作用
依赖预构建的实现(源码分析)
一、什么是依赖预构建
当你在项目中引用了
vue
和lodash-es
,那么你在启动 Vite 的时候,你会在终端看到这样的输出内容:而这表示 Vite 将你在项目中引入的
vue
和lodash-es
进行了依赖预构建!这里,我们通过大白话认识一下 Vite 的依赖预构建:默认情况下,Vite 会将 package.json 中生产依赖
dependencies
的部分启用依赖预构建,即会先对该依赖进行构建,然后将构建后的文件缓存在内存中(node_modules/.vite 文件下),在启动 DevServer 时直接请求该缓存内容。在 vite.config.js 文件中配置
optimizeDeps
选项可以选择需要或不需要进行预构建的依赖的名称,Vite 则会根据该选项来确定是否对该依赖进行预构建。在启动时添加
--force
options,可以用来强制重新进行依赖预构建。所以,回到文章开始所说的疑问,这里我们可以这样理解依赖预构建,它的出现是一种优化,即没有它其实 No Bundle 也可以,有它更好(xiang)! 而且,依赖预构建并非无米之炊,Vite 也是受 Snowpack 的启发才提出的。
那么,下面我们就来了解一下依赖预构建的作用是什么,即优化的意义~
二、依赖预构建的作用
对于依赖预构建的作用,Vite 官方也做了详细的介绍。那么,这里我们通过结合图例的方式来认识一下,具体会是两点:
1. 兼容 CommonJS 和 AMD 模块的依赖
因为 Vite 的 DevServer 是基于浏览器的 Natvie ES Module 实现的,所以对于使用的依赖如果是 CommonJS 或 AMD 的模块,则需要进行模块类型的转化(ES Module)。
2. 减少模块间依赖引用导致过多的请求次数
通常我们引入的一些依赖,它自己又会一些其他依赖。官方文档中举了一个很经典的例子,当我们在项目中使用
lodash-es
的时候:如果在没用依赖预构建的情况下,我们打开页面的 Dev Tool 的 Network 面板:
可以看到此时大概有 600+ 和
lodash-es
相关的请求,并且所有请求加载花了 1.11 s,似乎还好?现在,我们来看一下使用依赖预构建的情况:此时,只有 1 个和
lodash-es
相关的请求(经过预构建),并且所有请求加载才花了 142 ms,缩短了足足 7 倍多的时间! 而这里节省的时间,就是我们常说的冷启动时间。那么,到这里我们就已经了解了 Vite 依赖预构建概念和作用。我想大家都会好奇这个过程又是怎么实现的?下面,我们就深入 Vite 源码来更进一步地认识依赖预构建过程!
三、依赖预构建的实现
在 Vite 源码中,默认的依赖预构建过程会在 DevServer 开启之前进行。这里,我们仍然以在项目中引入了
vue
和lodash-es
依赖为例。3.1 Dev Server 启动前
首先,Vite 会创建一个 DevServer,也就是我们平常使用的本地开发服务器,这个过程是由
createServer
函数完成:可以看到在 DevServer 真正启动之前,它会先调用
runOptimize
函数,进行依赖预构建相关的处理(用bind
进行简单的重写)。runOptimize
函数:runOptimize
函数负责的是调用和注册处理依赖预构建相关的optimizeDeps
函数,具体来说会是两件事:1. 进行依赖预构建
optimizeDeps
函数是 Vite 实现依赖预构建的核心函数,它会根据配置 vite.config.js 的optimizeDeps
选项和 package.json 的dependencies
的参数进行第一次预构建。它会返回解析 node_moduels/.vite/_metadata.json 文件后生成的对象(包含预构建后的依赖所在的文件位置、原文件所处的文件位置等)。_metadata.json 文件:
这里,我们来分别认识一下这 4 个属性的含义:
hash
由需要进行预构建的文件内容生成的,用于防止 DevServer 启动时重复构建相同的依赖,即依赖并没有发生变化,不需要重新构建。browserHash
由hash
和在运行时发现的额外的依赖生成的,用于让预构建的依赖的浏览器请求无效。optimized
包含每个进行过预构建的依赖,其对应的属性会描述依赖源文件路径src
和构建后所在路径file
。needsInterop
主要用于在 Vite 进行依赖性导入分析,这是由importAnalysisPlugin
插件中的transformCjsImport
函数负责的,它会对需要预构建且为 CommonJS 的依赖导入代码进行重写。举个例子,当我们在 Vite 项目中使用react
时:此时
react
它是属于needsInterop
为true
的范畴,所以importAnalysisPlugin
插件的会对导入react
的代码进行重写:之所以要进行重写的缘由是因为 CommonJS 的模块并不支持命名方式的导出。所以,如果不经过插件的转化,则会看到这样的异常:
2. 注册依赖预构建相关函数
调用
createMissingImpoterRegisterFn
函数,它会返回一个函数,其仍然内部会调用optimizeDeps
函数进行预构建,只是不同于第一次预构建过程,此时会传人一个newDeps
,即新的需要进行预构建的依赖。那么,显然无论是第一次预构建,还是后续的预构建,它们两者的实现都是调用的
optimizeDeps
函数。所以,下面我们来看一下optimizeDeps
函数~3.2 预构建实现核心 optimizeDeps 函数
optimizeDeps
函数被定义在 packages/vite/node/optimizer/index.ts 中,它负责对依赖进行预构建过程:由于
optimizeDeps
内部逻辑较为繁多,这里我们拆分为 5 个步骤讲解:1. 读取该依赖此时的文件信息
既然是构建依赖,很显然的是每次构建都需要知道此时文件内容对应的 Hash 值,以便于依赖发生变化时可以重新进行依赖构建,从而应用最新的依赖内容。
所以,这里会先调用
getDepHash
函数获取依赖的 Hash 值:2. 对比缓存文件的 Hash
前面,我们也提及了如果启动 Vite 时使用了
--force
Option,则会强制重新进行依赖预构建。所以,当不是--force
场景时,则会进行比较新旧依赖的 Hash 值的过程:可以看到如果新旧依赖的 Hash 值相等的时候,则会直接返回旧的依赖内容。
3. 缓存失效或未缓存
如果上面的 Hash 不等,则表示缓存失效,所以会删除
cacheDir
文件夹,又或者此时未进行缓存,即第一次依赖预构建逻辑(cacheDir
文件夹不存在),则创建cacheDir
文件夹:前面在讲 DevServer 启动时,我们提及预构建过程会分为两种:第一次预构建和后续的预构建。两者的区别在于后者会传入一个
newDeps
,它表示新的需要进行预构建的依赖:并且,这里可以看到对于前者,第一次预构建,则会调用
scanImports
函数来找出和预构建相关的依赖deps
,deps
会是一个对象:而
missing
则表示在node_modules
中没找到的依赖。所以,当missing
存在时,你会看到这样的提示:那么,回到上面对于后者(
newDeps
存在时)的逻辑则较为简单,会直接给deps
赋值为newDeps
,并且不需要处理missing
。因为,newDeps
只有在后续导入并安装了新的dependencies
依赖,才会传入的,此时是不存在missing
的依赖的( Vite 内置的importAnalysisPlugin
插件会提前过滤掉这些)。4. 处理 optimizeDeps.include 相关依赖
在前面,我们也提及了需要进行构建的依赖也会由 vite.config.js 的
optimizeDeps
选项决定。所以,在处理完dependencies
之后,接着需要处理optimizeDeps
。此时,会遍历前面从
dependencies
获取到的deps
,判断optimizeDeps.iclude
(数组)所指定的依赖是否存在,不存在则会抛出异常:5. 使用 esbuild 构建依赖
那么,在做好上述和预构建依赖相关的处理(文件 hash 生成、预构建依赖确定等)后。则进入依赖预构建的最后一步,使用
esbuild
来对相应的依赖进行构建:ensureService
函数是 Vite 内部封装的util
,它的本质是创建一个esbuild
的service
,使用service.build
函数来完成构建过程。此时,传入的
flatIdDeps
参数是一个对象,它是由上面提及的deps
收集好的依赖创建的,它的作用是为esbuild
进行构建的时候提供多路口(entry
),flatIdDeps
对象:好了,到此我们已经分析完了整个依赖预构建的实现 😲(手动给看到这的大家👍)。
那么,接下来在 DevServer 启动后,当模块需要请求经过预构建的依赖的时候,Vite 内部的
resolvePlugin
插件会解析该依赖是否存在seen
中(seen
中会存储构建过的依赖映射),是则直接应用node_modules/.vite
目录下对应的构建后的依赖,避免直接去请求构建前的依赖的情况出现,从而缩短冷启动的时间。结语
通过了解 Vite 依赖预构建的作用、实现等相关知识,我想大家应该不会再去纠结 Bundle 或者 No Bundle 的问题了,仍然是那句话,存在即有价值。并且,依赖预构建这个知识点在面试场景下,可能也是一个很有趣的考题 😎。
The text was updated successfully, but these errors were encountered: