Vite 作为下一代前端的工具链,为开发提供极速的响应。具有:

  • 极速的服务启动,使用原生的 ESM 文件,无需打包。
  • 轻量快速的热重载,无论应用程序大小如何,都始终极快的模块热替换(HMR)。
  • 丰富的功能,对 TypeScript、JSX、CSS 等支持开箱即用。
  • 优化构建,可选 “多页应用” 或 “库” 模式的预配置 Rollup 构建
  • 通用的插件, 在开发和构建之间共享 Rollup-superset 插件接口。
  • 完全类型化的API,灵活的 API 和完整的 TypeScript 类型。

Vite,基于浏览器原生 ES imports 的开发服务器。利用浏览器去解析 imports,在服务器端按需编译返回,完全跳过了打包这个概念,服务器随起随用。同时不仅有 Vue 文件支持,还搞定了热更新,而且热更新的速度不会随着模块增多而变慢。

Vite 总结一下,最大的特点就是:

  • 基础 ESM,实现快速启动和模块热更新。
  • 在服务端实现按需编译。

开发者在代码中写到的 ESM 导入语法会直接发送给服务器,而服务器也直接将 ESM 模块内容运行处理后,下发给浏览器。接着,现代浏览器通过解析 script module,对每一个 import 到的模块进行 HTTP 请求,服务器继续对这些 HTTP 请求进行处理并响应。
image.png
那 Vite 是如何做到这一切的了?我们一起来分析一下。

准备工作

首先用 create-vue 创建一个 vite + vue3 的项目,安装依赖, npm run dev 把 vite 的开发服务跑起来。

  1. npm init vue@3
  2. npm install
  3. npm run dev

然后从 github 把 vite 源码下载下来,目的是方便源码的分享。还有一个目的是为源码调试做准备。调测教程可看这里。由于 vite 现在有多个版本,本文分析的源码都以当前最新源码为准。

注意:Vite 现在有 v1、v2、v3 三个大的版本。从 vite@2.x 开始 vite 不使用 koa来创建服务和管理中间件了,而是使用connect。原因在于,vite@2.x 更多基于 hooks 的插件的方式,对于 koa 中间件的需求大幅度减少,从依赖成本上看, connect 方便轻巧已经可以满足要求。如果你想去查阅源码,每一个版本都可以。

Vite 实现原理分析

现在我们创建的项目已经跑起来了。
image.png
我们启动项目是使用npm run dev,那这个命令发出之后,进行了什么处理了?

npm run dev 做了什么?

找到 vite 中的源代码,vite 首先通过 cac 作为简单的参数解析器,来对我们运行的命令参数进行解析。[cac](https://github.com/cacjs/cac#readme)是一个用于构建 CLI 应用程序的 JavaScript 库。

  1. const cli = cac('vite');

cac这里简单在插一嘴,非常的实用,具有:

  • 超轻量级:没有依赖,只有一个文件。
  • 易于学习。构建简单的 CLI 只需要学习 4 个 API cli.option cli.version cli.help cli.parse:.
  • 却如此强大。启用默认命令、类 git 子命令、验证所需参数和选项、可变参数、点嵌套选项、自动帮助消息生成等功能。
  • 开发人员友好。用 TypeScript 编写。

大家如果有 CLI 应用程序的需要可以用用它。
vite 利用 cac生成了很多命令的入口,根据不同的命令行命令,执行不同的入口函数。
image.png

源代码地址

当执行npm run dev时就会 cli dev 的 action 回调。
image.png
通过runServe 方法,启动了一个 Server,来实现对浏览器请求的响应。通过 connect创建服务,Connect 是一个用于节点的可扩展 HTTP 服务器框架。在 vite@1.x 是使用 koaServer 来启动服务,vite@2.x 开始更多基于 hooks 的插件的方式,减少 koa 中间件的使用。所以 从 vite@2.x 开始 vite 不使用 koa 来创建服务和管理中间件了,而是使用connect。
runServe 方法调用 createServer 方法,该执行做了很多工作,如整合配置项、创建 http 服务、创建 WebSocket 服务、创建源码的文件监听、插件执行、optimize 优化等。
image.png
这里runServe 方法具体的操作,有兴趣的同学可以去调试一下,看看发生了什么,调试的教程看这里
image.png
服务启动,然后实现对浏览器请求的响应。

预构建

当你首次启动 vite 时,vite 会将预构建的依赖缓存到 node_modules/.vite。
image.png
vite 预编译.gif
它根据几个源来决定是否需要重新运行预构建步骤:

  • package.json 中的 dependencies 列表
  • 包管理器的 lockfile,例如 package-lock.json, yarn.lock,或者 pnpm-lock.yaml
  • 可能在 vite.config.js 相关字段中配置过的

只有在上述其中一项发生更改时,才需要重新运行预构建。
如果出于某些原因,你想要强制 vite 重新构建依赖,你可以用 —force 命令行选项启动开发服务器,或者手动删除 node_modules/.vite 目录。
预构建过程其实有两个目的:

  • CommonJS 和 UMD 兼容性: 开发阶段中,Vite 的开发服务器将所有代码视为原生 ES 模块。因此,Vite 必须先将作为 CommonJS 或 UMD 发布的依赖项转换为 ESM。
  • 性能: Vite 将有许多内部模块的 ESM 依赖关系转换为单个模块,以提高后续页面加载性能。

接着我们在浏览器访问启动的服务。

index.html

浏览器在访问,http://127.0.0.1:5173/后,得到了响应主体。

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <script type="module" src="/@vite/client"></script>
  5. <meta charset="UTF-8" />
  6. <link rel="icon" href="/favicon.ico" />
  7. <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  8. <title>Vite App</title>
  9. </head>
  10. <body>
  11. <div id="app"></div>
  12. <script type="module" src="/src/main.js"></script>
  13. </body>
  14. </html>

这里有两个script type="module"第一个是 vite 给我们添加上的,我们后面再讲,第二个是我们添加的。
根据 ESM 规范在浏览器脚本请求中的实现。当出现 script 标签 type 属性为 module 时,浏览器将会请求模块相应内容。
当然 ESM 规范也可以将这里写成这样的形式。

  1. <script type="module">
  2. import xxx from '/src/main.js
  3. </script>

浏览器依然会发起 HTTP 请求,请求 HTTP Server 托管的脚本。接下往下,浏览器发现请求,请求main.js

main.js

image.png
如果认真观察就会发现和我们源码是不太一样的。
image.png
最大的不一样就是,import { createApp } from ‘vue’ 改为 import { createApp } from ‘/node_modules/.vite/deps/vue.js?v=19dbb026’。原因在于 import 对应的路径只支持 "/""./"或者"../"开头的内容,直接使用模块名 import,会立即报错。
vite 是怎么实现的了?
当我们进行浏览器访问时,vite 拦截到请求http://localhost:5173/src/main.js,然后获取请求的所有内容,
image.png

  1. 'import { createApp } from 'vue'\nimport App from './App.vue'\nimport router from './router'\n\nimport './assets/main.css'\n\nconst app = createApp(App)\n\napp.use(router)\n\napp.mount('#app')\n'

接着对请求的内容通过 es-module-lexermagic-string这两个库对模块的路径进行重写。

es-module-lexer:JS 模块语法词法分析器 magic-string:字符替换

也就将 import 直接导入的模块进行了转义。也就是预构建的缓存node_modules/.vite中。

  1. 'import { createApp } from '/node_modules/.vite/deps/vue.js?v=19dbb026'\nimport App from '/src/App.vue'\nimport router from '/src/router/index.js'\n\nimport '/src/assets/main.css'\n\nconst app = createApp(App)\n\napp.use(router)\n\napp.mount('#app')\n'

从原理分析 Vite 实现 - 图12
最后根据 main.js 的内容,进行资源的请求:

  1. '/node_modules/.vite/deps/vue.js?v=19dbb026'
  2. '/src/App.vue'
  3. '/src/router/index.js'
  4. '/src/assets/main.css'

image.png

App.vue

对 /src/App.vue 类请求进行处理,这就涉及 Vite 服务器的编译能力了。
image.png
这其实和我们写的源码完全不一样,当 vite 拦截到 App.vue 的请求时,
image.png
会对其内容进行获取,然后通过转换方法进行转换。
image.png
对于 .vue 这样的单文件组件,内容会有 script、style 和 template,在经过 Vite Server 处理时,服务端对 script、style 和 template 三部分分别处理。对于具体的编译处理实现,我这里不过多的赘述。对应中间件关键内容可在源码 plugin-vue 中找到。

  • 单文件组件中,对于 style 部分的编译,编译为对应 style 样式的 import 请求。
  • 单文件组件中,对于 template 部分的编译,编译为对应 template 样式的 import 请求。

总而言之,每一个 .vue 单文件组件都被拆分成多个请求。不同的请求,会有不同的 type,执行不同的解析操作。然后将其解析后的内容进行返回。
从原理分析 Vite 实现 - 图17
整体来说:

  • vite 利用浏览器原生支持 ESM,省略了对模板的打包过程,这和 webpack 完全不同,所以在初次启动是非常的快的。
  • 在更新时,由于浏览器原生支持 ESM,也不需要打包,所以对 HRM 也是非常的友好。
  • 在 vite 开发模式下,在服务端完成模块的改写和请求处理,将业务代码中的 import 第三方依赖路径转为浏览器可识别的依赖路径,对 .ts、.vue 等文件进行即时编译,对 Sass/Less 的需要预编译的模块进行编译,浏览器端建立 socket 连接,实现 HMR,实现真正的按需编译。

接下来在看说说 vite 的 HRM 更新机制。

Vite HRM

对于 HRM ,不管是 webpack 还是 vite,主要的原理都是通过监听模块内容的变动来响应浏览器。而 vite 的 HMR 特性,可总结为三步:

  1. 启动服务时,通过 watcher 监听文件改动。
  2. 模块变动时,通过服务端编译资源,推送新模块内容给浏览器。
  3. 浏览器收到新的模块内容,执行框架层面的重渲染。

而这一切的始作俑者就是在 index.html 中有一段引入 /vite/client 代码。
image.png
image.png
这段代码是 vite给我们添加上的,它是干什么的了?
它的目的就是进行 WebSocket 的注册和监听。在浏览器端通过 WebSocket 监听了一些更新的类型:

  • vue 组件更新
  • vue template 更新
  • vue style 更新
  • css 更新
  • css 移除
  • js 更新
  • 页面 roload

来触发更新操作,服务端通过创建的 watcher 来监听文件的改动,然后做出相应的处理操作,当处理完之后,发布变动,通知到浏览器。
从原理分析 Vite 实现 - 图20

总结

在浏览器支持 ES 模块之前,JavaScript 并没有提供的原生机制让开发者以模块化的方式进行开发。这也正是我们对 “打包” 这个概念熟悉的原因:使用工具抓取、处理并将我们的源码模块串联成可以在浏览器中运行的文件。
然而,当我们开始构建越来越大型的应用时,需要处理的 JavaScript 代码量也呈指数级增长。包含数千个模块的大型项目相当普遍。我们开始遇到性能瓶颈 —— 使用 JavaScript 开发的工具通常需要很长时间(甚至是几分钟!)才能启动开发服务器,即使使用 HMR,文件修改后的效果也需要几秒钟才能在浏览器中反映出来。如此循环往复,迟钝的反馈会极大地影响开发者的开发效率和幸福感。
Vite 旨在利用生态系统中的新进展解决上述问题:浏览器开始原生支持 ES 模块,且越来越多 JavaScript 工具使用编译型语言编写。

本文,通过流程分析了 Vite 的实现,分析了 Vite 如何利用 ESM。事实上,Vite 依赖优化的灵感来自 Snowpack,这类 bundleless 工具也代表着一种新趋势、新方向。

参考