
后端前端即时通讯社交【免费下载链接】spectrumSimple, powerful online communities.项目地址https://gitcode.com/gh_mirrors/sp/spectrum点击查看免费下载导读Fragments 是 Spectrum 开源社区项目中前端与 GraphQL API 交互的核心基础设施它帮助团队在任何组件中都能精确预知一次查询会返回什么数据从而避免在组件里写了user.username却发现查询里根本没取这个字段的混乱。本文以该文档为骨架结合仓库中 shared/graphql/fragments 与 shared/graphql/queries 的真实源码系统讲解 fragment 的定义、组合、目录结构规范以及性能边界读完后你可以在自己的 GraphQL React 项目中直接复刻这套可维护的查询组织方案。为什么需要 Fragments一场由数据不一致引发的灾难在大型前端应用中同一个user对象往往会被多个组件、多个页面反复查询。如果每个查询都各自为政、随手书写字段就会得到这样的局面# query 1 user { uid displayName } # query 2 user { uid username }原文档docs/backend/api/fragments.md明确指出当这类查询被散落在不同地方、服务于不同组件时你永远无法确定自己手里到底有什么数据。写 React 组件时你可能下意识写出{ user.username }但实际实现它的查询来自 query 1——于是运行时直接抛错或渲染空白。这种隐性契约的缺失是团队协作中极其隐蔽的 bug 来源。Fragments 的引入让这次查询到底返回什么变成一种显式、可复用、可审查的约定查询的字段集被收敛为命名片段组件与数据层之间的契约一目了然。最小可运行示例定义与消费一个 Fragment原文档给出了最经典的用法先用gql定义片段再在查询中通过展开操作符...userInfo引用它。定义片段对应仓库中的 shared/graphql/fragments/user/userInfo.jsimport { gql } from react-apollo; export const userInfoFragment gql fragment userInfo on User { uid photoURL displayName username } ;在查询文件中消费该片段const getUser gql query getUser($username: String, $after: String) { user(username: $username) { ...userInfo } } ${userInfoFragment} ;要点拆解片段名即契约名fragment userInfo on User中的userInfo是片段名User是它绑定的 GraphQL 对象类型。展开时必须在on指定的类型上使用。模板插值注入在gql模板字符串末尾用${userInfoFragment}将片段定义随查询一起发送Apollo 客户端才能在解析查询时找到并内联该片段。查询参数可省略getUser($username: String, $after: String)中$after在此示例中并未实际使用说明片段允许查询声明了比实际消费更多的变量但反向——片段需要而查询未声明——则会在解析时报错。仓库中的真实实现比示例更严谨在 shared/graphql/queries/user/getUser.js 中查询文件同时导出了 Flow 类型GetUserType它通过...$ExactUserInfoType直接复用片段文件里导出的类型保证 TypeScript/Flow 侧的静态类型与 GraphQL 侧的字段集永远同步// shared/graphql/queries/user/getUser.js import userInfoFragment from ../../fragments/user/userInfo; import type { UserInfoType } from ../../fragments/user/userInfo; export type GetUserType { ...$ExactUserInfoType, };同一份userInfoFragment还被 getUserByIdQuery按 id 查询、getUserByUsernameQuery按用户名查询以及getCurrentUserQuery当前登录用户三处复用充分体现一次定义、处处受惠的设计初衷。分页场景用 Fragment 固化 Connection 查询原文档强调fragments 在分页逻辑中尤其有价值它既帮你省去反复书写冗长查询的麻烦又能保证实现分页时不会漏掉cursor、hasNextPage这类关键字段。原文示例export const userCommunitiesFragment gql fragment userCommunities on User { communityConnection { pageInfo { hasNextPage hasPreviousPage } edges { node { ...communityInfo } } } } ${communityInfoFragment} ; export const communityInfoFragment gql fragment communityInfo on Community { id name slug } ;这里有两个值得注意的细节分页契约被内嵌进片段pageInfo含hasNextPage/hasPreviousPage和edges结构全部固化在userCommunities片段里。任何消费该片段的查询都自动拥有完整的分页能力组件层永远不需要猜测这次有没有返回 cursor。子片段嵌套...communityInfo展开在edges.node层级实现了一个 fragment 内部引用另一个 fragment 的组合关系。仓库中的实现印证并深化了这一模式。shared/graphql/fragments/user/userCommunityConnection.js 在edges.node上同时展开了...communityInfo与...communityMetaData并额外索取了contextPermissions当前用户在该社区下的权限状态export default gql fragment userCommunityConnection on User { communityConnection { pageInfo { hasNextPage hasPreviousPage } edges { node { ...communityInfo ...communityMetaData contextPermissions { communityId isOwner isModerator } } } } } ${communityMetaDataFragment} ${communityInfoFragment} ;类似的 Connection 型片段还有threadMessageConnection.js线程消息列表edges.node展开...messageInfo且使用 Apollo 的connection(key: messageConnection)指令对分页结果做规范化缓存注意它把after/first/before/last参数直接声明在片段内——这是片段需要由查询提供参数的典型场景userThreadConnection.js用户发布的线程列表edges.node展开...threadInfodirectMessageThreadMessageConnection.js私信消息列表。这些片段把分页字段是否齐全从凭记忆书写变成了引用即保证正是原文档所说save ourselves the hassle免去重复劳动与ensure we havent forgotten a key field杜绝漏字段的直接体现。目录结构与粒度策略如何对抗循环依赖原文档中有一张 2017 年的截图展示了 fragment 文件的目录结构截图来自旧版 GitHub 云存储仓库内已不可见并解释了这套结构背后的硬性原因之所以把 fragments 拆得如此琐碎obnoxiously granular是因为存在循环 fragment 依赖的可能——例如一个用户可能要求一个 story而 story 又可能要求用户。当文件互相 import 时webpack 会直接炸掉。Spectrum 的解法是把每个 fragment 放进独立文件按资源类型组织目录。从仓库的实际目录看shared/graphql/fragments这套结构已经演化为清晰的九大资源目录shared/graphql/fragments/ ├── channel/ channelInfo, channelMemberConnection, channelMetaData, channelThreadConnection ├── community/ communityInfo, communityChannelConnection, communityMembers, communityMetaData, communitySettings, communityThreadConnection ├── communityMember/ communityMemberInfo ├── directMessageThread/ directMessageThreadInfo, directMessageThreadMessageConnection ├── message/ messageInfo, directMessageInfo ├── notification/ notificationInfo ├── thread/ threadInfo, threadMessageConnection, threadParticipant └── user/ userInfo, userChannelConnection, userCommunityConnection, userDirectMessageThreadConnection, userEverythingConnection, userSettings, userThreadConnection这种组织方式的收益是双重的命名即自解释userInfo、communityInfo、threadInfo这类名字让从哪 import、这个片段是什么一目了然正如原文档所说从命名约定的角度看相当有用在 import 文件和实际使用片段时都非常清晰单向依赖、避免环每个片段文件只依赖比自己更底层的片段。从源码可以看到依赖方向是单向的——user依赖community/threadthread依赖user/community/channelchannel依赖community而communityInfo本身是自足的叶子片段shared/graphql/fragments/community/communityInfo.js。一旦某个片段出现跨层引用如userCommunityConnection引用community下的片段依赖方向依然保持由具体资源指向通用资源从而规避了 webpack 的循环引用问题。一个值得注意的演进点原文档示例中的字段名如story、Story类型在现行仓库中已经演化为thread/Threadshared/graphql/fragments/thread/threadInfo.js这是项目从早期 Story 概念重构为 Thread 概念的证据但文档所述的粒度原则与组织思路完全延续了下来。性能边界片段要浅查询要深原文档用一半篇幅强调性能纪律这是全文最容易被忽视、却最影响线上体验的部分Fragments 让创建深层查询变得异常诱人但这对性能是灾难。因此片段本身应当尽可能浅理想情况下绝不深入超过一层资源。story.id和story.content.title没问题story.community.channels.stories就过分了。对应到仓库实现shared/graphql/fragments/thread/threadInfo.jsthreadInfo片段包含了author作者信息fragment threadInfo on Thread { id messageCount createdAt modifiedAt lastActive editedBy { ...threadParticipant } author { ...threadParticipant } channel { ...channelInfo } community { ...communityInfo ...communityMetaData } ... }原文档对为什么带上作者、却不带频道给出了精确的取舍逻辑作者必带找不到任何展示一条 story/thread 却不需要作者信息的场景因此把author { ...userInfo }固化在片段内频道不带如果用户正处在该频道页内浏览让每一条 thread 都附带一份完整的 channel 数据会造成灾难性的查询膨胀massively underperforming query。注意即使需要频道数据也只在查询层追加而不是污染公共片段。这正是 Spectrum 性能准则的完整表述片段封装绝对必需的最小数据集查询层按需追加页面级额外数据。原文档的查询层追加示例const getStory gql query getStory($id: String) { ...storyInfo channel { ...frequencyInfo } } ${userInfoFragment} ${frequencyInfoFragment} ;⚠️ 需要指出的是上述示例在原文中本身存在不严谨之处——它在query顶层直接展开了...storyInfo但 GraphQL 规范要求片段展开必须位于具体字段之下规范示例中的片段是定义在Story类型上的正确用法应为story { ...storyInfo }。本文保留原文示例仅用于展示追加频道信息的意图按仓库现行规范更稳妥的写法是在资源字段下展开可参考 getThread.js 中thread(id: $id) { ...threadInfo }的标准姿势。仓库对threadInfo的实际实现进一步印证了浅片段策略它虽然嵌套了channel、community、author等子对象但这些子对象全部由更底层的叶子片段channelInfo、communityInfo、threadParticipant展开而来且communityInfo、channelInfo自身只索取单层资源如 channelInfo.js 只下探到community一层。深度被拆散到多个可复用、可裁剪的层级里而不是让任何一个片段变成大而全的深树。写作准则把心智模型固化下来原文档最后给出了一条朴素的写作原则值得作为团队内部约定抄进规范每当我写 fragment query 时都会问自己如果我使用这个 fragment我绝对会期望拿到哪些数据这条问题把 fragment 的设计从尽量多放字段扭转为只放绝对必需的字段期望值即契约字段集代表组件对这个数据对象的全部合理假设超出期望的部分一律不放性能与可维护性的平衡点片段浅、查询深的结构既保证了公共片段的最大复用率又防止了嵌套 N 层资源导致的 N1 查询与响应体膨胀可审查性任何人在任何组件里看到一个 fragment 名都能立刻说出它的字段集——这正是原文档反复强调的internal mental model内部心智模型。在仓库中继续深入如果你想在真实代码里观察这套实践的全貌推荐按以下路径阅读片段全集shared/graphql/fragments28 个片段文件按资源类型分目录查询消费端shared/graphql/queries如 getUser.js、getThread.js、getCurrentUserEverythingFeed.js 等注意观察每个查询如何组合多个片段GraphQL 服务端 schemaapi/typesUser、Thread、Community、Channel、Message等类型的字段定义可与片段字段一一对照验证变更操作对片段的依赖shared/graphql/mutations观察 mutation 的refetchQueries或更新逻辑如何引用片段查询结果。值得注意的是fragments 目录与 shared/graphql/fragments 中的每个文件都同时导出了对应的 Flow 类型如UserInfoType、ThreadInfoType并被查询文件以$Exact方式组合成查询结果类型——这意味着服务端 schema、GraphQL 查询、前端静态类型三者共用同一份单一事实来源任何字段增删都需要同步修改片段与类型从工程层面杜绝了类型说是这个、接口返回那个的漂移。这一层设计在原文档中未展开但正是仓库对fragment 保证数据一致性这一核心理念的最强落地。赞分享后端前端即时通讯社交【免费下载链接】spectrumSimple, powerful online communities.项目地址https://gitcode.com/gh_mirrors/sp/spectrum点击查看免费下载相关推荐browserify与GraphQL fragments前端数据复用的最佳实践browserify与GraphQL fragments前端数据复用的最佳实践 你是否还在为前端项目中的代码冗余和数据请求重复而烦恼当团队规模扩大多个组件前端构建CLI开发工具如何在Windows Vista和Windows Server 2008上运行现代Python 3.8PythonVista项目的完整指南如何在Windows Vista和Windows Server 2008上运行现代Python 3.8PythonVista项目的完整指南 你是否还在使用W操作系统如何快速构建250格式的本地WebAssembly文件转换器VERT开源项目终极指南如何快速构建250格式的本地WebAssembly文件转换器VERT开源项目终极指南 VERT 是一款革命性的开源文件转换工具通过创新的WebAssemb前端音视频创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考