Vite源码学习(八)——DEV流程中的核心类(上)
前言
在上一篇文章中,我们理清楚了VITE中的中间件,在这篇文章中,我们开始介绍DEV流程中VITE实现Rollup等价API中实现的核心类。
VITE的核心类主要包括几个:
-
PluginContainer
,用来管理插件集合,模拟Rollup等价的生命周期。 -
Environment
,用来管理当前的Env上下文,在DEV阶段,我们主要需要关注DevEnvironment
类。 -
ModuleGraph
,用来管理资源的引用关系,在DEV阶段,我们主要需要关注EnvironmentModuleGraph
类。
除此之外,在这篇文章中,我们还会把上一篇文章没有做详细分析的transformRequest
方法详细分析。
本文的内容可能是整个系列的文章中最复杂的,大家要有心理准备。
由于VITE可能经历过代码的重构或者API的调整,源码里面存在一些兼容老的API,但是在未来可能会删除的内容,对于这些内容我们就直接选择跳过。
好了,废话少说,我们就开始进入这篇文章的正题吧。
从Server的入口出发
因为很多配置都需要依赖主入口文件解析到的配置,所以我们还是像之前的文章那样,从VITE的DevServer创建的入口开始聊。
resolveConfig
首先,还是之前我们已经看过,但是可能分析的并不那么详细的resolveConfig
方法:
这次,我们需要关注的部分是这些位置:
首先是获取到用户传递的插件列表。
然后是初始化Environment
的配置上下文:
因为我们现在分析的是DEV流程,所以我们重点关注的是resolveDevEnvironmentOptions
:
本系列文章不会分析SSR相关的知识点(主要是我在实际项目中没有怎么用到,就不在这儿胡说八道,误人子弟了,哈哈哈,各位读者请见谅),所以我们只看client相关的处理逻辑即可:
使用工厂模式初始化DevEnvironment
的实例:
关于这个createEnvironment
方法,我们目前就只需要看到这儿,因为一会儿才会用到它,我们现在只需知道它创建的是DevEnvironment
的实例工厂方法即可。
接下来,VITE把我们用户的Plugin跟自己的内置Plugin进行合并:
把处理好的插件列表交给需要暴露给外部的变量:
在这小节中,我们主要是为了要搞明白DevEnvironment
和Plugin
列表的来源是什么,因为一会儿这些变量需要传递给对应的核心类进行管理。
关键变量初始化
在上一小节中我们搞懂了那些关键的参数是怎么来的,现在我们看一下这些关键变量是怎么初始化的。
这个config
变量上保存着我们之前得到的关键信息,比如插件列表。
现在把这个config
变量传递给之前我们关心的创建DevEnvironment
的工厂方法。
至此,我们明白了创建DevEnvironment
实例的整个过程,一会儿我们再关注DevEnvironment
这个类的实现细节。
然后是调用DevEnvironment
的init
方法,这个init
方法里面主要操作就是初始化插件容器,在下一小节就会讲到,大家如果不懂可以一会儿再来查阅。
最后,我们着重看transformMiddleware
的绑定过程。
首先,ViteDevServer上绑定着之前初始化的DevEnvironment
的实例:
transformMiddleware
中间件在绑定的时候,传入了ViteDevServer
的实例:
一会儿我们在讲述上一篇文章中没有讲到的transformRequest
的过程的时候会讲到这个ViteDevServer
实例变量传递的作用。
到这个位置,VITE在DEV过程中主线流程(为什么是主线流程,一会儿会给大家解释的)的关键变量就已经初始化完成了。
DevEnvironment
这个类有相对长的集成链,代码看起来不是那么舒服。
DevEnvironment
->BaseEnvironment
->PartialEnvironment
所以我们得事先关注一下它的祖先类。
PartialEnvironment
关于PartialEnvironment
类,我们只需要关注一个点,即设置配置和获取配置,这个配置就是上一节中我们通过resolveConfig
拿到的那个值。
其它的内容主要都是工具方法,我们无需额外关注。
BaseEnvironment
BaseEnvironment
相对来说就更简单了,它主要就是对外暴露插件列表,所以我们就只需要关注plugins
这个getter
就好了。
DevEnvironment
在搞清楚它的祖先类之后,我们就可以正式关注它了。
在DevEnvironment
这个类中,我们需要首先看的就是它的init
方法,在这个方法里创建了PluginContainer
,就是之前我们在关键变量初始化小节的时候没有展开的那个方法。
在之前我们阐述PartialEnvironment
的时候就已经知道了getTopLevelConfig
这个方法获取到的就是我们在先前的小节中阐述的resolveConfig
获取到的VITE将自己的默认配置和用户的配置合并之后的配置,所以这儿拿到的插件集合就是我们之前所阐述的:
好了,拿到了插件列表之后,我们就可以把插件传递给PluginContainer
了。
这小节我们重点先聊DevEnvironment
本尊,后面的小节我们重点阐述PluginContainer
。
DevEnvironment
同时也定义了获取PluginContainer
的getter
,这个getter
会广泛用到的。
最后,再看几个关键方法:
对于目前的我们来说,就只关注这么多,后面的文章,我们还会继续分析这篇文章没有涵盖到的细节内容的。
transformRequest
在上一篇文章中,我们只是粗略的向大家阐述了一个请求如何通过中间件得到资源,我们侧重的是请求侧,即http请求映射成资源标识符的这一过程,现在我们开始重点阐述资源如何处理并返回给客户端的。
还是从transformMiddleware
开始,在关键变量初始化小节,我们已经阐述了ViteDevServer
示例传递给了这个中间件。
这个transformRequest
方法传递的参数environment
就是DevEnvironment
的实例,这也是为什么我们在上一篇文章中不进行更细节阐述的原因,因为只有搞明白了DevEnvironment
的前世今生之后,这些问题才有的说。
这个environment
变量来自于它,即ViteDevServer
实例传递过来的数据:
VITE在初始化的时候,分别初始化了一个ssr
和一个client
的变量,由于我们不考虑SSR,所以我们就只看这个client
就可以了。
现在大家再看transformRequest
这个方法的话,是不是就已经觉得一目了然呢?
之前我们说过,DevEnvironment
这个类上定义了一个可以访问PluginContainer
的getter
,现在就派上用场啦。
至此,事儿就非常简单了,(我们暂时先不考虑缓存),拿到了http
请求的url
,将url
映射成资源的id
(即调用PluginContainer
的resolveId
方法触发生命周期),然后根据id
从磁盘加载资源(即调用PluginContainer
的load
方法触发生命周期),最后将从磁盘读取到的资源进行编译加工(即调用PluginContainer
的transform
方法触发生命周期),然后把得到的编译结果返回给客户端,这就是从一个http
请求发出,到客户端收到编译内容的全部过程啦。
所以,我们可以简单的用大白话总结一下DEV阶段一个资源的获取流程了,VITE开启了一个DevServer,当我们访问这个DevServer的时候,VITE会把我们的Http请求通过内置的中间件映射成获取资源的标识符,然后调用插件容器通过资源标识符获取到资源,接着对资源进行编译转换,最后把转换之后的结果返回给客户端。
如果你看到了这儿的话,是不是有一种你上你也行的冲动,哈哈哈,真的让人成就感爆棚啊!
PluginContainer
在明白了之前DevEnvironment
之后,现在我们再看PluginContainer
真的就会觉得太简单了。
这儿,我猜测VITE的代码可能也是经过重构或者API的变更了,在源码中,有两个Container
类,一个叫做PluginContainer
,一个叫做EnvironmentPluginContainer
。
接下来,我们一起看一下PluginContainer
:
大家可以看到,这个PluginContainer
依赖了Environment
(DevEnvironment
也是其中的一个类型之一),这个Container
上面的方法,都是在做桥接,调用的仍然是EnvironmentPluginContainer
的内容。
所以,我们现在搞明白了PluginContainer
和EnvironmentPluginContainer
的关系,PluginContainer
是为了保持已有API的稳定而做的桥接,在不经意之间我们就看到了桥接模式的实际应用,哈哈。
EnvironmentPluginContainer
之前我们看的是它的桥接类,现在我们来看正在干活儿的类。
这个代码是VITE的团队从Rollup的源码里面摘录,并且经过重构整合的代码,在之前的Rollup专栏里面,我在这篇文章向大家讲过Rollup各种类型的生命周期的实现方式。
在Rollup里面,是一个一个的函数,不过VITE团队把它们进行了封装,统一管理。
现在带着大家一起来看一下这个EnvironmentPluginContainer
类中的关键方法。
首先是构造器,在之前我们是已经知道了的,初始化这个容器的时候,已经把所有的插件列表传过来了的。
然后是hookParallel
,这个是在支持Rollup的parallel
类型的的生命周期:
大家看一下,是不是这段代码有点儿似曾相识,如果你看过我前面提到的阐述Rollup生命周期的那篇文章的话。
这段代码的含义也不再赘述了,大家查看《Rollup源码学习(六)——重识Rollup构建生命周期》这篇文章即可,里面有demo向大家展示它的等价代码。
接着是VITE封装了的buildStart
生命周期的处理:
另外,在之前的文章我们讲过,resolveId
,load
这样的生命周期是first
+async
类型的生命周期。
我们来看一下VITE是如何实现的。
看起来,定义这个handleHookPromise
方法是为了更好的追踪Promise
:
可以看到,上述的实现简化一下的伪代码如下:
async funtion runner() {
const plugins = [plugin1, plugin2, plugin3]
let output = null
for(const plugin of plugins) {
const result = await plugin();
if(result) {
output = result;
break;
}
}
return output;
}
再者,在之前的文章我们讲过,transform
这样的生命周期是sequential
+ async
类型的生命周期,我们也来看一下VITE是如何实现。
可以看到,上述的实现简化一下的伪代码如下:
funtion runner(initial) {
const plugins = [plugin1, plugin2, plugin3]
let output = initial
for(const plugin of plugins) {
const result = await plugin(output);
output = result;
}
return output;
}
runner(undefined)
对于其它的方法,我们在DEV阶段中其实不怎么用到,比如下面close
方法里面的逻辑,虽然我们一般不怎么用到,但是框架的开发者必须要实现,这就是严谨,这也是值得我们学习的,如何设计健壮的软件系统,都是靠这种点滴积累起来的,哈哈。
我个人觉得VITE的这个实现相对于Rollup的实现代码的可读性比较好,可维护性也比较好,业务逻辑都内聚到了一处,外界只需要调度PluginContainer
的方法就好了,体现了面向对象设计的封装的优势。
结语
考虑到篇幅的关系,就不在本文继续阐述较为复杂的ModuleGraph
相关类。
相信大家阅读完本文应该是成就感满满的吧,在本文中,我们理清楚了VITE中几个类的关系,并且我们已经完全分析清楚了从请求经过中间件分发,映射到插件容器处理,编译,得到结果并返回的这一全套的流程。
接着我们又带领大家学习到了VITE基于Rollup生命周期设计的自己的生命周期,尤其是这套生命周期管理的设计思想(插件容器管理所有的插件,监听不同生命周期的设计,不同类型的插件),应该是我们收获最大的了吧,这些都是我们学习到的核心科技,对于将来我们在设计自己的业务系统时,一定有较好的指导意义。