运行 `npm run dev` 会发生什么?
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依赖项的速度快得惊人。
它有两个作用:
- 将 CommonJS 转换为 ESM。浏览器无法原生加载 ESM
require(),因此 esbuild 会对其进行重写。 - 将包含大量内部文件的包合并成一个。该包
lodash包含 600 多个小文件。esbuild 会将它们合并,因此浏览器只需发出一个请求,而不是 600 个。
结果已缓存.vite/deps/。点击node_modules“?”按钮清除缓存。
否则,重启后将完全跳过此步骤。
步骤 3:开发服务器实际上就是一个 HTTP 服务器。
Vite 在 . 上启动一个普通的 Node.js HTTP 服务器localhost:5173。
没有webpack开发服务器的魔法,也没有复杂的中间件链。
当浏览器请求文件时,Vite:
- 从磁盘读取
- 即时转换(JSX → JS,TypeScript → JS,
.css→注入<style>) - 将其用作原生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 团队构建的一个独立系统。当新的组件模块发布时,它会:
-
比较钩子签名。如果之前
Button存在useState, useEffect且useState, useEffect之后仍然存在,则状态保持不变。添加或移除钩子?状态只会重置该组件。 -
查找React fiber 树中所有存活的实例。
-
精确地重新渲染仅针对受影响的子树。父组件不会重新渲染。兄弟组件也不会重新渲染。只有更改后的组件及其子组件会重新渲染。
这就是为什么在表单填写过程中,您可以编辑组件的样式,而表单不会重置的原因。表单状态保存在父组件或同级组件中——快速刷新从未触及过它。
完整画面
这为什么重要?
如果你使用的是 webpack 的开发服务器,即使启用了热模块替换 (HMR),文件保存也会触发打包重建。对于中等规模的项目来说,这会导致保存和浏览器更新之间出现明显的延迟。
使用 Vite 的原生 ESM 方法,服务器几乎什么都不做(只需转换一个文件),浏览器只需获取一个 URL,快速刷新功能只需修改一个子树。
整个往返过程通常非常快,并且随着项目的增长,速度也会保持很快。
这并非微小的改进。
它从根本上改变了你的工作方式——编辑→查看结果几乎是瞬间完成的。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐






所有评论(0)