使用 GraphQL Codegen 释放 Fragments 的力量
本文发表于 Fri Aug 05 2022 00:00:00 GMT+0000 (Coordinated Universal Time) byLaurin Quast@The Guild Blog 标题:“使用 GraphQL Codegen 释放片段的力量” 作者:劳林 标签: [graphql、codegen、react、typescript、relay、apollo、urql] 日期:20
本文发表于 Fri Aug 05 2022 00:00:00 GMT+0000 (Coordinated Universal Time) byLaurin Quast@The Guild Blog
标题:“使用 GraphQL Codegen 释放片段的力量”
作者:劳林
标签: [graphql、codegen、react、typescript、relay、apollo、urql]
日期:2022-08-05
描述:“Relay 最重要的部分是构建和扩展应用程序的概念,让我们展示如何在现有项目中使用这些模式,无论您当前使用什么客户端库。”
图片:/blog-assets/unleash-the-power-of-fragments-with-graphql-codegen/cover.png
缩略图:/blog-assets/unleash-the-power-of-fragments-with-graphql-codegen/thumbnail.png
毫无疑问,Relay 是目前最先进的 JavaScript GraphQL 客户端。然而,与此同时,与 Apollo Client、urql 或 GraphQL Request 等其他流行的替代方案相比,学习曲线和采用率仍然非常低。
结果,许多人甚至不知道 Relay 中使用的所有好处和模式(到目前为止是专有的)。
Guild 收集并参与了 GraphQL 客户端使用跨越当今所有可用解决方案的项目。
在我们看来,Relay 最重要的部分是构建和扩展应用程序的概念。
这些模式实际上适用于任何 GraphQL 客户端,您只需要适合该工作的代码生成工具。
在高层次上,这些好处如下:
一个碎片化的组件树。 无需为每个可见组件编写单个 GraphQL 操作文档或从单个查询操作文档类型中提取组件属性,这通常很麻烦),多个片段定义可以组成一个查询发送到服务器的操作。
这减少了并发请求的数量,并且与@defer
和@stream
一起实际提高了应用程序的性能(与批处理 GraphQL 操作相比)。
将 GraphQL 文档与组件代码放在一起。 不是在专用文件中编写 GraphQL 操作,而是在组件代码中编写操作。您是否曾经删除过组件代码,却忘记删除其他字段中的相应操作或片段定义?
我们一遍又一遍地遇到这个问题,生成了很多死代码。
import { gql, useFragment, FragmentType } from './gql'
const Avatar_UserFragment = gql(/* GraphQL */ `
fragment Avatar_UserFragment on User {
avatarUrl
}
`)
type AvatarProps = {
user: FragmentType<typeof Avatar_UserFragment>
}
export function Avatar(props: AvatarProps) {
const user = useFragment(Avatar_UserFragment, props.user)
return <CircleImage src={user.avatarUrl} />
}
进入全屏模式 退出全屏模式
数据(片段)屏蔽。 确保组件只能访问其片段定义中定义的数据,以使组件成为可重复使用的自包含构建块。
我们很高兴地宣布,我们现在有一个预设,可以将上述 Relay 模式带入您现有的项目,无论您当前使用什么客户端库,都无需切换并提交到 Relay 生态系统。
新的 GraphQL 代码生成器预设
新预设可替代所有为现有客户端(如 urql 和 apollo-client)生成挂钩的插件。
yarn add -D @graphql-codegen/gql-tag-operations-preset
进入全屏模式 退出全屏模式
schema: ./schema.graphql
documents:
- 'src/**/*.ts'
- '!src/gql/**/*'
generates:
./src/gql/:
preset: gql-tag-operations-preset
进入全屏模式 退出全屏模式
预设不是为现有客户端生成挂钩,而是使用函数签名重载和TypedDocumentNode
)来推断正确的 GraphQL 操作和变量类型。
您可以在graphql.wtf 第 41 集中了解有关
TypedDocumentNode
的更多信息。
在您的应用程序代码中,您现在将有效地编写以下代码。
import { useQuery } from 'urql'
import { gql } from './gql'
const ViewerQueryDocument = gql(/* GraphQL */ `
query ViewerQuery {
viewer {
id
name
}
}
`)
const Viewer = () => {
const [result] = useQuery({ query: ViewerQueryDocument })
const { data, fetching, error } = result
if (fetching) return <p>Loading…</p>
if (error) return <p>Oh no… {error.message}</p>
// data is fully typed!
return <p>{data?.viewer?.name}</p>
}
进入全屏模式 退出全屏模式
因为客户端内置了对TypedDocumentNode的支持,它会从传递给useQuery
的文档中提取正确的操作和变量类型。
前往 GraphQL 代码生成器文档了解更多详细信息。
这是我们现在推荐的使用 GraphQL 代码生成器进行前端开发的方法。
通过 GraphQL 片段描述组件数据需求
与其为整个页面编写一个大的 GraphQL 操作并将其传递给组件,不如从通过 GraphQL 片段描述组件的数据依赖关系开始。
通过这种方式,您可以将组件的数据依赖关系放在一起并显式,就像如果您使用样式化组件模式,某些人会将 TypeScript 定义或 CSS 放在一起。
import { gql, useFragment, FragmentType } from './gql'
const Avatar_UserFragment = gql(/* GraphQL */ `
fragment Avatar_UserFragment on User {
avatarUrl
}
`)
type AvatarProps = {
user: FragmentType<typeof Avatar_UserFragment>
}
export function Avatar(props: AvatarProps) {
const user = useFragment(Avatar_UserFragment, props.user)
return <CircleImage src={user.avatarUrl} />
}
进入全屏模式 退出全屏模式
用分片限制数据访问
通过使用useFragment
挂钩,我们确保组件只能访问在片段选择集中直接声明的数据的属性。通过附加分片扩展(Avatar_UserFragment
)声明的其他数据不能在UserListItem
中访问。
以下示例会引发 TypeScript 错误,因为avatarUrl
片段未在UserListItem_UserFragment
片段中选择。
import { gql, useFragment, FragmentType } from './gql'
import { Avatar } from './avatar'
const UserListItem_UserFragment = gql(/* GraphQL */ `
fragment UserListItem_UserFragment on User {
fullName
...Avatar_UserFragment
}
`)
type UserListItemProps = {
user: FragmentType<typeof UserListItem_UserFragment>
}
export function UserListItem(props: UserListItemProps) {
const user = useFragment(Item_UserFragment, props.user)
// ERROR: Property 'avatarUrl' does not exist on type
const icon = <img src={user.avatarUrl} />
return <ListItem icon={icon}>{user.fullName}</ListItem>
}
进入全屏模式 退出全屏模式
为 UI 组件编写片段
当我们将简单的可视化 UI 组件组合成更大的功能性 UI 组件时,我们还可以组合消费 UI 组件的片段。
import { gql, useFragment, FragmentType } from './gql'
import { Avatar } from './avatar'
const UserListItem_UserFragment = gql(/* GraphQL */ `
fragment UserListItem_UserFragment on User {
fullName
...Avatar_UserFragment
}
`)
type UserListItemProps = {
user: FragmentType<typeof UserListItem_UserFragment>
}
export function UserListItem(props: UserListItemProps) {
const user = useFragment(Item_UserFragment, props.user)
return <ListItem icon={<Avatar user={user} />}>{user.fullName}</ListItem>
}
进入全屏模式 退出全屏模式
为您的顶级路由或视图编写片段组件
我们现在可以进一步组合我们的组件,直到我们到达那些选择根 Query 对象类型的组件。
import { gql, useFragment, FragmentType } from './gql'
import { UserListItem } from './user-list-item'
const FriendList_QueryFragment = gql(/* GraphQL */ `
fragment FriendList_QueryFragment on Query {
friends(first: 5) {
id
...UserListItem_UserFragment
}
}
`)
type FriendListProps = {
query: FragmentType<typeof FriendList_QueryFragment>
}
export function FriendList(props: FriendListProps) {
const query = useFragment(FriendList_QueryFragment, props.query)
return (
<List>
{query.friends.map(user => (
<FriendListItem key={user.id} user={user} />
))}
</List>
)
}
进入全屏模式 退出全屏模式
之前,我们用Avatar_UserFragment
片段声明了Avatar
组件。我们现在可以通过再次传播片段然后将相关数据传递给Avatar
组件user
属性来在不同的上下文中重新使用此 Avatar。
import { gql, useFragment, FragmentType } from './gql'
const UserProfileHeader_QueryFragment = gql(/* GraphQL */ `
fragment UserProfileHeader_QueryFragment on Query {
viewer {
id
homeTown
registeredAt
...Avatar_UserFragment
}
}
`)
type UserProfileHeaderProps = {
query: FragmentType<typeof UserProfileHeader_QueryFragment>
}
export function UserProfileHeader(props: UserProfileHeaderProps) {
const query = useFragment(UserProfileHeader_QueryFragment, props.query)
return (
<>
<Avatar user={query.viewer} />
{query.viewer.homeTown}
{query.viewer.registeredAt}
</>
)
}
进入全屏模式 退出全屏模式
现在,我们有两个 UI 组件需要来自根 Query 类型的数据,FriendList
和UserProfileHeader
。
将所有 Query 片段组合成一个 Query 操作
最后,我们可以将所有片段分散到单个 GraphQL 查询操作中,该操作获取路由组件上的所有数据。这使我们能够在一次服务器往返中有效地获取路由所需的所有数据。
import { gql, useFragment, FragmentType } from './gql'
import { UserProfileHeader } from './user-profile-header'
import { FriendList } from './friend-list'
const UserProfileRoute_Query = gql(/* GraphQL */ `
query UserProfile_Query {
...UserProfileHeader_QueryFragment
...UserList_QueryFragment
}
`)
export function UserProfileRoute_Query() {
const { data, loading, error } = useQuery(UserProfile_Query)
if (loading) return <Loading />
if (error) return <Error />
return (
<>
<FriendList query={data} />
<UserProfileHeader query={data} />
</>
)
}
进入全屏模式 退出全屏模式
最后,如前所述,我们有以下组件树。
结论
这种模式允许您最大程度地重用和扩展您的组件,同时拥有从顶部路由到 UI 组件树末端的清晰和健全的数据依赖流。
您可以通过我们经过实战考验的全新 GraphQL 代码生成器预设gql-tag-operations-preset在您现有的 GraphQL 应用程序中使用任何支持TypedDocumentNode
的 GraphQL 客户端开始采用这种模式。
ANY 框架(React、Vue、Angular 等)支持以下所有客户端
-
网址
-
阿波罗客户端
-
“香草”Node.js
-
graphql-request (我们最近添加了支持)
按照预设文档,您无需太多配置即可快速启动。
也许这甚至会激发您深入挖掘 Relay 客户端运行时在这些模式之上所做的优化,并说服您在下一个项目中使用 Relay。您可以在relay.dev上了解更多信息。
作为 The Guild,我们希望您找到适合您的专业知识和正在构建的应用程序的工具。最重要的是适用于任何地方的模式!
还不够?
我最近在 GraphQL 柏林聚会上也谈到了这个话题!
更多推荐
所有评论(0)