乐趣区

关于vite:Vite-源码解读系列图文结合-插件篇

哈喽,很快乐你能点开这篇博客,本博客是针对 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 专属。(如下图)

在开始浏览源码之前,咱们须要先理解一下 viterollup 中每一个钩子函数的调用机会和作用。

字段 阐明 所属
name 插件名称 viterollup 共享
handleHotUpdate 执行自定义 HMR(模块热替换)更新解决 vite 独享
config 在解析 Vite 配置前调用。能够自定义配置,会与 vite 根底配置进行合并 vite 独享
configResolved 在解析 Vite 配置后调用。能够读取 vite 的配置,进行一些操作 vite 独享
configureServer 是用于配置开发服务器的钩子。最常见的用例是在外部 connect 应用程序中增加自定义中间件。 vite 独享
transformIndexHtml 转换 index.html 的专用钩子。 vite 独享
options 在收集 rollup 配置前,vite(本地)服务启动时调用,能够和 rollup 配置进行合并 viterollup 共享
buildStart rollup 构建中,vite(本地)服务启动时调用,在这个函数中能够拜访 rollup 的配置 viterollup 共享
resolveId 在解析模块时调用,能够返回一个非凡的 resolveId 来指定某个 import 语句加载特定的模块 viterollup 共享
load 在解析模块时调用,能够返回代码块来指定某个 import 语句加载特定的模块 viterollup 共享
transform 在解析模块时调用,将源代码进行转换,输入转换后的后果,相似于 webpackloader viterollup 共享
buildEnd vite 本地服务敞开前,rollup 输入文件到目录前调用 viterollup 共享
closeBundle vite 本地服务敞开前,rollup 输入文件到目录前调用 viterollup 共享

在理解了 viterollup 的所有钩子函数后,咱们只须要依照调用程序,来看看 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 钩子,读取了 rootisProduction 配置,存储在插件外部的 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 外部会将局部文件内容解析到另一个文件,而后通过在文件加载门路前面加上 ?vuequery 参数来解析该文件。比方解析 template(模板)、script(js 脚本)、cssstyle 模块)…(如下图)

而这几个模块(templatescriptstyle)都是由 complier.parse 解析而来(如下)

const {descriptor, errors} = compiler.parse(source, {
  filename,
  sourceMap
});

transform

load 返回了对应的代码片段后,进入到 transform 钩子。

transform 次要做的事件有三件:

  • 转译 vue 文件
  • 转译以 vue 文件解析的 template 模板
  • 转译以 vue 文件解析的 style 款式

简略了解,这个钩子对应的就是 webpackloader

这里,咱们以一个 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 文件的 scripttemplatestyle
  • 解析 vue 文件中的 script 代码;
  • 解析 vue 文件中的 template 代码;
  • 解析 vue 文件中的 style 代码;
  • 解析 vue 文件中的 自定义模块 代码;
  • 解决 HMR(模块热重载)的逻辑;
  • 解决 ssr 的逻辑;
  • 解决 sourcemap 的逻辑;
  • 解决 ts 的转换,转成成 es

接下来,咱们将深刻源码,将每一项工作深刻解析。

解构 scripttemplatestyle

vue 文件中蕴含 scripttemplatestyle 三大部分,transformMain 外部先通过 createDescriptor 中的 compiler 将这三大块拆散解析进去,作为一个大对象,而后不便前面的解析。(如下图)

compiler 中,会先应用 parse 办法,将源码 source 解析成 AST 树。(如下图)

在下图中能够看出,解析后的 AST 树有三个模块,次要就是 scripttemplatestyle

接下来,就是将各个模块的属性、代码行数记录起来,比方 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

这个函数所做的事件次要是上面几件事件:

  1. 解析 script 标签中定义的变量;
  2. 解析 script 标签中定义的引入 import,前面将会转换成相对路径引入;
  3. 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 参数中的 typelang,由 vite:vue 插件的 load 钩子(上一个解析的钩子)中的 transformStyle 函数来持续解决款式文件的编译。这部分我就不做开展了,感兴趣的同学能够自行浏览代码。

编译 tses

scripttemplatestyle 局部的代码都解析结束后,接下来还做了上面几个解决:

  • 解析 vue 文件中的 自定义模块 代码;
  • 解决 HMR(模块热重载)的逻辑;
  • 解决 ssr 的逻辑;
  • 解决 sourcemap 的逻辑;
  • 解决 ts 的转换,转成成 es

因为篇幅起因,这里只对 tses 的转换做个简略介绍,这一步次要是在外部通过 esbuild 实现了 tses 的转换,咱们能够看到这个工具有多快。(如下图)

输入代码

ts 也转译成 es 后,vite:vue 将转换成了 esscripttemplatestyle 代码合并在一起,而后通过 transform 输入,最终输入为一个 es 模块,被页面作为 js 文件加载。(如下图)

handleHotUpdate

最初,咱们来看看对于文件模块热重载的解决,也就是 handleHotUpdate 钩子。

咱们在启动我的项目后,在 App.vue 文件的 setup 中退出一行代码。

console.log('Test handleHotUpdate');

在代码退出并且保留后,被 vite 外部的 watcher 捕捉到变更,而后触发了 handleHotUpdate 钩子,将批改的文件传入。

vite:vue 外部会应用 compiler.parse 函数对 App.vue 文件进行解析,将 scripttemplatestyle 标签解析进去。(也就是下面解析的编译步骤)(如下图)。

而后,handleHotUpdate 函数外部会检测产生变更的内容,将变更的局部增加到 affectedModules 数组中。(如下图)

而后,handleHotUpdateaffectedModules 返回,交由 vite 外部解决。

最初,vite 外部会判断以后变更文件是否须要从新加载页面,如果不须要从新加载的话,则会发送一个 update 音讯给客户端的 ws,告诉客户端从新加载对应的资源并执行。(如下图)

好了,这样一来,模块热重载的内容咱们也分明了。

小结

本期对 @vitejs/plugin-vue 的解析就到这里完结了。

能够看出,vite 外部联合了 rollup 预设了插件的多个生命周期钩子,在编译的各个阶段进行调用,从而达到 webpackloader + plugin 的组合成果。

vite/rollup 间接应用 plugin 就代替了 webpackloader + plugin 性能,可能也是为了简化概念,整合性能,让插件的工作更简略,让社区的插件开发者也能更好的参加奉献。

vite 的快不仅仅是因为运行时不编译原生 es 模块,还有在运行时还利用了 esbuild 这类轻而快的编译库来编译 ts,从而使得整个本地开发时变得十分地轻快。

下一章,咱们将对 vite 插件进行实战练习:实现一个 vite 插件,它的性能是通过指定标签就能加载本地 md 文件。

最初一件事

如果您曾经看到这里了,心愿您还是点个赞再走吧~

您的点赞是对作者的最大激励,也能够让更多人看到本篇文章!

如果感觉本文对您有帮忙,请帮忙在 github 上点亮 star 激励一下吧!

退出移动版