哈喽,很快乐你能点开这篇博客,本博客是针对 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 篇 理解它的具体性能。
<script setup lang="ts">
import {DeleteOutlined, CheckOutlined, CheckCircleFilled, ToTopOutlined} from '@ant-design/icons-vue';
import {Input} from "ant-design-vue";
import {ref} from "vue";
import service from "@/service";
import {getUserKey} from '@/service/auth';
// 创立一个援用变量,用于绑定 Todo List 数据
const todoList = ref<{
id: string;
title: string;
is_completed: boolean;
is_top: boolean;
}[]>([]);
// 初始化 todo list
const getTodoList = async () => {const reply = await service.get('/todo/get-todo-list', { params: { key: getUserKey() } });
todoList.value = reply.data.data;
}
getTodoList();
// 删除、实现、置顶的逻辑都与 todoList 放在同一个中央,这样对于逻辑关注点就更加聚焦了
const onDeleteItem = async (index: number) => {const id = todoList.value[index].id;
await service.post('/todo/delete', { id});
todoList.value.splice(index, 1);
}
const onCompleteItem = async (index: number) => {const id = todoList.value[index].id;
await service.post('/todo/complete', { id});
todoList.value[index].is_completed = true;
// 从新排序,将曾经实现的我的项目往后排列
const todoItem = todoList.value.splice(index, 1);
todoList.value.push(todoItem[0]);
}
const onTopItem = async (index: number) => {const id = todoList.value[index].id;
await service.post('/todo/top', { id});
todoList.value[index].is_top = true;
// 从新排序,将曾经实现的我的项目往前排列
const todoItem = todoList.value.splice(index, 1);
todoList.value.unshift(todoItem[0]);
}
// 新增 Todo Item 的逻辑都放在一处
// 创立一个援用变量,用于绑定输入框
const todoText = ref('');
const addTodoItem = () => {
// 新增一个 TodoItem,申请新增接口
const todoItem = {key: getUserKey(),
title: todoText.value
}
return service.post('/todo/add', todoItem);
}
const onTodoInputEnter = async () => {if (todoText.value === '') return;
await addTodoItem();
await getTodoList();
// 增加胜利后,清空 todoText 的值
todoText.value = '';
}
</script>
<template>
<section class="todo-list-container">
<section class="todo-wrapper">
<!-- v-model:value 语法是 vue3 的新个性,代表组件外部进行双向绑定是值 key 是 value -->
<Input v-model:value="todoText" @keyup.enter="onTodoInputEnter" class="todo-input" placeholder="请输出待办项" />
<section class="todo-list">
<section v-for="(item, index) in todoList"
class="todo-item"
:class="{'todo-completed': item.is_completed,'todo-top': item.is_top}">
<span>{{item.title}}</span>
<div class="operator-list">
<CheckCircleFilled v-show="item.is_completed" />
<DeleteOutlined v-show="!item.is_completed" @click="onDeleteItem(index)" />
<ToTopOutlined v-show="!item.is_completed" @click="onTopItem(index)" />
<CheckOutlined v-show="!item.is_completed" @click="onCompleteItem(index)" />
</div>
</section>
</section>
</section>
</section>
</template>
<style scoped lang="less">
.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);
}
}
}
}
</style>
进入到 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
的语法糖,比方上面这行
<Input v-model:value="todoText" @keyup.enter="onTodoInputEnter" class="todo-input" placeholder="请输出待办项" />
像这种语法,浏览器是无奈辨认并将事件绑定到 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
激励一下吧!