react-grab 架构深度解析:如何冻结并检视一个活着的 React 应用
【免费下载链接】react-grabCopy any UI element for your agent项目地址: https://gitcode.com/GitHub_Trending/re/react-grab
导读
react-grab 是一个“把任意 UI 元素复制给你的 Agent”的浏览器工具:按住快捷键即可进入抓取模式,冻结页面上的所有渲染与动画,选中任意元素后得到它的组件栈、源码位置与选择器,供 Agent 或开发者直接使用。本篇以 packages/react-grab/docs/architecture.md 为骨架,结合packages/react-grab/src下的源码实现,完整讲解其内部的冻结机制、overlay 渲染、源码解析管线与插件系统。读完你将理解:在没有任何公开 React API 的情况下,react-grab 如何暂停 React 渲染;如何同时冻结 CSS、SVG SMIL、WAAPI 与 rAF 循环四套动画系统;如何把一个 DOM 元素反向解析成可打开的文件路径与行号。
阅读提醒:文档与源码都反复强调,本文描述的大量技术本质上是脆弱的——它们涉及补丁 React 内部 dispatcher、monkey-patch 浏览器原型、拦截从未被设计为可外部暂停的动画系统。随着 React 内部结构演化,部分细节必然需要调整,请把它当作理解思路的指南而非永久不变的契约。
一、设计原则:五条贯穿全局的架构决策
文档开头给出了五条设计原则,它们是理解所有后续模块的钥匙,全部可以在源码中找到对应实现。
1. 抓取模式激活时,暂停一切渲染与动画
页面进入抓取模式时必须“完全冻住”,用户才不会在检视过程中看到元素到处漂移。这比听起来困难得多,因为一个典型页面上同时运行着多套相互独立的系统:React 状态更新触发重渲染、CSS 动画与过渡、SVG SMIL 动画、Web Animations API(WAAPI)实例、GSAP 时间线,以及指针一靠近 overlay 就会消失的:hover伪状态。每一套都需要不同的冻结策略,并且必须协调一致,保证解冻时干净地回放或收尾,不出现视觉跳变。对应实现分布在src/utils/freeze-updates.ts、freeze-animations.ts、freeze-pseudo-states.ts等文件中,详见本文第三节。
2. 避免干扰宿主应用
overlay UI 放在 Shadow DOM 里,既不会继承页面样式,也不会干扰页面的事件处理。宿主元素挂载时带有pointer-events: none,不会拦截本应发给底层页面的点击。当 react-grab 认领某个键盘快捷键时,它补丁KeyboardEvent.prototype.key的 getter,让被认领的事件向应用自己的处理器报告空字符串,而不是尝试调用stopPropagation——后者在 React 的合成事件委托体系下并不可靠。实现见src/core/keyboard-handlers.ts与src/utils/mount-root.ts。
3. 编排逻辑集中在单点,工具函数拆分出去
src/core/index.tsx是一个刻意保持“大”的文件。SolidJS 组件是 setup 函数(只执行一次,而不是每次更新都重跑的 render 函数),因此init()实际上就是一个过程式的启动函数:接线 store、插件注册表、事件监听、副作用与 renderer。之后的所有更新都由响应式图驱动。元素检测、动画冻结、剪贴板写入、bounds 计算等小关注点被拆到src/utils/下的独立工具文件中,但编排逻辑始终留在同一处——这样从激活到复制的完整流程可以不用在文件间跳来跳去地追踪。
4. 所有面向用户的操作都实现为插件
复制、评论、在编辑器中打开——每个用户可见的操作都是注册了上下文菜单项与钩子的插件。核心不硬编码任何剪贴板行为。用户触发复制时,内容会依次经过插件变换管线(onBeforeCopy、transformSnippet、transformCopyContent)才到达剪贴板,外部消费者可以不 fork 库就修改或替换复制流程的任何一步。实现见src/core/plugin-registry.ts。
5. 渲染层与交互逻辑分层懒加载
SolidJS UI 组件(canvas overlay、工具栏、选择标签、上下文菜单)通过动态import()加载,这样src/core/index.tsx中的交互检测逻辑可以立即初始化,不必等待更重的渲染代码被解析执行。如果动态导入失败,react-grab 会记录错误,但在没有可视化 overlay 的情况下继续工作。
二、初始化与交互生命周期
运行时只有两个核心关注点:初始化与交互生命周期,两者都主要位于 src/core/index.tsx。
初始化流程
当包在浏览器环境中被导入时,src/index.ts 会检查window是否存在且未设置__REACT_GRAB_DISABLED__,然后调用init()。返回的 API 对象被赋给window.__REACT_GRAB__;在初始化完成前通过registerPlugin()注册的插件会从待处理队列中 flush;随后派发react-grab:init自定义事件,让页面上其他代码可以感知工具就绪。如果init()在 SSR 环境或工具被禁用时被调用,则返回一个形状相同的 no-op API 对象,这样消费方无需为每次调用做防御性判断。
进入init()后,第一件事是合并两路 options:程序化传入的 options 与 script 标签上data-optionsJSON 属性解析出的 options(通过getScriptOptions())。然后调用createPluginRegistry和createGrabStore搭建响应式状态。单个AbortController管理window和document上的所有事件监听,因此 teardown 就是一次abort()调用。三个内置插件(copy、comment、open)通过与外部插件完全相同的register()路径注册。最后,renderer 通过import("../components/renderer.js")异步加载,并挂载到 src/utils/mount-root.ts 创建的 Shadow DOM 容器中。
交互状态机
交互状态在 src/core/store.ts 中被建模为判别联合(discriminated union)GrabState:
用户可以通过两种方式激活:按住按键达到可配置时长(holding状态),或切换开关(toggle)。进入active后,工具处于若干阶段之一:
hovering:跟踪指针并高亮其下的元素;frozen:用户点击元素锁定选择;dragging:用户正在拖拽矩形框选多个元素;justDragged:拖拽结束后的短暂过渡阶段。
copying表示剪贴板内容正在生成的短暂时刻,justCopied在展示成功反馈后,根据wasActive标志决定回到active还是退出:按住模式复制后会回到active继续检视(按键仍被按住),toggle 模式复制后则直接停用(复制动作消耗掉了这次 toggle)。
active期间,指针与键盘事件处理器会把当前指针位置、指针下的检测元素(节流并缓存)以及拖拽矩形坐标写入 store。由这些信号派生的 SolidJS memo 计算 overlay bounds、选择标签内容、组件名等 UI 状态;canvas overlay 与基于 DOM 的 UI 组件订阅这些 memo 并响应式更新。
三、冻结机制:代码库中最有趣的部分
冻结是整个代码库技术上最有趣的部分。有四套相互独立的系统需要同时暂停(文档列了三套,源码实际覆盖了 rAF 循环这一第四套),每套都需要不同的技术。
3.1 React 更新冻结(dispatcher 补丁 + 更新队列拦截)
没有任何公开的 React API 可以暂停渲染。src/utils/freeze-updates.ts 通过补丁 React 的内部 dispatcher 绕过这一限制——dispatcher 是 React 在每次 hook 调用时读取的对象,用来决定该用哪个useState、useReducer等实现。
dispatcher 位于 bippy 暴露的 renderer 对象上的ReactCurrentDispatcher.H(React 19+)或ReactCurrentDispatcher.current(更早版本)。installDispatcherPatching用 getter 替换该属性,每次访问都触发patchDispatcher。patchDispatcher包装四个有状态的 hook,当isUpdatesPaused为 true 时重定向分发:
useState/useReducer的分发被推入pendingStateUpdates数组;useTransition回调被推入pendingTransitionCallbacks数组;useSyncExternalStore的订阅回调被收集进pendingStoreCallbacks集合。
除了拦截分发,pauseHookQueue还把每个 fiber 的queue.pending属性重定义为 getter/setter 对。React 内部更新队列是一个循环链表,queue.pending指向最后一个节点,pending.next指向第一个。暂停时 getter 返回null(让 React 以为没有待处理更新),setter 则把 React 尝试入队的新更新缓冲到bufferedPending。外部 store 的getSnapshot函数同样被替换为冻结版本——直接返回暂停时捕获的值。
解冻时按特定顺序回放缓冲的状态:先 store 回调,再 transition,最后状态更新。这个顺序很重要:store 订阅可能触发它们自己的状态更新,而 transition 需要正确批处理。回放后scheduleReactUpdate在每个 fiber root 上调用renderer.scheduleUpdate触发重渲染。所有回放路径都被 try/catch 包裹,因为整套方法耦合于 React 内部数据结构,可能随版本变化。
源码中还体现了两个工程细节:freezeOwnerCount引用计数让多个冻结请求(元素级与全局级)可以嵌套,只有最后一个持有者释放时才真正恢复更新;遍历 fiber 树采用迭代式前序遍历(child/sibling/return 指针)而非递归,避免大应用深层 fiber 树爆栈,并刻意做成两个单用途副本以保持调用点 monomorphic、避免 JIT 反优化。
3.2 动画冻结(四套动画系统)
src/utils/freeze-animations.ts 处理四套不同的动画系统。
CSS 动画与过渡:通过注入样式表设置animation-play-state: paused !important与transition: none !important。元素级冻结针对带data-react-grab-frozen属性的元素(及其后代),全局冻结则用*, *::before, *::after。
SVG SMIL 动画:对每个SVGSVGElement调用pauseAnimations()。这里有一个重要的深度计数设计:同一个 SVG 可能同时被元素级冻结和全局冻结命中,因此每个 SVG 在svgFreezeDepthMap中维护引用计数,只有所有冻结层都移除时才unpauseAnimations(),防止部分解冻提前恢复动画。
Web Animations API(WAAPI)实例:通过element.getAnimations({ subtree: true })收集并逐个pause()。全局冻结还有一个阈值优化:当文档运行中的动画数量超过WAAPI_GLOBAL_FREEZE_MAX_ANIMATIONS时改用 CSS 注入路线,避免逐个 pause 大量实例的开销。
动画帧循环(rAF loops):由 src/utils/freeze-animation-frame-loops.ts 处理。wrapper 能识别自我重排的回调,让 Three.js、GSAP 和手写的循环渲染 loop 暂停,同时放行一次性布局回调。
解冻时动画是finish()而不是恢复播放,这是刻意为之:从中点恢复暂停的动画会造成视觉跳变——例如一个正在播放入场动画的下拉菜单会突然快进完剩余帧。finish()把动画推进到结束状态,中途的transition: none规则防止清理期间的视觉闪烁。Shadow-root 动画被排除在全局冻结之外,因为注入的 CSS 只影响主文档元素;对从未被暂停的动画调用finish()会破坏 react-grab 自己的工具栏与标签动画(源码注释引用了 issue #163)。isShadowAnimation通过检查动画 target 的 rootNode 是否为 react-grab host 的 shadow root 来识别这类动画。
3.3 伪状态冻结(:hover / :focus)
src/utils/freeze-pseudo-states.ts 处理 CSS 伪类。当用户激活抓取模式并悬停在元素上时,该元素可能有只在:hover状态下生效的样式(例如按钮变色)。指针一旦移向 react-grab overlay,浏览器就移除原元素的:hover状态,视觉外观随之改变。
collectPseudoStates/applyPseudoStates(通过 src/utils/freeze-global-interactions.ts 中的freezeGlobalInteractions批处理)捕获与:hover、:focus伪类关联的当前计算样式,以内联样式应用到元素上。这样即使指针移开,元素也保持用户最初指向它时的样子;解冻时移除注入的内联样式。
四、Overlay 的实现细节
4.1 Shadow DOM 挂载
overlay 通过 src/utils/mount-root.ts 挂载。宿主<div>以position: fixed; inset: 0覆盖整个视口,z-index 很高,pointer-events: none保证不拦截底层页面的点击;各个子 UI 元素(工具栏按钮、标签文本区等)通过 CSS 单独 opt-in 指针事件。
宿主以open模式 attach shadow root,注入编译后的 CSS(作为<style>元素,支持通过detectCspNonce探测的 CSP nonce),并追加一个容器<div>作为 SolidJSrender()的挂载点。有两个值得一提的细节:
- 宿主始终挂到
<body>(绝不挂到<html>)。如果<body>尚未解析完成——例如在 Next.js App Router 中通过<Script strategy="beforeInteractive" />加载——挂载会延迟到DOMContentLoaded。直接把<div>append 到<html>会在 React hydration 期间抛出 "In HTML,<div>cannot be a child of<html>",并在 Next.js dev overlay 中显示为 hydration 错误。 - 宿主通过
setTimeout延迟后重新 append 到<body>。这处理两个实际场景:React/Next.js hydration 可能在应用替换<body>子树时清掉宿主元素;如果另一个工具(如 react-scan)在相同 z-index 下追加了自己的 overlay,DOM 中最后一个子元素赢得层叠平局,因此重新 append 保证 react-grab 保持在最顶层。源码中还通过MutationObserver监听documentElement的 childList 变化,宿主一旦脱离<body>立即重新挂载,并清理被应用克隆<body>时产生的无 shadow root 的“僵尸宿主”。
4.2 Canvas 渲染
视觉高亮 overlay 由 src/components/overlay-canvas.tsx 渲染:单个<canvas>元素叠加多个OffscreenCanvas层。每种视觉类型——选择高亮、拖拽矩形、抓取元素闪光——都有自己的离屏层与自己的动画 bounds。
每层的 bounds 在每一帧通过requestAnimationFrame向目标位置 lerp(线性插值),不同交互类型使用不同 lerp 因子:选择高亮用较慢的因子,避免用户在不同元素间移动时 overlay 抖动;拖拽矩形则更激进地跟踪指针。每帧主 canvas 清空自身并合成所有可见层。当动画收敛(当前 bounds 与目标 bounds 的差距小于阈值且 opacity 稳定)时动画循环停止,直到下一次响应式更新再次触发。帧率波动时adjustLerpForFrameDuration会按实际帧时长校正 lerp,避免动画速度随帧率变化。浏览器支持时 canvas 使用display-p3色彩空间以获得更广色域的 overlay 颜色,并跟踪 devicePixelRatio 保证高 DPI 显示清晰。
4.3 键盘事件认领
当 react-grab 使用键盘快捷键(如 Alt 激活抓取模式)时,应用自己的键盘处理器不应同时响应。src/core/keyboard-handlers.ts 通过补丁KeyboardEvent.prototype.key的 getter 实现。
setupKeyboardEventClaimer先保存key的原始属性描述符,再安装新 getter。新 getter 检查事件对象是否在“已认领”事件的WeakSet中;若是,则返回空字符串""而非真实按键。这样页面上其他检查event.key的处理器会看到空字符串,在多数情况下忽略该事件。这比调用stopPropagation()更可靠,因为 React 的合成事件系统在根部做事件委托,react-grab 的处理器运行时 React 的委托处理器可能已经捕获了事件。react-grab 销毁时恢复原始 getter,并通过__reactGrabPatched标记避免重复补丁。
五、源码解析管线:从 DOM 元素到文件路径
源码解析管线把用户指向的 DOM 元素转换为文件路径、行号与组件名,共分七层。
5.1 从 DOM 元素到 React fiber
入口是 src/core/context.ts 的getStack(),接收 DOM 元素返回StackFrame[]。结果按元素缓存在WeakMap中,用户移动时对同一元素的重复查询不必重做。
第一步是找到最近的、关联了 React fiber 的祖先元素(使用 bippy 的getFiberFromHostInstance)。并非每个 DOM 元素都映射到 fiber:文本节点、React 之外创建的元素、Shadow DOM 内的元素(如 react-grab 自己的 overlay)都没有。findNearestFiberElement通过parentElement向上遍历 DOM 树,直到找到带 fiber 的元素;跨越 shadow root 边界时沿rootNode.host继续。
5.2 从 fiber 到 owner 栈
拿到 fiber 后,bippy 的getOwnerStack()构建其上的组件层级,该逻辑因 React 版本而异。
React 19+:fiber 有_debugStack属性,包含一个.stack字符串编码 owner 链的Error对象。React 把真实组件帧夹在两个哨兵之间:顶部react-stack-top-frame、底部react-stack-bottom-frame。bippy 的formatOwnerStack剥离哨兵与初始 JSX 帧,只留下中间组件帧。
React 17–18:_debugStack不存在,bippy 构造“fallback” owner 栈——沿fiber.return向上走到根。对每个遇到的 composite fiber,describeNativeComponentFrame实际上会调用组件函数(类组件则通过Reflect.construct调用构造函数)来生成栈轨迹,然后把这份“样本”栈与从同一调用点抛出(不含组件)得到的“对照”栈比较,提取唯一不同的帧——即组件本身的帧。这与 React DevTools 内部生成组件栈的技术相同。临时调用被小心保护:dispatcher 置为null防止 hook 运行,console.error/console.warn临时静音以抑制 React 关于渲染上下文外调用组件的警告。
两种路径的结果都是扁平的StackFrame[],每帧包含functionName(组件名)、fileName(此时可能是http://localhost:3000/_next/static/chunks/app.js这样的 bundle URL、file:///URL 或虚拟的rsc://URL)以及仍指向打包产物位置的lineNumber/columnNumber。
5.3 Source map 符号化
此时的栈帧指向打包文件位置而非原始源码。bippy 的symbolicateStack通过抓取每个 bundle URL、扫描最后几行找//# sourceMappingURL=注释、用@jridgewell/sourcemap-codec抓取并解码引用的 source map、在解码的 mappings 中查找原始位置来解决。对包含多个sections的 index source map,先根据行列偏移找到正确 section 再查询。
Source map 在运行时支持时用WeakRef缓存,内存压力下可被 GC,同时避免正常使用中的重复抓取。同一 source map URL 的并发请求通过 pending-request map 去重,防止多个栈帧引用同一 bundle 时重复抓取。
5.4 Server 组件富化(RSC)
React Server Components 是特殊挑战:其栈帧使用rsc://React/Server/webpack-internal:///...这类不对应磁盘真实文件的虚拟 URL。react-grab 在src/core/context.ts中分两遍处理。
第一遍enrichServerFrameLocations处理“fallback owner 栈中出现带函数名但无文件名的 server 组件”的情况:遍历整个 fiber 树寻找_debugStack条目含rsc://URL 的 fiber,建立函数名到rsc://帧的映射,再按函数名匹配补齐缺失的文件名,使 server 帧获得虚拟文件 URL 供下一遍解析。
第二遍symbolicateServerFrames把富化后的帧批量 POST 到 Next.js dev server 的/__nextjs_original-stack-frames端点,由服务器用其自身 source map 把虚拟rsc://URL 解析回真实源码路径。函数先调用devirtualizeServerUrl剥离rsc://React/Server/前缀与尾部查询参数,得到 Next.js 符号化所期望的webpack-internal:///路径。请求通过AbortController设置超时,避免 dev server 缓慢或无响应时无限阻塞。getNextBasePath()用于构造端点 URL——对带basePath前缀部署的 Next.js 应用很重要(对应src/utils/next-server-frames.ts)。
5.5 文件名校准
整条管线中文件名以多种格式出现:HTTP URL(http://localhost:3000/src/App.tsx)、file:///URL、webpack-internal:///路径、rsc://React/Server/...虚拟路径,以及带 HMR 查询参数(?t=1711234567)的路径。bippy 的normalizeFileName处理所有这些:剥离 HTTP origin(只取 pathname)、循环移除内部 scheme 前缀(处理webpack-internal:///file:///...这类嵌套前缀)、解析about://React/URL、移除匹配 bundler 模式的查询参数。它还剥离子路径部署的单段 base path 前缀,但仅当剩余部分看起来像带多个路径段的真实源文件。
配套的isSourceFile过滤匿名文件模式、非源码扩展名与 bundle 文件模式(如 chunk hash),确保 UI 只展示用户自己编写的代码帧。
5.6 显示名过滤
即使源码位置解析完成,组件层级中仍常有框架内部名称。src/core/context.ts 维护三份过滤列表:
NEXT_INTERNAL_COMPONENT_NAMES:约二十个 Next.js App Router 包装名(InnerLayoutRouter、RedirectErrorBoundary、AppRouter等),否则检视 Next.js 应用中几乎任何元素都会显示框架包装器而非用户自己的组件;REACT_INTERNAL_COMPONENT_NAMES:React 内置组件如Suspense、Fragment、StrictMode、Profiler;NON_COMPONENT_PREFIXES:捕获库内部命名约定——以_或$开头(编译产物常见)的名称,以及motion.、styled.、chakra.、ark.、Primitive.、Slot.这类流行组件库的前缀名。
getComponentDisplayName从目标元素沿 fiber 树向上,返回第一个通过全部过滤的 composite fiber 显示名——通常就是用户自己的组件,也就是他们真正想在编辑器里找到的那个。
5.7 上下文行预算与选择器提示
formatStackContext用两个限制约束复制轨迹的行数:软预算maxLines(默认DEFAULT_MAX_CONTEXT_LINES = 3,见 src/constants.ts)保持常见情况的紧凑;硬上限MAX_TRACE_CONTEXT_LINES = 20约束最坏情况。只有高信号的 app 源码帧消耗软预算;低信号帧“免费”显示、只计入硬上限,因此包装器噪声永远不会挤掉有意义的源码。
低信号帧有两类:
node_modules里的库帧——按组件名渲染(如in Tabs (@radix-ui/react-tabs)),不与 app 路径竞争;- app 自有的共享 UI / 设计系统帧——位于
components/ui/、packages/ui/、design-system(s)/、primitives/段下的文件(shadcn 的components/ui、monorepo 的packages/ui、headless primitives),由 src/utils/is-shared-ui-source-path.ts 检测。裸ui/段被刻意排除,避免把 Next 的app/ui/特性约定误判为 primitive 库。
选择器提示同时考虑源码质量与选择器质量。没有可信特性源码存活时,复制上下文始终包含选择器作为具体回退(必要时含生成的结构化路径);有可信源码时,只有语义化选择器才被包含:稳定的作者 id、data-testid、aria-label、href等首选识别属性,或元素 adapter 提供的领域特定选择器。React 生成的 id、UUID、生成 class、标签与位置路径在有可信源码时被省略,因为它们只会增加脆弱噪音。跨 shadow-root 或 iframe 边界的选择器,只有当到达目标的每个分段都语义化时才视为语义化。
源码位置与组件标签独立排序:最近的幸存源码帧提供可编辑的文件位置,即使其组件名匿名、被压缩或被过滤;更深处祖先上的更好标签永远不会重定向源码位置。缓存解析结合 bippy 的持久 fiber id 与 React 的 debug owner/source/stack 元数据,因此 React 双缓冲的 alternate fiber 共享缓存条目、复用同一 DOM 节点的替换 fiber 不会继承条目、Fast Refresh 元数据变化仍会失效过期路径。
当默认仍不够深时,公开的maxContextLines选项可以提高软预算:它从Options流经插件注册表进入复制流程与getStackContextAPI,也可以通过 script 标签的data-options属性设置。
5.8 在编辑器中打开
管线最后一步是 src/utils/open-file.ts:拿到解析出的文件路径与可选行号后尝试在用户编辑器打开。它先尝试 dev server 内置的 open-in-editor 端点:Vite 的/__open-in-editor或 Next.js 的/__nextjs_launch-editor(通过isNextProjectRuntime()判断并配合getNextBasePath()构造完整 URL)。两个 dev server 都带中间件,会用用户配置的$EDITOR(或$VISUAL)以文件路径和行号启动编辑器,无需浏览器交互。
若 dev server 请求失败——例如应用使用不支持该中间件的框架,或 dev server 未运行——则回退到通过react-grab.com/open-file打开一个 URL,该 URL 重定向到 VS Code 原生处理的vscode://file/...协议地址。请求先经过transformOpenFileUrlhook 管线,插件可以在最终 URL 上做变换。
六、插件系统
插件系统实现在 src/core/plugin-registry.ts。每个插件是一个对象:name、可选静态属性(theme、actions、hooks、options)以及可选的setup函数——接收ReactGrabAPI与注册表的 hook dispatchers,返回PluginConfig。
注册时,静态属性与setup()返回的值合并:theme 深合并(插件可以只覆盖色相而无需替换整个 theme 对象)、options 浅合并、上下文菜单 actions 拼接。若同名插件已注册,先注销旧的(存在时调用其cleanup函数)。
每次注册或注销后,recomputeStore按插入顺序遍历所有插件,从零重建合并后的 theme、options 与 action 列表,存入 SolidJS 响应式 store——因此 UI 在插件增删时自动更新。setOptions支持运行期覆盖activationMode、keyHoldDuration、allowActivationInsideInput、activationKey、getContent、maxContextLines、freezeReactUpdates七个选项。
6.1 Hook 分发模式
注册表提供多种 hook 调用方式,适配不同场景:
callHook:调用每个已注册插件的某 hook 实现,忽略返回值。用于通知型 hook:onActivate、onDeactivate、onElementHover、onCopySuccess等。callHookWithHandled:调用每个插件的 hook 并跟踪是否有人返回true。用于onOpenFile——插件可能想拦截默认行为。onElementSelect有自己的专用实现而非使用callHookWithHandled,因为它需要同时支持同步true返回与异步Promise<boolean>返回的拦截。callHookReduce:把值顺序穿过每个插件的变换函数,每个插件接收前一个的输出,形成流水线。用于transformCopyContent、transformSnippet、transformHtmlContent、transformAgentContext。另有同步变体callHookReduceSync,用于transformOpenFileUrl、transformActionContext等无需异步的变换。
6.2 内置插件
三个内置插件在init()期间通过与外部插件相同的register()路径注册,架构上没有特殊性:
- copy:注册默认的 “Copy” 上下文菜单动作,对每个选中元素复制单行
[<tag …> in Component (at path:line) …]引用,包含至多DEFAULT_MAX_CONTEXT_LINES(3)条预算内组件栈帧(可通过maxContextLines选项提高)。 - comment:注册 “Comment” 动作,进入 prompt 模式(对应 src/core/plugins/comment.ts)。
- open:注册 “Open in editor” 动作,以解析出的源码位置调用
openFile,URL 先经过transformOpenFileUrlhook 管线(对应 src/core/plugins/open.ts)。
七、结语:脆弱的强大
react-grab 的架构价值在于一个清醒的取舍:在不 fork React 的前提下,用补丁与拦截逼近 DevTools 级别的检视能力。dispatcher 补丁让 React 更新可以缓冲与回放,四层动画冻结让页面真正“凝固”,owner 栈构建让组件层级可以反查,而插件管线让复制、评论、打开等一切用户动作都可以被外部扩展。与此同时,文档与源码反复提醒:这些技术耦合于 React 与浏览器的内部结构,属于本质脆弱的方案。理解这篇架构文档,意味着既掌握了这些技巧的实现方式,也理解了它们为何必然随生态演化而需要维护。
如需深入验证,可继续阅读 src/core/index.tsx(编排核心)、src/core/store.ts(状态机)、src/utils/freeze-updates.ts(dispatcher 补丁)以及 src/core/plugin-registry.ts(插件系统),并配合packages/react-grab/tests/下的单元测试(如freeze-renderers.test.ts、create-fiber-revision.test.ts、plugin-registry.test.ts)与packages/react-grab/e2e/下的 Playwright 用例(如freeze-updates.spec.ts、owner-stack.spec.ts、solid-source-location.spec.ts)交叉印证。
【免费下载链接】react-grabCopy any UI element for your agent项目地址: https://gitcode.com/GitHub_Trending/re/react-grab
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考