前端工具Vite的出现解决了什么?

lxf2023-03-19 19:51:01

前言

背景

在 ESM 出现之前,Javascript 是没有一个标准的模块方案。

比如说 CJS 是用于 Node 服务端的模块化方案,AMD 是用于浏览器的模块化方案。为了解决这个模块共用性问题,出现了 UMD 用于兼容这两种模块规范。

鉴于上面共用性问题,实际开发中配置的打包方式,采用的还是 UMD 模式。因为这样可以避免打包而产生的规范问题,并且在 ESM 不能使用的情况下也会选择 UMD。

ESMES Module)的出现,则为 Javascript 提出了一个标准模块系统的方案。ESM 可以替代 CJS 与 AMD,并且兼备 UMD 特性(任何环境都可使用)。

ESM 自身的静态化特点,在编译时加载,也使得页面加载速度更快,相比 CJS、AMD 与 UMD 更有优势。

ESM 也真正意义上做到了按需使用。使用import并不会直接执行模块,而是生成一个动态的只读引用,等到真的需要用到时,才会到模块里面去读取。

主流构建工具

前端工具Vite的出现解决了什么?

目前主流构建工具是先打包生成 Bundle,然后再启动开发服务器,如 webpack。同时 HMR 也是需要把改动的模块代码及相关依赖全部编译后,才会更新界面。

这也是为什么项目代码量越来越大的时候,项目启动时间会变的越来越长,而且稍微改一点代码,也会好长时间才热更新界面

而 Vite 的出现,则解决了这个状况。

Vite介绍

Vite 是一个基于浏览器原生ESM的构建工具。

Vite 由两个主要部分组成:

一个开发服务器:基于本地搭建的服务器,借助浏览器原生的 ESM 能力。

一套构建指令:采用 Rollup 进行打包,同时继承了 Rollup plugin,可以使用 Rollup 的生态。

Vite 核心的理念目前体现在开发服务器,具体原因我们往下看。

开发服务器

相比目前主流的打包工具,无论在启动还是 HMR 都会先 Bundle,这样就会导致更新界面的速度不如 ESM

前端工具Vite的出现解决了什么?

ESM

Vite 开发环境的服务是直接冷启动的,省略了 Bundle 打包的过程。

使用浏览器原生 ESM 的能力,浏览器直接去解析 imports,省略了开发环境的打包过程,在服务端直接按需编译返回。这里的编译只是编译当前文件返回给浏览器,不需要管理依赖或者解析整个项目代码的依赖。

Vite 中 HMR 也是在原生 ESM 上执行的,所以 HMR 速度也非常快,且 HMR 速度不会随着模块增多而变慢。

esbuild

对于一些较大的依赖和文件,或者不同的模块规范(如 CJS 、 AMD 、ESM)的处理。

Vite 采用了 esbuild 预构建依赖。esbuild 会统一将文件(如CJS)转换成浏览器支持的 ESM 形式。如下图:

前端工具Vite的出现解决了什么?

对于这些预构建的文件,vite 会统一放在 node_modules 中的 .vite 文件夹下。

esbuild 的构建速度非常快。它不仅可以编译 JavaScript 代码,而且由于 esbuild 底层是用 Go 编写的,Go 天生具备多线程运行能力,所以比使用 JavaScript 编写的打包器预构建依赖快 10-100 倍。

虽然目前 JavaScript 编写的打包器,也可以实现充分利用 GPU 打包编译,但是 JavaScript 本质上依然是一门解释型语言,每次执行都需要将源码翻译成机器语言执行。相反 Go 是一种编译型语言,在编译阶段就已经将源码转译为机器码,启动时只需要执行即可,所以 Go 相比 JavaScript 少一步编译的过程。

http 缓存

Vite 的另一个特性之一就是使用了 http 缓存的能力。

说到 http 缓存,不得不说一下浏览器缓存过程

  • 浏览器第一次加载资源,服务器返回200,浏览器此时会将资源从服务器下载,同时将 response header 与返回时间一起缓存。
  • 再次请求加载资源的时候,浏览器会比较与上一次下载资源的时间差,如果没有超过 Cache-Control 设置的 max-age,则没有过期,此时就会从本地缓存读取资源。如果浏览器不支持 HTTP1.1,那么则会用 Expires 判断是否过期(这一过程称为强缓存)。

Expires 是 HTTP1.0 的,Cache-Control 优先级高于 Expires,可以理解为 Expires 是处理浏览器兼容的。

  • 如果对比时间后,发现已过期。服务器则会查看请求的 header 中 If-None-Match 里值,与该请求资源的 Etag 做比较,如果相同则代表资源没有发生改变,返回304。否则,直接返回新的资源,并返回200(这一过程称为协商缓存)。
  • 如果服务器收到的请求 header 中,没有 Etag 值,则会读取 If-Modified-Since 和被请求文件的最后修改时间做对比,如果相同则代表没有发生改变,返回304。否则,直接返回新的资源,并返回200(这一过程称为协商缓存)。

下载资源的同时,在 response header 中会携带 Etag(资源唯一标识,资源发生改变,标识也随之改变)、Last-Modified(资源文件最后一次更改时间),而浏览器会把这两个保存下来。

向服务端发送资源请求时,在 request header 中,会把保存的 ETag 值放到 If-None-Match 中,把保存的 Last-Modified 值放到 If-Modified-Since 中发给服务端。

Vite 使用 http 缓存,对资源文件做了缓存处理:

  • 源码模块的请求会根据 304 Not Modified 进行协商缓存。
  • 依赖模块请求则会通过 Cache-Control: max-age=31536000,immutable 进行强缓存。

这样就具备一个优势,一旦被缓存它们将不需要再次请求,除非依赖或者代码发生变化。

但是紧接着也会暴露问题出来

在生产环境中,存在着使用 ESM import 这种大量嵌套的文件,就会产生大量的网络请求。随着代码量的不断增加,也会导致网络请求的不断增加。即使 Vite 采用了最新的 HTTP2.X 中的多路复用与首部压缩,仍不能解决性能低的问题。

所以,这也是其中一个为什么生产环境还需要打包的原因

当然还有其他原因,我们接着往下看。。。

构建指令

相比开发环境使用 esbuild 构建依赖,生产环境则使用借鉴了更为成熟的 Rollup 来打包。

这里的构建指令,指的是使用 Rollup 预配置指令来构建打包。

目的

鉴于大量嵌套的文件下的 ESM import,会导致生产环境产生的大量的网络请求。

为了在生产环境中获得最佳的加载性能,所以仍然需要对代码进行tree-shaking、懒加载以及chunk分割,以获得更好的缓存。

说到这里大家可能觉得 Vite 算是个半成品,这样的优势,竟然在生产服不能使用。

但是想一想,每次启动和 HMR 项目代码的时候,相比 Bundle 的漫长等待,Vite 的快速响应,为我们解决了大量等待的开发时间,尤其是代码量特别大的项目,这种优势更明显。

为什么使用 Rollup 构建打包

有的小伙伴可能会有疑问,生产环境为什么用 Rollup 打包,而不用 esbuild 打包

这是因为 esbuild 目前还不够成熟,虽然 esbuild 预构建速度很快,但针对应用级别的代码分割、CSS 处理仍然不够稳定,同时也未能兼容一些未提供 ESM 的 SDK。

我在项目开发中也遇到过不稳定的情况,每次都是重新启动一下就好了。如果大家遇到这个问题,希望对大家有所借鉴帮助。

而对于这些不稳定因素,所以目前只能放弃使用 esbuild。

有的小伙伴可能会说 esbuild 不成熟,那为什么生产环境使用 Rollup,而不用 webpack 呢?。

这是因为相比其他打包工具,Rollup 能打出更小体积的文件。而且因为 Rollup 基于 ES6 模块,比 webpack 使用的 CommonJS 模块机制更高效,而且毕竟开发环境也是基于 ESM 预构建运行的。

Vite 对于构建指令也做了其他方面的努力。比如说 Vite 兼容了 Rollup 的插件生态,从而使开发人员可以在 Vite 中使用 Rollup 成熟的插件。