前言
在上一篇文章中,我们借着讲DevEnvironment
这个类的机会,阐述清楚了DEV流程中VITE是如何处理客户端一个请求发出,中间件分发请求,插件容器调度,插件处理内容并返回结果这一整套流程。
我们几乎是把和DevEnvironment
相关的核心类都分析了个大概,在这篇文章中,我们继续来分析VITE中的核心类,这篇文章主要阐述*ModuleNode
和*ModuleGraph
。
为什么这两个类前面都加了一个*
呢?因为VITE有两套Node
和Graph
的处理逻辑。
如果我们不分析热更新相关的内容的话,*ModuleNode
和*ModuleGraph
涵盖的内容还是相对比较简单的,为了行文顺序的考虑,我们还是把一些内容放到VITE的WebSocket
和热更新相关的篇章上去讲述。
好了,废话不多说,咱们就开始吧。
图的基本概念
在大学的数据结构课程中,老师教我们使用图(Graph
)用来表达复杂的逻辑关系。
图在正常的前端业务开发中很少用得到,我也是刷算法的过程中逐渐掌握的图的相关知识点(除非绘制逻辑关系数字大屏业务,比如地图相关的业务,拓扑图等)(我们说的是严格意义上的图,因为树也是特殊的图,而实际应用中树的使用场景还是比较多的)。
大学的数据结构课程中,老师教我们使用邻接矩阵或者邻接表表示图,但是我们在实际的实现中几乎都是使用引用关系表示,实质上也是邻接表的表示。
图,主要有两个概念非常重要,一个是顶点
(Vertex
),另外一个是边
(Edge
),如果边有方向,则称为有向图
,否则为无向图
;
对于有向图,从一个顶点A到另外一个顶点B的边E,称为A的出度
,称为B的入度
。
一个图中,从一个顶点开始出发,通过边的关系,能够访问到所有的顶点组成的集合,称为一个连通分量
,如果一个图只有一个连通分量
,那么这个图则称之为连通图
。
为了方便大家的理解,接下来我就直接以VITE的业务场景来阐述图的相关知识点了。
比如,我们的一个文件,就是VITE资源依赖图中的一个节点,这个节点上保存有很多信息,比如它的文件名称,路径,格式,最后修改时间,文件内容,等等这些属性。
我们在一个文件中,可能引入其它的文件,这个代码层面的引用关系,会被Rollup进行依赖分析(在VITE的DEV流程中不是的),它首先读取代码的内容,转成AST(抽象语法树),然后在遍历AST节点的过程中,分析和其它文件的依赖关系,这个依赖关系就是边,明显这个引用关系是有方向的,所以是一个有向图。
另外,我们在处理的时候,比如一个文件变化了的时候,我们想知道哪些文件也应该随之一起变化,我们还需要知道谁是引用者,谁是被引用者。
在这个图中,我们肯定是从一个主文件开始解析,然后递归的分析其中的依赖,直到分析完成所有的依赖,我们的这个依赖关系图肯定是一个连通图,但是由于我们在写代码过程中,由于可能存在循环引入的关系,则这个依赖关系图中可能存在环。
在之前分析Rollup
的文章中我们在分析打包的时候其实已经向大家聊过拓扑排序
这个概念了,拓扑排序也是图中的一个重要知识点,考虑到有些同学还不知道这个概念,我就再顺便提一嘴。
对应构建工具来说,我们最终打包结果代码应该如何组织,这是一个问题,合并之后的代码块肯定是先定义再使用吧?如何组织这样的代码片段的输出,比如a.js
->b.js
->c.js
,最终合并的Chunk
肯定是先输出c.js
,然后输出b.js
,最后输出a.js
,这个过程就是拓扑排序。
关于拓扑排序大家可以查看我之前的文章:拓扑排序在前端开发中的应用场景
在之前Rollup的文章中我们说过,如果依赖图中存在循环依赖的话,要确保这个依赖环被分包到一个Chunk
中,否则打包的结果是无法正常加载的。
下面的这个图,就是我以某个依赖关系画的一个依赖关系图(它既是一张图,它表达的也是一个图,哈哈),其中,
- 绿色带边框的矩形表示的整个依赖关系图。
- 蓝色的色块表示的是某个资源文件。
- 蓝色的线(
importModules
)表示的是引用关系,比如index.js
->a.js
。 - 红色的线(
importers
),表示的是被引用关系,a.js
需要知道它被index.js
引用着。
EnvironmentModuleNode
我们在上节中已经向大家阐述了图的一些基本知识点,接下来我们看VITE的资源图中的节点类:
这个类超级简单,其中importedModules
存储的是当前资源文件引用的资源集合,而importers
存储的是自己被谁引用着。(不要问我是怎么知到的,哈哈哈,我也是打断点调试出来的)
给大家看一下为什么是这样的:
EnvironmentModuleGraph
这个类就是我们上上小节中的那个背景框,这个类中存储了项目中所有的资源。 这个类关联着上一篇文章中我们聊过的DevEnviroment
的实例,并且,这个类在初始化的时候,把如何加载资源路径的方法传递进来了,这个resolveId
方法就是插件系统的resolveId
方法。
VITE为了考虑到资源的高效加载,在处理过程中有效利用了缓存,所以我们看到这个这个Graph
类中定义了4个哈希表,为的就是能够快速的找到资源,如果没有找到资源,则从磁盘读取,并且将获取到的结果关联到这些哈希表上,下次再取用的时候,直接利用缓存,从而提高了DevServer
的效率。
在上述代码中,为什么只有fileToModulesMap
是一个多级的Set
,因为有些文件它是可能对应多个文件的,比如我们的.vue文件,它会被编译成2-3(主要是看你的写法,具体大家可以用@vitejs/vue-plugin
试一下,有些时候template
部分也会被抽出来,所以说是2-3个部分)个部分,以app.vue
文件为例,主体部分会被编译成app.vue?lang=js
,这个文件中包含template
编译结果render
函数和我们本来写在script
标签里面的JS脚本,而我们写的style
标签则会编译成app.vue?lang=css&index=0
这样的内容,这个查询字符串中index标记的顺序,可以使得我们在vue文件中书写多个style
标签。所以这就是为什么VITE会这样设计的原因。
以下是一个断点调试的内存信息:
以下是在初始化的过程中传入的插件系统的resolveId
方法:
VITE的缓存有几种策略,我们就以URL的处理过程来看一下处理逻辑: 调用插件容器的resolveId
方法,确定url和资源id的关联关系: 在之前我们聊transformRequest方法的时候,当时我们还没有聊缓存,现在在回过头来看的话,大家就觉得一目了然了。
如果URL能够命中缓存则走缓存,否则尝试以文件id查找缓存,若没有查询到,则真正的走磁盘加载了。
EnvironmentModuleGraph
还有很多方法,我们只是在这篇文章中暂时不讲,后续的热更新的文章中我们还会再分析它的,大家敬请期待。
ModuleGraph和ModuleNode
这两个类,从目前VITE源码的注释来看,似乎是不会再用了。
目前看起来里面的处理逻辑主要是内聚了SSR和CSR的处理逻辑,可能是VITE经过了重构,对此,我们只需要知道这两个曾经的功臣已经功成身退,无需关心就好了。
总结
本文主要向大家阐述了一些图的知识点,并且向大家阐述了VITE是如何使用图这个数据结构处理DEV流程中的资源引用关系的,本文的内容相对来说还是比较简单的,不过大家不要太过乐观,因为在本文我们暂时还跳过了较为核心的热更新相关的内容。
关于VITE中的核心类,我认为重要的就这些了,当你掌握了这几个核心类之后,再弄清楚VITE使用中间件映射请求到最终的结果这个整套流程,可以说就已经把VITE整个DEV流程中最核心的内容掌握了,不过我们要的是全部,所以还要接着继续往更深的知识盲区钻研。
从下一篇文章开始,我们将开始阐述VITE中WebSocket的处理流程以及热更新相关的处理逻辑,届时我们再详细分析EnvironmentModuleGraph
类中的内容,未完待续,敬请期待......