前言
上一篇我们介绍了 React Grab 是什么、怎么用。
这一篇,我们来聊点硬核的——它到底是怎么做到的?
当你按下 ⌘C 点击一个按钮,它是怎么知道这个按钮:
- 住在哪个文件
- 第几行代码
- 属于哪个组件
- 有什么样式
让我们掀开它的底裤看看。
整体架构:三层蛋糕
React Grab 的架构可以理解为三层蛋糕:
graph TB
subgraph ReactGrab["React Grab"]
subgraph Layers["三层架构"]
Event["🎯 事件层<br/>(原生 DOM)"]
Analysis["🔍 分析层<br/>(bippy)"]
UI["🎨 UI 层<br/>(Solid.js)"]
end
Core["⚙️ 核心协调器<br/>(core.tsx)"]
Clipboard["📋 剪贴板 API"]
Event --> Core
Analysis --> Core
UI --> Core
Core --> Clipboard
end
style Event fill:#e1f5fe
style Analysis fill:#fff3e0
style UI fill:#f3e5f5
style Core fill:#e8f5e9
style Clipboard fill:#fce4ec
第一层:事件层 - 监听你的键盘和鼠标,知道你什么时候想"抓"东西
第二层:分析层 - 拿到你点击的元素后,去 React 内部"偷"信息
第三层:UI 层 - 在页面上画高亮框、显示标签
最后,核心协调器把所有信息打包,扔进剪贴板。
第一层:事件监听
它怎么知道你要抓?
用户交互的状态机:
stateDiagram-v2
[*] --> 待命: 页面加载
待命 --> 抓取模式: 按下 ⌘C / Ctrl+C
抓取模式 --> 待命: 松开按键
抓取模式 --> 悬停高亮: 鼠标移动到元素
悬停高亮 --> 抓取模式: 鼠标移开
悬停高亮 --> 执行抓取: 鼠标点击
执行抓取 --> 复制完成: 写入剪贴板
复制完成 --> 抓取模式: 继续选择其他元素
复制完成 --> 待命: 松开按键
React Grab 监听三类事件:
// 使用 AbortController 统一管理事件监听器
const controller = new AbortController();
// 1. 键盘事件:检测 Cmd+C / Ctrl+C
window.addEventListener('keydown', (e) => {
if ((e.metaKey || e.ctrlKey) && e.key === 'c') {
activateGrabMode(); // 进入"抓取模式"
}
}, { signal: controller.signal });
window.addEventListener('keyup', (e) => {
if (e.key === 'Meta' || e.key === 'Control') {
deactivateGrabMode(); // 退出"抓取模式"
}
}, { signal: controller.signal });
// 2. 鼠标移动:追踪悬停位置
window.addEventListener('mousemove', handleMouseMove, { signal: controller.signal });
// 3. 鼠标点击:执行抓取
window.addEventListener('mousedown', handleMouseDown, { signal: controller.signal });
window.addEventListener('mouseup', handleMouseUp, { signal: controller.signal });
为什么高亮不会"抖"?
如果鼠标稍微动一下就切换高亮元素,体验会很差。
React Grab 用了一个小技巧——稳定检测:
- 鼠标移动超过 200px 才重新计算
- 停留 100ms 后才显示高亮
- 只有"稳定"悬停时才触发
这就是为什么你快速划过元素时,高亮框不会疯狂闪烁。
第二层:React Fiber 访问(核心黑魔法)
这是整个工具最精彩的部分。
什么是 Fiber?
Fiber 是 React 16+ 的内部数据结构。你可以把它理解为 React 的"私人笔记本",记录了每个组件的:
- 它是谁(组件类型)
- 它长什么样(props)
- 它现在的状态(state)
- 它的家人是谁(父/子/兄弟节点)
- 它从哪里来(源代码位置)
最后一条是关键——React 在开发模式下,会偷偷记录每个组件的源代码位置。
怎么访问 Fiber?
React Grab 使用了一个叫 bippy 的库(也是作者 Aiden Bai 写的)来访问 Fiber:
import { _fiberRoots as fiberRoots, instrument } from "bippy";
// 注册一个"间谍",监听 React 的 commit 阶段
instrument({
onCommitFiberRoot(_, fiberRoot) {
// 每次 React 渲染完成,就把 fiber 根节点收集起来
fiberRoots.add(fiberRoot);
},
});
这段代码做了什么?
- React 每次渲染完成(commit),都会触发这个回调
bippy把 Fiber 根节点存起来- 之后我们就可以从这些根节点遍历整棵 Fiber 树
从 DOM 到 Fiber
当你点击一个 DOM 元素,React Grab 需要找到它对应的 Fiber 节点:
flowchart LR
DOM["🖱️ DOM 元素<br/>(HTMLElement)"]
Fiber["🧬 Fiber 节点<br/>(_debugSource)"]
Source["📍 源代码位置<br/>(fileName, lineNumber)"]
DOM -->|"__reactFiber$xxx"| Fiber
Fiber -->|"读取 _debugSource"| Source
style DOM fill:#e3f2fd
style Fiber fill:#fff8e1
style Source fill:#e8f5e9
具体怎么做?React 会在 DOM 元素上挂一个隐藏属性,指向对应的 Fiber 节点。bippy 封装了这个逻辑:
import {
getSourceFromHostInstance,
normalizeFileName,
isSourceFile
} from "bippy/dist/source";
// 从 DOM 元素获取源代码位置
const source = getSourceFromHostInstance(domElement);
// 返回: { fileName: 'src/Button.tsx', lineNumber: 42, columnNumber: 5 }
一行代码,就拿到了文件路径和行号。
源代码位置是哪来的?
你可能好奇:React 怎么知道每个组件在源代码的哪一行?
答案是:Babel 插件。
编译时注入
整个流程是这样的:
flowchart TB
subgraph Dev["开发时"]
JSX["📝 JSX 代码<br/><Button>提交</Button>"]
Babel["🔧 Babel 编译"]
JS["📄 带 __source 的 JS"]
end
subgraph Runtime["运行时"]
React["⚛️ React 创建 Fiber"]
Fiber["🧬 Fiber 节点<br/>_debugSource"]
end
subgraph Tool["React Grab"]
Grab["🎯 读取 _debugSource"]
Output["📋 输出位置信息"]
end
JSX -->|"@babel/plugin-transform-react-jsx-source"| Babel
Babel --> JS
JS -->|"React.createElement()"| React
React --> Fiber
Fiber -->|"bippy"| Grab
Grab --> Output
style JSX fill:#e3f2fd
style Babel fill:#fff3e0
style JS fill:#e8f5e9
style React fill:#e1f5fe
style Fiber fill:#fff8e1
style Grab fill:#f3e5f5
style Output fill:#fce4ec
当你写 JSX:
<Button onClick={handleClick}>提交</Button>
Babel 在开发模式下会把它转换成:
React.createElement(Button, {
onClick: handleClick,
__source: {
fileName: "/src/App.tsx",
lineNumber: 42,
columnNumber: 5
}
}, "提交")
看到了吗?__source 属性是 Babel 自动加的。
这是由 @babel/plugin-transform-react-jsx-source 插件完成的,React 脚手架(Create React App、Vite、Next.js)在开发模式下都会自动启用它。
存储在 Fiber 中
React 拿到 __source 后,会把它存到 Fiber 节点的 _debugSource 字段:
fiber._debugSource = {
fileName: "/src/App.tsx",
lineNumber: 42,
columnNumber: 5
}
这就是 React Grab 能精准定位源代码的秘密——React 自己就记着呢,它只是把信息读出来而已。
第三层:UI 渲染
一个有趣的选择
React Grab 的 UI(高亮框、标签)是用 Solid.js 写的,不是 React。
为什么?
import { createSignal, createMemo, createRoot } from 'solid-js';
// 响应式状态
const [isActive, setIsActive] = createSignal(false);
const [hoveredElement, setHoveredElement] = createSignal<Element | null>(null);
// 派生状态:高亮框的位置
const highlightStyle = createMemo(() => {
const el = hoveredElement();
if (!el) return null;
const rect = el.getBoundingClientRect();
return {
top: rect.top + 'px',
left: rect.left + 'px',
width: rect.width + 'px',
height: rect.height + 'px'
};
});
为什么不用 React?
四个原因:
- 隔离性 - 不会和你应用的 React 冲突。想象一下,一个检测 React 的工具自己也用 React,那不是套娃吗?
- 轻量 - Solid.js 的运行时只有几 KB,React 要大得多
- 性能 - Solid.js 是细粒度响应式,更新 UI 更高效
- 无依赖 - 不需要完整的 React 运行时
这是一个很聪明的架构决策。
最后一步:剪贴板
数据结构
React Grab 把收集到的信息打包成一个结构化对象:
interface GrabData {
// 源代码信息
source: {
fileName: string;
lineNumber: number;
columnNumber: number;
};
// 组件信息
component: {
name: string;
ancestors: string[]; // 组件层级链
};
// DOM 信息
dom: {
selector: string;
tagName: string;
className: string;
dimensions: { width: number; height: number };
};
// HTML 片段
htmlSnippet: string;
}
CSS 选择器生成
为了让 AI 能精准定位元素,还需要生成一个唯一的 CSS 选择器:
import { finder } from '@medv/finder';
const selector = finder(element);
// 返回: "#app > div.container > button.submit-btn"
@medv/finder 是一个专门干这个的库,它会生成最短且唯一的选择器。
写入剪贴板
最后一步,把数据写入剪贴板:
// 使用现代 Clipboard API
navigator.clipboard.write([
new ClipboardItem({
'text/html': new Blob([htmlContent], { type: 'text/html' }),
'text/plain': new Blob([plainText], { type: 'text/plain' })
})
]);
注意它同时写了两种格式:
text/html- 结构化数据,AI 工具可以解析text/plain- 纯文本,直接粘贴也能看
技术栈总结
React Grab 用到的核心技术:
mindmap
root((React Grab))
事件层
原生 DOM API
键盘事件
鼠标事件
AbortController
分析层
bippy
Fiber 访问
_debugSource
组件层级
UI 层
Solid.js
高亮框
标签提示
响应式更新
输出层
Clipboard API
@medv/finder
结构化数据
| 模块 | 技术 | 作用 |
|---|---|---|
| 事件监听 | 原生 DOM API | 捕获键盘/鼠标事件 |
| Fiber 访问 | bippy | 读取 React 内部数据 |
| 源码定位 | Babel 插件 | 编译时注入位置信息 |
| 选择器生成 | @medv/finder | 生成唯一 CSS 选择器 |
| UI 渲染 | Solid.js | 绘制高亮框和标签 |
| 数据传递 | Clipboard API | 复制到剪贴板 |
几个精妙的设计
1. 不侵入应用代码
React Grab 完全是"外挂"式的:
- 不需要修改你的组件代码
- 不需要安装 Babel 插件(React 自带的就够了)
- 不会影响应用的正常运行
2. 开发模式限定
它依赖的 _debugSource 只在开发模式存在,所以:
- 生产环境天然无效
- 不会泄露源码信息
- 不会增加生产包体积
3. 轻量且独立
用 Solid.js 而不是 React 来做 UI,既保证了轻量,又避免了和应用 React 的冲突。
局限性
当然,这套方案也有局限:
flowchart LR
subgraph Works["✅ 能用"]
React["React 16+"]
Dev["开发模式"]
Modern["现代浏览器"]
end
subgraph NotWork["❌ 不能用"]
Vue["Vue"]
Angular["Angular"]
Svelte["Svelte"]
Prod["生产环境"]
IE["IE 浏览器"]
end
React -.->|"Fiber 架构"| Works
Dev -.->|"_debugSource"| Works
Vue -.->|"无 Fiber"| NotWork
Angular -.->|"不同架构"| NotWork
Svelte -.->|"编译时框架"| NotWork
Prod -.->|"无调试信息"| NotWork
style Works fill:#e8f5e9
style NotWork fill:#ffebee
只能用于 React
它深度依赖 React Fiber 架构,所以:
- ❌ Vue - 没有 Fiber
- ❌ Angular - 完全不同的架构
- ❌ Svelte - 编译时框架,运行时没有组件树
只能用于开发模式
生产环境的代码:
- 没有
_debugSource - 被压缩混淆了
- Source Map 通常也不开放
所以只能在开发时用。
依赖 Source Map
如果你的构建工具没有生成 Source Map,或者禁用了 JSX source 插件,定位就会失效。
总结
React Grab 的原理可以用一句话概括:
利用 React 在开发模式下自带的源码位置信息,通过 Fiber 树反向查找,把元素的"身份证"复制给 AI。
它不是什么黑科技,而是巧妙地利用了 React 已有的调试机制。
这也是为什么它能做到:
- 零配置
- 零侵入
- 零性能影响
因为脏活累活,React 和 Babel 已经帮你干完了。
相关资源
- React Grab GitHub:github.com/aidenybai/r…
- bippy(Fiber 访问库):github.com/aidenybai/b…
- @medv/finder(选择器生成):github.com/antonmedv/f…
- Solid.js:www.solidjs.com/