Vite 浅入浅出

avatar
前端 @北京字节跳动科技有限公司

(图来源于网络:www.google.com.hk/search?q=vi…)

Vite是什么?

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

Vite 与 Webpack

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

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

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

那么,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…

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

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

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

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

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

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

未编译前的main.js

从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的文件资源

如上面截图,这是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中,那么就需要在浏览器端实现识别返回的类型,并做相对应的处理。

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

上图中的代码来自于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

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

如图,这是在一个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的文件。

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

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

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

Vite 的表现如何?

冷启动:

从左到右依次是: 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 很快就可以完成编译,然后启动服务了。

生产环境构建:

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。

命令行支持:

自定义配置:

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

所以最后,听听Vite 作者是怎么描述 Vite 的?(5:30 - 14:00)

player.bilibili.com/player.html…