前言
在上一篇文章中,我们理清楚了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生命周期设计的自己的生命周期,尤其是这套生命周期管理的设计思想(插件容器管理所有的插件,监听不同生命周期的设计,不同类型的插件),应该是我们收获最大的了吧,这些都是我们学习到的核心科技,对于将来我们在设计自己的业务系统时,一定有较好的指导意义。