npm run dev原理讲解与AI Code Review工具
讲解npm dev命令执行流程,并介绍git-lrc AI审查工具。工具宣传性质为主,技术深度有限。
完整中文译文
你好,我是 Maneshwar。我正在开发 git-lrc——一个会在每次 commit 时运行的微型 AI 代码审查工具。它完全免费,并已在 Github 上公开源代码。欢迎为 git-lrc 点 Star,帮助更多开发者发现这个项目。也请试用一下,并分享你的反馈,帮助我们改进项目。
你这辈子大概已经运行过 1,000 次 npm run dev 了。
你看着终端闪烁。
然后切换到浏览器。
但你真的知道背后发生了什么吗?我是说,真正了解吗?
今天,我们将一路深入底层,理解 shell、进程生成、模块图、Hot Module Replacement 和 React Fast Refresh。
第 0 步:你按下 Enter
npm run dev
shell 会在 PATH 中查找 npm 并找到它——通常位于 nvm 安装 Node 的某个目录中,或者 /usr/local/bin/npm 这样的系统路径下。随后,它会把控制权交给 npm CLI,而 npm CLI 本身其实只是一个 Node.js 脚本。
接下来,npm 只做一件事:读取你的 package.json。
{
"scripts": {
"dev": "vite"
}
}
它找到 "dev": "vite",然后准备运行这条命令。
第 1 步:npm 生成一个子进程
npm 会临时把 ./node_modules/.bin 添加到你的 PATH 中,然后生成一个运行 dev 命令的子进程。
因此,"vite" 会解析到 ./node_modules/.bin/vite。它是一个符号链接,指向 node_modules/vite/bin/vite.js 中真正的 Vite 可执行文件。
没有什么魔法。只是 Node 在运行一个 JS 文件。
第 2 步:Vite 启动
Vite 会读取你的 vite.config.js(或 .ts)文件、注册插件,并在提供任何内容之前完成一项非常巧妙的工作。
esbuild 预打包技巧
Vite 使用由 Go 编写、速度快得离谱的 esbuild,对 node_modules 中的依赖进行预处理。
将 CommonJS 转换为 ESM。浏览器无法原生加载 require(),因此 esbuild 会重写它。
把包含大量内部文件的 package 合并成一个文件。lodash 有 600 多个小文件,esbuild 会把它们合并起来,这样浏览器只需发起一个请求,而不是 600 个。
处理结果会缓存在 .vite/deps/ 中。动过 node_modules?缓存就会失效。否则,重新启动时会完全跳过这一步。
第 3 步:dev server 其实就是……一个 HTTP server
Vite 会在 localhost:5173 上启动一个普通的 Node.js HTTP server。
没有 webpack dev server 的魔法,也没有复杂的 middleware 调用链。
当浏览器请求一个文件时,Vite 会:
- 即时转换文件(JSX → JS、TypeScript → JS、
.css→ 注入的<style>) - 将其作为原生 ES Module 提供
浏览器自身会通过 <script type="module"> 处理模块解析。
关键点在于:Vite 在 dev mode 下根本不会打包你的源代码。
浏览器会把每个文件作为 ES module 单独获取。
这就是为什么无论项目有多大,Vite 的 dev 启动速度都接近瞬时完成——它只处理浏览器真正请求的内容。
每一条箭头都代表一次真实的 HTTP 请求。浏览器正在替你遍历模块图。
第 4 步:模块图
随着文件陆续被请求,Vite 会在内部构建一个模块依赖图,用于记录谁导入了谁。
这个图是惰性构建的,只有被请求过的节点才会存在于图中。
如果你从未进入某个路由,对应的组件就永远不会被获取,也不会出现在图中。
正是这种结构让 HMR 如此迅速。
当文件发生变化时,Vite 不会扫描你的全部代码。
它只需要遍历这个图。
第 5 步:保存文件——有意思的部分来了
5a. OS 立即通知 Vite
Vite 使用 chokidar,它对 OS 原生的文件系统事件进行了封装:
完全不需要轮询。编辑器把内容写入磁盘的那一刻,OS 就会立即拍一拍 Vite 的肩膀。
5b. 遍历模块图,寻找 HMR 边界
Vite 从发生变化的文件开始,沿模块图向上遍历,寻找第一个通过 import.meta.hot.accept() 注册了 HMR handler 的祖先模块。
使用 React 时,你不需要亲自编写 import.meta.hot.accept()。
@vitejs/plugin-react 插件使用 React Fast Refresh,在转换阶段自动把它注入每一个组件文件中。整个过程对你不可见。
如果一路向上直到 main.jsx 都没有找到边界,就会执行整页刷新。
当你编辑 vite.config.js,或者编辑一个不支持 HMR 的非组件 JS 工具文件时,就会看到这种情况。
5c. Server 只重新转换发生变化的文件
Vite 只会让 Button.jsx 重新通过它的插件流水线,包括 JSX 转换、所有 Babel 插件等。
最终结果是内存中的一个全新 ES module。
5d. 浏览器通过动态 import 获取新模块
HMR client(Vite 注入页面的一小段脚本)会收到有更新可用的通知,然后调用:
import('/components/Button.jsx?t=1718023456789')
其中的 ?t=timestamp 查询参数用于绕过缓存。浏览器看到一个从未获取过的 URL,就会发起一次真实的 HTTP 请求,获得新模块。
5e. 模块替换——hot.dispose 与 hot.accept
Vite 的 HMR runtime 围绕两个函数构建。你需要在 import.meta.hot 上注册它们;使用 React 时,则由插件负责注册:
// In a module that wants to handle its own updates:
// Clean up before this module is replaced —
// cancel timers, unsubscribe, remove listeners.
import.meta.hot.dispose((data) => {
clearInterval(timer)
})
// Receive the freshly imported module and re-wire things.
import.meta.hot.accept((newModule) => {
// newModule is the updated exports
})
这里有一个细微但很重要的细节:Vite 的 HMR 并不会真的为调用链上游的所有模块替换原来导入的模块对象。
接受更新的模块,也就是 HMR 边界,需要负责获取新的 exports 并应用它们。
正是这个简化模型,让 Vite 可以跳过重写所有 importer 的昂贵工作;对于几乎所有真实的开发场景来说,这已经足够了。
你可以在 dispose 中取消 setInterval、退订 store,或者移除 event listener——也就是清理那些在旧模块直接被废弃时可能造成泄漏的内容。
关于 CSS:stylesheet 采用的路径略有不同。当 .css 文件发生变化时,Vite 不会动态重新导入一个 JS module,而是将 <link>(或 <style>)标签替换为更新后的 stylesheet,从而避免出现无样式内容闪烁。
第 6 步:React Fast Refresh 执行真正的重新渲染
React Fast Refresh 是 React 团队构建的一套独立系统。当新的组件模块到达时,它会:
- 比较 hook signature。如果 Button 之前包含
useState、useEffect,更新后仍然包含useState、useEffect,那么 state 就会保留。添加或删除了一个 hook?只会重置这个组件的 state。 - 比较 hook signature。如果 Button 之前包含
useState、useEffect,更新后仍然包含useState、useEffect,那么 state 就会保留。添加或删除了一个 hook?只会重置这个组件的 state。 - 找出 React fiber tree 中所有仍在运行的实例。
- 找出 React fiber tree 中所有仍在运行的实例。
- 精准地只重新渲染受影响的子树。父组件不会重新渲染,兄弟组件不会重新渲染。只有发生变化的组件及其子组件会重新渲染。
- 精准地只重新渲染受影响的子树。父组件不会重新渲染,兄弟组件不会重新渲染。只有发生变化的组件及其子组件会重新渲染。
这就是为什么你可以在表单填写到一半时修改组件样式,而表单不会被重置。表单 state 存在于父组件或兄弟组件中——Fast Refresh 根本没有动过它。
为什么这很重要?
如果你使用 webpack dev server,保存文件会触发整个 bundle 的重新构建,即使启用了 HMR 也是如此。对于中等规模的项目,这可能意味着从保存文件到浏览器完成更新之间会出现明显的延迟。
借助 Vite 的原生 ESM 方案,server 几乎什么都不用做——只转换一个文件;浏览器只获取一个 URL;Fast Refresh 也只会更新一棵子树。
整个往返过程通常非常快,而且随着项目规模增长,依然能够保持这种速度。
这绝不是微不足道的改进。
它从根本上改变了你的工作方式——编辑 → 看到结果,几乎瞬间完成。
AI Agent 写代码很快,但它们也会在不通知你的情况下,悄悄删除逻辑、改变行为并引入 bug。你往往要到 production 环境中才会发现。
git-lrc 解决了这个问题。它会接入 git commit,在每一个 diff 落地之前进行审查。60 秒即可完成配置。完全免费。*
欢迎提供任何反馈,也欢迎参与贡献!它已经上线、公开源代码,任何人都可以直接使用。
在 Commit 时运行的免费微型 AI 代码审查
| 🇩🇰 Dansk | 🇪🇸 Español | 🇮🇷 Farsi | 🇫🇮 Suomi | 🇯🇵 日本語 | 🇳🇴 Norsk | 🇵🇹 Português | 🇷🇺 Русский | 🇦🇱 Shqip | 🇨🇳 中文 | 🇮🇳 हिन्दी |
|---|
在 Commit 时运行的免费微型 AI 代码审查
AI Agent 写代码很快,但它们也会在不通知你的情况下,悄悄删除逻辑、改变行为并引入 bug。你往往要到 production 环境中才会发现。
git-lrc 解决了这个问题。它会接入 git commit,在每一个 diff 落地之前进行审查。60 秒即可完成配置。完全免费。
看看 git-lrc 如何发现严重的安全问题,例如凭据泄漏、成本高昂的云端操作,以及 log statement 中的敏感信息。
🤖 AI Agent 会悄悄破坏代码:代码被删除、逻辑被修改、边界情况消失。直到问题进入 production,你才会注意到。
🔍 在代码发布之前发现问题。由 AI 驱动的 inline comment 会准确告诉你哪些内容发生了变化,以及哪些地方看起来不对劲。
部分评论可能仅对已登录的访客可见。请登录以查看全部评论。
如需采取进一步措施,你可以考虑屏蔽此人和/或举报滥用行为。