vite 下一代前端开发与构建工具(一)

6,229 阅读7分钟

vite是什么

vite是尤大在更新vue3之后,再次开发的一个新的构建工具。那什么构建工具让尤大在过年都在疯狂更新呢?它到底有什么魔力呢?

WechatIMG19.png 在vite开发出来之后,就连webpack团队的核心成员都在感叹

WechatIMG18.png

Vite(法语意思是 “快”,发音为 /vit/,类似 veet)是一种全新的前端构建工具。你可以把它理解为一个开箱即用的开发服务器 + 打包工具的组合,但是更轻更快。Vite 利用浏览器原生的 ES 模块支持和用编译到原生的语言开发的工具(如 esbuild)来提供一个快速且现代的开发体验。

注意:vite是一个全新的前端构建工具(重点:构建工具),很多小伙伴总在问vite和vue-cli的区别。vue-cli是一个脚手架工具,用于自动生成vue.js+webpack的项目模板。方便我们快速开发。而vite是一个构建工具,用于我们对项目进行构建(类似于webpack的功能)。而在未来不排除vue-cli里面会集成vite

构建工具的前世今生

构建工具的作用

构建工具:用来让我们不再做机械重复的事情,解放我们的双手的。
在早期开发过程中。有很多令我们不爽的地方

    1. js是弱类型
    1. 手动维护依赖很麻烦
    1. 浏览器的兼容性
    1. 没有热更新 .......

于是 前端构建工具应运而生。构建工具可以帮助我们做以下工作

    1. 代码检查
    1. 代码压缩,混淆
    1. 依赖分析,打包
    1. 语言编译(比如ts转化js,scss转化css) ...

在了解vite之前我们先了解一下其他的构建工具,这样我们才能更好的对比vite的强大之处。
前端的构建工具有很多。比如 Grunt Gulp FIS3 Webpack Rollup Parcel snowpack vite 接下来我会挑几个构建工具进行简单的介绍

常见的打包工具的工作流

Gulp

Gulp.js 是一个自动化构建工具,开发者可以使用它在项目开发过程中自动执行常见任务。Gulp.js 是基于 Node.js 构建的,利用 Node.js 流的威力,你可以快速构建项目

var gulp = require('gulp');
    var jshint = require('gulp-jshint');
    var concat = require('gulp-concat');
    var rename = require('gulp-rename');
    var uglify = require('gulp-uglify');

    // Lint JS
    gulp.task('lint', function() {
    return gulp.src('src/*.js')
        .pipe(jshint())
        .pipe(jshint.reporter('default'));
    });

    // Concat & Minify JS
    gulp.task('minify', function(){
        return gulp.src('src/*.js')
        .pipe(concat('all.js'))
        .pipe(gulp.dest('dist'))
        .pipe(rename('all.min.js'))
        .pipe(uglify())
        .pipe(gulp.dest('dist'));
    });

    // Watch Our Files
    gulp.task('watch', function() {
        gulp.watch('src/*.js', ['lint', 'minify']);
    });

    // Default
    gulp.task('default', ['lint', 'minify', 'watch']);

gulp的执行是从上倒下的执行一个个任务。然后文件内容通过管道。进行传递。如果你想了解更多gulp的知识,可以去gulp官网

Webpack

webpack 是一个用于现代 JavaScript 应用程序的 静态模块打包工具。当 webpack 处理应用程序时,它会在内部构建一个 依赖图(dependency graph),此依赖图对应映射到项目所需的每个模块,并生成一个或多个 bundle。
接下来我们看看Webpack的工作原理图 WechatIMG21.png 从上图我们可以看出webpack打包分为一下步骤

    1. 查找入口文件

      从webpack的配置文件中查找entry的配置,从而找到入口文件

    1. 分析依赖关系

      接到入口文件之后,从入口文件出发,分析入口文件中依赖了哪些文件,并且这些依赖的文件中还可能依赖别的文件,就这么递归的找下去。

    1. 模块函数

      找到依赖中的所有文件,把这些文件转化成模块的函数,为了方便后面webpack进行调用

    1. 打包

      打包完毕的文件可以产出到配置文件的output指定路径里,生成一个bundle

    1. 启动服务

      node创建本地服务器并启动静态页面 这大概是我们webpack在开发环境打包的主要流程。

熟悉了我们常见的打包工具的工作流。接下来我们看看vite是怎么工作的。 看看vite为什么被称为下一代开发和构建工具。

vite的原理和优势

目前打包工具的困境

在目前的工作中,我们主要利用webpack+vue(webpack+react)进行项目开发。然而,当我们构建越来越大型的应用的时候,打包工具需要处理的javascript代码量也呈指数级增长。在大型项目中包含几百甚至几千个模块的情况也越来越多。我们开始遇到性能瓶颈,使用javascript的工具通常需要很长的时间,才能启动开发服务器

WechatIMG25.png 这是我工作中的一个真实项目, 项目不大,但是启动服务器的时间花了30s。这作为一个讲究效率的程序员肯定是无法忍受的

当项目越来越大,项目的hmr热更新速度越来越慢。有时候甚至改动一个字段,页面几十秒之后才会进行热更新。

为什么会产生这种问题

其实这和webpack打包的原理有关系,我们前面其实已经大概了解webpack的主要工作流,这里就不在详细讲解

  1. 启动服务器慢,是因为在每次服务器启动之前。webpack需要执行一系列的事情。找模块间的依赖,将各个模块进行合并,生成一个build,存入内存中,最后在启动服务器。所以速度上随着项目的增加,速度会越来越慢
  2. webpack的hmr。当你改动一个文件的时候,Webpack 的热更新会以当前修改的文件为入口重新 build 打包,所有涉及到的依赖也都会被重新加载一次。所以速度也随着项目的增加而降低(后面会写一遍文章专门介绍webpack的hmr的实现) ......

vite的优势

vite的三大特点

    1. 快速的冷启动 在开发预览中,它是不进行打包的。
    1. 即时热模块更新(HMR,Hot Module Replacement)
    1. 真正按需进行加载

当然vite也有一定的问题,

  1. vite的生态还不够完善。

  2. 在生产模式下仍然需要打包。尽管原生 ESM 现在得到了广泛支持,但由于嵌套导入会导致额外的网络往返,在生产环境中发布未打包的 ESM 仍然效率低下(即使使用 HTTP/2)。为了在生产环境中获得最佳的加载性能,最好还是将代码进行 tree-shaking、懒加载和 chunk 分割(以获得更好的缓存)。

vite的原理

通过上面的了解,我们已经知道了利用vite的很多优势,那么接下来我们看看vite是怎么实现的。

ES Modules

vite的成功得益于现代浏览器对于基于ECMAScript 标准原生模块系统(ES Modules)实现。 目前主流浏览器(IE11除外)都已经支持。他允许我们在浏览器使用export、import 的方式导入和导出模块,在 script 标签里设置 type="module"

<script type="module">
  import { createApp } from './main.js‘
  createApp()
</script>

浏览器会识别添加type="module"的 <script> 元素,浏览器会把这段内联 script 或者外链 script 认为是 ECMAScript 模块。然后浏览器会被这里面的import引用发起一个http请求,请求获取文件中的内容。 因此我们对于第三方的模块,可以不用打包合并,而是通过import 这种方式去发起http 请求,获取代码。这也是vite的主要实现思路。 如果你对ES Modules 不够了解。可以去看看ES Modules的规范

vite的工作流和冷启动

首先看一张图

WechatIMG26.png

  1. 首先是启动一个静态资源服务器
  2. 找到项目的入口,开始加载入口文件
  3. 当声明一个 script 标签类型为 module 时,浏览器就会像服务器发起一个GET
  4. Vite 通过劫持浏览器劫持浏览器劫持浏览器的这些请求,并在后端进行相应的处理,将项目中使用的文件通过简单的分解与整合,然后再返回给浏览器。

从上面的分析可知: vite主要做了以下事情

    1. 启动了一个静态资源服务器
    1. 只需要在浏览器请求源码时进行转换并按需提供源码 vite整个过程中没有对文件进行打包编译,至于其他加载的工作就交给了浏览器,所以其运行速度比原始的webpack开发编译速度快出许多。

vite的热更新

传统打包器是将项目打包之后的资源存入电脑的内存之中,这样他们只需要在文件更改的时候,将对应的模块进行失活,但是它仍然需要重新构建并重载页面。 所以像webpack这类的打包工具支持了动态模块热重载(HMR),允许一个模块替换自己,而对其余页面没有影响。但是在实践中。我们发现HMR的速度会随着项目的增大而降低(原因在 目前打包工具的困境 这一节已经分析过了)

而在vite中HMR 是在原生 ESM 上执行的。当编辑一个文件时,Vite 只需要精确地使已编辑的模块与其最近的 HMR 边界之间的链失效(大多数时候只需要模块本身),使 HMR 更新始终快速,无论应用的大小。

vite的按需加载

为什么说vite才是真正的按需加载呢?难道webpack不是真正的按需加载吗?
如果你想知道,那么你可以看看去看看webpack的原理,这里我简单介绍一下 webpack其实在开始构建打包的时候,还是对所有的文件进行一次打包构建,只是在webpack遇到 import( * ) 这种语法的时候,会另外生成一个chunk; 只有在合适的时候去加载import中的内容
从上面的分析可以知道。不管我们这段import的代码何时执行,我们对需要对它进行一定的打包

但是vite不一样,只有在你真正的需要加载的时候,浏览器才会发送import请求,去请求文件中的内容,所以才说vite才是真正的按需加载

实现一个基础版vite

我们已经知道了vite的大概工作原理,那么我们接下来实现一个最基础的vite

vite和webpack编译过后文件的区别

webpack编译后的文件

WechatIMG27.png vite编译后的文件

image.png main.js image.png 第一步: 从上图我们可以看出vite首先也是找到一个入口文件,然后修改一些依赖模块的路径,比如把vue替换成了/@modules/vue.js. 把./App.vue替换成了绝对路径

WechatIMG29.png 第二步: 首先去寻找第一个组件App.vue。将App.vue中的js复制给一个常量,然后在App.vue中的html转化为一个render函数,挂载到js的那个常量身上。

实现我们自己的vite

首先我们初始化一个项目

 npm init vite-app <project-name>
 cd  <project-name>  
 npm i 
 npm run dev 

这样我们就能成功启动一个vite+vue的项目。 接下来我们不用vite,自己来实现一个我们自己的vite

image.png

新建yj-vite.js文件 从上图我们对vite分析可以得知,实现vite主要有以下几个步骤

搭建一个服务器

我们这里主要使用koa来实现

image.png 然后执行 node yj-vite.js

image.png 可以看到,这样一个静态资源服务器就已经搭建成功了

拦截请求,替换模块

image.png 如果我们请求的是首页,那么我们拿到绝对地址,进行拼接,读取文件,并返回,如果不是,我们就直接读取文件。并返回

image.png

这时候我们看到请求其实已经成功,但是控制台报了一个错误

image.png 原因是浏览器不能识别 import ..form 'vue' 要把vue变成一个绝对路径地址

更换路径

所以我们需要对上一步的内容进行改造,当以js结尾的文件中包含import 加载第三方模块的时候,我们首先将

```
import vue from 'vue'
替换
import vue from '/@modules/vue'
```

image.png

这样我们就可以看到main.js中已经成功替换了

image.png 那么接下来看看我们怎么处理 /@module/vue

image.png 这样我们就能正确处理第三方模块了。并且第三方模块也已经成功加载了。
但此时浏览器中会有一个错误 image.png 这是因为在vue的源码中,是有process.env这个环境变量,所以在index.html中我们可以加下以下内容,进行简单处理一下。

<script>
  window.process = {
    env: {
      NODE_ENV: 'dev'
    }
  }
</script>

这样我们就能够成功执行。

识别vue文件

但是在我们main.js中一般都不会这样写

createApp({
  render: () => h('div','3333333')
}).mount('#app')

而是通过单文件组件的形式去编写代码

import App from './App.vue'
createApp(App).mount('#app')

那么我们怎么去识别.vue中文件的内容,并且成功获取呢
这里简单说一下接下来的大概思路

  1. 匹配.vue结尾的文件,读取其中的js代码 把export default 替换成 const __script =
  2. 将渲染过后template相关的部分挂在到 __script

image.png 这样我们就成功手写了一个vite

总结

源码地址

github.com/yujun96/myv…

vite

其实官方的vite远远比这个复杂。在http请求之前,其实vite做了一个预打包,预打包的目的是为了减少http请求,因为可能一个模块依赖了成百上千个模块,过多的请求增大了浏览器的负担。 如果你想了解vite怎么实现预打包,你可以看看esbuild

当然vite怎么实现按需加载, 热更新 ... 等等功能,你可以关注我,等待后续更新

后续更新内容

vite 下一代前端开发与构建工具(二)
主要内容: vite更多功能的实现,以及如何将vue2项目迁移到vite
webpack hmr的实现原理
主要内容: 主要讲解webpack中hmr的底层实现
vue2 vue2的原理
主要内容: MVVM的实现 虚拟dom diff算法等等