前言
上一篇我们介绍了 React Grab 是什么、怎么用。
这一篇,我们来聊点硬核的------它到底是怎么做到的?
当你按下 ⌘C 点击一个按钮,它是怎么知道这个按钮:
- 住在哪个文件
- 第几行代码
- 属于哪个组件
- 有什么样式
让我们掀开它的底裤看看。
整体架构:三层蛋糕
React Grab 的架构可以理解为三层蛋糕:
(原生 DOM)"] Analysis["🔍 分析层
(bippy)"] UI["🎨 UI 层
(Solid.js)"] end Core["⚙️ 核心协调器
(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 层 - 在页面上画高亮框、显示标签
最后,核心协调器把所有信息打包,扔进剪贴板。
第一层:事件监听
它怎么知道你要抓?
用户交互的状态机:
React Grab 监听三类事件:
typescript
// 使用 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:
typescript
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 节点:
(HTMLElement)"] Fiber["🧬 Fiber 节点
(_debugSource)"] Source["📍 源代码位置
(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 封装了这个逻辑:
typescript
import {
getSourceFromHostInstance,
normalizeFileName,
isSourceFile
} from "bippy/dist/source";
// 从 DOM 元素获取源代码位置
const source = getSourceFromHostInstance(domElement);
// 返回: { fileName: 'src/Button.tsx', lineNumber: 42, columnNumber: 5 }
一行代码,就拿到了文件路径和行号。
源代码位置是哪来的?
你可能好奇:React 怎么知道每个组件在源代码的哪一行?
答案是:Babel 插件。
编译时注入
整个流程是这样的:
<Button>提交</Button>"] Babel["🔧 Babel 编译"] JS["📄 带 __source 的 JS"] end subgraph Runtime["运行时"] React["⚛️ React 创建 Fiber"] Fiber["🧬 Fiber 节点
_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:
jsx
<Button onClick={handleClick}>提交</Button>
Babel 在开发模式下会把它转换成:
javascript
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 字段:
typescript
fiber._debugSource = {
fileName: "/src/App.tsx",
lineNumber: 42,
columnNumber: 5
}
这就是 React Grab 能精准定位源代码的秘密------React 自己就记着呢,它只是把信息读出来而已。
第三层:UI 渲染
一个有趣的选择
React Grab 的 UI(高亮框、标签)是用 Solid.js 写的,不是 React。
为什么?
typescript
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 把收集到的信息打包成一个结构化对象:
typescript
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 选择器:
typescript
import { finder } from '@medv/finder';
const selector = finder(element);
// 返回: "#app > div.container > button.submit-btn"
@medv/finder 是一个专门干这个的库,它会生成最短且唯一的选择器。
写入剪贴板
最后一步,把数据写入剪贴板:
typescript
// 使用现代 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 用到的核心技术:
| 模块 | 技术 | 作用 |
|---|---|---|
| 事件监听 | 原生 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 的冲突。
局限性
当然,这套方案也有局限:
只能用于 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/