阅读视图

发现新文章,点击刷新页面。

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方法: image.png 这次,我们需要关注的部分是这些位置:

首先是获取到用户传递的插件列表。 image.png 然后是初始化Environment的配置上下文: image.png 因为我们现在分析的是DEV流程,所以我们重点关注的是resolveDevEnvironmentOptionsimage.png 本系列文章不会分析SSR相关的知识点(主要是我在实际项目中没有怎么用到,就不在这儿胡说八道,误人子弟了,哈哈哈,各位读者请见谅),所以我们只看client相关的处理逻辑即可: image.png 使用工厂模式初始化DevEnvironment的实例: image.png 关于这个createEnvironment方法,我们目前就只需要看到这儿,因为一会儿才会用到它,我们现在只需知道它创建的是DevEnvironment的实例工厂方法即可。

接下来,VITE把我们用户的Plugin跟自己的内置Plugin进行合并: image.pngimage.png 把处理好的插件列表交给需要暴露给外部的变量: image.png 在这小节中,我们主要是为了要搞明白DevEnvironmentPlugin列表的来源是什么,因为一会儿这些变量需要传递给对应的核心类进行管理。

关键变量初始化

在上一小节中我们搞懂了那些关键的参数是怎么来的,现在我们看一下这些关键变量是怎么初始化的。

这个config变量上保存着我们之前得到的关键信息,比如插件列表。 image.png 现在把这个config变量传递给之前我们关心的创建DevEnvironment的工厂方法。 image.png 至此,我们明白了创建DevEnvironment实例的整个过程,一会儿我们再关注DevEnvironment这个类的实现细节。

然后是调用DevEnvironmentinit方法,这个init方法里面主要操作就是初始化插件容器,在下一小节就会讲到,大家如果不懂可以一会儿再来查阅。 image.png

最后,我们着重看transformMiddleware的绑定过程。

首先,ViteDevServer上绑定着之前初始化的DevEnvironment的实例: image.pngimage.pngtransformMiddleware中间件在绑定的时候,传入了ViteDevServer的实例: image.png

一会儿我们在讲述上一篇文章中没有讲到的transformRequest的过程的时候会讲到这个ViteDevServer实例变量传递的作用。

到这个位置,VITE在DEV过程中主线流程(为什么是主线流程,一会儿会给大家解释的)的关键变量就已经初始化完成了。

DevEnvironment

这个类有相对长的集成链,代码看起来不是那么舒服。

DevEnvironment->BaseEnvironment->PartialEnvironment

所以我们得事先关注一下它的祖先类。

PartialEnvironment

关于PartialEnvironment类,我们只需要关注一个点,即设置配置和获取配置,这个配置就是上一节中我们通过resolveConfig拿到的那个值。

image.pngimage.png 其它的内容主要都是工具方法,我们无需额外关注。

BaseEnvironment

BaseEnvironment相对来说就更简单了,它主要就是对外暴露插件列表,所以我们就只需要关注plugins这个getter就好了。 image.png

DevEnvironment

在搞清楚它的祖先类之后,我们就可以正式关注它了。

DevEnvironment这个类中,我们需要首先看的就是它的init方法,在这个方法里创建了PluginContainer,就是之前我们在关键变量初始化小节的时候没有展开的那个方法。

image.png 在之前我们阐述PartialEnvironment的时候就已经知道了getTopLevelConfig这个方法获取到的就是我们在先前的小节中阐述的resolveConfig获取到的VITE将自己的默认配置和用户的配置合并之后的配置,所以这儿拿到的插件集合就是我们之前所阐述的: image.png 好了,拿到了插件列表之后,我们就可以把插件传递给PluginContainer了。 image.png 这小节我们重点先聊DevEnvironment本尊,后面的小节我们重点阐述PluginContainer

DevEnvironment同时也定义了获取PluginContainergetter,这个getter会广泛用到的。 image.png 最后,再看几个关键方法: image.pngimage.png

对于目前的我们来说,就只关注这么多,后面的文章,我们还会继续分析这篇文章没有涵盖到的细节内容的。

transformRequest

在上一篇文章中,我们只是粗略的向大家阐述了一个请求如何通过中间件得到资源,我们侧重的是请求侧,即http请求映射成资源标识符的这一过程,现在我们开始重点阐述资源如何处理并返回给客户端的。

还是从transformMiddleware开始,在关键变量初始化小节,我们已经阐述了ViteDevServer示例传递给了这个中间件。 image.png 这个transformRequest方法传递的参数environment就是DevEnvironment的实例,这也是为什么我们在上一篇文章中不进行更细节阐述的原因,因为只有搞明白了DevEnvironment的前世今生之后,这些问题才有的说。

这个environment变量来自于它,即ViteDevServer实例传递过来的数据: image.png VITE在初始化的时候,分别初始化了一个ssr和一个client的变量,由于我们不考虑SSR,所以我们就只看这个client就可以了。

现在大家再看transformRequest这个方法的话,是不是就已经觉得一目了然呢? image.png 之前我们说过,DevEnvironment这个类上定义了一个可以访问PluginContainergetter,现在就派上用场啦。

image.png 至此,事儿就非常简单了,(我们暂时先不考虑缓存),拿到了http请求的url,将url映射成资源的id(即调用PluginContainerresolveId方法触发生命周期),然后根据id从磁盘加载资源(即调用PluginContainerload方法触发生命周期),最后将从磁盘读取到的资源进行编译加工(即调用PluginContainertransform方法触发生命周期),然后把得到的编译结果返回给客户端,这就是从一个http请求发出,到客户端收到编译内容的全部过程啦。

image.pngimage.pngimage.pngimage.png

所以,我们可以简单的用大白话总结一下DEV阶段一个资源的获取流程了,VITE开启了一个DevServer,当我们访问这个DevServer的时候,VITE会把我们的Http请求通过内置的中间件映射成获取资源的标识符,然后调用插件容器通过资源标识符获取到资源,接着对资源进行编译转换,最后把转换之后的结果返回给客户端

如果你看到了这儿的话,是不是有一种你上你也行的冲动,哈哈哈,真的让人成就感爆棚啊!

PluginContainer

在明白了之前DevEnvironment之后,现在我们再看PluginContainer真的就会觉得太简单了。

这儿,我猜测VITE的代码可能也是经过重构或者API的变更了,在源码中,有两个Container类,一个叫做PluginContainer,一个叫做EnvironmentPluginContainer

接下来,我们一起看一下PluginContainerimage.png 大家可以看到,这个PluginContainer依赖了Environment(DevEnvironment也是其中的一个类型之一),这个Container上面的方法,都是在做桥接,调用的仍然是EnvironmentPluginContainer的内容。

image.pngimage.png

所以,我们现在搞明白了PluginContainerEnvironmentPluginContainer的关系,PluginContainer是为了保持已有API的稳定而做的桥接,在不经意之间我们就看到了桥接模式的实际应用,哈哈。

EnvironmentPluginContainer

之前我们看的是它的桥接类,现在我们来看正在干活儿的类。

这个代码是VITE的团队从Rollup的源码里面摘录,并且经过重构整合的代码,在之前的Rollup专栏里面,我在这篇文章向大家讲过Rollup各种类型的生命周期的实现方式。

Rollup源码学习(六)——重识Rollup构建生命周期

在Rollup里面,是一个一个的函数,不过VITE团队把它们进行了封装,统一管理。

现在带着大家一起来看一下这个EnvironmentPluginContainer类中的关键方法。

首先是构造器,在之前我们是已经知道了的,初始化这个容器的时候,已经把所有的插件列表传过来了的。 image.png 然后是hookParallel,这个是在支持Rollup的parallel类型的的生命周期: image.png 大家看一下,是不是这段代码有点儿似曾相识,如果你看过我前面提到的阐述Rollup生命周期的那篇文章的话。

这段代码的含义也不再赘述了,大家查看《Rollup源码学习(六)——重识Rollup构建生命周期》这篇文章即可,里面有demo向大家展示它的等价代码。

接着是VITE封装了的buildStart生命周期的处理: image.png

另外,在之前的文章我们讲过,resolveIdload这样的生命周期是first+async类型的生命周期。

我们来看一下VITE是如何实现的。 image.pngimage.png 看起来,定义这个handleHookPromise方法是为了更好的追踪Promiseimage.pngimage.png 可以看到,上述的实现简化一下的伪代码如下:

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是如何实现。 image.pngimage.png 可以看到,上述的实现简化一下的伪代码如下:

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方法里面的逻辑,虽然我们一般不怎么用到,但是框架的开发者必须要实现,这就是严谨,这也是值得我们学习的,如何设计健壮的软件系统,都是靠这种点滴积累起来的,哈哈。 image.png

我个人觉得VITE的这个实现相对于Rollup的实现代码的可读性比较好,可维护性也比较好,业务逻辑都内聚到了一处,外界只需要调度PluginContainer的方法就好了,体现了面向对象设计的封装的优势。

结语

考虑到篇幅的关系,就不在本文继续阐述较为复杂的ModuleGraph相关类。

相信大家阅读完本文应该是成就感满满的吧,在本文中,我们理清楚了VITE中几个类的关系,并且我们已经完全分析清楚了从请求经过中间件分发,映射到插件容器处理,编译,得到结果并返回的这一全套的流程。

接着我们又带领大家学习到了VITE基于Rollup生命周期设计的自己的生命周期,尤其是这套生命周期管理的设计思想(插件容器管理所有的插件,监听不同生命周期的设计,不同类型的插件),应该是我们收获最大的了吧,这些都是我们学习到的核心科技,对于将来我们在设计自己的业务系统时,一定有较好的指导意义。

❌