Vite源码学习(十)——热更新(上)

568 阅读12分钟

前言

在前面的文章中,我们已经把VITE在构建和开发过程中主体处理逻辑梳理的差不多了。

在构建过程中,VITE主要是调用Rollup的JS API,通过预设一些参数和内置的插件控制Rollup的行为完成构建。

在Dev过程中,VITE通过自己启动一个内置的DevServer,监听我们在客户端发出的请求,经它的中间件分发,通过内置的插件容器转换规则进行转换,完成从磁盘读取内容和编译转换,最后返回给浏览器相应的内容。

接着,在Dev阶段还有2个非常重要的处理逻辑到目前为止,我们还几乎没有做讲解,它们分别是热更新(Hot Update)依赖预构建

热更新是每一个构建工具提升开发效率最显著的一个功能之一,在之前,我曾经写过一篇关于VITE热更新相关的文章: 深入浅出Vite环境下vue组件的HMR的过程,之前关注点重点没有在VITE侧。

从这篇文章开始,我们将开始阐述VITE热更新的相关处理逻辑,本系列的文章的关注重点将会在VITE侧,不会过分关心相关支持生态的处理(比如Vue),但是我们会深入去学习VITE热更新过程中依赖模块更新这些复杂逻辑的处理等内容。

热更新的基本原理

假设现在是一场面试,如果面试官要我们自己设计热更新你会怎么设计呢?不知道大家有没有想过这个问题。

其实这个场景跟我们使用AJAX进行局部刷新的逻辑是差不多的,首先,当我们在编辑器中按下Ctrl+S保存源代码的更改时,构建工具需要监听到哪些文件进行了变化,并且这些变化是否是有意义的变化(比如是Git忽略掉的文件,那监听这些文件就没有什么意义了),如果某个文件变化了,需要知道这个文件的变化会导致哪些依赖文件的变化,这些也是很重要的,因为如果只是一个单纯监听某个文件变动那将会使得热更新变得毫无意义。

当我们知道了哪些文件变化了,这些文件应该被更新的时候,怎么样通知客户端进行更新呢?不管是Ajax还是WebSocket还是SSE(Server-Side-Events),无论选择哪一个,我们最终目的都是实现客户端和服务端的少量数据更新。

很明显,这是一个服务端主动向客户端推送消息的过程,如果我们使用Ajax肯定是无法办到这个事儿的(开发阶段开发者的浏览器又不会存在兼容性问题,用轮循,那岂不是太不聪明了吗?),那么,能选择的方案就只有WebSocket或SSE了。

很明显,这个过程,广播内容可能带着文件id和文件最新的哈希码,如果客户端有用到这个文件,然后客户端再重新导入更新后的文件,最终重新渲染完成更新,这个过程只能设计成这样,为什么是这样呢?因为这个变化的源头在服务端,服务端只能告知变化,只有客户端自己才知道是否需要消费这个变更。

拿到更新文件重新渲染的这个过程,这已经跟构建工具没有任何的关系了,不同的框架有不同的实现,所以,我们接下来的几篇文章就是聊服务端监听变化向服务端推送文件变化的处理逻辑。

最后,还有一点进阶的非常有用的知识点。很明显,热更新只会在DEV阶段用到,处理这些代码逻辑自然会增加打包体积,所以在生产环境中必须要把这些代码TreeShaking掉,怎么做呢?我们在构建工具中提供一个环境变量,比如VITE的import.meta.hot,这样开发者在开发过程中就可以把需要热更新处理的逻辑包裹在if分支下,这样就可以在打包的时候摇掉热更新的处理逻辑,就可以在高效开发和较小的生产打包体积中得兼了。

好了,我们已经向大家阐述清楚热更新的基本原理了,接下来我们就开始阐述VITE侧的相关实现了。

Vite开发服务器中的WebSocket

在前面我们聊VITE的DevServer的时候就知道WebSocket(后文统一简称WS)的Server是在何时创建的了,只不过当时还不是我们关注的重点,WS的Server也是在初始化DevServer的时候创建的,并且把这个WS Server的实例也关联到ViteDevServer的实例上去了。

创建WS Server实例: image.png 关联WS Server实例至ViteDevServer: image.png 在VITE的WS实现中,它使用的是一个叫做ws的库,WS,目前这个库在Github上已经有22k的star数量。

接下来,我们就来看一下VITE是如何实现WebSocket处理的。

一致化WebSocketServer: image.png 初始化WS Server: image.png 处理客户端连接逻辑: image.png 定义一致化,供外界的可操作WS的API: image.png 最后返回可操作API: image.png

VITE在服务端处理WS的逻辑还是比较简单的,如果你不是VITE的插件开发者,这部分的逻辑几乎不会关注到,大家可以了解一下,在有插件开发需求的时候再回过头来仔细看就好。

Vite开发服务器向客户端注入的辅助类

不知道大家有没有观察到,我们的css文件,经过VITE处理之后,返回给前端的文件是一个JS文件,只不过这个文件里面带有这个css文件编译之后的结果,很明显,JS要变成生效的css,这个操作肯定不是凭空来的,这是VITE注入到客户端中的辅助类帮我们解析JS,创建style标签,然后插入到DOM树中的,这个过程跟webpack的style-loader完成的功能类似(本文重点在阐述VITE,有兴趣的同学可以自行了解)。

image.png image.png

接下来,我们就来探究一下VITE注入到客户端的辅助类或方法。

以下是刚才我们提到的辅助css操作的方法:

image.png

到这个位置,我们还暂时不要着急去了解css的热更新是如何完成的,就像厨师在做饭时候得先准备材料一样,我们先把准备工作做好,一会儿一鼓作气,全部拿下它们。

接下来是我们最熟悉不过的错误遮罩: image.png image.png image.png image.png

接着是最复杂的热更新上下文: image.png 热更新的上下文,我们先走马观花的看一遍,后面的文章会有更细节的讲述,现在我们先回想一下,VITE提供给我们的热更新上下文API是什么样的呢? image.png image.png 我们在Callback里面调用更新之后的模块内容,这个accept方法的来源,正是HRMContext类里面所定义的。 image.png

最后是辅助类文件在导入时就需要初始化WS连接操作逻辑。

我们首先看客户端WS上下文的创建,这个作用域中,处理了WS的连接,消息发送,WS关闭的逻辑: image.png 使用一个对象包裹刚才的那个实例,主要是为了考虑异常情况: image.png 然后看处理服务端发送过来的消息内容处理逻辑: image.png 更新逻辑: image.png 这里面,对于JS的处理,全部都给到了hmrClient类的处理,我们暂时也不看HMRClient的实现逻辑,它将会在详细聊HMRContext这个类的时候一并阐述。

到现在,我们明白了,在一个/@vite/client这个文件中,初始化了WS的连接,接管了服务端发送过来的WS消息,并且向客户端提供了热更新的上下文,提供了错误的遮罩用于展示开发中的错误信息,提供了更新css和js的方法。

到目前为止,除了WS的逻辑是在这个文件加载的时候就初始化了,其它内容暂时还只是提供了,需要这些方法的业务逻辑还尚未加载这些内容。

接下来,我们就要看业务逻辑侧是如何使用这些辅助方法的。

热更新上下文的注入

CSS

在上面例子中的App.vue?vue&type=style&scope=xxxx&lang=scss这个文件中,import { updateStyle } from '@/vite/client'这行代码是从哪儿来的呢?

它来源于VITE的内置插件vite:css-postimage.png 这个代码只会处理在Dev阶段的css更新处理逻辑: image.png 现在,当这个文件加载的时候,我们就可以立即更新相应的css内容。 image.png css的处理逻辑相对来说非常简单,因为css不存在文件相互依赖的问题,只需要更新当前的这一个文件就可以了(哪怕是@import导入的文件,大家想一下是不是这个道理)

JS

对于JS来说,这个流程就变得非常复杂了,在上一个小节我们说过了,css是单个的更新,js文件的更新可能会牵涉到一系列的更新,并且还需要考虑TreeShaking的问题,热更新的代码逻辑不能打包到生产代码中去。

import.meta.hot这个上下文是浏览器提供的吗?我们先做个实验来探索一下(我是新建了一个html使用liveServer启动的哈): image.png 咦,好像不是的: image.png 那么,很明显,import.meta.hot就是VITE注入的了。

VITE又是在什么时机注入的呢?我们在源码里面搜索了一下,找到了vite:import-analysis处理逻辑中,注入上下文的位置: image.png

可以看到,在JS文件的前面能找到追加的热更新的上下文: image.png 别高兴的太早哈,我们再看一个JS文件: image.png 为什么这个文件里面没有给我们注入热更新的API呢?

那我们必须得看看这个热更新API的注入条件了: image.png

也就是说,我们想要实现对这个index.ts实现热更新操作,我们在源码里面得手动调用import.meta.hot.accpect的API,这样VITE就会给我们注入热更新上下文了。 image.png

为什么.vue文件就有直接的热更新支持呢?大家不要忘了,VITE是无法直接识别.vue文件的,.vue文件的处理逻辑在@vitejs/plugin-vue里面加入的。 image.png 学到这里面,有的同学应该已经成就感满满了,恭喜你,我们一起搞懂了VITE生态中一些复杂的问题。

监听文件的变化

在之前,我们聊ViteDevServer创建过程的时候,我们就已经看过chokidar用来监听文件的变化了,现在我们对这个监听的详细流程探究。

首先是创建watcher的实例: image.png 可以看到,监听的目录有项目的根目录、配置文件、publicDir和一些env文件以及用户明确指定需要监听的目录。

接着,处理监听文件的增加和删除: image.png

我们来看一下处理文件变化的核心逻辑:

如果命中了配置文件或者env文件,直接重启服务,这就是为什么我们在开发的时候一改变配置文件VITE服务就重启了,有时候我们改了配置,可能脑子抽风了忘了重启服务,一直不生效,要找很久的问题(反正我是遇到过),这样对开发者非常友好,尤其是初级开发者。 image.png 接着,针对文件的创建和和更新处理hotUpdate的插件上下文: image.png 最后是把更新文件的WS消息发送给客户端: image.png image.png 现在,客户端就拿到最新的文件信息,完成调用之前部署好的热更新API完成热更新啦。 image.png 顺便再给大家补充一下,有的同学可能会比较有疑问,上面那个type为custom的消息的处理逻辑是在哪儿呢? 这个处理逻辑仍然在@vitejs/plugin-vue里面哦: image.png 正是因为VITE暴露给了我们WS的API,使得我们可以在插件的热更新hook里面完成自定义的业务逻辑处理。

结语

在这篇文章中,我主要目的是向大家大致理清楚热更新的整体处理流程,如果您读完这篇文章,大家已经可以搞明白在VITE中的热更新的流程是怎么一回事了,应付常规的面试题应该已经不再话下了呢,恭喜恭喜。

VITE在处理热更新的过程中,首先需要在服务端创建WebSocket服务,并且监听源码文件的变化,与此同时向客户端注入了辅助类,并且创建了和服务端通信的WebSocket服务。当服务端监听到文件变化时,向客户端发送最新的文件内容信息,客户端的WebSocket的上下文监听到服务端发送过来的消息,这些辅助类根据待更新文件类型实现部分更新。

在下一篇文章开始,我们会向大家阐述热更新过程中JS文件依赖更新等复杂的场景(在核心类章节,我们跳过了EnvironmentGraph类上很多的内容),以及详细聊聊之前我们没有阐述的HMRClient类和HMRContext这些类,当我们学懂这些内容之后,对VITE热更新的理解几乎就算吃透了。

未完待续.....