React Grab 原理篇:它是怎么"偷窥" React 的?

前言

上一篇我们介绍了 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 监听三类事件:

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);
  },
});

这段代码做了什么?

  1. React 每次渲染完成(commit),都会触发这个回调
  2. bippy 把 Fiber 根节点存起来
  3. 之后我们就可以从这些根节点遍历整棵 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 封装了这个逻辑:

typescript 复制代码
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/>&lt;Button&gt;提交&lt;/Button&gt;"] 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:

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?

四个原因:

  1. 隔离性 - 不会和你应用的 React 冲突。想象一下,一个检测 React 的工具自己也用 React,那不是套娃吗?
  2. 轻量 - Solid.js 的运行时只有几 KB,React 要大得多
  3. 性能 - Solid.js 是细粒度响应式,更新 UI 更高效
  4. 无依赖 - 不需要完整的 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 用到的核心技术:

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 已经帮你干完了。


相关资源

相关推荐
智能RPA4 分钟前
农业与矿业行业智能体自动化平台对比评测(计量与巡检场景)
运维·人工智能·python·自动化·agent·rpa
easyeye1234 分钟前
用开源的Toonflow和MiniMax H3一步步复刻万妖
人工智能
byte轻骑兵6 分钟前
VCP核心缩写概览
人工智能·音视频·le audio·低功耗蓝牙音频
茶杯6757 分钟前
AI重构电商视觉生产 极睿科技AGI Ecpro助力行业数字化升级
人工智能·ai重构电商·极睿科技·agi ecpro·极睿科技—agi ecpro
liferecords8 分钟前
笔记本硬跑 744B 大模型:GitHub 上的『蜂鸟』把 SSD 当显存用
人工智能·开源·大模型·推理优化
sg_knight9 分钟前
ZCode Bug 定位实战:把报错丢给 ZCode,它是怎么修的
llm·bug·agent·ai编程·glm·智谱·zcode
工业设备方案笔记9 分钟前
I3568开发板 vs X3576开发板:RK3568与RK3576到底怎么选?
人工智能
ting94520009 分钟前
深度拆解|Dif.Sh 开源特性开关(Feature Flags)技术架构与工程落地全解析
人工智能·架构·开源
2601_9623807615 分钟前
历史解说与民俗科普视频的AI自动配素材实现教程
人工智能·音视频
TomEval16 分钟前
【测AI】第05篇:Python 爬虫进阶 —— 动态页面爬取与 Scrapy 框架
人工智能·爬虫·python·scrapy·自动化