Vite原理从0到0.1

168 阅读8分钟

近几年vue发展突飞猛进,同时尤大大及其团队在前端构建工具上也迈出了很大的一步。

为什么选Vite?

借用vite官方文档描述:

在浏览器支持 ES 模块之前,JavaScript 并没有提供的原生机制让开发者以模块化的方式进行开发。这也正是我们对 “打包” 这个概念熟悉的原因:使用工具抓取、处理并将我们的源码模块串联成可以在浏览器中运行的文件。

然而,当我们开始构建越来越大型的应用时,需要处理的 JavaScript 代码量也呈指数级增长。包含数千个模块的大型项目相当普遍。我们开始遇到性能瓶颈 —— 使用 JavaScript 开发的工具通常需要很长时间(甚至是几分钟!)才能启动开发服务器,即使使用 HMR,文件修改后的效果也需要几秒钟才能在浏览器中反映出来。如此循环往复,迟钝的反馈会极大地影响开发者的开发效率和幸福感。

Vite 旨在利用生态系统中的新进展解决上述问题:浏览器开始原生支持 ES 模块,且越来越多 JavaScript 工具使用编译型语言编写。

vite为什么快

当冷启动开发服务器时,传统的基于打包器的方式启动必须优先抓取并构建你的整个应用,然后才能提供服务。这个在之前的webpack原理讲解时提到过。

传统的打包工具比如webpack即使采用了 HMR 模式,其热更新速度也会随着应用规模的增长而显著下降。

avatar

而Vite通过在一开始将应用中的模块区分为 依赖 和 源码 两类,改进了开发服务器启动时间。

  • 依赖 Vite 将会使用 esbuild 预构建依赖。Esbuild 使用 Go 编写,并且比以 JavaScript 编写的打包器预构建依赖快 10-100 倍。

  • 源码 通常包含一些并非直接是 JavaScript 的文件,需要转换(例如 JSX,CSS 或者 Vue/Svelte 组件),时常会被编辑。同时,并不是所有的源码都需要同时被加载(例如基于路由拆分的代码模块)。

    Vite 以 原生 ESM 方式提供源码。这实际上是让浏览器接管了打包程序的部分工作:Vite 只需要在浏览器请求源码时进行转换并按需提供源码。 在 Vite 中,HMR 是在原生 ESM 上执行的。当编辑一个文件时,Vite 只需要精确地使已编辑的模块与其最近的 HMR 边界之间的链失活(大多数时候只是模块本身),使得无论应用大小如何,HMR 始终能保持快速更新。

    Vite 同时利用 HTTP 头来加速整个页面的重新加载(再次让浏览器为我们做更多事情):源码模块的请求会根据 304 Not Modified 进行协商缓存,而依赖模块请求则会通过 Cache-Control: max-age=31536000,immutable 进行强缓存,因此一旦被缓存它们将不需要再次请求。

avatar

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

vite具体做了哪些事

结合 vite为什么快这一小节,可以得出以下几点:

  • Vite 主要对应的场景是开发模式,原理是拦截浏览器发出的 ES imports 请求并做相应处理。

  • Vite 在开发模式下不需要打包,只需要编译浏览器发出的 HTTP 请求对应的文件即可,所以热更新速度很快。

下面我们结合一个实际例子来探索下vite背后的原理,并且验证这些猜想: 使用官方推荐的步骤先生成一个原始的demo项目:

$ npx create-vite-app <project-name>
$ cd <project-name>
$ npm install
$ npm run dev

很好,一切正常(端口号冲突顺手改了个端口号:http://localhost:5177/ ),可以看到如下界面:

image.png

以下一些分析基于vite6.0.3版本

基于ESM的Dev server

Vite其核心原理是利用浏览器现在已经支持ES6import,碰见import就会发送一个HTTP请求去加载文件,Vite启动一个 koa 服务器拦截这些请求,并在后端进行相应的处理将项目中使用的文件通过简单的分解与整合,然后再以ESM格式返回返回给浏览器。Vite整个过程中没有对文件进行打包编译,做到了真正的按需加载,所以其运行速度比原始的webpack开发编译速度快出许多。

拦截浏览器HTTP请求并处理

首先我们去浏览器审查下元素,可以发现html中使用esm方式去加载入口文件main.ts:

image.png 打开network不难发现,和原始的main.ts相比,下面的main.ts是经过了vite改造过的:

image.png

这里,import { createApp } from 'vue' 被替换成了 import {createApp} from "/node_modules/.vite/deps/vue.js?v=6603da4b"

那么vite为什么要做这层转换呢?

这里就不得不说浏览器对 import 的模块发起请求时的一些局限了,平时我们写代码,如果不是引用相对路径的模块,而是引用 node_modules 的模块,都是直接 import xxx from 'xxx',由 Webpack 等工具来帮我们找这个模块的具体路径。但是浏览器不知道你项目里有 node_modules,它只能通过相对路径去寻找模块。

因此 Vite 在拦截的请求里,对直接引用 node_modules 的模块都做了路径的替换,换成了 /node_modules/.vite/deps/ 并返回回去。而后浏览器收到后,会发起对 /node_modules/.vite/deps/ 的请求,然后被 Vite 再次拦截,并由 Vite 内部去访问真正的模块,并将得到的内容再次做同样的处理后,返回给浏览器。

核心原理

当我们执行npm run dev的时候,vite会跑一个node服务:

image.png

使用中间件

connect 是一个基于 Node.js 的中间件框架,常用于创建 HTTP 服务器。Vite 使用 connect 作为其开发服务器的基础,用来处理请求和响应

  • servePublicMiddleware 静态文件服务
  • transformMiddleware 文件转换中间件
  • serveRawFsMiddleware 静态文件服务
  • serveStaticMiddleware 静态文件服务
  • indexHtmlMiddleware index.html 转换中间件

image.png

这个中间件中有一步很重要的动作,就是通过ast遍历解析html,然后找到所有的script:

image.png 我们断点看下这里的运行结果:

image.png

然后进行必要的预编译: image.png

vite会解析出来不同的类型,如vite:cssvite:esbuild等,然后通过不同的plugin去做编译:

image.png

这样,浏览器只要访问了 index.html,那么你依赖的所有的 js 模块,就都给你编译了。

HMR热更新实现

Vite热更新核心原理:

  1. 创建一个websocket服务端和client文件,启动服务;
  2. 通过chokidar监听文件变更;
  3. 当代码变更后,服务端进行判断并推送到客户端;
  4. 客户端根据推送的信息执行不同操作的更新

image.png

优化策略

预构建

为什么要有预构建
  1. 支持commonJS依赖:上面提到Vite是基于浏览器原生支持ESM的能力实现的,但要求用户的代码模块必须是ESM模块,因此必须将commonJs的文件提前处理,转化成 ESM 模块并缓存入 node_modules/.vite
  2. 减少模块和请求数量:常用的工具库,里面有很多包通过单独的文件相互导入,比如 lodash-es这种包会有几百个子模块。如果每个模块都是请求时编译,那向 lodash-es这种包,它可是有几百个模块的 import。这样跑起来,一个 node_modules 下的包就有几百个请求,依赖多了以后,很容易就几千个请求。

Vite 将有许多内部模块的 ESM 依赖关系转换为单个模块,以提高后续页面加载性能:Vite把 node_modules 下代码的 commonjs 提前转成 es module,还有提前对这些包做一次打包,变成一个 es module 模块。

预构建原理
  • 扫描依赖: Vite 从项目的入口文件index.html的内容开始对JS代码进行语法分析,使用Vite自己实现的用来记录依赖的esbuildScanPlugin插件,在每种模块路径解析的时候做处理,确定入口文件所依赖的其他模块。
  • 使用esbuild去打包这些模块,输出esm 格式的模块到 node_modules/.vite 下。

Vite预编译之后,将文件缓存在node_modules/.vite/文件夹下。

image.png

Vite根据以下地方来决定是否需要重新执行预构建:

  • package.json中:dependencies发生变化
  • 包管理器的lockfile
  • vite.config.js中相关字段

如果想强制让Vite重新预构建依赖,可以使用--force启动开发服务器,或者直接删掉node_modules/.vite/文件夹。

使用esbuild

Vite底层使用Esbuild实现对.tsjsx.js代码文件的转化。

Esbuild官方文档)是一个JavaScript`` Bundler 打包和压缩工具,它提供了与WebpackRollup等工具相似的资源打包能力。可以将JavaScriptTypeScript代码打包分发在网页上运行。但其打包速度却是其他工具的10~100倍。

目前他支持以下的功能:

  • 加载器
  • 压缩
  • 打包
  • Tree shaking
  • Source map生成

esbuild总共提供了四个函数:transformbuildbuildSyncService

image.png

强制缓存

vite 会在这些预打包的模块后加一个 query 字符串带上 hash,然后用 max-age 强缓存。因为这些依赖一般不会变,不用每次都请求,强缓存就行。

image.png