哈喽,很高兴你能点开这篇博客,本博客是针对 Vite
源码的解读系列文章,认真看完后相信你能对 Vite
的工作流程及原理有一个简单的了解。
Vite
是一种新型的前端构建工具,能够显著提升前端开发体验。
我将会使用图文结合的方式,尽量让本篇文章显得不那么枯燥(显然对于源码解读类文章来说,这不是个简单的事情)。如果你还没有使用过
Vite
,那么你可以看看我的前两篇文章,我也是刚体验没两天呢。(如下)- Vite + Vue3 初体验 —— Vite 篇
- Vite + Vue3 初体验 —— Vue3 篇
Vite
源码解读系列的第三篇文章,往期文章可以看这里:- Vite 源码解读系列(图文结合) —— 本地开发服务器篇
- Vite 源码解读系列(图文结合) —— 构建篇
vite
源码本体,在往期文章中,我们了解到:vite
在本地开发时通过connect
库提供开发服务器,通过中间件机制实现多项开发服务器配置,没有借助webpack
打包工具,加上利用rollup
(部分功能)调度内部plugin
实现了文件的转译,从而达到小而快的效果。vite
在构建生产产物时,将所有的插件收集起来,然后交由rollup
进行处理,输出用于生产环境的高度优化过的静态资源。
vite
的插件 —— @vitejs/plugin-vue
来进行源码解析。好了,话不多说,我们开始吧!
vite:vue
vite:vue
插件是在初始化 vue
项目的时候,就被自动注入到 vite.config.js
中的插件。(如下)import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'// https://vitejs.dev/config/
export default defineConfig({
plugins: [
vue()
]
});
该插件导出了几个钩子函数,这几个钩子函数,部分是用于
rollup
,部分是 vite
专属。(如下图)文章图片
在开始阅读源码之前,我们需要先了解一下
vite
和 rollup
中每一个钩子函数的调用时机和作用。字段 | 说明 | 所属 |
---|---|---|
name |
插件名称 | vite 和 rollup 共享 |
handleHotUpdate |
执行自定义 HMR(模块热替换)更新处理 | vite 独享 |
config |
在解析 Vite 配置前调用。可以自定义配置,会与 vite 基础配置进行合并 |
vite 独享 |
configResolved |
在解析 Vite 配置后调用。可以读取 vite 的配置,进行一些操作 |
vite 独享 |
configureServer |
是用于配置开发服务器的钩子。最常见的用例是在内部 connect 应用程序中添加自定义中间件。 | vite 独享 |
transformIndexHtml |
转换 index.html 的专用钩子。 |
vite 独享 |
options |
在收集 rollup 配置前,vite (本地)服务启动时调用,可以和 rollup 配置进行合并 |
vite 和 rollup 共享 |
buildStart |
在 rollup 构建中,vite (本地)服务启动时调用,在这个函数中可以访问 rollup 的配置 |
vite 和 rollup 共享 |
resolveId |
在解析模块时调用,可以返回一个特殊的 resolveId 来指定某个 import 语句加载特定的模块 |
vite 和 rollup 共享 |
load |
在解析模块时调用,可以返回代码块来指定某个 import 语句加载特定的模块 |
vite 和 rollup 共享 |
transform |
在解析模块时调用,将源代码进行转换,输出转换后的结果,类似于 webpack 的 loader |
vite 和 rollup 共享 |
buildEnd |
在 vite 本地服务关闭前,rollup 输出文件到目录前调用 |
vite 和 rollup 共享 |
closeBundle |
在 vite 本地服务关闭前,rollup 输出文件到目录前调用 |
vite 和 rollup 共享 |
vite
和 rollup
的所有钩子函数后,我们只需要按照调用顺序,来看看 vite:vue
插件在每个钩子函数的调用期间都做了些什么事情吧。config
config(config) {
return {
define: {
__VUE_OPTIONS_API__: config.define?.__VUE_OPTIONS_API__ ?? true,
__VUE_PROD_DEVTOOLS__: config.define?.__VUE_PROD_DEVTOOLS__ ?? false
},
ssr: {
external: ['vue', '@vue/server-renderer']
}
}
}
vite:vue
插件中的 config
做的事情比较简单,首先是做了两个全局变量 __VUE_OPTIONS_API__
和 __VUE_PROD_DEVTOOLS__
的替换工作。然后又设置了要为 SSR 强制外部化的依赖。configResolved
在
config
钩子执行完成后,下一个调用的是 configResolved
钩子。(如下)configResolved(config) {
options = {
...options,
root: config.root,
sourceMap: config.command === 'build' ? !!config.build.sourcemap : true,
isProduction: config.isProduction
}
},
vite:vue
中的 configResolved
钩子,读取了 root
和 isProduction
配置,存储在插件内部的 options
属性中,以便提供给后续的钩子函数使用。然后,判断当前命令是否为
build
,如果是构建生产产物,则读取 sourcemap
配置来判断是否生成 sourceMap
,而本地开发服务始终会生成 sourceMap
以供调试使用。configureServer
在
configureServer
钩子中,vite:vue
插件只是将 server
存储在内部 options
选项中,并无其他操作。(如下)configureServer(server) {
options.devServer = server;
}
buildStart
在
buildStart
钩子函数中,创建了一个 compiler
,用于后续对 vue
文件的编译工作。(如下)buildStart() {
options.compiler = options.compiler || resolveCompiler(options.root)
}
文章图片
该
complier
中内置了很多实用方法,这些方法负责按照规则对 vue
文件进行庖丁解牛。load
在运行完了上述几个钩子后,
vite
本地开发服务就已经启动了。我们打开本地服务的地址,对资源发起请求后,将会进入下一个钩子函数。(如下图)
文章图片
打开服务后,首先进入的是
load
钩子,load
钩子主要做的工作是返回 vue
文件中被单独解析出去的同名文件。vite
内部会将部分文件内容解析到另一个文件,然后通过在文件加载路径后面加上 ?vue
的 query
参数来解析该文件。比如解析template
(模板)、script
(js 脚本)、css
(style
模块)...(如下图)文章图片
而这几个模块(
template
、script
、style
)都是由 complier.parse
解析而来(如下)const { descriptor, errors } = compiler.parse(source, {
filename,
sourceMap
});
transform
在
load
返回了对应的代码片段后,进入到 transform
钩子。transform
主要做的事情有三件:- 转译
vue
文件 - 转译以
vue
文件解析的template
模板 - 转译以
vue
文件解析的style
样式
简单理解,这个钩子对应的就是webpack
的loader
。
文章图片
这里,我们以一个
TodoList.vue
文件为例,展开聊聊 transform
所做的文件转译工作。下面是
TodoList.vue
源文件,它做了一个可供增删改查的 TodoList
,你也可以通过 第二期文章 - Vite + Vue3 初体验 —— Vue3 篇 了解它的详细功能。{{item.title}}
.todo-list-container {
display: flex;
justify-content: center;
width: 100vw;
min-height: 100vh;
box-sizing: border-box;
padding-top: 100px;
background: linear-gradient(rgba(219, 77, 109, .02) 60%, rgba(93, 190, 129, .05));
.todo-wrapper {
width: 60vw;
.todo-input {
width: 100%;
height: 50px;
font-size: 18px;
color: #F05E1C;
border: 2px solid rgba(255, 177, 27, 0.5);
border-radius: 5px;
}
.todo-input::placeholder {
color: #F05E1C;
opacity: .4;
}
.ant-input:hover, .ant-input:focus {
border-color: #FFB11B;
box-shadow: 0 0 0 2px rgb(255 177 27 / 20%);
}
.todo-list {
margin-top: 20px;
.todo-item {
box-sizing: border-box;
padding: 15px 10px;
cursor: pointer;
border-bottom: 2px solid rgba(255, 177, 27, 0.3);
color: #F05E1C;
margin-bottom: 5px;
font-size: 16px;
transition: all .5s;
display: flex;
justify-content: space-between;
align-items: center;
padding-right: 10px;
.operator-list {
display: flex;
justify-content: flex-start;
align-items: center;
:first-child {
margin-right: 10px;
}
}
}.todo-top {
background: #F05E1C;
color: #fff;
border-radius: 5px;
}.todo-completed {
color: rgba(199, 199, 199, 1);
border-bottom-color: rgba(199, 199, 199, .4);
transition: all .5s;
background: #fff;
}.todo-item:hover {
box-shadow: 0 0 5px 8px rgb(255 177 27 / 20%);
border-bottom: 2px solid transparent;
}.todo-completed:hover {
box-shadow: none;
border-bottom-color: rgba(199, 199, 199, .4);
}
}
}
}
进入到
transformMain
函数,可以发现 transformMain
内部主要做了几件事情:- 解构
vue
文件的script
、template
、style
- 解析
vue
文件中的script
代码; - 解析
vue
文件中的template
代码; - 解析
vue
文件中的style
代码; - 解析
vue
文件中的自定义模块
代码; - 处理 HMR(模块热重载)的逻辑;
- 处理
ssr
的逻辑; - 处理
sourcemap
的逻辑; - 处理
ts
的转换,转成成es
;
解构
script
、template
、style
vue
文件中包含 script
、template
、style
三大部分,transformMain
内部先通过 createDescriptor
中的 compiler
将这三大块分离解析出来,作为一个大对象,然后方便后面的解析。(如下图)文章图片
在
compiler
中,会先使用 parse
方法,将源码 source
解析成 AST
树。(如下图)文章图片
在下图中可以看出,解析后的
AST
树有三个模块,主要就是 script
、template
、style
。文章图片
接下来,就是将各个模块的属性、代码行数记录起来,比如
style
标签,就记录了 lang: less
的信息,以供后面的解析。解析
Template
vue
文件中的 template
写了很多 vue
的语法糖,比如下面这行
像这种语法,浏览器是无法识别并将事件绑定到
vue
的内部函数中的,所以 vite
对这类标签先做了一遍内部转换,转换成可执行的函数,再通过浏览器执行函数生成一套 虚拟 DOM
,最后再由 vue
内部的渲染引擎将 虚拟 DOM
渲染成 真实 DOM
。现在我们就可以看看
vite
内部对 template
语法的转译过程,vite
内部是通过 genTemplateCode
函数来实现的。在
genTemplateCode
内部,首先是将 template
模板语法解析成了 AST
语法树。(如下图)文章图片
然后再通过不同的转译函数,对对应的 AST 节点进行转换。
文章图片
下面我们以
Input
节点为例来简单解释一下转译的过程。文章图片
将这个步骤重复,直到整棵
template
树都解析完成。解析 script 标签 下面,我们来看看对
script
标签的解析部分,对应的内部函数是 genScriptCode
这个函数所做的事情主要是下面几件事情:
- 解析
script
标签中定义的变量; - 解析
script
标签中定义的引入import
,后面将会转换成相对路径引入; - 将
script
标签编译成一个代码片段,该代码片段导出_defineComponent
(组件)封装的对象,内置setup
钩子函数。
文章图片
解析 style 标签
style
标签解析的比较简单,只是将代码解析成了一个 import
语句(如下)import "/Users/Macxdouble/Desktop/ttt/vite-try/src/components/TodoList.vue?vue&type=style&index=0&scoped=true&lang.less"
随后,根据该请求中
query
参数中的 type
和 lang
,由 vite:vue
插件的 load
钩子(上一个解析的钩子)中的 transformStyle
函数来继续处理样式文件的编译。这部分我就不做展开了,感兴趣的同学可以自行阅读代码。编译
ts
到 es
在 script
、template
、style
部分的代码都解析完毕后,接下来还做了下面几个处理:- 解析
vue
文件中的自定义模块
代码; - 处理 HMR(模块热重载)的逻辑;
- 处理
ssr
的逻辑; - 处理
sourcemap
的逻辑; - 处理
ts
的转换,转成成es
;
ts
到 es
的转换做个简单介绍,这一步主要是在内部通过 esbuild
完成了 ts
到 es
的转换,我们可以看到这个工具有多快。(如下图)文章图片
输出代码 在
ts
也转译成 es
后,vite:vue
将转换成了 es
的 script
、template
、style
代码合并在一起,然后通过 transform
输出,最终输出为一个 es
模块,被页面作为 js
文件加载。(如下图)文章图片
handleHotUpdate
最后,我们来看看对于文件模块热重载的处理,也就是
handleHotUpdate
钩子。我们在启动项目后,在
App.vue
文件的 setup
中加入一行代码。console.log('Test handleHotUpdate');
在代码加入并且保存后,被
vite
内部的 watcher
捕获到变更,然后触发了 handleHotUpdate
钩子,将修改的文件传入。vite:vue
内部会使用 compiler.parse
函数对 App.vue
文件进行解析,将 script
、template
、style
标签解析出来。(也就是上面解析的编译步骤)(如下图)。文章图片
然后,
handleHotUpdate
函数内部会检测发生变更的内容,将变更的部分添加到 affectedModules
数组中。(如下图)文章图片
然后,
handleHotUpdate
将 affectedModules
返回,交由 vite
内部处理。最后,
vite
内部会判断当前变更文件是否需要重新加载页面,如果不需要重新加载的话,则会发送一个 update
消息给客户端的 ws
,通知客户端重新加载对应的资源并执行。(如下图)文章图片
好了,这样一来,模块热重载的内容我们也清楚了。
小结 本期对
@vitejs/plugin-vue
的解析就到这里结束了。可以看出,
vite
内部结合了 rollup
预设了插件的多个生命周期钩子,在编译的各个阶段进行调用,从而达到 webpack
的 loader
+ plugin
的组合效果。而
vite/rollup
直接使用 plugin
就替代了 webpack
的 loader
+ plugin
功能,可能也是为了简化概念,整合功能,让插件的工作更简单,让社区的插件开发者也能更好的参与贡献。vite
的快不仅仅是因为运行时不编译原生 es
模块,还有在运行时还利用了 esbuild
这类轻而快的编译库来编译 ts
,从而使得整个本地开发时变得非常地轻快。下一章,我们将对
vite
插件进行实战练习:实现一个 vite
插件,它的功能是通过指定标签就能加载本地 md
文件。最后一件事 如果您已经看到这里了,希望您还是点个赞再走吧~
您的点赞是对作者的最大鼓励,也可以让更多人看到本篇文章!
如果觉得本文对您有帮助,请帮忙在 github 上点亮
star
鼓励一下吧!