开发 React 项目时,最烦的事情之一就是在页面上看到一个元素,想知道它是哪个组件渲染的,然后在编辑器里翻来翻去找对应的源码。
组件嵌套深了以后,找起来特别痛苦。一个按钮可能经过了好几层 Provider、Wrapper、Container,最后才到真正的 Button 组件。靠肉眼找,效率很低。
有没有办法直接点击页面上的元素,就能跳转到编辑器里对应的源码位置?
有。react-dev-inspector 就是干这个的。
效果演示
开发模式下,按住 Ctrl + Shift,鼠标点击页面上任意元素,编辑器会自动打开对应的源码文件,并定位到那一行。
点击页面上的「立即购买」按钮 -> VSCode 自动打开 src/components/ProductCard.tsx -> 光标定位到第 42 行
不需要手动找文件,不需要打断点,点击就跳。
基本使用
安装:
npm install react-dev-inspector -D
配置 Babel 插件。如果用的是 Vite:
// vite.config.tsimport { defineConfig } from 'vite';import react from '@vitejs/plugin-react';export default defineConfig({ plugins: [ react({ babel: { plugins: ['@react-dev-inspector/babel-plugin'], }, }), ],});
如果用的是 Webpack + Babel:
// babel.config.jsmodule.exports = { plugins: ['@react-dev-inspector/babel-plugin'],};
在入口组件里加上 Inspector:
import { Inspector, InspectParams } from 'react-dev-inspector';function App() { return ( <> <Inspector // 点击后的回调,可以自定义行为 onInspectElement={(params: InspectParams) => { console.log('inspect:', params); }} // 是否禁用(生产环境应该禁用) disable={process.env.NODE_ENV === 'production'} /> {/* 你的应用代码 */} <HomePage /> </> );}
开发模式下,按住 Ctrl + Shift,点击任意元素,就会自动跳转到源码。
原理分析
这套机制能工作,核心依赖三个环节:
- 编译时注入源码位置信息
- 运行时获取点击元素的位置信息
- 通知本地 IDE 打开对应文件
编译时:Babel 插件注入位置信息
@react-dev-inspector/babel-plugin 做的事情很简单:遍历 AST,给每个 JSX 元素添加源码位置属性。
编译前:
function Button() { return <button className="btn">点击</button>;}
编译后:
function Button() { return ( <button className="btn" data-inspector-relative-rect="..." data-inspector-source="src/components/Button.tsx:3:6" > 点击 </button> );}
data-inspector-source 的格式是 文件路径:行号:列号。有了这个属性,运行时就能知道每个 DOM 元素对应的源码位置。
Babel 插件的核心逻辑(简化版):
import { NodePath } from '@babel/traverse';const sourcePlugin = () => ({ visitor: { JSXElement(path: NodePath, state: any) { const { line, column } = path.node.loc?.start || {}; const filename = state.filename; if (!line || !filename) return; // 计算相对于项目根目录的路径 const relativePath = path.relative(process.cwd(), filename); // 添加 data-inspector-source 属性 const sourceAttr = `data-inspector-source`; const sourceValue = `${relativePath}:${line}:${column}`; path.node.openingElement.attributes.push( t.jsxAttribute( t.jsxIdentifier(sourceAttr), t.stringLiteral(sourceValue) ) ); }, },});
注意:这个插件只在开发模式下启用。生产构建不需要、也不应该带上这些调试属性。
运行时:事件监听与信息获取
Inspector 组件挂载后,会监听全局的 keydown 和 click 事件。
核心逻辑:
function Inspector() { useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { // 检测 Ctrl + Shift if (e.ctrlKey && e.shiftKey) { document.body.style.cursor = 'crosshair'; // 进入检查模式 } }; const handleKeyUp = (e: KeyboardEvent) => { if (!e.ctrlKey || !e.shiftKey) { document.body.style.cursor = ''; // 退出检查模式 } }; const handleClick = (e: MouseEvent) => { // 只在检查模式下响应 if (!isInspecting) return; const element = e.target as HTMLElement; const source = element.getAttribute('data-inspector-source'); if (source) { // 解析文件路径和行号 const [filePath, line, column] = parseSource(source); // 通知开发服务器打开文件 openInEditor(filePath, line, column); } e.preventDefault(); e.stopPropagation(); }; window.addEventListener('keydown', handleKeyDown); window.addEventListener('keyup', handleKeyUp); window.addEventListener('click', handleClick, true); return () => { window.removeEventListener('keydown', handleKeyDown); window.removeEventListener('keyup', handleKeyUp); window.removeEventListener('click', handleClick, true); }; }, []);}
点击元素后,从 data-inspector-source 属性中读取文件路径和行号,然后发送请求通知开发服务器。
通知开发服务器
获取到源码位置后,需要通知本地开发服务器,让开发服务器去调用 IDE 的命令行工具打开文件。
通知方式有两种:
方式一:HTTP 请求
async function openInEditor(filePath: string, line: number, column: number) { const params = new URLSearchParams({ fileName: filePath, lineNumber: String(line), colNumber: String(column), }); // 请求开发服务器的特定端点 await fetch(`/__open-in-editor?${params.toString()}`);}
开发服务器(Vite / Webpack Dev Server)收到请求后,调用编辑器的命令:
// Vite 的实现(简化版)server.middlewares.use('/__open-in-editor', async (req, res) => { const { fileName, lineNumber, colNumber } = req.query; // 调用 VSCode 命令行工具 const command = `code -g ${fileName}:${lineNumber}:${colNumber}`; await exec(command); res.end('OK');});
方式二:直接调用编辑器 URI Scheme
function openInEditor(filePath: string, line: number, column: number) { // VSCode URI Scheme const uri = `vscode://file/${filePath}:${line}:${column}`; window.open(uri);}
这种方式更直接,但需要编辑器注册了对应的 URI Scheme。
支持的编辑器
react-dev-inspector 支持多种编辑器:
| 编辑器 | URI Scheme | 命令行 |
|---|---|---|
| VSCode | vscode://file/ | code -g |
| WebStorm | webstorm://open?file= | webstorm |
| Atom | atom://core/open/file? | atom |
| Sublime | subl://open?url= | subl |
可以通过配置指定使用哪个编辑器:
<Inspector onInspectElement={(params) => { // 自定义打开逻辑 const { lineNumber, columnNumber, relativePath } = params; // 使用 VSCode window.open(`vscode://file/${relativePath}:${lineNumber}:${columnNumber}`); }}/>
云开发机场景
现在很多团队用云开发机开发。本地 VSCode 通过 SSH Remote 连接到开发机,项目代码在开发机上,开发服务器也跑在开发机上。
这种场景下,点击页面元素还能跳转到本地 VSCode 吗?
能,而且默认方式就支持。
VSCode Remote 的工作原理
先理解 VSCode SSH Remote 的架构:
graph TB
subgraph 本地电脑
VSCode[VSCode UI]
end
subgraph 远程开发机
VSServer[VSCode Server<br/>文件操作/终端]
CodeCLI[code 命令<br/>VSCode Server 提供]
DevServer[开发服务器<br/>Vite / Webpack]
end
VSCode <-->|SSH 连接| VSServer
CodeCLI -->|IPC 通知| VSServer
DevServer -->|调用| CodeCLI
关键点:远程服务器上的 code 命令不是原生 VSCode 的 CLI,而是 VSCode Server 提供的 CLI。它的行为和本地的 code 命令不同。
为什么默认方式能工作
react-dev-inspector 的 gotoServerEditor 方式会请求开发服务器,开发服务器调用 launch-editor,最终执行:
childProcess.spawn('code', ['/path/to/file.ts', '--line', '10', '--column', '5'])
在本地开发时,这个 code 命令打开本地 VSCode。
在 VSCode Remote 环境下,这个 code 命令被 VSCode Server 拦截:
sequenceDiagram
participant Browser as 浏览器
participant DevServer as 开发服务器
participant LaunchEditor as launch-editor
participant CodeCLI as code 命令
participant VSServer as VSCode Server
participant VSCode as 本地 VSCode
Browser->>DevServer: fetch /__open-in-editor
DevServer->>LaunchEditor: launchEditor(filePath)
LaunchEditor->>CodeCLI: spawn('code', ['file', '--line', '10'])
CodeCLI->>VSServer: IPC 通知
VSServer->>VSCode: SSH 通道转发
VSCode->>VSCode: vscode-remote://ssh-remote+host/file 打开文件
整个过程是自动的,不需要手动构造 vscode-remote:// URI。
前提条件
要让这个机制工作,需要确保:
- 本地 VSCode 通过 SSH Remote 连接到开发机(不是普通的 SSH 终端)
- 远程服务器上
code命令可用:VSCode Remote 连接时会自动安装,位于~/.vscode-server/bin/下 - 开发服务器能访问
code命令:确保PATH包含 VSCode Server 的 bin 目录
可以验证一下:
# 在 VSCode Remote 的终端中执行which code# 应该输出类似:~/.vscode-server/bin/xxx/bin/code
常见问题
问题 1:code 命令找不到
如果开发服务器通过 supervisor 或 pm2 启动,可能没有加载 VSCode Server 的 PATH。
解决方案:在启动脚本中手动添加 PATH:
export PATH="$HOME/.vscode-server/bin/$VSCODE_SERVER_COMMIT/bin:$PATH"npm run dev
或者在 Vite 配置中指定 LAUNCH_EDITOR 环境变量:
# 找到 code 命令的路径which code# 假设输出:/home/user/.vscode-server/bin/abc123/bin/code# 设置环境变量export LAUNCH_EDITOR=/home/user/.vscode-server/bin/abc123/bin/code
问题 2:打开了服务器上的 VSCode(如果有桌面环境)
如果远程服务器有桌面环境且安装了 VSCode,code 命令可能指向本地安装的 VSCode 而不是 VSCode Server 的 CLI。
解决方案:显式指定使用 VSCode Server 的 CLI:
export LAUNCH_EDITOR="$HOME/.vscode-server/bin/$VSCODE_SERVER_COMMIT/bin/code"
自定义行为
如果默认方式不满足需求,可以通过 onInspectElement 回调自定义:
import { Inspector } from 'react-dev-inspector';const isDev = process.env.NODE_ENV === 'development';export function DevInspector() { if (!isDev) return null; return ( <Inspector disable={!isDev} onInspectElement={(params) => { const { lineNumber, columnNumber, relativePath } = params; // 自定义 URI Scheme const uri = `vscode://file/${relativePath}:${lineNumber}:${columnNumber}`; window.open(uri); }} /> );}
开发机上的配置
如果使用 Vite,开发机上的配置和本地一样,不需要特殊处理:
// vite.config.tsexport default defineConfig({ plugins: [ react({ babel: { plugins: ['@react-dev-inspector/babel-plugin'], }, }), ], server: { // 如果需要,可以配置 host 允许外部访问 host: '0.0.0.0', port: 3000, },});
开发机上启动开发服务器后,本地 VSCode 通过 SSH Remote 浏览器打开页面,按住 Ctrl + Shift 点击元素,本地 VSCode 就会自动打开对应文件。
一些实用技巧
高亮当前元素
默认情况下,按住 Ctrl + Shift 时光标会变成十字形,但元素没有高亮效果。可以加一点 CSS 让体验更好:
/* 全局样式 */[data-inspector-source] { position: relative;}/* Inspector 组件内部会根据状态添加高亮 *//* 可以通过 onInspectElement 回调自定义高亮逻辑 */
配合 React DevTools
react-dev-inspector 和 React DevTools 可以配合使用:
- react-dev-inspector:点击元素,跳转到源码
- React DevTools:查看组件树、Props、State
两个工具解决不同的问题,互不冲突。
生产环境安全
确保生产构建不会包含调试属性:
// babel.config.jsmodule.exports = { plugins: [ process.env.NODE_ENV === 'development' && '@react-dev-inspector/babel-plugin', ].filter(Boolean),};
或者在构建配置中排除:
// vite.config.tsexport default defineConfig({ plugins: [ react({ babel: { plugins: process.env.NODE_ENV === 'development' ? ['@react-dev-inspector/babel-plugin'] : [], }, }), ],});
总结
react-dev-inspector 的原理并不复杂:
编译时 Babel 插件给 JSX 元素添加 data-inspector-source 属性 属性值包含文件路径、行号、列号运行时 Inspector 组件监听 Ctrl + Shift + Click 读取点击元素的 data-inspector-source 属性 获取文件路径和位置信息通知编辑器 本地开发:调用 code 命令,VSCode 用 vscode://file/ URI 打开 云开发机:调用 code 命令,VSCode Server 自动转发给本地 VSCode 编辑器打开对应文件并定位到指定行
原理简单,但效果很好。开发 React 项目时,这基本是必备工具。