本文发表于 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 类型的数据,FriendListUserProfileHeader

将所有 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 柏林聚会上也谈到了这个话题!

Logo

React社区为您提供最前沿的新闻资讯和知识内容

更多推荐