@react-three/test-renderer(RTTR)版本演进与 API 全解析:为 react-three-fiber 场景图编写 Node 环境测试
【免费下载链接】react-three-fiber🇨🇭 A React renderer for Three.js项目地址: https://gitcode.com/GitHub_Trending/re/react-three-fiber
@react-three/test-renderer(简称 RTTR)是 react-three-fiber 官方仓库中用于在 Node 环境下测试 Three.js 场景图的实验性 React 渲染器:它基于@react-three/fiber的 reconciler,把<mesh>、<boxGeometry>等 JSX 元素渲染成真实的THREE对象树,而无需 WebGL 与浏览器。本文以 packages/test-renderer/CHANGELOG.md 的版本演进为脉络,结合源码与测试,完整讲解 RTTR 的安装使用、全部 API、底层实现原理,以及从 v6 到 v9 每个关键版本背后的技术决策,帮助你为自己的 R3F 应用写出可维护的单元测试。
一、RTTR 要解决的问题
使用@react-three/fiber开发 WebGL 体验后,你想用react-dom测试组件——但立刻会发现一个矛盾:THREE元素根本不在 DOM 里。即使能拿到容器和<canvas>,也看不到场景内部的对象树。原因是 R3F 用自己的 reconciler 渲染到独立的 React root 中(见 fiber/src/core/reconciler.tsx),react-dom无从感知 scene graph。
RTTR 的解法(见 packages/test-renderer/README.md)是:在@react-three/fiber之上再套一层测试渲染器,把 R3F 渲染出的场景图包装成带查询工具的 "test instance"。本质上,它让你无需 WebGL 和浏览器即可抓取 Three.js 场景图的快照并对其断言:
- 默认不会创建真实的
THREE.WebGLRenderer,也没有渲染循环; - 但会完整渲染 scene graph,所有 R3F 元素都会被实例化为真实的
THREE对象; - 测试库无关,可与 jest、jasmine 等任意断言框架配合。
从源码看,RTTR 的入口 src/index.tsx 会在模块加载时调用extend(THREE as any)把全部 Three.js 类注册进 R3F 的 catalogue,随后通过createRoot(canvas)+_root.configure({ frameloop: 'never', ... })挂载到 R3F reconciler 上,再以act()包裹_root.render(element)完成首轮渲染。
二、安装与最小示例
RTTR 以独立包发布,安装时需同时安装其 peer 依赖(见 packages/test-renderer/package.json):
# 运行时依赖 yarn add @react-three/fiber three # 测试依赖 yarn add -D @react-three/test-renderer当前仓库中 RTTR 的 peerDependencies 为:react ^19.0.0、@react-three/fiber >=9.0.0、three >=0.156,说明其 v9 系列要求 React 19 与 fiber v9。仓库根目录的 jest.config.js 与 babel.config.js 展示了 monorepo 内测试的基础配置方式。
最小示例(来自 README):
import ReactThreeTestRenderer from '@react-three/test-renderer' const renderer = await ReactThreeTestRenderer.create( <mesh> <boxGeometry args={[2, 2]} /> <meshStandardMaterial args={[ { color: 0x0000ff, }, ]} /> </mesh>, ) // 通过 TestInstance 与 Scene Graph 进行断言 console.log(renderer.toGraph())create是异步的,返回的renderer对象提供了scene、toTree()、toGraph()、fireEvent()、advanceFrames()、update()、unmount()、getInstance()等能力(类型定义见 src/types/public.ts)。
三、Renderer 核心 API 详解
以下 API 的完整说明位于 packages/test-renderer/markdown/rttr.md。
3.1create(element, options)
const renderer = ReactThreeTestRenderer.create(element, options)创建一个渲染器实例。默认无真实 WebGL、无循环,但场景图会被完整渲染。CreateOptions由CreateCanvasParameters与 R3F 的RenderProps<HTMLCanvasElement>合并而成:
interface CreateOptions extends RenderProps<HTMLCanvasElement> { width?: number // canvas 宽度,默认 1280 height?: number // canvas 高度,默认 800 }在 src/index.tsx 的create实现中,默认以frameloop: 'never'关闭循环,以{ width: options?.width ?? 1280, height: options?.height ?? 800, top: 0, left: 0 }初始化尺寸,并显式将events: undefined传入(事件系统由 RTTR 自己的fireEvent模拟驱动)。
3.2scene
renderer.scene返回根 "React Three Test Instance",是后续所有查询的起点。实现上取自 store 中场景对象的__r3f实例,并经wrapFiber包装(src/createTestInstance.ts 用WeakMap保证同一 fiber 只包装一次)。
3.3getInstance()
renderer.getInstance()返回根 three 元素对应的实例;若根元素是函数组件(无实例)则不可用。源码实现沿 fiber 树向下遍历child直到找到带stateNode的节点,再通过reconciler.getPublicRootInstance(root)取出公开实例。
3.4toTree()与toGraph()
renderer.toTree() renderer.toGraph()toTree():返回类似react-test-renderer的树结构对象(含所有 React 组件元素、props 与 children),见 src/helpers/tree.ts。节点type为对象类型名首字母小写(如mesh),props保留原始 props。toGraph():返回场景图(Scene Graph)结构对象,不含attach等附加元素,见 src/helpers/graph.ts。每个节点包含type、name、children三元组,name为空时使用空字符串。
3.5fireEvent(testInstance, eventName, mockEventData)
renderer.fireEvent(testInstance, eventName, mockEventData)在树中指定元素上触发事件。事件名遵循 camelCase(如pointerUp),也可直接传 handler 名(如onPointerUp)。第三个参数会并入传给事件 handler 的MockSyntheticEvent:
type MockSyntheticEvent = { camera: Camera // 渲染场景的默认相机 stopPropagation: () => void target: ReactThreeTestInstance currentTarget: ReactThreeTestInstance sourceEvent: MockEventData ...mockEventData // 你的自定义数据展开在此 }事件机制由 src/fireEvent.ts 与 src/helpers/events.ts 实现,基于 R3F 的 store 构造并派发模拟事件。
3.6advanceFrames(frames, delta)
renderer.advanceFrames(frames, delta)手动推进 N 帧,从而触发useFrame等 GL 渲染循环订阅者。delta为传给订阅者的增量时间(可为数字或数组,数组时逐帧取对应值)。实现上遍历 store 的internal.subscribers,对每个订阅者按帧数逐个调用其ref.current(state, delta)。
3.7update(element)与unmount()
renderer.update(element) renderer.unmount()update用新根元素重新渲染整棵树,模拟一次 React 更新(同 type 与 key 时原地更新而非重挂载);unmount卸载整棵树并触发相应生命周期。两者均以act()包裹,且更新已卸载 root 时会输出console.warn('RTTR: attempted to update an unmounted root!')提示。
3.8act(callback)
ReactThreeTestRenderer.act(callback)与react-test-renderer的act()语义一致,用于"准备组件以便断言"。与 react-test-renderer 不同的是:你无需手动用act包裹create和update(它们内部已处理),只需在涉及advanceFrames、异步更新等场景时显式调用。官方文档中的 jest 示例:
const Mesh = () => { const meshRef = React.useRef() useFrame((_, delta) => { meshRef.current.rotation.x += delta }) return ( <mesh ref={meshRef}> <boxGeometry args={[2, 2]} /> <meshBasicMaterial /> </mesh> ) } const renderer = await ReactThreeTestRenderer.create(<Mesh />) expect(renderer.scene.children[0].instance.rotation.x).toEqual(0) await ReactThreeTestRenderer.act(async () => { await renderer.advanceFrames(2, 1) }) expect(renderer.scene.children[0].instance.rotation.x).toEqual(2)delta=1、推进 2 帧,rotation.x从 0 变为 2,精确验证了useFrame的回调累加逻辑。
3.9waitFor(callback, options)
import { waitFor } from '@react-three/test-renderer' await waitFor( () => renderer.scene.findByProps({ ready: true }), { interval: 50, timeout: 5000 }, )v8.2.0 新增(见 CHANGELOGa5ffb08e: feat(RTTR): waitFor util)。轮询执行回调直到返回真值或耗尽超时(默认每 50ms 一次、超时 5000ms),全程包裹在act中,适合等待异步加载完成。实现见 src/helpers/waitFor.ts:回调返回true或undefined即视为满足,超时抛出Timed out after ${timeout}ms.。
四、ReactThreeTestInstance 查询 API
renderer.scene及所有派生的节点都是ReactThreeTestInstance,其全部属性和方法与react-test-renderer的 TestInstance 高度对齐(完整文档见 packages/test-renderer/markdown/rttr-instance.md)。
4.1 属性
| 属性 | 说明 |
|---|---|
instance | 返回该节点对应的 THREE 实例对象(如THREE.Mesh) |
type | THREE 类型名,如Scene、Mesh |
props | 当前传给元素的 props,含attach="geometry"这类由 reconciler 自动附加的隐藏 props |
parent | 父 test instance,无则返回null |
children | 直接子节点;不含geometry、material 等通过attach附加的对象(默认exhaustive: false过滤) |
allChildren | 全部子节点,深度与toTree()一致,包含所有 React 组件 |
源码实现(src/createTestInstance.ts):children默认过滤child.props.attach的节点;同时当节点类型为primitive时,会把 THREE.js 对象自身的object.children递归包装成"虚拟实例"一并返回(type转小写以贴合 R3F 约定)。
4.2 方法
| 方法 | 行为 |
|---|---|
find(predicate) | 找到恰好一个满足谓词的实例,0 个或多个都会抛错 |
findAll(predicate) | 找到所有满足谓词的实例,无匹配返回[] |
findByType(type) | 按 THREE 类型名精确查找单个实例,非恰好一个则抛错 |
findAllByType(type) | 按类型名查找全部实例 |
findByProps(props) | 按 props 子集匹配查找单个实例(支持 RegExp 匹配值,如{ name: /^mesh/i }) |
findAllByProps(props) | 按 props 子集匹配查找全部实例 |
匹配逻辑在 src/helpers/testInstance.ts:expectOne负责"恰好一个"的约束并生成带上下文的报错信息(如with node type: "Mesh"、matching custom checker: ...),matchProps递归比对 props 子集且支持正则。
五、版本演进史:从 CHANGELOG 看 RTTR 的技术路线
packages/test-renderer/CHANGELOG.md 记录了 6.1.1 至 9.1.0 的全部变更。逐条阅读可以发现三条清晰的演进主线:底层兼容性、渲染正确性、查询与工具能力。
5.1 v9:React 19 与 primitives 支持
- 9.0.0(Major):
feat: React 19 support——同步 R3F v9 全面切换 React 19,因此 9.x 的 peerDependencies 固定为react ^19.0.0、@react-three/fiber >=9.0.0。 - 9.1.0(Minor):
feat(RTTR): handle primitives in test-renderer and fix queries in TestInstances——这是源码中createVirtualInstance与toGraph/getChildren里primitive分支的直接来源(见 src/createTestInstance.ts 与 src/helpers/graph.ts)。此前<primitive object={...}>的 THREE 子对象无法通过 TestInstance 查询,v9.1.0 之后它们会被包装成虚拟实例并纳入children、toGraph()。
5.2 v8:React 18 兼容与稳定性修复
- 8.0.0(Major):
v8 major, react-18 compat,与@react-three/fiber@8.0.0同步发布。 - 8.1.x 系列:集中补齐 Node 环境缺失的浏览器能力——
support WebGL2(8.1.3)、fallback to canvas shim(8.1.4)、implement HTMLCanvasElement.getContext(8.1.5)、transpile class properties(8.1.2)、backport traverse, update fixes(8.1.1)。这些正是 src/createTestCanvas.ts 与 src/WebGL2RenderingContext.ts 的成因:Node 下无document时构造假 canvas 对象,并兜底globalThis.WebGLRenderingContext/WebGL2RenderingContext。 - 8.2.x 系列:
narrow React peer dep range(8.2.2)、republish with types(8.2.3)、include types in output(8.2.4),以及核心工具feat(RTTR): waitFor util(8.2.0)与set initial size for NaN in viewport(8.2.1)。 - 8.0.x 补丁:多与 fiber 8.0.x 的联动修复相关,例如
update viewport on camera changes(8.0.15)、infinite loop updating cam viewport(8.0.16)、allow invalidate to preempt more than 1 frame(8.0.12)、Add support for recoverable errors(8.0.11),体现了 RTTR 与 R3F 版本严格同步的发布策略。
5.3 v7:事件与 attach 体系的早期打磨
- 7.0.x:
fix rttr didn't work with r130(7.0.1,适配 three r130)、Add controls state field(7.0.1)、Allow elements to define attachFns(7.0.4)、Add useLoader.clear(Loader, input)(7.0.5)、Simplify useframe, support instanced event cancelation(7.0.7)、Fix diffProps dashed keys(7.0.8)、cleanup captured pointers when released(7.0.23)等。 - 7.0.0(Major):
fix javascript interpreting renderpriority as positive——处理渲染优先级数值被 JS 解释为正数的问题。 - 事件能力在此阶段成形:6.2.2 的
use more helpful name with event handling in rttr与 6.1.3 的exclude event functions from event data说明fireEvent机制在 v6 末期已具备雏形。
5.4 测试用例对版本能力的验证
仓库内的测试直接对应上述能力(见 packages/test-renderer/src/tests):
- RTTR.core.test.tsx:验证 JSX 渲染、带 hooks 的组件、
useTransition、空场景、复合组件与toGraph()结构(expect(renderer.scene.children[0].type).toEqual('Mesh')等); - RTTR.events.test.tsx:覆盖
fireEvent与MockSyntheticEvent; - RTTR.hooks.test.tsx:覆盖
useFrame与advanceFrames的帧推进行为; - RTTR.methods.test.tsx:覆盖
find/findAll/findByType/findByProps等查询方法的约束与错误信息; - 快照文件 RTTR.core.test.tsx.snap 展示了
toTree()/toGraph()的实际输出结构。
六、底层实现原理
6.1 无 WebGL 的 canvas shim
src/createTestCanvas.ts 是 RTTR 能在 Node 运行的关键:
- 若环境有
document.createElement,直接创建真实 canvas;否则构造假 canvas 对象,提供style、空事件监听、clientWidth/clientHeight与返回WebGL2RenderingContext的getContext; - 若存在
globalThis.HTMLCanvasElement(如 jsdom 环境),则改写其原型getContext:以webgl开头的 context 请求一律返回 mock 的WebGL2RenderingContext,其余透传; - 兜底注入
globalThis.WebGLRenderingContext与globalThis.WebGL2RenderingContext,避免 three 在初始化时因缺少全局 WebGL 类而报错。
6.2 场景图序列化
toGraph()(src/helpers/graph.ts):沿 R3F 实例的children递归,输出{ type, name, children };遇到primitive节点时,额外把 THREE 对象自身的object.children(THREE.Object3D[])处理进 children。toTree()(src/helpers/tree.ts):输出{ type, props, children },类型名经lowerCaseFirstLetter(src/helpers/strings.ts)转为小写开头。
6.3 渲染流程
create的关键调用链(src/index.tsx):
createCanvas(options) → 得到 mock canvas createRoot(canvas) → R3F reconciler 创建 root _root.configure({ frameloop:'never', size, events: undefined }) mockRoots.get(canvas).store → 取 R3F 全局 store act(() => _root.render(element)) → 在 act 中完成首次渲染 wrapFiber(scene.__r3f) → 得到 renderer.sceneadvanceFrames直接驱动store.getState().internal.subscribers逐帧回调,这正是 R3F 渲染循环在测试环境下的"手动时钟"替代品。
七、实战建议
- 优先用
findByType/findByProps而不是索引访问:children顺序受attach过滤与 primitive 展开影响(v9.1.0 起 primitive 子对象也会进入 children),语义化查询更稳定。 - 异步场景交给
waitFor:模型加载、useTransition等异步更新,用waitFor(() => ...)轮询而非固定setTimeout。 - 动画逻辑用
advanceFrames精确控制:指定确定性的delta,配合act断言,避免真实时钟带来的 flaky 测试。 - 注意版本对齐:RTTR 与
@react-three/fiber严格同版发布(CHANGELOG 中大量Updated dependencies条目可证),升级时两者必须保持匹配;当前 v9 系列要求 React 19。 toGraph()与toTree()分工:断言"场景结构"用toGraph()(不含 attach 元素,贴近 three 视角),断言"React 组件树"用toTree()(含全部 props 与组件)。
八、参考文件索引
- 版本演进:packages/test-renderer/CHANGELOG.md
- 快速开始:packages/test-renderer/README.md
- 渲染器 API:packages/test-renderer/markdown/rttr.md
- 实例 API:packages/test-renderer/markdown/rttr-instance.md
- 核心实现:packages/test-renderer/src/index.tsx、packages/test-renderer/src/createTestInstance.ts、packages/test-renderer/src/createTestCanvas.ts、packages/test-renderer/src/WebGL2RenderingContext.ts
- 测试用例:packages/test-renderer/src/tests/RTTR.core.test.tsx、RTTR.events.test.tsx、RTTR.hooks.test.tsx、RTTR.methods.test.tsx
【免费下载链接】react-three-fiber🇨🇭 A React renderer for Three.js项目地址: https://gitcode.com/GitHub_Trending/re/react-three-fiber
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考