前言
我将会从本文开始更新自己学习Vite相关的经验与心得。
Vite,从我的使用体验来说,我的它的整体认知是一个Bundleless
的Dev Server
+增强自Rollup
的打包器。
关于这些基础概念,我在本文中不会做讲解,默认大家已知,如果您还不知道的话,请查阅相应的学习资料。
我在本系列文章中可能会借鉴到之前学习Rollup源码的专栏文章中所提到的知识点,如果你没有阅读过我这部分的文章的话,建议您先阅读我的这个专栏:Rollup
本文仍然会采用之前分析Rollup源码的方式,对一些非主线流程的代码,会进行选择性忽视,如果大家觉得我的阐述忽略了重要的内容的话,可以联系我。
因为Vite的这种架构模式,我们将先学习Vite的构建流程,再学习Vite的Dev Server启动流程,这样将有效的降低我们的学习难度,从而更好的掌握Vite的核心原理。
如何调试Vite的源码?
调试Vite的源码稍微跟Rollup不同,我采用的方式是直接利用Vite源码提供的playground进行调试。(相比于Rollup的release包,Vite的release包是丑化、压缩过的,无法追踪,Rollup仅仅是做了一个TS编译成JS,然后打包JS)
各位读者,如果有兴趣的话,大家可以跟着我的示例一起实践。
首先将Vite的源码Clone到本地,github.com/vitejs/vite
然后,我们打开到Vite的子目录:
bash
pnpm i
npm run dev
然后,我们随便找一个Playground的子目录:
bash
pnpm i
npm run dev
如何调试Vite的源码呢? 我们利用VSCode的JavaScript Debug Terminal
,然后在Vite的源码里面编写一个debugger
断点指令。 再运行Playground中Demo项目的某个命令(我以build为例),就会命中断点: 如果你在调试过程中,遇到structuredClone is not defined
的错误的话,请用比较新的Node版本哦。
从CLI开始
Vite使用的是cac
这个包来管理命令的,这个包非常好用,看了Vite的源码之后,我在自己开发公司的CLI工具时也才用这个包,仓库地址:github.com/cacjs/cac
首先是创建cac的实例: 关于cac
的使用方法,本文不会阐述,大家有兴趣的话,请参考cac
的文档。
vite的CLI主要提供了4个命令,分别是dev
,build
,preview
,optimize
。
我们会按build
->dev
->preview
->optimize
的顺序分别来阐述Vite所提供的能力。
因为之前我们已经学过了Rollup
,所以build
命令就是最简单的一个命令了,我们的关注点将会是Vite对Rollup的构建进行了哪些方面的增强。
build 命令分析
我们从cac
注册build命令的位置开始看。 它引入build文件中的createBuilder
方法,接下来我们就看看这个createBuilder
方法做了什么工作? 上面的函数我们折起来了一些内容,主线流程就是读取Vite最终的build配置,然后设置环境,返回构建的上下文。
接下来看一下这里面的细节:
所以,在builder的buildApp
调用时,await
等待的就是ViteBuilder
的实例builder
的build
方法的解决。 最终,这个build
方法就是来自于跟它一块儿定义的build
方法。 接下来,我们就要看这个超级超级长的buildEnvironment
方法,这个方法有200多行代码。
我把非关键的代码都折叠起来。 最终,可以很清晰的看到,导入了Rollup
, 然后,得到Rollup的打包结果,Vite根据用户的配置,决定是把内容写入到磁盘还是io(这个,我们在Rollup源码讲解的时候已经聊过了,大家可以回看一下)。
刚才折起来的代码,我们只挑关键的内容看: Vite增强了Rollup的生命周期,我们看一下是怎么增强这个生命周期的: Vite传递了当前的环境变量,这个环境变量比较复杂,是一个类。 这个位置,暂时我们不展开了,后面我们讲Plugin的时候,应该会再讲到,大家可以不用着急。
可以看到,这个增强,只是改变了插件的执行上下文,并没有改变插件的参数规格,这也符合Vite的文档描述,如果开发者编写的插件没有用到Vite的特征生命周期的话,建议兼容Rollup插件 。
到这个位置,我们对于Rollup的构建流程其实已经掌握的差不多了,不过,我们暂时还忽略了一个重要的东西,那就是Vite独有的生命周期。
我们单独开一个小节来阐述。
Vite独有的生命周期
在说这个知识点之前,我要向大家阐述Vite扩展了Rollup没有的生命周期,如果我们现在只讨论build环境的话,那么可以参与讨论的生命周期钩子有以下几个:
config
configResolved
transformIndexHtml
在这个位置加载了Vite内置的默认配置,然后跟用户的配置进行合并,接下来,我们的关注重点就可以切到Vite解析配置和加载插件的处理逻辑上面去了。 在加载配置文件的时候,Vite把文件进行了一下打包,其实就是调用的是我们以JS API
形式调用的那个build
方法,这就是为什么Vite的配置文件支持几乎各种形式的配置的根本原因,怎么样,非常巧妙吧,嘿嘿。 下面这个图中的build方法,就是我们通过JS API调用vite的build方法。 就跟C语言的编译器是C语言写的一样,哈哈哈。
好了,让配置加载完成的时候,就要开始触发config
生命周期了: 如果你对Rollup的生命周期不熟悉的话,请参考我的这篇文章。在这篇文章中,详细的讲述了Rollup几种生命周期的原理。
然后,后面就开始了做各种根据环境判断进行合并的逻辑,这儿的代码就不向大家展示了,大家明白这个含义即可。
然后,就触发configResolved
生命周期了。
我个人习惯在实际开发中一般在configResolved
生命周期中编写逻辑获取Vite的配置,我个人感觉是这样可以拿到的是最终的配置,比较确定。
最后,返回确定的配置给外界,即最终传递给build
方法的参数。
Vite内置插件
在本节我们不会浓墨重彩的讲Vite的内置插件,我们只会讲一个插件,向大家说明Vite是如何对构建进行增强的。
之前我们提到了transformIndexHtml
这个生命周期,这个生命周期是在一个内置插件中完成的,这个插件叫做vite:build-html
。
回到之前我们分析Vite解析配置的逻辑处理: Vite在这儿处理插件,就加入了自己的内置插件。 这儿有太多的插件了,后续带着大家分析一些常见的插件,本文中,我们只关注这个vite:build-html
插件。
接下来,看一下这个插件的实现: 这儿有接近千行代码,我们先不关注具体实现,我们就只看个大概,Vite利用transform钩子往这个里面处理了html的逻辑。
然后它在生成Bundle的时候,把一些内容注入到html文件中,再把Bundle中已经注入的内容删除。
之前在Rollup源码学习中,向大家表示过,generateBundle
这个生命周期非常重要,我们需要重点掌握。
后面的插件分析中,我们着重对这个插件进行分析,在这节,我们的重点关注是Vite如何处理transformIndexHtml
这个生命周期的。
Vite把定义transformIndexHtml
了的生命周期筛选出来,然后准备调用:
这三类Hooks的调用时机不同,preHooks
,调用在transform
生命周期, 而剩下的两类Hook调用在generateBundle
生命周期。 关于这两者的区别,我们同样放在后面的文章解释为什么。
至此,我们对Vite的构建流程就有了一个整体的认知了。
总结
Vite的对外暴露一个叫做build
的方法,这个方法调用Rollup
的JS API进行打包,我们可以通过传入Vite所需要的配置直接调用这个方法,这就是所谓的JS API
。
Vite的CLI同样调用的是这个方法,只不过Vite在CLI中处理了很多默认的参数,其余并没有什么差异。
接下来,我们总结一下Vite的整体构建的流程,当我们调用Vite的CLI时,Vite会尝试去加载我们传入的配置,并且做一些标准化的处理,Vite在加载我们传递的配置文件的时候,会调用JS API
中的build
方法,把我们的配置进行编译处理,这样可以使得我们可以支持绝大多数类型的配置文件。当解析到配置的时候,Vite就会触发自己独有的生命周期config
。
然后Vite就会根据各种环境处理一些自己的逻辑,然后跟用户的配置进行合并,合并完成之后,对外触发自己独有的生命周期configResolved
。
紧接着,Vite就会调用Rollup的构建方法,开始走Rollup的构建逻辑,在Rollup的构建钩子transform
中,Vite首先会根据当前处理的文件是否是html文件,然后触发自己独有的生命周期钩子:transformIndexHtml
(order为pre的钩子),在Rollup的生成钩子generateBundle
的中会再次触发自己独有的生命周期钩子transformIndexHtml
(order不为pred的钩子)。
最后,得到Rollup的构建产物,将产物输出到io或者磁盘,完成构建。
以上是我自己总结的Vite的构建流程,欢迎大家刊误。
本文只是简单的讲述了一下Vite的构建逻辑,后面的文章我们还会接着本文作跳过的内容进行讲解,未完待续......