阅读视图

发现新文章,点击刷新页面。

Vue 项目开发全攻略:从搭建到上线的技术盛宴

一、项目搭建

在开始开发 Vue 项目时,首先要进行项目搭建。这里我们选用 vite 来负责工程化,它能极大地提升项目构建和开发的效率。

使用 vite 搭建 Vue 项目非常简单,只需在命令行中输入 npm init vite 这一指令,就能快速初始化一个全新的 Vue 项目框架。vite 是新一代的前端构建工具,它基于 ES 模块导入,在开发环境下无需打包操作,可直接启动开发服务器,实现快速冷启动。在生产环境中,vite 又能利用 Rollup 进行高效的打包,为项目提供优化后的代码输出。通过这种方式,我们能轻松搭建起一个基础的 Vue 项目架构,为后续的开发工作奠定坚实的基础。

二、核心技术栈

2.1 Vue 核心语法

Vue 的核心语法是构建项目的基石 ,在本项目中,响应式原理通过ref和reactive两个函数来实现。例如,当需要创建一个简单的响应式数据时,使用ref函数:

import { ref } from 'vue';
const count = ref(0);

若要处理复杂的对象或数组,reactive则更为合适:

import { reactive } from 'vue';
const userInfo = reactive({
  name: 'John',
  age: 30
});

组件化开发让代码的可维护性和复用性大大提高。在项目里,我们将页面拆分成多个组件,每个组件都有独立的逻辑和视图。以一个按钮组件为例,其template部分定义了按钮的外观:

<template>
  <button>{{ buttonText }}</button>
</template>

script部分则负责组件的逻辑,如:

<script setup>
import { ref } from 'vue';
const buttonText = ref('点击我');
</script>

指令方面,v - if、v - show用于控制元素的显示与隐藏。v - for则常用于列表的渲染,假设我们有一个用户列表:

const userList = reactive([
  { id: 1, name: 'Alice' },
  { id: 2, name: 'Bob' }
]);

在模板中使用v - for进行渲染:

<template>
  <ul>
    <li v - for="user in userList" :key="user.id">{{ user.name }}</li>
  </ul>
</template>

通过这些核心语法的运用,我们能够构建出灵活且高效的 Vue 应用程序。

2.2 Vue - Router 路由

在 Vue - Router 的配置中,多级路由的设置让页面结构更加清晰。例如,我们有一个主页面Home,其下包含About和Contact两个子页面。在路由配置文件中可以这样定义:

import { createRouter, createWebHistory } from 'vue - router';
import Home from './views/Home.vue';
import About from './views/About.vue';
import Contact from './views/Contact.vue';
const router = createRouter({
  history: createWebHistory(),
  routes: [
    {
      path: '/home',
      component: Home,
      children: [
        { path: 'about', component: About },
        { path: 'contact', component: Contact }
      ]
    }
  ]
});

路由懒加载是提升性能的关键。我们使用import()函数来实现,当访问特定路由时,对应的组件才会被加载。比如:

const About = () => import('./views/About.vue');

添加路由守卫则能有效控制页面的访问权限。以登录验证为例,在全局前置守卫中可以这样实现:

router.beforeEach((to, from, next) => {
  const isLoggedIn = localStorage.getItem('token');
  if (to.meta.requiresAuth &&!isLoggedIn) {
    next('/login');
  } else {
    next();
  }
});

在需要验证的路由中,设置meta字段:

{
  path: '/dashboard',
  component: Dashboard,
  meta: { requiresAuth: true }
}

通过这样的配置,确保了只有登录用户才能访问受保护的页面。

2.3 Pinia 状态管理

Pinia 在项目中负责状态的管理,极大地简化了状态共享的过程。首先,安装 Pinia 并在main.js中进行配置:

import { createApp } from 'vue';
import { createPinia } from 'pinia';
import App from './App.vue';
const app = createApp(App);
const pinia = createPinia();
app.use(pinia);
app.mount('#app');

接着,定义一个store来管理用户相关的状态。例如:

import { defineStore } from 'pinia';
export const useUserStore = defineStore('user', {
  state: () => ({
    userInfo: null,
    isLoggedIn: false
  }),
  actions: {
    login(user) {
      this.userInfo = user;
      this.isLoggedIn = true;
      localStorage.setItem('token', 'valid - token');
    },
    logout() {
      this.userInfo = null;
      this.isLoggedIn = false;
      localStorage.removeItem('token');
    }
  }
});

在组件中使用该store时,只需引入并调用相应的方法:

import { useUserStore } from '@/stores/user';
const userStore = useUserStore();
userStore.login({ name: 'John', age: 30 });

Pinia 的优势在于其简洁的 API 和良好的模块化设计,使得状态管理变得轻松且高效。

2.4 Element3 UI 组件库

Element3 是一个功能强大的 UI 组件库,为项目提供了丰富的组件。在使用时,我们采用按需加载的方式来优化性能。首先,安装相关的插件:

npm install -D unplugin - vue - components unplugin - auto - import

然后,在vue.config.js中进行配置:

const AutoImport = require('unplugin - auto - import/webpack');
const Components = require('unplugin - vue - components/webpack');
const { ElementPlusResolver } = require('unplugin - vue - components/resolvers');
module.exports = {
  configureWebpack: {
    plugins: [
      AutoImport({
        resolvers: [ElementPlusResolver()]
      }),
      Components({
        resolvers: [ElementPlusResolver()]
      })
    ]
  }
};

这样,在组件中使用 Element3 组件时,如按钮组件,只需直接引入:

<template>
  <el - button type="primary">点击我</el - button>
</template>

Element3 组件以el -开头,通过按需加载,我们避免了引入不必要的组件,有效减少了项目的打包体积,提升了页面的加载速度。

2.5 Stylus CSS 预处理器

Stylus 作为 CSS 预处理器,为项目带来了诸多便利。它允许我们使用变量、混入、嵌套等功能,使 CSS 代码更加简洁和易于维护。例如,定义一个颜色变量:

$primaryColor = #1890ff

在样式中使用该变量:

button {
  background - color: $primaryColor;
  color: white;
}

混入功能可以复用一些常用的样式,如圆角样式:

border - radius() {
  border - radius: 5px;
}
.box {
  +border - radius();
}

样式的嵌套则让代码结构更加清晰,以导航栏为例:

.nav {
  display: flex;
  justify - content: space - between;
  li {
    list - style: none;
    a {
      text - decoration: none;
      color: #333;
      &:hover {
        color: $primaryColor;
      }
    }
  }
}

通过 Stylus 的这些特性,我们能够高效地编写和管理项目的样式。

2.6 Axios AJAX 请求封装库

Axios 用于与后端进行数据交互,我们对其进行了封装,以提高代码的复用性和可维护性。首先,创建一个api.js文件,设置基础 URL 和请求拦截器:

import axios from 'axios';
const service = axios.create({
  baseURL: 'https://api.example.com',
  timeout: 5000
});
service.interceptors.request.use(
  config => {
    const token = localStorage.getItem('token');
    if (token) {
      config.headers['Authorization'] = `Bearer ${token}`;
    }
    return config;
  },
  error => {
    return Promise.reject(error);
  }
);

然后,封装常用的请求方法,如get和post:

export const get = (url, params = {}) => {
  return service.get(url, { params });
};
export const post = (url, data = {}) => {
  return service.post(url, data);
};

在组件中使用时,只需引入相应的方法:

import { get } from '@/api';
get('/user/info').then(response => {
  console.log(response.data);
}).catch(error => {
  console.error(error);
});

通过这样的封装,我们能够方便地进行各种 AJAX 请求,与后端进行稳定的数据交互。

三、项目亮点展示

3.1 ES6 风格的全面应用

在整个项目中,我们全面采用了 ES6 风格的代码编写方式,这使得代码在简洁性、易读性和易维护性上都有了显著提升。例如,在定义路由时,使用对象解构的方式简化了代码结构。原本需要完整书写routes: routes,现在直接写成routes即可。这种简洁的写法不仅减少了冗余代码,还让代码逻辑更加清晰,开发者能够一眼看清路由的配置关系。

在函数定义方面,ES6 的箭头函数也被广泛应用。比如在处理一些简单的回调函数时,箭头函数的使用使得代码更加紧凑。例如,在数组的map方法中,使用箭头函数可以快速对数组中的每个元素进行处理:

const numbers = [1, 2, 3, 4];
const squaredNumbers = numbers.map((number) => number * number);

相比于传统的函数定义方式,箭头函数的语法更加简洁,同时也避免了this指向的问题,让代码的维护更加轻松。

3.2 良好的注释与代码可读性

良好的注释是提高代码可读性的关键。在项目中,我们在关键的代码块、函数定义以及复杂的逻辑处都添加了详细的注释。例如,在路由守卫的代码中,我们添加了注释来说明其作用和逻辑:

// 全局前置守卫,用于验证用户是否登录
router.beforeEach((to, from, next) => {
  const isLoggedIn = localStorage.getItem('token');
  if (to.meta.requiresAuth &&!isLoggedIn) {
    // 如果目标路由需要登录且用户未登录,则重定向到登录页面
    next('/login');
  } else {
    // 否则,允许用户访问目标路由
    next();
  }
});

这样的注释使得其他开发者在阅读代码时,能够快速理解代码的意图和功能,降低了代码的理解成本。同时,对于一些自定义的函数和组件,我们也添加了注释来解释其输入参数、返回值以及功能用途,确保代码的每一部分都清晰易懂。

3.3 规范的 Git 提交记录和习惯

在项目开发过程中,我们始终保持着规范的 Git 提交记录和良好的提交习惯。每次提交都有明确的提交信息,描述本次提交所做的修改内容。例如,“修复登录页面的验证码验证问题”“优化首页的加载速度” 等。这样的提交信息使得项目的版本历史清晰可追溯,团队成员能够快速了解每个提交的目的和影响范围。

同时,我们遵循一定的分支管理策略,如使用master分支作为主分支,用于发布稳定版本;develop分支用于开发新功能,通过创建特性分支进行功能开发,开发完成后再合并到develop分支。这种规范的分支管理和提交习惯,不仅有助于团队协作开发,还能在出现问题时快速定位和解决,提高了项目的开发效率和质量。

四、实战技巧与注意事项

4.1 表单组件的使用

在项目中,表单组件的使用非常频繁。我们使用 :model来收集表单数据,这是一种双向数据绑定的方式,能够实时同步表单输入与数据模型。例如:

<el - form :model="formData">
  <el - form - item label="用户名">
    <el - input v - model="formData.username"></el - input>
  </el - form - item>
  <el - form - item label="密码">
    <el - input type="password" v - model="formData.password"></el - input>
  </el - form - item>
</el - form>

在上述代码中,formData是一个包含username和password字段的对象,通过v - model指令,表单输入框的值会实时更新到formData中,反之亦然。

通过ref可以获取表单实例,这在需要手动操作表单时非常有用。在模板中,使用ref标记表单组件:

<el - form ref="formRef" :model="formData">
  <!-- 表单内容 -->
</el - form>

在script部分,通过ref获取表单实例:

import { ref } from 'vue';
const formRef = ref(null);

当表单挂载后,formRef就会获取到实际的表单实例。此时,我们可以调用表单实例的方法,如validate方法进行表单校验:

formRef.value.validate((valid) => {
  if (valid) {
    // 校验通过,提交表单或执行其他操作
    console.log('表单校验通过');
  } else {
    // 校验失败,提示用户错误信息
    console.log('表单校验失败');
  }
});

表单的校验规则通过rules属性来定义。例如,对用户名和密码设置必填校验:

const formData = reactive({
  username: '',
  password: ''
});
const rules = {
  username: [
    { required: true, message: '用户名不能为空', trigger: 'blur' }
  ],
  password: [
    { required: true, message: '密码不能为空', trigger: 'blur' }
  ]
};

在表单组件中,将rules属性绑定到对应的form - item上:

<el - form :model="formData" :rules="rules">
  <el - form - item label="用户名" prop="username">
    <el - input v - model="formData.username"></el - input>
  </el - form - item>
  <el - form - item label="密码" prop="password">
    <el - input type="password" v - model="formData.password"></el - input>
  </el - form - item>
</el - form>

这样,当用户输入完成并离开输入框(blur事件触发)时,表单会根据设置的规则进行校验,并显示相应的错误提示信息。

4.2 布局组件的应用

布局组件在构建页面结构时起着关键作用。我们常用的布局组件包括Elcontainer、Elheader、ElAside、ElMain等。

以一个常见的后台管理页面布局为例,使用Elcontainer作为容器,将页面分为头部、侧边栏和主体内容区域:

<el - container>
  <el - header>
    <!-- 头部内容,如导航栏 -->
    <h1>后台管理系统</h1>
  </el - header>
  <el - container>
    <el - aside width="200px">
      <!-- 侧边栏菜单 -->
      <el - menu :default - active="activeIndex" class="el - menu - vertical - demo" @select="handleSelect">
        <el - menu - item index="1">菜单1</el - menu - item>
        <el - menu - item index="2">菜单2</el - menu - item>
      </el - menu>
    </el - aside>
    <el - main>
      <!-- 主体内容区域 -->
      <p>这里是主要内容</p>
    </el - main>
  </el - container>
</el - container>

在上述代码中,Elheader定义了页面的头部,通常包含导航栏等信息。Elaside作为侧边栏,设置了固定的宽度为200px,并在其中放置了菜单组件。Elmain则用于展示主体内容。

对于页面内的布局,ElRow和ElCol经常被用于实现栅格化布局。例如,将一行分为两列,左列占 8 格,右列占 4 格:

<el - row>
  <el - col :span="8">
    <p>左列内容</p>
  </el - col>
  <el - col :span="4">
    <p>右列内容</p>
  </el - col>
</el - row>

通过span属性可以灵活调整每列所占的比例,从而实现各种复杂的页面布局。

4.3 性能优化策略

在项目开发过程中,性能优化至关重要。我们采用了多种策略来提升项目的性能。

按需加载是其中一个重要的策略。在引入 Vue 组件库 Element3 时,我们通过配置实现了按需加载,避免一次性加载所有组件,从而减少了初始加载时间。在路由方面,也采用了懒加载技术,只有当用户访问特定路由时,对应的组件才会被加载。例如:

const Home = () => import('./views/Home.vue');
const About = () => import('./views/About.vue');
const router = createRouter({
  history: createWebHistory(),
  routes: [
    { path: '/home', component: Home },
    { path: '/about', component: About }
  ]
});

这样,在应用启动时,只有必要的路由组件会被加载,大大提高了应用的启动速度。

此外,还对静态资源进行了优化。通过使用 Webpack 插件对 JavaScript 和 CSS 文件进行压缩,减少了文件体积,加快了文件的传输速度。同时,合理利用缓存机制,对于不经常变化的静态资源设置较长的缓存时间,避免用户每次访问都重新下载。

在图片处理方面,对图片进行了压缩和格式优化,选择合适的图片格式(如 WebP 格式,在保证图片质量的前提下,文件体积更小),并根据不同的设备屏幕尺寸提供相应分辨率的图片,避免加载过大的图片资源,从而提升页面的加载速度和用户体验。

五、总结与展望

通过本次项目的开发,我收获了许多宝贵的经验。从项目搭建到技术栈的运用,再到项目亮点的打造和实战技巧的积累,每一个环节都让我对 Vue 开发有了更深入的理解。在项目中,我学会了如何高效地运用各种工具和技术,解决实际开发中遇到的问题。同时,也深刻体会到团队协作、代码规范以及性能优化的重要性。

展望未来,我希望能够进一步优化项目。在性能方面,持续探索更有效的优化策略,如进一步优化图片加载、减少 HTTP 请求等,以提升用户体验。在功能上,根据用户反馈和业务需求,不断添加新的功能模块,使项目更加完善。同时,也会关注 Vue 技术的发展动态,及时引入新的特性和最佳实践,保持项目的技术先进性。

Vite源码学习(八)——DEV流程中的核心类(上)

前言

在上一篇文章中,我们理清楚了VITE中的中间件,在这篇文章中,我们开始介绍DEV流程中VITE实现Rollup等价API中实现的核心类。

VITE的核心类主要包括几个:

  • PluginContainer,用来管理插件集合,模拟Rollup等价的生命周期。
  • Environment,用来管理当前的Env上下文,在DEV阶段,我们主要需要关注DevEnvironment类。
  • ModuleGraph,用来管理资源的引用关系,在DEV阶段,我们主要需要关注EnvironmentModuleGraph类。

除此之外,在这篇文章中,我们还会把上一篇文章没有做详细分析的transformRequest方法详细分析。

本文的内容可能是整个系列的文章中最复杂的,大家要有心理准备。

由于VITE可能经历过代码的重构或者API的调整,源码里面存在一些兼容老的API,但是在未来可能会删除的内容,对于这些内容我们就直接选择跳过。

好了,废话少说,我们就开始进入这篇文章的正题吧。

从Server的入口出发

因为很多配置都需要依赖主入口文件解析到的配置,所以我们还是像之前的文章那样,从VITE的DevServer创建的入口开始聊。

resolveConfig

首先,还是之前我们已经看过,但是可能分析的并不那么详细的resolveConfig方法: image.png 这次,我们需要关注的部分是这些位置:

首先是获取到用户传递的插件列表。 image.png 然后是初始化Environment的配置上下文: image.png 因为我们现在分析的是DEV流程,所以我们重点关注的是resolveDevEnvironmentOptionsimage.png 本系列文章不会分析SSR相关的知识点(主要是我在实际项目中没有怎么用到,就不在这儿胡说八道,误人子弟了,哈哈哈,各位读者请见谅),所以我们只看client相关的处理逻辑即可: image.png 使用工厂模式初始化DevEnvironment的实例: image.png 关于这个createEnvironment方法,我们目前就只需要看到这儿,因为一会儿才会用到它,我们现在只需知道它创建的是DevEnvironment的实例工厂方法即可。

接下来,VITE把我们用户的Plugin跟自己的内置Plugin进行合并: image.pngimage.png 把处理好的插件列表交给需要暴露给外部的变量: image.png 在这小节中,我们主要是为了要搞明白DevEnvironmentPlugin列表的来源是什么,因为一会儿这些变量需要传递给对应的核心类进行管理。

关键变量初始化

在上一小节中我们搞懂了那些关键的参数是怎么来的,现在我们看一下这些关键变量是怎么初始化的。

这个config变量上保存着我们之前得到的关键信息,比如插件列表。 image.png 现在把这个config变量传递给之前我们关心的创建DevEnvironment的工厂方法。 image.png 至此,我们明白了创建DevEnvironment实例的整个过程,一会儿我们再关注DevEnvironment这个类的实现细节。

然后是调用DevEnvironmentinit方法,这个init方法里面主要操作就是初始化插件容器,在下一小节就会讲到,大家如果不懂可以一会儿再来查阅。 image.png

最后,我们着重看transformMiddleware的绑定过程。

首先,ViteDevServer上绑定着之前初始化的DevEnvironment的实例: image.pngimage.pngtransformMiddleware中间件在绑定的时候,传入了ViteDevServer的实例: image.png

一会儿我们在讲述上一篇文章中没有讲到的transformRequest的过程的时候会讲到这个ViteDevServer实例变量传递的作用。

到这个位置,VITE在DEV过程中主线流程(为什么是主线流程,一会儿会给大家解释的)的关键变量就已经初始化完成了。

DevEnvironment

这个类有相对长的集成链,代码看起来不是那么舒服。

DevEnvironment->BaseEnvironment->PartialEnvironment

所以我们得事先关注一下它的祖先类。

PartialEnvironment

关于PartialEnvironment类,我们只需要关注一个点,即设置配置和获取配置,这个配置就是上一节中我们通过resolveConfig拿到的那个值。

image.pngimage.png 其它的内容主要都是工具方法,我们无需额外关注。

BaseEnvironment

BaseEnvironment相对来说就更简单了,它主要就是对外暴露插件列表,所以我们就只需要关注plugins这个getter就好了。 image.png

DevEnvironment

在搞清楚它的祖先类之后,我们就可以正式关注它了。

DevEnvironment这个类中,我们需要首先看的就是它的init方法,在这个方法里创建了PluginContainer,就是之前我们在关键变量初始化小节的时候没有展开的那个方法。

image.png 在之前我们阐述PartialEnvironment的时候就已经知道了getTopLevelConfig这个方法获取到的就是我们在先前的小节中阐述的resolveConfig获取到的VITE将自己的默认配置和用户的配置合并之后的配置,所以这儿拿到的插件集合就是我们之前所阐述的: image.png 好了,拿到了插件列表之后,我们就可以把插件传递给PluginContainer了。 image.png 这小节我们重点先聊DevEnvironment本尊,后面的小节我们重点阐述PluginContainer

DevEnvironment同时也定义了获取PluginContainergetter,这个getter会广泛用到的。 image.png 最后,再看几个关键方法: image.pngimage.png

对于目前的我们来说,就只关注这么多,后面的文章,我们还会继续分析这篇文章没有涵盖到的细节内容的。

transformRequest

在上一篇文章中,我们只是粗略的向大家阐述了一个请求如何通过中间件得到资源,我们侧重的是请求侧,即http请求映射成资源标识符的这一过程,现在我们开始重点阐述资源如何处理并返回给客户端的。

还是从transformMiddleware开始,在关键变量初始化小节,我们已经阐述了ViteDevServer示例传递给了这个中间件。 image.png 这个transformRequest方法传递的参数environment就是DevEnvironment的实例,这也是为什么我们在上一篇文章中不进行更细节阐述的原因,因为只有搞明白了DevEnvironment的前世今生之后,这些问题才有的说。

这个environment变量来自于它,即ViteDevServer实例传递过来的数据: image.png VITE在初始化的时候,分别初始化了一个ssr和一个client的变量,由于我们不考虑SSR,所以我们就只看这个client就可以了。

现在大家再看transformRequest这个方法的话,是不是就已经觉得一目了然呢? image.png 之前我们说过,DevEnvironment这个类上定义了一个可以访问PluginContainergetter,现在就派上用场啦。

image.png 至此,事儿就非常简单了,(我们暂时先不考虑缓存),拿到了http请求的url,将url映射成资源的id(即调用PluginContainerresolveId方法触发生命周期),然后根据id从磁盘加载资源(即调用PluginContainerload方法触发生命周期),最后将从磁盘读取到的资源进行编译加工(即调用PluginContainertransform方法触发生命周期),然后把得到的编译结果返回给客户端,这就是从一个http请求发出,到客户端收到编译内容的全部过程啦。

image.pngimage.pngimage.pngimage.png

所以,我们可以简单的用大白话总结一下DEV阶段一个资源的获取流程了,VITE开启了一个DevServer,当我们访问这个DevServer的时候,VITE会把我们的Http请求通过内置的中间件映射成获取资源的标识符,然后调用插件容器通过资源标识符获取到资源,接着对资源进行编译转换,最后把转换之后的结果返回给客户端

如果你看到了这儿的话,是不是有一种你上你也行的冲动,哈哈哈,真的让人成就感爆棚啊!

PluginContainer

在明白了之前DevEnvironment之后,现在我们再看PluginContainer真的就会觉得太简单了。

这儿,我猜测VITE的代码可能也是经过重构或者API的变更了,在源码中,有两个Container类,一个叫做PluginContainer,一个叫做EnvironmentPluginContainer

接下来,我们一起看一下PluginContainerimage.png 大家可以看到,这个PluginContainer依赖了Environment(DevEnvironment也是其中的一个类型之一),这个Container上面的方法,都是在做桥接,调用的仍然是EnvironmentPluginContainer的内容。

image.pngimage.png

所以,我们现在搞明白了PluginContainerEnvironmentPluginContainer的关系,PluginContainer是为了保持已有API的稳定而做的桥接,在不经意之间我们就看到了桥接模式的实际应用,哈哈。

EnvironmentPluginContainer

之前我们看的是它的桥接类,现在我们来看正在干活儿的类。

这个代码是VITE的团队从Rollup的源码里面摘录,并且经过重构整合的代码,在之前的Rollup专栏里面,我在这篇文章向大家讲过Rollup各种类型的生命周期的实现方式。

Rollup源码学习(六)——重识Rollup构建生命周期

在Rollup里面,是一个一个的函数,不过VITE团队把它们进行了封装,统一管理。

现在带着大家一起来看一下这个EnvironmentPluginContainer类中的关键方法。

首先是构造器,在之前我们是已经知道了的,初始化这个容器的时候,已经把所有的插件列表传过来了的。 image.png 然后是hookParallel,这个是在支持Rollup的parallel类型的的生命周期: image.png 大家看一下,是不是这段代码有点儿似曾相识,如果你看过我前面提到的阐述Rollup生命周期的那篇文章的话。

这段代码的含义也不再赘述了,大家查看《Rollup源码学习(六)——重识Rollup构建生命周期》这篇文章即可,里面有demo向大家展示它的等价代码。

接着是VITE封装了的buildStart生命周期的处理: image.png

另外,在之前的文章我们讲过,resolveIdload这样的生命周期是first+async类型的生命周期。

我们来看一下VITE是如何实现的。 image.pngimage.png 看起来,定义这个handleHookPromise方法是为了更好的追踪Promiseimage.pngimage.png 可以看到,上述的实现简化一下的伪代码如下:

async funtion runner() {
    const plugins = [plugin1, plugin2, plugin3]
    let output = null
    for(const plugin of plugins) {
       const result = await plugin();
       if(result) {
           output = result;
           break;
       }
    }
    return output;
}

再者,在之前的文章我们讲过,transform这样的生命周期是sequential + async类型的生命周期,我们也来看一下VITE是如何实现。 image.pngimage.png 可以看到,上述的实现简化一下的伪代码如下:

funtion runner(initial) {
    const plugins = [plugin1, plugin2, plugin3]
    let output = initial
    for(const plugin of plugins) {
       const result = await plugin(output);
       output = result;
    }
    return output;
}

runner(undefined)

对于其它的方法,我们在DEV阶段中其实不怎么用到,比如下面close方法里面的逻辑,虽然我们一般不怎么用到,但是框架的开发者必须要实现,这就是严谨,这也是值得我们学习的,如何设计健壮的软件系统,都是靠这种点滴积累起来的,哈哈。 image.png

我个人觉得VITE的这个实现相对于Rollup的实现代码的可读性比较好,可维护性也比较好,业务逻辑都内聚到了一处,外界只需要调度PluginContainer的方法就好了,体现了面向对象设计的封装的优势。

结语

考虑到篇幅的关系,就不在本文继续阐述较为复杂的ModuleGraph相关类。

相信大家阅读完本文应该是成就感满满的吧,在本文中,我们理清楚了VITE中几个类的关系,并且我们已经完全分析清楚了从请求经过中间件分发,映射到插件容器处理,编译,得到结果并返回的这一全套的流程。

接着我们又带领大家学习到了VITE基于Rollup生命周期设计的自己的生命周期,尤其是这套生命周期管理的设计思想(插件容器管理所有的插件,监听不同生命周期的设计,不同类型的插件),应该是我们收获最大的了吧,这些都是我们学习到的核心科技,对于将来我们在设计自己的业务系统时,一定有较好的指导意义。

❌