ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Relay 数据驱动依赖(3D)完全指南:从 @module/@match 到 MatchContainer 的动态组件加载实战

Relay 数据驱动依赖(3D)完全指南:从 @module/@match 到 MatchContainer 的动态组件加载实战 前端开发工具【免费下载链接】relayRelay is a JavaScript framework for building>项目地址https://gitcode.com/gh_mirrors/relay29/relay点击查看免费下载数据驱动依赖Data Driven Dependencies简称 3D是 Relay 中用于按需动态加载组件与数据的关键特性当同一份数据可能由多种组件渲染时应用只下载实际被选中组件的代码与数据从而显著减小 JavaScript bundle 体积、提升加载性能。本篇指南以官方>fragment Comment_comment on Comment { image { ...CommentImage_image module(name: CommentImage.react) } }Relay Compiler 会把它改写为服务器实际收到的查询展开为内联片段并附加__component、__fragment两个js()元数据字段用于指示运行时下载哪个组件与哪份 normalization 片段fragment Comment_comment on Comment { image { ... on CommentImage { ...CommentImage_image __component: js(CommentImage.react) __fragment: js(CommentImage_image$normalization.graphql) } } }在消费端不要静态require该组件那会引入静态依赖破坏按需加载而是用MatchContainer动态渲染const {useFragment, graphql, MatchContainer} require(react-relay); function CommentRenderer(props) { const comment useFragment( /* fragment Comment_comment from above */, props.comment, ); if (comment.image null) { // 处理字段加载失败或为 null 的情况 return null; } // MatchContainer 在加载组件或其数据时可能挂起suspend // 建议用 React.Suspense 包裹。 return ( Suspense fallback{null} MatchContainer match{comment.image} props{{ /* ...other props... */ }} / /Suspense ); } module.exports CommentRenderer;高级场景match指令的客户端/服务端协商当同一内容存在多种渲染策略如Comment可渲染为 Markdown 或纯文本时客户端与服务器需要协商出每个内容的最优策略。Relay 用match支持这种协商其设计原则是客户端声明它支持的策略集合、每种策略对应的 React 组件以及每种策略需要的数据片段服务器具体来说是 schema 中的产品逻辑根据用户、数据与客户端支持的策略选出最优策略被选中策略的代码组件与数据GraphQL在选中后动态下载数据走普通 GraphQL 通道代码元数据则通过 GraphQL 响应extensions字段这条旁路side-channel下发。客户端写法如下fragment Comment_comment on Comment { comment_content_renderer match { ...CommentMarkdownRenderer_comment module(name: CommentMarkdownRenderer.react) ...CommentPlaintextRenderer_comment module(name: CommentPlaintextRenderer.react) } }服务器收到的查询会自动带上supported参数该参数由编译器根据你提供的片段类型自动生成fragment Comment_comment on Comment { comment_content_renderer(supported: [CommentMarkdownRenderer, CommentPlaintextRenderer]) { ... on CommentMarkdownRenderer { ...CommentMarkdownRenderer_comment __component: js(CommentMarkdownRenderer.react) __fragment: js(CommentMarkdownRenderer_comment$normalization.graphql) } ... on CommentPlaintextRenderer { ...CommentPlaintextRenderer_comment __component: js(CommentPlaintextRenderer.react) __fragment: js(CommentPlaintextRenderer_comment$normalization.graphql) } } }消费端同样使用MatchContainerconst React require(React); const {Suspense} React; const {graphql, useFragment, MatchContainer} require(react-relay); function CommentRenderer(props) { const comment useFragment( /* fragment from above */, props.comment, ); if (comment.comment_content_renderer null) { return null; } return ( Suspense fallback{null} MatchContainer match{comment.comment_content_renderer} props{{/* other props */}} / /Suspense ); } module.exports CommentRenderer;注意同一父 3D 片段下用module标注的内联片段必须落在不同的具体类型上。若它们落在同一具体类型Relay Compiler 会直接报错。例如上例中CommentMarkdownRenderer_comment与CommentPlaintextRenderer_comment必须分属不同具体类型如MarkdownComment与PlaintextComment两者可以同时实现父接口Comment。Client 3D 中同样遵循此规则不能在同一个具体类型上使用多个module但可以在同一个抽象类型联合或接口下使用。同一片段内的多个 3D 选择key参数如果一个组件需要在单个片段中选择多个数据驱动依赖那么每个 3D 字段都必须用不同的key命名。key通过给父字段加match(key: ...)提供。下面这种写法是错误的# DOESNT WORK fragment Example_comment on Comment { comment_content_renderer match { ...CommentMarkdownRenderer_comment module(name: CommentMarkdownRenderer.react) } attachments { attachment_renderer { ...CommentAttachmentPhotoRenderer_comment module(name: CommentPhotoRenderer.react) } } }编译器会报出类似如下错误Error: Invalid module selection: documents with multiple fields containing 3D selections must specify a unique key value for each field: use attachment_renderer match(key: ExampleComment_localName).修正方式是按报错提示为第二个 3D 字段补上match(key: ...)# OK - 使用 match 指定不同 key fragment Example_comment on Comment { comment_content_renderer match { ...CommentMarkdownRenderer_comment module(name: CommentMarkdownRenderer.react) } attachments { attachment_renderer match(key: Example_comment__attachment) { ...CommentAttachmentPhotoRenderer_comment module(name: CommentPhotoRenderer.react) } } }key的内部作用是Relay 用它在 store 中隔离每个字段的查询结果即使两个字段返回了同一个对象也不会发生结果冲突。与 Relay Hooks 结合使用推荐通过useFragment使用 3D// CommentRenderer.react.js const {graphql, useFragment, MatchContainer} require(react-relay); function CommentRenderer(props) { const comment useFragment( graphql fragment Comment_comment on Comment { image { ...CommentImageRenderer_image module(name: CommentImageRenderer.react) } } , props.comment, ); if (comment.image null) { return null; } return ( Suspense fallback{null} MatchContainer match{comment.image} props{{...other props...}} / /Suspense ); } module.exports CommentRenderer;被动态加载的组件本身也可以是一个使用useFragment的组件// CommentImageRenderer.react.js import type {CommentImageRenderer_image$key} from CommentImageRenderer_image.graphql const {useFragment} require(react-relay); type Props { image: CommentImageRenderer_image$key, }; function CommentImageRenderer(props) { const data useFragment( graphql fragment CommentImageRenderer_image on Image { src } , props.image, ); return (...); } module.exports CommentImageRenderer;动态加载非 React 模块3D 并不局限于 React 组件也可以动态加载任意 JS 模块。此时MatchContainer不适用应改用ModuleResource.read()手动读取module结果。下面是matchmodule场景的手动读取改写const React require(React); const {Suspense} React; const {graphql, useFragment, ModuleResource} require(react-relay); const CommentFragment graphql fragment Comment_comment on Comment { comment_content_renderer match { ...CommentMarkdownRenderer_comment module(name: CommentMarkdownRenderer.react) ...CommentPlaintextRenderer_comment module(name: CommentPlaintextRenderer.react) } } ; function CommentRenderer(props) { const comment useFragment(CommentFragment, props.comment); if (comment.image null) { return null; } // 注意模块未加载完成时这里会挂起suspend // 应由 *父组件* 用 Suspense 边界包裹本组件。 // MatchedModule 取值 // - null 表示没有匹配结果 // - CommentMarkdownRenderer.react 当结果是 markdown 时 // - CommentPlaintextRenderer.react 当结果是 plaintext 时 const MatchedModule ModuleResource.read(comment.image); if (MatchedModule null) { return null; // 没有匹配 } // 这里需要确保所有可能的匹配组件都在同一个 prop key本例为 comment上接收数据。 // 注意MatchContainer 会自动确定匹配数据使用的正确 prop key。 return ( MatchedModule comment{comment.image} / ); } module.exports CommentRenderer;也可以只用module不配合match在字段非空时直接加载某个非 React 模块并同样用ModuleResource.read()消费function CommentRenderer(props) { const comment useFragment( graphql fragment Comment_comment on Comment { image { ...CommentImage_image module(name: ImageProcessingModule) } } , props.comment, ); if (comment.image null) { return null; } // 注意模块未加载完成时这里会挂起 const ImageProcessingModule ModuleResource.read(comment.image); if (ImageProcessingModule null) { return null; // 没有匹配 } // ... }注意module需要一个片段且该片段不能为空。如果你不需要从服务器拉取任何数据只想条件性地加载文件可以为字段定义一个占位dummy片段// 定义一个包装片段供 module 使用 graphql fragment FragmentForModule_image on Image { __typename # 这里只需要 __typename因为我们不需要任何数据 } ; function CommentRenderer(props) { const comment useFragment( graphql fragment Comment_comment on Comment { image { ...FragmentForModule_image module(name: ImageProcessingModule) } } , props.comment, ); // ... }Client 3D基于 Relay Resolvers 的客户端数据驱动加载Client 3D 适用于 3D 组件所需的数据全部由客户端侧 Relay Resolvers 解析的场景。它的最大优势是不需要改动 GraphQL 服务端 schema 与解析逻辑并且是 OSS 中完整支持的形式。完整示例假设在客户端 schema 扩展文件中有一个接口IClient3D作为 query 上某个字段的返回类型type Client3DData { type: String! info: String! } interface IClient3D { id: ID! data: Client3DData! } extend type Query { client3D: IClient3D }再定义 3 个返回实现IClient3D的具体对象的 Relay Resolvers例如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与Client3DHelloWorld的 resolver 写法完全同构分别返回type: FOO/type: HELLO_WORLD。在使用 Client 3D 之前组件需要手工写条件分支来分发组件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 ); }使用 Client 3D 时不需要修改任何 Relay Resolvers 或 schema只需按以下三步改造组件为IClient3D的每个具体类型分别声明一个片段本例为FOO_FRAGMENT、BAR_FRAGMENT、HELLO_WORLD_FRAGMENT在片段上添加module指令并把对应 UI 组件的名字作为参数传入用 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} / ); }这里用到的useClientQuery是渲染客户端纯查询的 Hook其实现位于 packages/react-relay/relay-hooks/useClientQuery.js它本质上是对useLazyLoadQuery的封装并强制使用fetchPolicy: store-only——即不发起网络请求只从本地 store 读取由 client schema extensions 和 Relay Resolvers 定义的数据。Client 3D 的局限Client 3D 在带来更直观的开发体验、更好的可维护性与更快性能的同时也存在 Server 3D 没有的限制——主要是网络往返次数Server 3D 最多需要两次往返一次到服务器取数据、一次到 CDN 取代码Client 3D 在渲染组件的过程中才会执行 resolver 代码来发现需要哪些 JS 代码因此客户端需要先渲染一遍组件才能知道要下载什么。当存在嵌套的 Client 3D时例如一篇博客先用 Client 3D 决定渲染成图文帖还是纯文本帖纯文本帖内部又用 Client 3D 决定文本展示格式就会产生多次往返造成性能退化。Relay 团队正在解决这一问题但相关方案尚未生产化。因此使用 Client 3D 时请避免嵌套使用。编译器配置启用 Client 3DServer 3D 默认启用无需任何配置。要使用 Client 3D需要在 Relay 的编译器配置文件中新增moduleImportConfig字段。该配置项的底层结构定义在 compiler/crates/relay-config/src/module_import_config.rs 中包含两个子配置dynamicModuleProvider定义 3D 组件的导入方式dynamicModuleProvider有两个子字段mode和statement。在配置解析层面它对应源码中的ModuleProvider枚举module_import_config.rsJSResource指示 Relay Compiler 把 3D 组件作为可动态加载的 JS 模块JSResource导入此时可以完全省略statement子字段。例如moduleImportConfig: { dynamicModuleProvider: { mode: JSResource } },Custom使用自定义的 import 语句加载 3D 组件。在statement中用$module作为组件名的占位符。例如moduleImportConfig: { dynamicModuleProvider: { mode: Custom, statement: function() { var JSResource require(JSResource); return JSResource(m#$module); } } }surface指定启用 Client 3D 的面surface对应源码中的Surface枚举module_import_config.rs可以取以下值resolversClient 3D 仅对完全由客户端 Relay Resolvers 决定的数据字段生效可视为纯正的 Client 3D 用例None省略该字段这是 Server 3D 的一种特殊变体——3D 组件的模块信息由 Relay Compiler 在客户端侧维护而不是随数据从服务器下载。该变体相比 Server 3D 没有性能收益主要是为了支持平台间代码共享而设计all同时启用以上两种情况。OSS 中的推荐配置在 OSS 场景下dynamicModuleProvider.mode应设为Customstatement按你的模块系统编写$module占位surface应设为resolvers。一个完整的 OSS 配置示例moduleImportConfig: { dynamicModuleProvider: { mode: Custom, statement: () require(./.$module) }, surface: resolvers }从编译器实现来看ModuleImportConfig还包含可选的operationModuleProvider子字段用于定义NormalizationModuleImport节点上的operationModuleProvider函数服务于 exec-time Client 3D 场景见 module_import_config.rs。此外project_config.rs中的effective_module_import_config()project_config.rs负责在未显式配置时推导出生效的模块导入配置。编译器侧的实现原理match 变换match与module的校验与改写由 Relay Compiler 的 match 变换完成入口为 compiler/crates/relay-transforms/src/match_/match_transform.rs 中的transform_matchmatch_transform.rs它会读取module_import_config与特性开关遍历 Program 完成变换并在出错时返回诊断信息。指令名称常量定义在 compiler/crates/relay-transforms/src/match_/constants.rsjs_field_module_arg对应module参数、match_directive_name对应match指令、module_directive_name对应module指令。配合同目录下的split_module_import.rs、split_operation_metadata.rs、hash_supported_argument.rs等模块编译器完成为module片段生成__component/__fragment元数据字段与模块导入代码为match字段自动生成supported参数由片段覆盖的具体类型集合哈希而来校验同一具体类型上不得有多个module多 3D 字段必须提供唯一key等规则并产出前文所述错误信息。这解释了为什么上文的supported: [CommentMarkdownRenderer, CommentPlaintextRenderer]参数无需手写——它是编译器根据片段类型自动推导的。常见问题与排查结合官方文档与源码实现使用 3D 时最常见的坑有MatchContainer会挂起suspend它会等到被选中的组件加载完成才渲染所以务必用Suspense包裹否则会出现意外的挂起或错误。这是文档 server-3d.md 在 Troubleshooting 一节强调的最重要一点。props 命名不匹配动态组件必须使用与片段后缀相同的 prop 名Comment_comment→comment否则MatchContainer构造的数据引用无法被组件消费对应 MatchContainer.js 中基于__fragmentPropName拼接 props 的逻辑。同一具体类型上的多个module编译器会报错请把各module内联片段放到不同的具体类型上。多 3D 字段缺少key报错信息会直接给出建议的match(key: ...)写法按提示补齐即可。module片段为空module需要非空片段无数据需求时用只含__typename的占位片段。嵌套使用 Client 3D会导致额外网络往返应避免嵌套。小结数据驱动依赖3D把该渲染什么组件、该下载哪些代码和数据的决定权交给数据本身module覆盖字段通常为空 / 联合类型场景match覆盖多渲染策略协商场景MatchContainer/ModuleResource负责在运行时消费匹配结果而 Client 3D 则通过 Relay Resolvers 在纯客户端闭环中实现同样的按需加载。OSS 用户可以开箱即用地启用 Client 3D仅需在编译器配置中加入moduleImportConfigServer 3D 则需要配套的服务端能力。理解了编译器的 match 变换与MatchContainer的渲染协议之后你就能在实际项目中安全地按需加载组件把所有可能的代码都打包换成只打包用户真正看到的。赞分享前端开发工具【免费下载链接】relayRelay is a JavaScript framework for building>项目地址https://gitcode.com/gh_mirrors/relay29/relay点击查看免费下载相关推荐Relay Server 3D数据驱动依赖实战指南module、match 与 MatchContainer 全解析Relay Server 3D数据驱动依赖实战指南module、match 与 MatchContainer 全解析 导读 本文围绕 Relay 的前端开发工具Relay 数据驱动依赖module端到端实践从 schema 定义到 MatchContainer 渲染与快照验证Relay 数据驱动依赖module端到端实践从 schema 定义到 MatchContainer 渲染与快照验证 Relay 的数据驱动依赖Dat前端开发工具Relay Data-Driven Dependenciesmodule实战基于 Union 类型与 MatchContainer 的按需组件加载Relay Data Driven Dependenciesmodule实战基于 Union 类型与 MatchContainer 的按需组件加载 本篇前端开发工具上一篇Git.js: 更好地管理和操作你的 Git 仓库下一篇Teoria.js 音乐理论库使用教程创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表