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

前言

上一篇我们介绍了 React Grab 是什么、怎么用。

这一篇,我们来聊点硬核的------它到底是怎么做到的?

当你按下 ⌘C 点击一个按钮,它是怎么知道这个按钮:

  • 住在哪个文件
  • 第几行代码
  • 属于哪个组件
  • 有什么样式

让我们掀开它的底裤看看。


整体架构:三层蛋糕

React Grab 的架构可以理解为三层蛋糕:

graph TB subgraph ReactGrab["React Grab"] subgraph Layers["三层架构"] Event["🎯 事件层
(原生 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 层 - 在页面上画高亮框、显示标签

最后,核心协调器把所有信息打包,扔进剪贴板。


第一层:事件监听

它怎么知道你要抓?

用户交互的状态机:

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 元素
(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 插件

编译时注入

整个流程是这样的:

flowchart TB subgraph Dev["开发时"] JSX["📝 JSX 代码
<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?

四个原因:

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


相关资源

相关推荐
田里的水稻19 分钟前
AI_常见“XX学习”术语速查表
人工智能·学习
桜吹雪34 分钟前
DeepAgents官方文档(一)
人工智能
用户479492835691539 分钟前
别再当 AI 的"人肉定位器"了:一个工具让 React 组件秒定位
前端·aigc·ai编程
甄心爱学习1 小时前
数据挖掘-聚类方法
人工智能·算法·机器学习
WYiQIU2 小时前
面了一次字节前端岗,我才知道何为“造火箭”的极致!
前端·javascript·vue.js·react.js·面试
飞哥数智坊2 小时前
给 TRAE SOLO 一台服务器,它能干什么?
ai编程·trae·solo
Dev7z2 小时前
面向公共场所的吸烟行为视觉检测系统研究
人工智能·计算机视觉·视觉检测
橙露2 小时前
视觉检测硬件分析
人工智能·计算机视觉·视觉检测
长桥夜波3 小时前
机器学习日报21
人工智能·机器学习