npm run dev你一生中大约跑过1000 次。

你看到终端闪烁了一下。

您已切换到浏览器。

奇迹发生了。

但你真的了解事情的真相吗?我是说,真的了解吗?

今天我们将深入理解 shell、进程生成、模块图、热模块替换、React 快速刷新。

步骤 0:按下 Enter 键

<span style="color:#f8f8f2"><span style="color:#f8f8f2"><code>npm run dev
</code></span></span>

npm你的 shell 会在你的目录中查找PATH并找到它——通常位于nvm安装 Node 的任何目录中,或者位于类似 的系统路径中/usr/local/bin/npm。它将控制权交给 npm CLI,而 npm CLI 本身只是一个 Node.js 脚本。

然后 npm 只做一件事:读取你的package.json.

<span style="color:#f8f8f2"><span style="color:#f8f8f2"><code><span style="color:#f8f8f2">{</span>
  <span style="color:#f8f8f2">"scripts"</span><span style="color:#f8f8f2">:</span> <span style="color:#f8f8f2">{</span>
    <span style="color:#f8f8f2">"dev"</span><span style="color:#f8f8f2">:</span> <span style="color:#f2ca27">"vite"</span>
  <span style="color:#f8f8f2">}</span>
<span style="color:#f8f8f2">}</span>
</code></span></span>

它找到"dev": "vite"并准备运行它。

步骤 1:npm 生成一个子进程

npm 会暂时将命令添加./node_modules/.bin到你的 PATH环境变量中PATH,然后生成一个子进程来运行该dev命令。

因此,"vite"它解析为./node_modules/.bin/vite,这是一个指向内部实际 Vite 二进制文件的符号链接node_modules/vite/bin/vite.js

没什么神奇的,只是Node运行了一个JS文件而已。

步骤 2:Vite 启动

Vite会读取你的vite.config.js(或.ts),注册插件,并在提供任何内容之前执行一些非常巧妙的操作。

esbuild 预打包技巧

Vite 使用Go 语言编写的esbuild,预处理node_modules依赖项的速度快得惊人。

它有两个作用:

  1. 将 CommonJS 转换为 ESM。浏览器无法原生加载 ESM require(),因此 esbuild 会对其进行重写。
  2. 将包含大量内部文件的包合并成一个。该包 lodash包含 600 多个小文件。esbuild 会将它们合并,因此浏览器只需发出一个请求,而不是 600 个。

结果已缓存.vite/deps/。点击node_modules“?”按钮清除缓存。
否则,重启后将完全跳过此步骤。

步骤 3:开发服务器实际上就是一个 HTTP 服务器。

Vite 在 . 上启动一个普通的 Node.js HTTP 服务器localhost:5173

没有webpack开发服务器的魔法,也没有复杂的中间件链。

当浏览器请求文件时,Vite:

  1. 从磁盘读取
  2. 即时转换(JSX → JS,TypeScript → JS,.css→注入<style>
  3. 将其用作原生ES 模块

浏览器本身使用 . 来处理模块解析<script type="module">

关键在于:Vite 在开发模式下根本不会打包你的源代码。

浏览器将每个文件作为 ES 模块单独获取。

这就是为什么无论项目大小,Vite 的开发启动速度几乎是瞬间完成的,因为它只处理浏览器实际请求的内容。

每个箭头都代表一个真实的HTTP请求。浏览器会自动为你完成图遍历。

步骤 4:模块图

当请求文件时,Vite 会构建一个内部模块依赖关系图 ,即谁导入了什么的映射。

这个图是惰性的,只有被请求的节点才会存在于其中。

如果你从未导航到某个路由,那么这些组件就永远不会被获取,也不会出现在图中。

这就是HMR速度快的原因。

当文件发生更改时,Vite 不会扫描所有代码。

它只是沿着这个图走。

第五步:保存文件——精彩之处就在这里。

5a. 操作系统立即通知 Vite

Vite 使用chokidar,它封装了操作系统原生的文件系统事件:

操作系统 API 机制
Linux inotify 内核直接监视 inode。
macOS FSEvents 核心服务文件系统事件 API
视窗 ReadDirectoryChangesW Win32 API

零轮询。操作系统会在编辑器刷新到磁盘的瞬间立即通知 Vite。

5b. 遍历图,找到 HMR 边界

Vite 从更改的文件开始向上遍历模块图,查找第一个通过import.meta.hot.accept().

使用 React,你无需import.meta.hot.accept()自己编写代码。

@vitejs/plugin-react插件(使用React Fast Refresh)会在组件转换时自动将其注入到每个组件文件中。您对此不可见。

如果找不到边界,则一直进行main.jsx 页面完全重新加载。

vite.config.js当您编辑非组件化的 JS 实用程序(不支持 HMR)时,就会发生这种情况。

5c. 服务器仅重新转换已更改的文件

Vite 只是重新运行Button.jsx其插件管道 JSX 转换、任何 Babel 插件等。

结果是在内存中创建了一个新的 ES 模块。

5d. 浏览器通过动态导入获取新模块

HMR客户端(Vite注入到页面中的一个小型脚本)被告知有更新可用,并调用:

<span style="color:#f8f8f2"><span style="color:#f8f8f2"><code><span style="color:#f39c12">import</span><span style="color:#f8f8f2">(</span><span style="color:#f2ca27">'</span><span style="color:#f2ca27">/components/Button.jsx?t=1718023456789</span><span style="color:#f2ca27">'</span><span style="color:#f8f8f2">)</span>
</code></span></span>

查询?t=timestamp参数是清除缓存的关键。浏览器看到一个之前从未获取过的URL,就会发出真正的HTTP请求,从而获取新的模块。

5e. 模块交换——hot.dispose以及hot.accept

Vite 的 HMR 运行时围绕两个函数构建,您(或者,如果使用 React,则为插件)需要注册这两个函数import.meta.hot

<span style="color:#f8f8f2"><span style="color:#f8f8f2"><code><span style="color:#808080">// In a module that wants to handle its own updates:</span>

<span style="color:#808080">// Clean up before this module is replaced —</span>
<span style="color:#808080">// cancel timers, unsubscribe, remove listeners.</span>
<span style="color:#f39c12">import</span><span style="color:#f8f8f2">.</span><span style="color:#7ed07e">meta</span><span style="color:#f8f8f2">.</span><span style="color:#7ed07e">hot</span><span style="color:#f8f8f2">.</span><span style="color:#7ed07e">dispose</span><span style="color:#f8f8f2">((</span><span style="color:#7ed07e">data</span><span style="color:#f8f8f2">)</span> <span style="color:#f9690e">=></span> <span style="color:#f8f8f2">{</span>
  <span style="color:#7ed07e">clearInterval</span><span style="color:#f8f8f2">(</span><span style="color:#7ed07e">timer</span><span style="color:#f8f8f2">)</span>
<span style="color:#f8f8f2">})</span>

<span style="color:#808080">// Receive the freshly imported module and re-wire things.</span>
<span style="color:#f39c12">import</span><span style="color:#f8f8f2">.</span><span style="color:#7ed07e">meta</span><span style="color:#f8f8f2">.</span><span style="color:#7ed07e">hot</span><span style="color:#f8f8f2">.</span><span style="color:#7ed07e">accept</span><span style="color:#f8f8f2">((</span><span style="color:#7ed07e">newModule</span><span style="color:#f8f8f2">)</span> <span style="color:#f9690e">=></span> <span style="color:#f8f8f2">{</span>
  <span style="color:#808080">// newModule is the updated exports</span>
<span style="color:#f8f8f2">})</span>
</code></span></span>

一个微妙但重要的细节:Vite 的 HMR 并不会真的替换链上所有导入的原始模块对象。

接收模块(即 HMR 边界)负责接收新的导出数据并应用它们。

这种简化的模型使得 Vite 可以省去重写每个导入器的昂贵工作,并且足以满足几乎所有实际开发场景的需求。

dispose这里可以取消操作setInterval、取消订阅商店或移除事件监听器,任何如果旧模块被弃用就会泄漏的数据都可以在这里处理。

关于 CSS 的说明:样式表的处理方式略有不同。当.css文件发生更改时,Vite 不会动态地重新导入 JS 模块,而是将 `<style>` 标签<link>(或 `<style> <style>` 标签)替换为更新后的样式表,从而避免出现未样式化内容的闪烁。

步骤 6:React 快速刷新执行实际的重新渲染。

React Fast Refresh 是 React 团队构建的一个独立系统。当新的组件模块发布时,它会:

  1. 比较钩子签名。如果之前Button存在useState, useEffectuseState, useEffect之后仍然存在,则状态保持不变。添加或移除钩子?状态只会重置该组件。

  2. 查找React fiber 树中所有存活的实例。

  3. 精确地重新渲染仅针对受影响的子树。父组件不会重新渲染。兄弟组件也不会重新渲染。只有更改后的组件及其子组件会重新渲染。

这就是为什么在表单填写过程中,您可以编辑组件的样式,而表单不会重置的原因。表单状态保存在父组件或同级组件中——快速刷新从未触及过它。

完整画面

这为什么重要?

如果你使用的是 webpack 的开发服务器,即使启用了热模块替换 (HMR),文件保存也会触发打包重建。对于中等规模的项目来说,这会导致保存和浏览器更新之间出现明显的延迟。

使用 Vite 的原生 ESM 方法,服务器几乎什么都不做(只需转换一个文件),浏览器只需获取一个 URL,快速刷新功能只需修改一个子树。

整个往返过程通常非常快,并且随着项目的增长,速度也会保持很快。

这并非微小的改进。

它从根本上改变了你的工作方式——编辑→查看结果几乎是瞬间完成的。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐