Vite 2.0 初探

avatar
大前端 @阿里巴巴

文/灵傒

Vite 作者尤雨溪原话:

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

发展原因

在浏览器支持 ES 模块之前,开发者没有以模块化的方式开发 JavaScript 的原生机制。所以,我们一般基于 webpack、Rollup等构建工具,在本地开发的时候,提前把模块打包成浏览器可读取的 js bundle。

但是随着业务的发展,构建的应用也越来越大,模块可能动不动就上百个甚至上千个,就会遇到性能瓶颈,通常需要很长时间才能启动,启动了之后,即使用了HMR热更新,文件的修改也需要几秒钟才能在浏览器里看到效果。

但是现在,浏览器原生就支持了ES模块化,可以直接在html里写如下代码:

支持原生引入ES模块的浏览器

所以,可以借助浏览器的自助加载模块功能,来替代构建工具。

问题和解法

假设让你从零开始,利用浏览器可引入ES模块的特性,来开发一个开发服务器,会遇到什么问题呢?

引入路径问题

首先不可能去改变大家写代码的形式,平时我们引入一个模块,通常用的是称为 bare import 的写法,即 import lodash from 'lodash',webpack 等工具会去 node_modules 下查找对应模块,帮我们去处理js之间的依赖关系。尝试一下在浏览器直接去import lodash from 'lodash',它会告诉你,只会识别相对路径或者绝对路径:

我们来通过demo,来看看 Vite 是如何处理这一问题的:

我们看vite处理后的代码,将 import React from 'react' 处理成 import __vite__cjsImport_react from "/node_modules/.vite/react.js?v=6c9747e8" 绝对路径:

看看源码里是如何做到的:

  1. 首先是启动了一个server

  1. 修改引入方式
  • 通过 es-module-lexer 解析代码生成 ast 语法树,拿到 import 的内容,并转换路径

大量零散模块

依赖预构建prebuild

Vite 2.0 在为用户启动开发服务器之前,默认会先用 esbuild 把检测到的依赖预先构建了一遍。

为什么要做这个事情,举个栗子:

当你用 import debounce from 'lodash-es/debounce' ,理想中的场景就是浏览器只加载这个函数的文件。但由于debounce 内部又依赖了 3 个模块:isObjectnowtoNubmer,而这3个模块又有其他的依赖,实际上最后这个函数会引入了14个模块,意味着将带来14次请求:

这是笔者将 lodash-es 加入到 vite.config.js里的 optimizeDeps.exclude,这样子就不会将 lodash-es 预先构建,此时,看一下在浏览器打开的效果:

和lodash-es相关的一共发了14次请求,所以,为了解决这个问题,Vite 利用 Esbuild 的超快构建速度,让你在没有感知的情况下在启动的时候预先帮你把 debounce 所用到的所有内部模块全部打包成一个传统的 js bundle

Esbuild 使用 Go 编写,并且比 JavaScript 编写的构建器预构建依赖快 10-100 倍:

那我们看看 Vite 源码里这里是如何实现的:

首先,在 server 启动之前, 劫持了 http.listen,重写函数,执行依赖预构建 runOptimize

其中,runOptimize 所做的事情大致如下:

  1. 扫描依赖,形成依赖路径map,类似如下结构

{ "lodash-es_debounce": "node_modules/lodash-es/_debounce" }

  1. 使用 Esbuild 把它们提前打包成单文件的 bundle,写入到缓存文件里

对 runOptimize 感兴趣的可以看这篇文章:Vite 2.0 预构建源码解析

前面我们通过 vite.config.js 关闭了对lodash-es的预构建,现在开启,看看最后生成的文件,可以发现,lodash-es_debounce.js 一个文件已经聚合了其内部所依赖的所有模块:

此外,在预构建这个步骤中,还会对 CommonJS 模块进行分析,方便后面需要统一处理成浏览器可以执行的 ES Module。

利用HTTP 2的特性

可能总会有一些场景你不想让依赖预构建,想让浏览器直接加载它,那应该怎么优化多个并发请求呢?

HTTP 1.x 中,如果想并发多个请求,必须使用多个 TCP 链接,且浏览器为了控制资源,还会对单个域名有 6-8 个的 TCP 链接请求限制; HTTP 2 则可以使用多路复用,代替原来的序列和阻塞机制。所有请求都是通过一个 TCP 连接并发完成。

所以vite也支持了http2,如果 server.https 配置为true,将启用 TLS + HTTP/2,源码:

真正的按需加载

此外,和webpack不同的一点是,vite做到了真正的按需加载,只有屏幕上用到的代码才会真正加载进来:

webpack

基于 ESM 的构建模式:

灰色部分是暂时没有用到的路由,甚至完全不会参与构建过程,随着项目里的路由越来越多,构建速度也不会变慢。

其他

react的热更新

react的热更新是在 webpack 下是需要和 react-hot-loader 一起来实现的,这个工具虽然很赞,但是 bug 非常多,你甚至需要熟读问题清单才能很好的用它。

其实 react 官方实现了 react-refresh 来代替 react-hot-loader,react-refresh 优点在于热更新是不会丢失组件的state的,有人基于 react-refresh 实现了webpack插件 react-refresh-webpack-plugin,Vite 也集成了 react-refresh。

结合业务,适用的场景

如果要在业务中使用vite,可能面临的问题

  1. 所使用的第三方包,是否有导出标准的 esm 模块

  2. 内部开发的包,尽量导出一份 esm 模块

  3. 外部的包,建议不要用没有 esm 模块的包(都这个年代了,没有 esm 模块导出的包质量有待商榷),或者自行通过工具去转换成 esm 模块

  4. Vite 默认只支持原生支持 ESM 的现代浏览器,但可以通过官方的 @vitejs/plugin-legacy 来支持旧浏览器。legacy 插件会自动额外生成一个针对旧浏览器的包,并且在 html 中插入根据浏览器 ESM 支持来选择性加载对应包的代码(类似 vue-cli 的 modern mode)。

  5. 本地代理调试问题

  6. 配套生态完善,例如 babel-plugin-import 可以用 vite-plugin-imp 来代替,但是更多的需要等待 Vite 的生态丰富

适用的场景:

  1. 组件库的开发

  2. 内部应用的开发,对浏览器的兼容性要求不严苛

  3. 其他应用,可以利用 Vite 来本地开发,线上构建使用老的方式,可能有人担心这样到线上了会不会和本地不一致,导致一些奇怪的问题,但是实际上vite的生产环境构建也是用比较成熟的 rollup,并不是用 esbuild

总之,如果你面临过以下痛点,可以尝试一下Vite:

  • 项目的启动耗时很长

  • 改动后二次编译时间长

  • 开发React应用,缺少稳定的 HMR 能力 (当然,也可以选择用webpack插件react-refresh-webpack-plugin)

示例

最后,笔者将之前在工作过程中开发过的一个模块,使用 Vite 进行改造,对比了一下启动时间:

旧的脚手架,启动时间,6586ms:

使用Vite, 首次启动,1861ms,二次启动只需260ms:

参考文章

浅谈 Vite 2.0 原理,依赖预编译,插件机制是如何兼容 Rollup 的?
bundleless热更新原理探索
What is React Fast Refresh?