# Vite 是什么?

Vite (读音类似于 [weɪt],法语,快的意思) 是一个由原生 ES Module 驱动的 Web 开发构建工具。在开发环境下基于浏览器原生 ES imports 开发,在生产环境下基于 Rollup 打包。

# Vite 与 Webpack

webpack 最初是为了解决前端模块化以及使用 Node.Js 生态的问题而出现,随着 webpack 的大规模使用和社区生态的逐渐繁荣,webpack 的能力越来越强大。webpack 本质上是一个现代 JavaScript 应用程序的静态模块打包器 (module bundler)。当 webpack 处理应用程序时,会将所有这些模块打包成一个或多个 bundle。

但因为多了打包构建这一层,随着项目的增长,打包构建速度会越来越慢,冷启动和热更新的速度会越来越长,影响开发效率。然后启动一轮构建优化,随着项目的进一步增大,构建速度又会降低,陷入不断优化的循环。在项目达到一定的规模时,基于 bundle 的构建优化的收益变得越来越有限,无法实现质的提升。

img

webpack 之所以慢,主要的原因还是在于他将各个资源打包整合在一起形成 bundle,如果我们不需要 bundle 打包的过程,直接让浏览器去加载对应的资源,那么是不是能解决这个问题呢?

img

那么,Vite 来了,Vite 基于浏览器特性 ES Module,实现了 bundleless 架构。真正无打包启动 & 构建。

# Vite 特点

  • Lightning fast cold server start - 闪电般的冷启动速度
  • Instant hot module replacement (HMR) - 即时热模块更换(热更新)
  • True on-demand compilation - 真正的按需编译

上面三个特点,现在市面上已经有很多解决方案。比较出名的冷启动脚手架: vue-cli, create-react-app 等,热更新也早是已经实现的功能,webpack 都已经集成了。也可以通过动态 import 的方式(import ('xx.js'))来实现编译时的一次性编译,引入时的按需引入,来达到按需编译的效果。 那么 vite 有什么特别的地方呢?用作者在微博上的原话:

Vite,一个基于浏览器原生 ES imports 的开发服务器。利用浏览器去解析 imports,在服务器端按需编译返回,完全跳过了打包这个概念,服务器随起随用。同时不仅有 Vue 文件支持,还搞定了热更新,而且热更新的速度不会随着模块增多而变慢。针对生产环境则可以把同一份代码用 rollup 打。虽然现在还比较粗糙,但这个方向我觉得是有潜力的,做得好可以彻底解决改一行代码等半天热更新的问题。

# ES Module 又是什么呢?

随着我们的应用程序越来越大,我们希望将其拆分为多个文件,即所谓的 “Module”, 模块。但是长期以来,JavaScript 一直没有语言级别的模块语法。社区中产生了一些方法来解决这个问题,比如:

  • AMD –最古老的模块系统之一,最初由 require.js 库实现。
  • CommonJS –为 Node.js 服务器创建的模块系统。
  • UMD –另一种模块系统,建议作为通用模块系统,与 AMD 和 CommonJS 兼容。

语言级模块系统于 2015 年出现在标准中,此后逐渐发展,现在已得到所有主要浏览器和 Node.js 的支持。也就是 ES Module。

一个简单的 :

1  // sayHi.js
2
3  export function sayHi(userName) {
4     alert(`Hi, ${userNmae}`)
5  }
6  // html
7  <script type="module">
8  import { sayHi } from './sayHi.js'
9  sayHi()
10 </script>

用一句话来形容 ES Module: 就是在浏览器和 Node.js 中原生增加了模块化的支持。

# Vite 基于 bundless 的实践

# 第一步: 让浏览器自主加载模块。

Vite 采用了基于 web 标准的 ES Module 来实现的浏览器自主加载模块功能,目前基于 web 标准的 ES Module 已经覆盖了超过 90% 的浏览器。(具体兼容性:caniuse.com/#search=jav…

img(图片截图自:caniuse.com/#search=jav…)

# 第二步: 支持 bare import (不带相对路径或绝对路径的 import)

像我们平常引入位于 node_modules 中的依赖会采用下面的 这样引用

1  import Vue from 'vue'
2  import _ from 'lodash'

这样的引入其实就是 bare import。那么如何解决浏览器 bare import 时返回正确的路径呢。

我们看一个例子 (开发环境):

未编译前的 main.js

img

img

从 main.js 的返回中可以看到

1  import { createApp } from '/@modules/vue.js'

在引入的时候前面加了 '/@module/' 这样的字符。并在后面加上了.js 的后缀。在启动 本地 server 的时候,vite 会特殊处理这样依赖。

ES Module 会自动解析 import 语法,然后去请求 import 的模块,那么当 '/@module/vue.js' 这样请求到达 DevServer 的时候,DevServer 可以根据 ' /@module/' 这个特殊的前缀来判断这次请求的文件是外部依赖,需要从 node_modules 中获取。

# 第三步:能够支持加载非 js 的文件资源

img

如上面截图,这是 Vite 处理请求时根据请求的后缀不同分配不同的 plugin 去处理。最后都会返回 ES module,vite/hmr 会根据返回的内容进行判断而进行不同的处理。

额外的多说一下 ES Build:

Vite 针对 TypeScript 做了内置支持,并不需要额外去配置。在编译器方面,Vite 并没有采用官方的 tsc 来编译。而是采用了 ESBuild 这个工具。

ESBuild 是一个用 Go 语言实现,能够把 TypeScript,Jsx,Tsx 编译到原生 JavaScript 的一个工具。性能会比 tsc 好上很多。(单线程 ES Build 性能大概会好个 2,30 倍,多线程可能会好到百倍以上 -- 基于 16GRAM 的 6 核 2019 MacBook Pro)。

ES Build 快在哪里?

首先了解一下 tsc 干了什么,tsc 是一个 TypeScript 编译器,可以把 TypeScript 编译成 JavaScript, tsc 在把目标文件解析成 AST 之后,会进行两步操作,1: 类型检查 2: AST -> JavaScript 代码。

ESBuild 在把目标文件解析成 AST 之后,则只进行了其中一步操作: AST -> JavaSciript 代码。这就是 ESBuild 比 tsc 快的原因之一。换句话说, ESBuild 没有类型检查的功能。想要做到类型检查需要配合 tsc 命令或者一些 IDE 插件去解决。了解更多

# 第四步: 浏览器端能够处理包含在 ES Module 中不同类型的返回

如第三步,所有的文件类型返回都包在 ES Module 中,那么就需要在浏览器端实现识别返回的类型,并做相对应的处理。

img

如上图,在本地开发的时候,会在 html 中插入这么一段代码。/vite/hmr 是一个 js 文件。只在本地开发时引入,是在浏览器端建立和 DevServer 的 websocket 链接,注册 websocket 回调,接收不同类型 websocket 的消息,并处理。(在 DevServer 启动时,会自动监听当前目录下除了 node_modules 外的所有文件,根据文件类型不同,文件改动时会触发不同的回调函数,也就会发送不同的 webscoket 消息。)

img

上图中的代码来自于 vite/hmr.js ,有两个主要功能,一个是监听 websocket 的消息的功能,第二个是处理消息的功能。在处理消息的函数 handleMessage 中。可以看出针对不同类型的返回,有不同的处理方法。比如: vue-reload, vue-rerender 这两种类型会通过 动态import 的的方法访问本地的 devServer , 然后将返回值通过 __VUE_HMR_RUNTIME__ 的 对应方法处理。

VUE_HMR_RUNTIME 是从 Vue3 暴漏的一个Api,内含 createRecord, rerender, 
reload 三个方法。暴漏名为:HMRRuntime

比如 Css 类型的返回 style-update, style-remove , 则会通过 document.adoptedStyleSheets 来进行 css 属性的添加与删除。

如果更新的文件会有多个文件依赖它,那么则会返回 multi 类型(如下图消息所示),那么则会针对每一项执行一次函数 handleMessage

img

# 第五步: 优化 & 合并大量网络请求

img

如图,这是在一个 demo 中引入一个 lodash-es 的包,可以看到有 600 多个请求。对于 lodash-es 这样自身文件数量较多,或者一些有较多外部依赖的模块。在请求这些模块的时候,会发起大量请求。这会导致页面加载的时间变长,非常影响用户体验。

在这部分呢,Vite 也做了相关优化。在 Vite 中有一个 optimize 的命令(使用方法: vite optimize)。这个命令会将 package.json 中的 dependencies 依赖借助 @rollup/plugin-commonjs 这个插件将 commonjs 的外部依赖打包为 ESModule 的形式引入。然后将打包后的文件,存放在 /node_modules/.vite_opt_cache 文件夹当中。在下次分析带有 ‘/@module/’ 前缀的这种请求时,会先去 cache 文件夹中寻找是否有对应的缓存文件。

在 vite devServer 启动的过程中,也调用了 optimize 命令对应的方法,将依赖都打包成了 ES6 Module 的文件。

img

img

这是优化之后的请求列表。可以看到关于 lodash-es 的请求已经全部聚合在 lodash-es.js 这一个文件之中。

# 第六步:支持生产环境构建

在生产环境构建上,Vite 并没有采用纯 ES Module 来实现,而是基于 Rollup 将多个模块打包成 bundle。这是因为:如果不打包成 bundle,大量的 ES Module 引入会导致浏览器发出大量请求,这会导致页面加载时间变长。

# Vite 的表现如何?

# 冷启动:

img

从左到右依次是: vue-cli3 + vue3 的 demo, vite 1.0.0-rc + vue 3 的 demo, vue-cli3 + vue2 的 demo

从这个 gif 可以明显感受到 vite 比其他两个在启动速度上有明显的优势,vue-cli 3 启动 Vue2 大概需要 5s 左右,vue-cli3 启动 Vue3 需要 4s 左右,而 vite 只需要 1s 左右的时间。

从一个 demo 上已经可以很明显的看出 Vite 的优势,从理论上讲,Vite 是基于 ES Module 实现的,可以真正意义上实现按需引入和不需要打包。那么在越大的项目上,Vite 的表现是越好的, 因为一次启动,首屏或者根路由,大部分情况下也就需要那么十几个,最多几十个组件,Vite 很快就可以完成编译,然后启动服务了。

# 生产环境构建:

img

Vite 是基于 Rollup 实现的生产环境构建,所以在生产环境构建时,Vite 与 基于 webpack 实现的 vue-cli 在构建时间上相比差距不大。

Vite 的生产环境构建可以简单理解为:默认配置了一些 rollup 的配置(比如: rollup-plugin-vue),然后通过 vite.config.js 来接收一些 rollup 的相关配置,直接传入到 rollup 中参与构建。

# Vite 使用:

# 创建一个 demo 并启动

1  use npm: version >= npm@6.1.0
2
3  $ npm init vite-app <project-name>
4  $ cd <project-name>
5  $ npm install
6  $ npm run dev
7
8  use yarn: version >= yarn@1.0
9 
10 $ yarn create vite-app <project-name>
11 $ cd <project-name>
12 $ yarn
13 $ yarn dev

尽管 Vite 主要是为与 vue3 一起工作而设计的,但它也可以支持其他框架。例如,尝试 npm init vite-app --template react 或 --template preact。可以启动一个 react | preact 的 demo。

# 命令行支持:

img

# 自定义配置:

可以使用 vite.config.js/vite.config.ts
或者自定义文件名配合 vite --config my-config.js 来使用
具体配置项参考:config.ts

所以最后,听听 Vite 作者是怎么描述 Vite 的?(5:30 - 14:00)
player.bilibili.com/player.html…