Hello, I'm glad you can click on this blog. This blog is a series of articles on the interpretation of the source code of Vite
. After reading it carefully, I believe you can have a simple understanding of the workflow and principles of Vite
.
Vite
is a new front-end build tool that can significantly improve the front-end development experience.
I will use the combination of pictures and texts to try to make this article less boring (obviously for source code interpretation articles, this is not a simple thing).
If you haven't used Vite
, then you can read my first two articles, I just experienced it for two days. (as follows)
This article is the third article in the Vite
source code interpretation series. Previous articles can be found here:
- Vite Source Code Interpretation Series (Graphic and Text Combination) - Local Development Server
- Vite Source Code Interpretation Series (Graphic and Text Combination) - Construction
This article mainly interprets the source code ontology of vite
. In previous articles, we learned that:
vite
provides a development server through theconnect
library during local development, and implements multiple development server configurations through the middleware mechanism, without the help of thewebpack
packaging tool, plus the use ofrollup
(part of the function) to schedule the internalplugin
to achieve file translation, so as to achieve a small and fast effect.- When building a production product,
vite
collects all the plug-ins, and then hands it over torollup
for processing, and outputs highly optimized static resources for the production environment.
In this article, I will analyze the source code of vite
, the plugin of @vitejs/plugin-vue
that runs through the previous two articles.
Well, without further ado, let's get started!
vite:vue
vite:vue
plugin is a plugin that is automatically injected into vite.config.js
when the vue
project is initialized. (as follows)
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
vue()
]
});
The plugin exports several hook functions, some of which are for rollup
and some are exclusive to vite
. (As shown below)
Before starting to read the source code, we need to understand the calling timing and function of each hook function in vite
and rollup
.
field | illustrate | belong |
---|---|---|
name | plugin name | vite and rollup |
handleHotUpdate | Perform custom HMR (Hot Module Replacement) update processing | vite Exclusive |
config | Called before parsing the Vite configuration. The configuration can be customized and will be merged with the basic configuration of vite | vite Exclusive |
configResolved | Called after parsing the Vite configuration. You can read the configuration of vite and perform some operations | vite Exclusive |
configureServer | is a hook for configuring the development server. The most common use case is adding custom middleware to an internal connect application. | vite Exclusive |
transformIndexHtml | Special hook for converting index.html . | vite exclusive |
options | Before collecting the rollup configuration, vite (local) is called when the service starts, which can be merged with the rollup configuration | vite and rollup |
buildStart | In the rollup build, vite (local) is called when the service starts, in this function you can access the configuration of rollup | vite and rollup |
resolveId | Called when parsing a module, can return a special resolveId to specify a import statement to load a specific module | vite and rollup shared |
load | Called when parsing a module, can return a code block to specify a import statement to load a specific module | vite and rollup shared |
transform | Called when parsing the module, converts the source code, and outputs the converted result, similar to webpack of loader | vite and rollup shared |
buildEnd | vite before rollup output file to directory before local service shuts down | vite and rollup shared |
closeBundle | Called before vite local service shutdown, rollup output file to directory | vite and rollup |
After understanding all the hook functions of vite
and rollup
, we only need to follow the calling order to see what the vite:vue
plugin does during the call of each hook function.
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']
}
}
}
The 06223118a622b4 in the vite:vue
config
does relatively simple things. The first is to replace the two global variables __VUE_OPTIONS_API__
and __VUE_PROD_DEVTOOLS__
. Then set up the dependencies to force externalization for SSR.
configResolved
After the config
hook is executed, the next call is the configResolved
hook. (as follows)
configResolved(config) {
options = {
...options,
root: config.root,
sourceMap: config.command === 'build' ? !!config.build.sourcemap : true,
isProduction: config.isProduction
}
},
The vite:vue
hook in configResolved
reads the root
and isProduction
configurations, and stores them in the options
property inside the plugin for use by subsequent hook functions.
Then, determine whether the current command is build
. If it is to build a production product, read the sourcemap
configuration to determine whether to generate sourceMap
, and the local development service will always generate sourceMap
for debugging.
configureServer
In the configureServer
hook, the vite:vue
plugin just stores server
in the internal options
option and does nothing else. (as follows)
configureServer(server) {
options.devServer = server;
}
buildStart
In the buildStart
hook function, a compiler
is created for subsequent compilation of the vue
file. (as follows)
buildStart() {
options.compiler = options.compiler || resolveCompiler(options.root)
}
There are many practical methods built in the complier
, and these methods are responsible for deciphering the vue
file according to the rules.
load
After running the above hooks, the vite
local development service has been started.
We open the address of the local service, and after we initiate a request for the resource, we will enter the next hook function. (As shown below)
After opening the service, the first thing to enter is the load
hook. The main job of the load
hook is to return the vue
file with the same name that is parsed separately.
vite
internally parses part of the file content to another file, and then parses the file by appending the ?vue
's query
parameter to the file load path. For example, parsing template
(template), script
(js script), css
( style
module)... (as shown below)
And these modules ( template
, script
, style
) are all parsed from complier.parse
(as follows)
const { descriptor, errors } = compiler.parse(source, {
filename,
sourceMap
});
transform
After load
returns the corresponding code fragment, enter the transform
hook.
transform
mainly does three things:
- Translate
vue
file - Translate
template
template parsed withvue
file - Translate
style
style parsed invue
file
Simple understanding, this hook corresponds towebpack
ofloader
.
Here, we take a TodoList.vue
file as an example to expand the file translation work done by transform
.
The following is the source file of TodoList.vue
, which makes a TodoList
for addition, deletion, modification and inspection. You can also learn its detailed functions through the second article of - Vite + Vue3 first experience - Vue3 article .
<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>
Entering the transformMain
function, you can find that transformMain
mainly does several things internally:
- Deconstructs
vue
,script
,template
of thestyle
file - Parse the
script
code in thevue
file; - Parse the
template
code in thevue
file; - Parse the
style
code in thevue
file; - Parse the
custom module code in the
vue
file; - Logic to handle HMR (hot reload of modules);
- Logic to handle
ssr
; - Logic to handle
sourcemap
; - Process the conversion of
ts
and convert it toes
;
Next, we'll dig into the source code and analyze each task in depth.
Deconstructed script
, template
, style
vue
file contains three parts: script
, template
, and style
. Inside createDescriptor
, the transformMain
in compiler
is used to separate the three large blocks as a single parsing object. (As shown below)
In compiler
, the parse
method will be used first to parse the source code source
into a AST
tree. (As shown below)
As can be seen in the figure below, the parsed AST
tree has three modules, mainly script
, template
, and style
.
The next step is to record the attributes and code lines of each module, such as the style
tag, which records the information of lang: less
for later analysis.
Parse Template
vue
in the template
file writes a lot of syntactic sugar for vue
, such as the following line
<Input v-model:value="todoText" @keyup.enter="onTodoInputEnter" class="todo-input" placeholder="请输入待办项" />
Like this syntax, the browser cannot recognize and bind the event to the internal function of vue
, so vite
does an internal conversion for such tags, converts them into executable functions, and then executes the function generation through the browser. A set of virtual DOM, and finally the rendering engine inside
vue
renders the virtual DOM into a
real DOM.
Now we can look at the translation process of vite
grammar inside template
. The inside of vite
is implemented by the genTemplateCode
function.
Inside genTemplateCode
, the first step is to parse the template
template syntax into a AST
syntax tree. (As shown below)
Then, the corresponding AST nodes are converted through different translation functions.
Let's take the Input
node as an example to briefly explain the translation process.
Repeat this step until the entire template
tree is parsed.
Parse script tags
Next, let's take a look at the parsing part of the script
tag, the corresponding internal function is genScriptCode
What this function does is mainly the following things:
- Parse the variables defined in the
script
tag; - Parse the import
import
defined in thescript
tag, and it will be converted into a relative path import later; - Compile the
script
tag into a code fragment that exports the object encapsulated by_defineComponent
(component) with built-insetup
hook function.
We use a diagram to illustrate the above three steps. (As shown below)
Parse style tags
style
tag parsing is relatively simple, just parsing the code into a import
statement (below)
import "/Users/Macxdouble/Desktop/ttt/vite-try/src/components/TodoList.vue?vue&type=style&index=0&scoped=true&lang.less"
Then, according to the type
and lang
in the query
parameter in the request, the transformStyle
function in the vite:vue
plugin's load
hook (the last parsed hook) continues to process the style file compilation. I will not expand this part, and interested students can read the code by themselves.
Compile ts
to es
After the codes of script
, template
, and style
are all parsed, the following processing is also done:
- Parse the
custom module code in the
vue
file; - Logic to handle HMR (hot reload of modules);
- Logic to handle
ssr
; - Logic to handle
sourcemap
; - Process the conversion of
ts
and convert it toes
;
Due to space reasons, here is only a brief introduction to the conversion from ts
to es
. This step is mainly to complete the conversion from ts
to es
internally through esbuild
. We can see how fast this tool is. (As shown below)
output code
In ts
also translated es
after, vite:vue
be converted into es
of script
, template
, style
codes are combined, then transform
output as a final output es
module, as the page is js
file is loaded. (As shown below)
handleHotUpdate
Finally, let's take a look at the handling of hot reloading of file modules, which is the handleHotUpdate
hook.
After we start the project, we add a line of code to setup
in the App.vue
file.
console.log('Test handleHotUpdate');
After the code is added and saved, the changes are captured by vite
inside watcher
, and then the handleHotUpdate
hook is triggered to pass in the modified file.
vite:vue
will use the compiler.parse
function to parse the App.vue
file, and parse the script
, template
, style
tags. (that is, the compilation steps parsed above) (as shown below).
Then, the handleHotUpdate
function internally detects the changed content and adds the changed part to the affectedModules
array. (As shown below)
Then, handleHotUpdate
will return affectedModules
to vite
internal processing.
Finally, vite
will internally determine whether the current change file needs to reload the page. If it does not need to be reloaded, it will send a update
message to the client's ws
to notify the client to reload the corresponding resource and execute it. (As shown below)
Well, in this way, we are also clear about the content of module hot reloading.
summary
The analysis of @vitejs/plugin-vue
in this issue ends here.
It can be seen that vite
is internally combined with rollup
preset multiple life cycle hooks of the plugin, which are called at various stages of compilation to achieve the combined effect of loader
+ webpack
of plugin
.
And vite/rollup
directly uses plugin
to replace the webpack
+ plugin
function of loader
. It may also be to simplify the concept, integrate functions, make the work of the plug-in easier, and allow plug-in developers in the community to better participate in contributions.
The speed of vite
is not only because the native es
module is not compiled at runtime, but also uses a light and fast compilation library such as esbuild
to compile ts
at runtime, which makes the entire local development very light.
In the next chapter, we will do a practical exercise for the vite
plugin: implement a vite
plugin, whose function is to load the local md
file by specifying a tag.
one last thing
If you have seen this, I hope you will give a like and go~
Your likes are the greatest encouragement to the author, and can also allow more people to see this article!
If you think this article is helpful to you, please help to light up star
on github and encourage it!
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。