资讯动态

Relay Client 3D 完整指南:基于客户端 Relay Resolvers 的数据驱动依赖

发布时间:2026/9/23 17:09:20 来源:尧图企业网站定制
Relay Client 3D 完整指南基于客户端 Relay Resolvers 的数据驱动依赖【免费下载链接】relayRelay is a JavaScript framework for building>项目地址: https://gitcode.com/gh_mirrors/relay29/relayRelay 的Client 3D客户端数据驱动依赖Client Data Driven Dependencies允许你在渲染 3D 组件所需的全部数据字段都由客户端侧的 Relay Resolvers 解析时按数据内容动态加载对应的 React 组件与代码。本文将围绕 Relay 19 官方文档《Client 3D》展开结合仓库中react-relay的MatchContainer、useClientQuery实现以及编译器测试用例完整讲解 Client 3D 的配置方式、完整示例、module指令约束与底层原理帮助你直接上手并在实际项目中规避已知的性能陷阱。什么是 Client 3D在 Relay 中数据驱动依赖Data Driven Dependencies简称 3D允许根据正在渲染的数据本身动态决定加载哪个组件。当一段数据可能有多种渲染方式时传统做法是把所有可能组件的代码与数据全部打包下发再由客户端写一堆条件分支去选择而 3D 让应用只下载实际被选中的那一个组件及其数据从而显著降低 JavaScript bundle 体积、减少不必要的网络开销并把复杂的条件渲染逻辑收敛到声明式的 GraphQL 指令中。Relay 支持两种 3DServer 3D3D 组件中渲染所需的所有数据都由 GraphQL 服务器解析适合服务端场景Client 3D3D 组件中渲染所需的所有数据字段都由客户端侧的 Relay Resolvers 解析。适用前提只有当 3D 组件渲染所需的全部数据字段均由客户端 Relay Resolvers 解析时才使用 Client 3D。如果数据来自 GraphQL 服务器应使用 Server 3D。Relay Resolvers 是 Relay 的一项特性它允许你用客户端代码扩充 Relay 的 GraphQL 图把只有客户端才知道的值如本地数据、从其他字段推导出的派生数据以与服务器状态一致的方式建模进 schema并通过 Relay 熟悉的取数 API 访问。其底层机制是用带RelayResolverdocblock 注释的导出函数定义 resolverRelay 编译器据此构建客户端 schema 并自动把函数引入生成的产物。Client 3D 正是建立在这些字段全部由 resolver 解析这一前提之上。使用 Client 3D 的配置前提Client 3D 并非开箱即用Server 3D 无需任何配置即可启用你需要在 relay 编译器配置文件中额外添加一个moduleImportConfig字段。配置细节详见>moduleImportConfig: { dynamicModuleProvider: { mode: Custom, statement: () require(./.$module) }, surface: resolvers }dynamicModuleProvider的这些子字段是为了在 Meta 内部代码库中区分不同用例而设计的OSS 场景下按上述方式配置即可。仓库中的编译器集成测试印证了这一配置结构例如client-3D-resolvers-enabled-client-3D-fragment测试夹具见 client-3D-resolvers-enabled-client-3D-fragment.graphql在%project_config%中使用了JSResource模式与surface: resolvers的组合来验证 Client 3D 片段在 resolver 场景下的编译行为moduleImportConfig: { dynamicModuleProvider: { mode: JSResource }, surface: resolvers }另一个测试夹具query-with-module-directive-custom-import.graphql见 query-with-module-directive-custom-import.graphql则展示了Custom模式配合动态import()的写法moduleImportConfig: { dynamicModuleProvider: { mode: Custom, statement: () import($module) } }从源码结构看JSResource模式在 OSS 中主要面向 Meta 内部的 JSResource 体系OSS 开发者按官方文档采用Customrequire/import语句即可两种模式最终都会在编译器产物中把 3D 组件替换为配置的导入语句。完整示例从 Schema 到组件改造下面以一个 React 应用中的完整例子走一遍 Client 3D 的落地过程。核心思路是使用 Client 3D 时你不需要修改任何 Relay Resolvers 或 schema只需改造组件。第一步定义客户端 Schema 扩展在客户端 schema 扩展文件中定义一个接口IClient3D它是查询上一个字段的返回类型type Client3DData { type: String! info: String! } interface IClient3D { id: ID! data: Client3DData! } extend type Query { client3D: IClient3D }第二步定义实现该接口的 Relay Resolvers需要 3 个 Relay Resolvers分别返回实现了IClient3D接口的具体对象。每个 resolver 都包含两个部分一个带implements IClient3D注释的模型 resolver返回带__id的模型对象以及一个定义data字段的字段 resolver。Client3DBar对应BAR类型export type Client3DModel { __id: DataID, }; /** * RelayResolver Client3DBar implements IClient3D */ function Client3DBar(id: DataID): ?Client3DModel { if (id INVALID_ID) { return null; } return { __id: id, }; } /** * RelayResolver Client3DBar.data: Client3DData */ function data(client3DModel: Client3DModel): Client3DData { return { type: BAR, info: someBarInfo, } }Client3DFoo对应FOO类型/** * RelayResolver Client3DFoo implements IClient3D */ function Client3DFoo(id: DataID): ?Client3DModel { if (id INVALID_ID) { return null; } return { __id: id, }; } /** * RelayResolver Client3DFoo.data: Client3DData */ function data(client3DModel: Client3DModel): Client3DData { return { type: FOO, info: someFooInfo, } }Client3DHelloWorld对应HELLO_WORLD类型/** * RelayResolver Client3DHelloWorld implements IClient3D */ function Client3DHelloWorld(id: DataID): ?Client3DModel { if (id INVALID_ID) { return null; } return { __id: id, }; } /** * RelayResolver Client3DHelloWorld.data: Client3DData */ function data(client3DModel: Client3DModel): Client3DData { return { type: HELLO_WORLD, info: someHelloWorldInfo, } }可以看到这三个 resolver 本身并没有任何 3D 相关的痕迹——它们就是普通的 Relay Resolvers。3D 的动态性完全由查询端的module指令与MatchContainer承担。第三步改造前的组件手动条件渲染在使用 Client 3D 之前组件通常长这样先用useClientQuery发起一个纯客户端查询拿到数据后在 JSX 里手写一串if / else if分支按data.type的值选择渲染Client3DFooComponent、Client3DBarComponent还是Client3DHelloWorldComponentcomponent Client3DRelayRenderer() { const CLIENT_3D_FRAGMENT graphql fragment Client3DRelayRendererClient3DFragment on IClient3D { data { type info } } ; const client3DData useClientQuery( graphql query Client3DRelayQuery { client3D { ...Client3DRelayRendererClient3DFragment } } ); let component; if (client3DData?.data?.type FOO): component Client3DFooComponent data{client3DData.data} / else if (client3DData?.data?.type BAR): component Client3DBarComponent data{client3DData.data} / else if (client3DData?.data?.type HELLO_WORLD): component Client3DHelloWorldComponent data{client3DData.data} / return ( component ); }这种写法的痛点是三个子组件的代码全部被静态打包进主 bundle无论type最终是什么都会下载而且每新增一种类型条件分支就要再长一截。第四步改造后的组件module MatchContainer使用 Client 3D 时不需要修改 Relay Resolvers 或 schema只需按以下三步改造组件为每个实现了IClient3D的具体类型分别声明 fragment。本例中即FOO_FRAGMENT、BAR_FRAGMENT、HELLO_WORLD_FRAGMENT给 fragment 加上module指令并把与该 fragment 数据对应的 UI 组件名作为name参数传入用 Relay 的MatchContainer返回最终组件把查询返回的数据作为matchprop 传入。改造后的组件代码const {graphql, useFragment, useClientQuery, MatchContainer} require(react-relay); component Client3DRelayRenderer() { const FOO_FRAGMENT graphql fragment Client3DFooComponent_Fragment on Client3DFoo { data { type info } } ; const BAR_FRAGMENT graphql fragment Client3DBarComponent_Fragment on Client3DBar { data { type info } } ; const HELLO_WORLD_FRAGMENT graphql fragment Client3DHelloWorldComponent_Fragment on Client3DHelloWorld { data { type info } } ; const client3DData useClientQuery( graphql query Client3DRelayQuery { client3D { ...Client3DFooComponent_Fragment module(name: Client3DFooComponent.react) ...Client3DBarComponent_Fragment module(name: Client3DBarComponent.react) ...Client3DHelloWorldComponent_Fragment module(name: Client3DHelloWorldComponent.react) } } ); return ( MatchContainer match{client3DData.client3D} / ); }对比改造前后可以发现原来分散在 JSX 中的if / else if条件分支全部消失取而代之的是三个声明式的module片段展开。每种具体类型对应的组件及其数据fragment变成了一个可动态获取的依赖只有当该类型被选中时才真正加载。module 的合法使用边界Client 3D 与 Server 3D 一样不能在同一个具体类型concrete type上的多个 fragment 上使用module但可以分布在同一个抽象类型上即 union 或 interface。以上面例子来说Client3DFooComponent_Fragment位于具体类型Client3DFoo上Client3DBarComponent_Fragment位于具体类型Client3DBar上。如果Client3DBarComponent_Fragment也放在了Client3DFoo上relay 编译器会直接报错。而这三个具体类型都实现了同一个父接口IClient3D这是完全允许的——编译器可以据此在运行期分辨应该加载哪个组件。底层原理MatchContainer 与 useClientQueryMatchContainer 如何渲染动态组件MatchContainer是 Client 3D 的消费端核心组件源码位于 packages/react-relay/relay-hooks/MatchContainer.js。它接收match一个module选择产生的不透明对象包含__id、__fragments、__fragmentOwner、__fragmentPropName、__module_component等元数据、可选的loader根据模块引用加载对应 React 组件的函数与props透传给动态选中组件的属性。核心逻辑对应 MatchContainer.js对match值做形状校验如果它不是对象且非 null/undefined或缺少合法的 fragment 展开结构__fragments、__id等会抛出MatchContainer: Invalid match value, expected an object that has a ...SomeFragment spread.之类的错误通过loader(__module_component)获得动态加载的组件LoadedContainer并用useMemo基于__fragmentPropName/__id/__fragments/__fragmentOwner构造要传给该组件的 fragment props当组件与 fragment props 都就绪时渲染LoadedContainer {...props} {...fragmentProps} /否则渲染fallback ?? null。从源码注释可以确认MatchContainer的 props 中fallback用于兜底、loader用于异步解析模块引用、props会被透传给所有可能被选中的组件——这要求所有module候选组件都能接受同一组 props。需要特别注意的是MatchContainer在加载组件或数据时可能会 suspend因此建议像 Server 3D 一样用React.Suspense包裹。useClientQuery纯客户端查询的入口Client 3D 的查询端使用useClientQuery发起纯客户端查询。其源码位于 packages/react-relay/relay-hooks/useClientQuery.js实现上它只是对useLazyLoadQuery的一层封装hook useClientQueryTVariables extends Variables, TData, TRawResponse( gqlQuery: ClientQueryTVariables, TData, TRawResponse, variables: NoInferTVariables, options?: { UNSTABLE_renderPolicy?: RenderPolicy, }, ): TData { // client queries can be used with useLazyLoadQuery, but only with store-only policy. const query: QueryTVariables, TData gqlQuery; return useLazyLoadQuery(query, variables, { ...options, fetchPolicy: store-only, }); }要点它强制使用fetchPolicy: store-only即只从本地 Relay Store 读取数据、不向服务器发起网络请求——这与数据全部由客户端 resolver 解析的定位完全一致当查询里只包含客户端定义的字段时例如只有 resolver 字段和客户端 schema 扩展字段必须使用useClientQuery这类客户端查询 API而不是useLazyLoadQuery或usePreloadedQuery如果查询同时包含服务器数据则仍可使用标准 API参见 Relay Resolvers 介绍。编译产物侧的证据在 Relay 编译器层面Client 3D 的module片段会通过moduleImportConfig的配置生成对应的动态导入代码。仓库的relay-compiler集成测试中保存了真实的编译产物例如client-3D-resolvers-enabled-client-3D-fragment夹具见 编译输入 与 编译输出 .expected它同时包含一个定义在ClientUser/SpecialUser两个 resolver 模型类型上的module片段展开以及一个 Server 3D fragment 的对照夹具用于验证两类 3D 在编译器中的不同处理路径。另一个夹具query-with-module-directive-custom-import.graphql则验证了Custom模式下生成自定义导入语句的编译行为。如果你要深入调试 Client 3D 的产物形态这些测试夹具是很好的参照物。局限性往返次数与嵌套问题Client 3D 带来了更直观的开发体验、更强的可维护性和更快的性能但它也存在 Server 3D 所没有的局限。关键差异在于获取数据所需的往返round trip次数Server 3D最多需要两次往返一次向服务器取数据一次向 CDN 取代码Client 3D在渲染组件的过程中才执行 resolver 代码这意味着客户端必须先渲染组件才能发现到底需要哪些 JavaScript 代码。这可能导致额外的往返尤其是在嵌套使用 Client 3D时。举个官方文档中的例子一篇博客文章用 Client 3D 决定渲染图片博文还是文本博文而文本博文内部又用 Client 3D 决定采用哪种文本排版格式。这种嵌套会让组件加载变成层层递进的过程产生多次往返。关于这一点文档明确说明Relay 目前正在着手解决这一缺陷但相关方案尚未生产化productionized。因此在使用 Client 3D 时请务必避免嵌套使用以防出现性能退化。如果确实存在嵌套诉求建议评估 Server 3D 或把内层动态选择上移到更外层。总结Client 3D 是 Relay 数据驱动依赖体系在纯客户端数据场景下的落地方案它把 Relay Resolvers 解析出的数据与按需加载的 React 组件通过module指令和MatchContainer组合在一起配置在 relay 编译器配置中新增moduleImportConfigOSS 下使用dynamicModuleProvider.mode Custom自定义statement与surface resolvers开发流程定义客户端 schema 扩展 → 编写实现同一接口的多个 Relay Resolvers → 为每个具体类型声明独立 fragment 并加module(name: ...)→ 用useClientQuery发起纯客户端查询 → 用MatchContainer渲染约束同一具体类型上不能出现多个modulefragment但同一抽象类型union/interface下可以原理useClientQuery强制store-only取数策略MatchContainer负责校验 match 结构、动态加载组件并注入 fragment props源码见 MatchContainer.js代价组件加载依赖先渲染才能发现依赖嵌套使用会放大往返次数当前应避免嵌套以规避性能退化。如需进一步了解 3D 的整体概念与 Server 3D 的完整语法含match指令、多 3D 选择key、非 React 模块的ModuleResource.read()等可继续阅读 数据驱动依赖介绍、Server 3D 与 3D 配置 等配套文档。【免费下载链接】relayRelay is a JavaScript framework for building>项目地址: https://gitcode.com/gh_mirrors/relay29/relay创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价