Relay Client 3D 完整指南:基于客户端 Relay Resolvers 的数据驱动依赖
2026/9/23 17:09:15 网站建设 项目流程

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

【免费下载链接】relayRelay is a JavaScript framework for building>项目地址: https://gitcode.com/gh_mirrors/relay29/relay

Relay 的Client 3D(客户端数据驱动依赖,Client Data Driven Dependencies)允许你在渲染 3D 组件所需的全部数据字段都由客户端侧的 Relay Resolvers 解析时,按数据内容动态加载对应的 React 组件与代码。本文将围绕 Relay 19 官方文档《Client 3D》展开,结合仓库中react-relayMatchContaineruseClientQuery实现以及编译器测试用例,完整讲解 Client 3D 的配置方式、完整示例、@module指令约束与底层原理,帮助你直接上手并在实际项目中规避已知的性能陷阱。

什么是 Client 3D

在 Relay 中,数据驱动依赖(Data Driven Dependencies,简称 3D)允许根据"正在渲染的数据本身"动态决定加载哪个组件。当一段数据可能有多种渲染方式时,传统做法是把所有可能组件的代码与数据全部打包下发,再由客户端写一堆条件分支去选择;而 3D 让应用只下载实际被选中的那一个组件及其数据,从而显著降低 JavaScript bundle 体积、减少不必要的网络开销,并把复杂的条件渲染逻辑收敛到声明式的 GraphQL 指令中。

Relay 支持两种 3D:

  • Server 3D:3D 组件中渲染所需的所有数据都由 GraphQL 服务器解析,适合服务端场景;
  • Client 3D:3D 组件中渲染所需的所有数据字段都由客户端侧的 Relay Resolvers 解析。

适用前提:只有当 3D 组件渲染所需的全部数据字段均由客户端 Relay Resolvers 解析时,才使用 Client 3D。如果数据来自 GraphQL 服务器,应使用 Server 3D。

Relay Resolvers 是 Relay 的一项特性,它允许你用客户端代码扩充 Relay 的 GraphQL 图,把"只有客户端才知道的值"(如本地数据、从其他字段推导出的派生数据)以与服务器状态一致的方式建模进 schema,并通过 Relay 熟悉的取数 API 访问。其底层机制是:用带@RelayResolverdocblock 注释的导出函数定义 resolver,Relay 编译器据此构建客户端 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 开发者按官方文档采用Custom+require/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的值选择渲染Client3DFooComponentClient3DBarComponent还是Client3DHelloWorldComponent

component 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,只需按以下三步改造组件:

  1. 为每个实现了IClient3D的具体类型分别声明 fragment。本例中即FOO_FRAGMENTBAR_FRAGMENTHELLO_WORLD_FRAGMENT
  2. 给 fragment 加上@module指令,并把与该 fragment 数据对应的 UI 组件名作为name参数传入;
  3. 用 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 与 useClientQuery

MatchContainer 如何渲染动态组件

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 useClientQuery<TVariables extends Variables, TData, TRawResponse>( gqlQuery: ClientQuery<TVariables, TData, TRawResponse>, variables: NoInfer<TVariables>, options?: { UNSTABLE_renderPolicy?: RenderPolicy, }, ): TData { // client queries can be used with useLazyLoadQuery, but only with `store-only` policy. const query: Query<TVariables, TData> = gqlQuery; return useLazyLoadQuery(query, variables, { ...options, fetchPolicy: 'store-only', }); }

要点:

  • 它强制使用fetchPolicy: 'store-only',即只从本地 Relay Store 读取数据、不向服务器发起网络请求——这与"数据全部由客户端 resolver 解析"的定位完全一致;
  • 当查询里只包含客户端定义的字段时(例如只有 resolver 字段和客户端 schema 扩展字段),必须使用useClientQuery这类客户端查询 API,而不是useLazyLoadQueryusePreloadedQuery;如果查询同时包含服务器数据,则仍可使用标准 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 编译器配置中新增moduleImportConfig,OSS 下使用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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询