Vite 构建原理

  • 快速的冷启动:No Bundle + esbuild 预构建
  • 即时的模块热更新: 基于 ESM 的 HMR,同时利用浏览器缓存策略提升速度
  • 真正的按需加载: 利用浏览器的 ESM 支持,实现真正的按需加载

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

核心原理

基于 ESM 的 Dev server

在 Vite 出来之前,传统的 打包工具 如 webpack 是先解析依赖、打包构建再启动开发服务器,Dev server 必须等所有的模块构建完成,当我们修改了 bundle 模块中的一个子模块,整个 bundle 文件都会重新打包然后输出。项目应用越大,启动时间越长。
而 Vite 利用浏览器对 ESM 的支持,当 import 模块时,浏览器就会下载被导入的模块。先启动开发服务器,当代码执行到模块加载时再请求对应模块的文件,本质上实现了动态加载。暂时没有用到的路由代码不会参与构建过程。项目应用增多,也不会影响其构建速度

基于 ESM 的 HMR 热更新

目前所有的打包工具实现热更新的思路都大同小异,主要是通过 websocket 创建浏览器和服务器的通信监听文件的改变,当文件被修改时,服务端发送消息通知客户端修改相应的代码,客户端对应不同的文件进行不同的操作更新。

  • Webpack: 重新编译,请求变更后模块的代码,客户端重新加载
  • Vite: 请求变更的模块,再重新加载

Vite 通过 chokidar 来监听文件系统的变更,只用对发送生变更的模块重新加载,只需要精确的使相关模块与其临近的 HMR 边界连接失效即可,这样 HMR 更新速度就不会因为应用体积的增加而变慢。
而 Webpack 还要经历一次打包构建。所以 HMR 场景下,Vite 表现也比 Webpack 好

Vite 的整个热更新分为 4 步:

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

Vite本地启动时会创建一个WebSocket连接,同时去监听本地的文件变化
当用户修改了本地的文件时,WebSocket的服务端会拿到变化的文件的ID或者其他标识,并推送给客户端
客户端获取到变化的文件信息之后,便去请求最新的文件并刷新页面

客户端:websocket 通信和更新处理
当我们配置了热更新且不是 ssr 的时候,Vite 底层在处理 html的时候会把 HMR 相关的客户端代码写入到我们的代码中,当接收到服务端推送的消息,通过不同的消息类型做相应的处理,如(connected、update、custom…)在实际开发热更新中使用最频繁的是 update和full-reload 事件。

优化:浏览器的缓存策略提高响应速度:**Vite 还利用HTTP加速整个页面的重新加载。设置响应头使得依赖模块(dependency module)进行强缓存,而源码文件通过设置 304 Not Modified 而变成可依据条件而进行更新。

基于 esbuild 的依赖预编译优化

为什么需要预构建 ?

  1. 支持 common JS 依赖
  2. 上面提到 Vite 是基于浏览器原生支持 ESM 的能力实现的,但要求用户的代码模块必须是 ESM 模块,因此必须将 common JS 的文件提前处理,转成 ESM 模块并缓存如 node_modules/.vite
  3. 减少模块和请求数量

Vite 将有许多内部模块的 ESM 依赖关系转换为单个模块,以提高后续页面性能加载。

为什么使用 esbuild

  1. 编译运行 VS 解释运行
    大多数前端打包工具都是基于 JavaScript 实现的,大家都知道 JavaScript 是解释型语言,边运行边解释。而 ESBuild 采用 go 编写,该语言可以编译成原生代码,在编译的时候都将语言转为机器语言,在启动时直接执行,更具性能优势。

  2. 多线程 VS 单线程

JavaScript 本质上是一门单线程语言,直到引入 webworker 之后才有可能在浏览器、Node 中实现多线程操作。
Go 天生具有多线程优势

  1. 对构建流程进行了优化 充分利用 CPU 资源

实现原理
Vite 预编译之后,将文件缓存在 node_modules/.vite 文件夹下。根据以下地方来决定是否需要重新执行预构建

  • package.json 中 dependencies 发送变化
  • 包管理器的 lockfile

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

基于 Rollup 的 Plugins

Vite 从 preat 的 WMR 中得到启发,将 Vite Plugins 继承 Rollup Plugins API,在其基础上进行一些扩展,同时 Vite 也基于 Rollup plugins 机制提供了强大的插件 API

vite 插件是什么
使用 vite 插件可以扩展 vite 能力,通过暴露一些构建打包过程的一些时机配合工具函数,让用户可以自定义写一些配置代码,执行在打包过程中。比如解析用户自定义的文件输入,在打包代码前转译代码

在实际的实现中,vite 只需要基于 rollup 设计的接口进行扩展。

Vite 钩子函数

  • config: 可以在 Vite 被解析之前修改 Vite 的相关配置
  • configResolved: 解析Vite 配置后调用,配置确认
  • configureserverL: 主要用来转换 index.html,为 dev-server 添加自定义的中间件
  • transformindexhmlt: 主要用来转换 index.html,钩子接收当前的 HTML 字符串和转换上下文
  • handlehotupdate: 执行自定义 HMR 更新,可以通过 ws 往客户端发送自定义事件