深入理解 Vite 的实现原理:从浏览器原生 ESM 到预构建与热更新
KongHou

深入理解 Vite 的实现原理:从浏览器原生 ESM 到预构建与热更新

前言

Vite 是目前最受欢迎的前端构建工具之一,以其“秒级启动”和“极速热更新”闻名。但很多人可能只知道它“快”,却不清楚它“为什么快”。

这篇文章,我们就来完整拆解一下 Vite 的实现原理:
从浏览器的原生 ES Module 机制开始,到 esbuild 的依赖预构建,再到 HMR 热更新机制,看看 Vite 是如何用一套极其优雅的设计,让开发体验提升一个量级的。


一、Vite 基于浏览器的原生 ESM 机制

Vite 的核心理念非常简单:

“让浏览器自己去加载模块,而不是像 Webpack 一样提前把所有模块打包好。”

在现代浏览器中,我们可以直接使用:

1
<script type="module" src="/src/main.js"></script>

浏览器会根据 import 路径自动发起网络请求,去下载对应的模块文件。

Vite 就是基于这种机制构建的。
它不再“先打包再启动”,而是起了一个 开发服务(Dev Server)

  • 拦截浏览器对模块的请求;
  • 根据请求的 URL 对源码进行编译;
  • 调用 Vite 插件体系执行对应的 transform
  • 最后把编译结果以 ESM 模块的形式返回给浏览器。

这意味着:
开发时的 Vite 根本不需要打包。
只要浏览器请求到哪个文件,Vite 才编译哪个文件。


二、依赖问题与预构建(deps optimize)

问题在于:
node_modules 下的依赖,往往不是原生 ESM 模块。
例如:

  • 有的包是 CommonJS
  • 有的包体积庞大;
  • 有的包内部又 require 了几十个文件。

这时,如果浏览器直接去加载,就会:

  • 无法识别 CommonJS;
  • 发起成百上千次请求;
  • 启动速度极慢。

为了解决这个问题,Vite 在启动时会进行一次“依赖预构建(dependency pre-bundling)”,也称为 deps optimize


三、Vite 的依赖预构建原理

Vite 使用 esbuild 来做预构建。
过程可以简单理解为:

  1. 启动 Vite 开发服务

    • 这个开发服务是基于 Connect 框架实现的,其中包含了各种各样的中间件处理请求
  2. 扫描依赖

    • 当浏览器请求某个文件时(比如 index.html),

    • Vite 会通过 AST 遍历找出所有需要的 script

    • 提前对这些文件进行编译,编译由不同插件完成

    • 插件就是一个对象,它导出了 transform 方法的话,就会在 transform 的时候被调用。
      (ps: vite 还有其他各种各样的 hooks ,每当执行到某个 hook 时,就会调用 vite 插件对应的钩子函数,而 vite 插件本身是一个对象)
      (ps:伪代码实现如下)

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      const plugins = [
      { name: 'pluginA', transform() { ... } },
      { name: 'pluginB', load() { ... } },
      { name: 'pluginC', transform() { ... } },
      ]
      async function transformRequest(code, id) {
      for (const plugin of plugins) {
      if (plugin.transform) { // 👉 检查插件是否实现了 transform 钩子
      const result = await plugin.transform(code, id)
      if (result) code = result.code || result
      }
      }
      return code
      }
    • 这样,浏览器只要访问了 index.html,那么你依赖的所有的 js 模块,就都给你编译了。

  3. 预构建

    • 调用 esbuild,扫描出所有的依赖;
    • 但是由于 esbuild 不原生支持 import html ,所以 Vite 团队开发了一个 esbuild-scan-plugin ,他会在各种模块解析时做处理
    • 这样 esbuild 便知道哪一些依赖需要预打包并做路径扁平化打包了
    • 从每个依赖包作为入口打包,输出 esm 格式的模块到 node_modules/.vite 下。
    • 并生成一个 _metadata.json 文件,记录每个依赖的 hash 和入口。
  4. 缓存与 hash 控制

    • 打包后的模块路径形如:

      1
      /node_modules/.vite/vue.js?v=1730271856874
    • 浏览器通过 max-age 强缓存;

    • 当重新 build 时,Vite 修改 ?v= 的 hash;

    • 浏览器检测到 query 变化后,自动重新请求最新文件。

    • 但是在 lock 文件变化或者 config 有一些变化的时候也需要重新 build:

也就是说:

Vite 通过浏览器 type 为 module 的 script 可以直接下载 es module + esbuild 机制,实现了“按需加载 + 缓存更新”的完美平衡。


四、开发服务与 Vite 插件体系

在开发模式中,Vite 使用了基于 connect 的轻量 HTTP 服务器(其实是 koa 的简化实现)。

其流程大致如下:

  1. 启动本地开发服务器;
  2. 拦截浏览器请求;
  3. 根据路径判断文件类型;
  4. 调用对应的 Vite 插件 执行 transform(比如 Vue 插件会把 .vue 转成 .js);
  5. 最终返回编译后的模块。

整个过程 不需要打包、不需要压缩、不需要构建目录结构,极其高效。


五、生产环境的打包:Rollup

开发环境靠 esbuild + Dev Server 实现即开即用;
生产环境则使用 Rollup 来进行打包构建。

Vite 的插件体系与 Rollup 插件规范完全兼容:

  • Vite 插件能在 Rollup 构建中直接复用;
  • 确保开发和生产环境的构建逻辑一致;
  • 避免“开发跑得好好的,打包后却出错”的问题。

因此:

Vite 是“开发环境基于 esbuild + 浏览器原生 ESM”,
而“生产环境基于 Rollup 的打包工具链”。


六、HMR:热更新机制

Vite 的模块热更新(HMR)是通过 chokidar + websocket 实现的。

原理如下:

  1. 通过 chokidar 监听文件变化;

  2. 当文件内容发生变动时:

    • 服务端生成新的 timestamp;
    • 通知浏览器通过 WebSocket 收到更新事件;
  3. 浏览器根据模块依赖关系,只重新请求变化的模块;

  4. 更新 DOM 或组件状态,而不刷新整页。

这种机制既保证了 局部刷新 的体验,又保持了 状态不丢失


七、总结:Vite 的巧妙设计

回顾整个流程,Vite 的设计思路非常优雅:

功能 实现原理
模块加载 基于浏览器原生 ESM
源码编译 Dev Server + 插件 transform
依赖处理 esbuild 预构建(deps optimize)
缓存机制 强缓存 + hash query 更新
插件体系 兼容 Rollup 插件标准
热更新 chokidar + WebSocket 实现 HMR

一句话总结:

Vite 是一个基于浏览器原生 ESM 的开发服务器,结合 esbuild 的极速依赖预构建和 Rollup 的生产打包体系,实现了“开发快、构建稳”的现代化前端工具链。


八、结语

从原生 ESM、esbuild 预构建、到 HMR 热更新,每一步的设计都精准地解决了传统打包工具的痛点。

Vite 的出现,不仅仅是“构建更快”,
更是让开发者重新思考前端工程化的本质:

“让浏览器和工具各自做最擅长的事。”


Powered by Hexo & Theme Keep
Total words 23.5k