前言
在上一篇文章中,我们主要聊了VITE热更新过程中所牵涉到的客户端的核心类,并且向大家解释了热更新边界的概念,这篇文章,我们就要分析VITE在服务端是如何判别热更新边界的。
这篇文章我们会详细聊VITE的内置插件:vite:import-analysis
,并且会把之前阐述EnvironmentModuleGraph
这个核心类遗留的一些逻辑补充完毕,从而完成VITE热更新完整流程的知识体系建设。
好了,废话不多说,我们就开始进入今天的正题吧......
vite:import-analysis
在这个插件中,VITE主要是利用词法分析来处理模块的依赖关系,这个插件的代码比较多,因此我们只挑关键的代码向大家进行展示。
首先,VITE是用的是es-module-lexer
这个包来进行词法分析的,在使用之前,需要init
一下,然后我们就可以得到所有的imports
语句。
给大家展示一下我的例子运行起来,
imports
的内容是什么样的? 再给大家看一下我的这个文件的源码是什么样的,大家就明白了。
根据我的实践,即带有引入标识的,n的值就不是
undefined
,一会儿,我们就可以利用这个特性,分析到源码里面哪些文件是都要import.meta
的语句了。
接着,VITE对所有的imports的内容进行分析:
我们从源码里面取字符串长度就可以取到我们预期的字符串内容: 接着,仍然通过取字符串长度,我们就可以知道源码里面有没有
import.meta.hot.accept
这样的语句。 所以,现在大家明白了为什么VITE的官方文档所说的对空格敏感了吗?
因我们的写法可能是
import.meta.hot.accept
或者是import.meta.hot?.accept
我们在写热更新的时候,如果是需要处理自己的话,我们的写法可以是:
js
if(import.meta.hot) {
import.meta.hot.accept();
}
//=======================================
if(import.meta.hot) {
import.meta.hot.accept(() => {
// 完成一些更新的逻辑
})
}
刚才我们已经说过了,
specifier
变量代表的就是我们写的import
的资源定位地址,现在VITE会重写我们的这个路径。 先做一些前置的判断,简化后续的处理逻辑:
接着就可以真正的处理了:
VITE重写资源的过程中,我们比较关心的一个点是为热更新注入的查询字符串,因为这个查询字符串可以避免浏览器的缓存。
接下来的处理逻辑,这儿我们就只分析最简单的处理情况,即直接预热资源的逻辑,完成预热的话,一会儿就不用再次转换资源了,可以提高一定的效率。
到这个位置,我们把map回调函数内的逻辑就简单看了一遍了。
根据之前我们在VITE文档里面提到的,有import.meta.hot.accept
这样的语句,VITE就会认为这是一个热更新边界,就会为我们注入热更新的方法import.meta.hot
的实现。 最后,我们可以看到,VITE会更新依赖关系,如果有些内容是之前引入过,但是现在已经不需要了的内容,会把它分析出来,然后通过WS通知客户端,客户端在接收到WS消息之后,会调用
import.meta.hot.prune
回调函数,从而完成清理工作。 发送消息:
EnvironmentModuleGraph
在之前的文章中,我们只讲了一部分EnvironmentModuleGraph
类的内容,在这篇文章,我们会把关于它剩下的核心内容补全一下。
我们就结合着热更新的整体流程来看这部分的剩余内容哈。
之前我们讲过,VITE使用chokidar
这个库进行文件内容的监听,当文件发送变化的时候,VITE需要调用EnvironmentModuleGraph
这个类上的方法进行依赖的更新。 之前我们讲过,同一个文件可以在内存中映射成多个
Module
,比如.vue
文件。 在之前我们讲VITE的核心类的时候,我们讲过,在
EnvironmentModuleNode
中,importer
变量存储的是当前资源的被引用者 。 所以,现在当前文件发生了变化,我们就要开始以当前节点为起点,沿着它的被引用者关系进行DFS(深度优先)的遍历,从而更新整个引用链。
如果你对DFS
遍历不清楚的话,可以查看我之前撰写的关于这部分知识的文章,理解深度优先思想和广度优先思想以及一些实际应用。
好了,说回来,我们知道这个方法对资源引用进行处理之后,还有一个关键的地方,是关于热更新处理的。 在之前上一小节,我们阐述
vite:import-analysis
这个插件的时候,当一个文件中如果引用其它内容的时候,如果有lastHMRTimestamp
,VITE会在资源引用的后边附加上时间戳的参数,这样我们在热更新的时候,得到的内容肯定是不会被浏览器缓存的,这才是我们预期的。
说完了invalidateModule
方法之后,我们现在看一下之前在阐述vite:import-analysis
这个插件的时候看到的更复杂的updateModuleInfo
方法。
这个方法的目的是更新资源的引用,并且还能计算出哪些资源是之前有引用,现在是不需要的。
我们把这个方法拆成3部分来看的话,其实就不觉得很难了。
到此为止,关于EnvironmentModuleGraph
剩余的内容我们几乎就讲完了,现在大家应该明白为什么需要这个类了吧,这个类核心的功能就是加载资源,管理资源集合,当资源发生变化的时候,这个类会刷新资源的引用关系,计算出更新和删除的资源。
其它内容
热更新边界检测的实现
到目前为止,我们还差一个内容没有搞明白,就是热更新的时候,VITE在服务端是如何处理热更新边界的呢?在之前的文章中,我们讲过,当我们在源代码中定义了import.meta.hot.accept
内容的时候,就划分了热更新边界。
为了让大家直观的看到VITE的处理流程,我把VITE的调用栈帧都展示出来。
我给大家举个例子,比如我现在有一个依赖链,
index.ts->api.js->config.js->dep.js
,假设我在index.ts
这个文件中有定义import.meta.hot.accept
这个代码,那么VITE将会为我们找到热更新的起点文件为index.ts
,我们根据这个例子来做实验看一看。
我们等会儿再来看一下这个
boundaries
是怎么找出来的,先不着急,我们先看后面发送给客户端更新的逻辑。
这样,客户端拿到了更新边界信息,只需要把更新边界里面的资源重新加载一遍,所有的更改就都应用了。
好,现在我们再看一下propagateUpdate
是如何实现的。其实从刚才我们看到的它的参数就知道,这又是一个DFS遍历(因为我对图这个数据结构足够熟悉)。
当这个方法返回false的时候,浏览器就不会整页刷新了。
现在,我们来看一下这个isSelfAccepting
这个变量是怎么来的?我们得回到vite:import-analysis
这个插件中:
lexAcceptedHmrDeps
这个方法上,通过检测词法,如果有import.meta.hot.accept
(不依赖其它节点的写法),这个方法就会返回true。
所以,现在大家为什么热更新链的更新逻辑是我在上一篇文章中所阐述的那样了吗?这是一个很简单的DFS遍历就可以求得依赖链中包含
import.meta.hot.accept
语句深度最深的节点,DFS在处理的时候是反向的,第一个遇到的isSelfAccepting
为true的节点就是包含import.meta.hot.accept
语句深度最深的节点,然后就停止遍历返回结果给外界了。
到这个位置为止,propagateUpdate
这个方法还没有完,我们还得看整页刷新的逻辑是在什么情况下出现。
还是回到之前的位置: 假设我们在源码里面没有一句
import.meta.hot.accept
,那么,hasDeadEnd
将会是true,因此就会走整页刷新的逻辑。
还有一些其它页面需要走整页刷新的逻辑,比如我们改了当前的一个文件,发现没有一个文件引用当前修改的文件。 最后一种情况,就是当我们的源码中存在循环引用,此刻也应该走整页刷新。
invalidate
最后,我们再看一下之前我们看的有点儿迷迷糊糊的热更新API->import.meta.hot.invalidate
是怎么样实现的?
VITE是在DevEnvironment初始化的时候设置的监听: 在invalidateModule这个方法中调用
updateModules
,即我们在上一小节中讲到的寻找热更新边界的方法,此刻,VITE的热更新的行为是继续更新,还是整页刷新,就取决于你的源码是如何编写的了。
VITE热更新的整体流程梳理
现在,我们就已经大致完成VITE的热更新实现学习了,我们来总结一下VITE热更新的总体流程。
VITE在dev阶段,会启动一个开发服务器,开发服务器会启动一个WebSocket
服务,VITE在启动时,会向客户端注入一些工具方法,在这些工具方法中,我们就可以和VITE的服务端进行通信。
我们的VITE项目中的源码,每一个文件都是一个资源节点,VITE使用一个图来管理这些资源的引用关系。
VITE在处理源代码时,会重写我们编写的引用资源标识符,然后,VITE就可以知道这些源码的引用和被引用关系。在重写资源标识符时,VITE会为我们注入资源的最后更新时间的查询参数,这样可以有效避免资源的缓存。
VITE在对源码分析时,会根据我们的源码中是否包含import.meta.hot.accept
这样的代码去划分热更新边界,也会根据这个依据决定是否向当前文件中注入热更新上下文(import.meta.hot
)的实现方法,当我们的在代码编辑器中修改文件时,VITE会监听到文件的修改,然后VITE会根据资源图中的引用关系,刷新修改后的资源引用关系,这个过程中,VITE会找到不需要的资源和需要级联更新的资源,并且把这些资源信息通过WS消息发送给客户端,客户端在拿到WS消息时,就可以决定是更新资源还是移除资源了。
在更新资源时,VITE会找到依赖树中源码含有import.meta.hot.accept
的且深度最深的资源信息,将其发送到客户端,当这个资源发生了更新,其引用的资源也就一并完成了更新,比如index.js->api.js->request.js->config.js
,假设有且仅有index.js
这个文件中划定了热更新边界,我们修改这个依赖链中的任何一个文件,VITE都将会更新index.js
。
以上就是热更新的整个过程了,这是我通过阅读VITE的源码和自己的实践总结出来的整体流程,如果各位读者觉得不全或者有错误,欢迎大家指正。
结语
我们通过3篇文章来学习了VITE热更新的处理过程,尤其是认知到VITE的热更新边界的设计在热更新时是极好的,即保证了所有的文件都可以更新到,也有较少的开销。
在这三篇文章中,因为主要都是在进行图数据机构的处理和计算,所以要求大家对数据结构和算法的基础知识有一定的掌握才能看懂,如果您还没有掌握的话,可以利用自己的空余时间刷一些算法题就掌握了,哈哈哈。
从下一篇文章开始,我们就开始学习VITE的依赖预构建相关知识点了,未完待续......