6
头图

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:

This article mainly interprets the source code ontology of vite . In previous articles, we learned that:

  • vite provides a development server through the connect library during local development, and implements multiple development server configurations through the middleware mechanism, without the help of the webpack packaging tool, plus the use of rollup (part of the function) to schedule the internal plugin 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 to rollup 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)

image

Before starting to read the source code, we need to understand the calling timing and function of each hook function in vite and rollup .

fieldillustratebelong
nameplugin namevite and rollup
handleHotUpdatePerform custom HMR (Hot Module Replacement) update processingvite Exclusive
configCalled before parsing the Vite configuration. The configuration can be customized and will be merged with the basic configuration of vitevite Exclusive
configResolvedCalled after parsing the Vite configuration. You can read the configuration of vite and perform some operationsvite Exclusive
configureServeris a hook for configuring the development server. The most common use case is adding custom middleware to an internal connect application.vite Exclusive
transformIndexHtmlSpecial hook for converting index.html .vite exclusive
optionsBefore collecting the rollup configuration, vite (local) is called when the service starts, which can be merged with the rollup configurationvite and rollup
buildStartIn the rollup build, vite (local) is called when the service starts, in this function you can access the configuration of rollupvite and rollup
resolveIdCalled when parsing a module, can return a special resolveId to specify a import statement to load a specific modulevite and rollup shared
loadCalled when parsing a module, can return a code block to specify a import statement to load a specific modulevite and rollup shared
transformCalled when parsing the module, converts the source code, and outputs the converted result, similar to webpack of loadervite and rollup shared
buildEndvite before rollup output file to directory before local service shuts downvite and rollup shared
closeBundleCalled before vite local service shutdown, rollup output file to directoryvite 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)
}

image

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)

image

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)

image

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 with vue file
  • Translate style style parsed in vue file
Simple understanding, this hook corresponds to webpack of loader .

image

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 the style file
  • Parse the script code in the vue file;
  • Parse the template code in the vue file;
  • Parse the style code in the vue 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 to es ;

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)

image

In compiler , the parse method will be used first to parse the source code source into a AST tree. (As shown below)

image

As can be seen in the figure below, the parsed AST tree has three modules, mainly script , template , and style .

image

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)

image

Then, the corresponding AST nodes are converted through different translation functions.

image

Let's take the Input node as an example to briefly explain the translation process.

image

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:

  1. Parse the variables defined in the script tag;
  2. Parse the import import defined in the script tag, and it will be converted into a relative path import later;
  3. Compile the script tag into a code fragment that exports the object encapsulated by _defineComponent (component) with built-in setup hook function.

We use a diagram to illustrate the above three steps. (As shown below)

image

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 to es ;

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)

image

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)

image

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).

image

Then, the handleHotUpdate function internally detects the changed content and adds the changed part to the affectedModules array. (As shown below)

image

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)

image

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!


晒兜斯
1.8k 声望535 粉丝