Graphql学习路径

GraphQL 是一种前端技术”、“Federation 是统一图和服务编排的唯一解决方案”、“GraphQL 是 REST 的替代品”这三个观点是最常见的Graphql认知误解。

Graphql学习路径

GraphQL 是一种前端技术”、“Federation 是统一图和服务编排的唯一解决方案”、“GraphQL 是 REST 的替代品”这三个观点是最常见的Graphql认知误解。

由于过去几年生态系统的快速演变,2022 年学习 GraphQL 比以往更具挑战性。

出于这个原因,这篇文章在很大程度上受到“如何不学习 TypeScript”和“如何不学习 Rust”的启发,将指导您避免围绕 GraphQL 的常见学习偏见和误解。

错误 #1:GraphQL 是一种前端技术


是的,GraphQL 最初是作为一种解决客户端(移动/网络应用程序)问题的技术而被普及的:

  • 减少 API 的往返次数
  • 通过仅请求必要的字段来减少获取数据的大小
  • 消除从多个端点获取和处理数据的开销

简而言之,GraphQL 将控制权交还给了消费者(移动应用程序、Web 应用程序)。

GraphQL 作为微服务编排的创新解决方案

然而,从 2018 年开始,GraphQL 生态系统开始增长,特别是在后端,开发了 GraphQL 的新视野。

许多开源参与者开始提出统一模式/服务编排的解决方案,举几个主要的:GraphQL ToolsApollo FederationHasuraGraphQL Mesh

很快,一些公司开始提供 SaaS/PaaS 等企业解决方案来大规模管理 GraphQL 架构:Apollo StudioAWS AppSyncHasura Cloud

GraphQL 在后端用例上的路径

不仅限于服务编排,GraphQL 继续其目标是成为数据的首选语言。

最近的项目,如 GraphQL Mesh 或 Hasura Cloud 等产品证明,GraphQL 的目的不仅仅是简单的前端/移动应用程序获取数据的应用。

其他参与者,例如历史图形数据库参与者 Neo4J,宣布支持 GraphQL 作为数据查询语言。

错误 #2:像 REST API 一样设计 GraphQL API


在设计 GraphQL Schema 时,许多项目决定与底层数据模式(数据库或其他微服务)保持 1-1 的关系。

一个好的 GraphQL Schema 设计应该:

  • 通过提供适当的抽象来简化使用(例如:聚合字段、跨多个微服务的原子突变)
  • 提供代表特定行为的特殊突变,而不是直接链接到底层数据模式的 CRUD 突变

例如,对于公开查询以更新用户类型的架构,如下所示:

type User {
  id: ID!
  name: String!
  email: String!
  firstName: String
  lastName: String
  plan: UserPlan!
  premiumPreferences: UserPremiumPreferences
}

enum UserPlan {
  DEFAULT
  PREMIUM
}

type UserPremiumPreferences {
  alerts: Boolean
  alertsFrequency: Boolean
  # ...
}

updateUser 的 GraphQL Mutation 如下:

input UpdateUserPremiumPreferences {
  alerts: Boolean!
  alertsFrequency: String
}

input UpdateUser {
  name: String
  email: String
  firstName: String
  lastName: String
  premiumPreferences: UserPremiumPreferences
}

type Mutation {
  updateUser(id: ID!, input: UpdateUser!): User
}

通过删除输入字段上的任何可空性约束,我们几乎将所有这些验证推迟到运行时,而不是使用模式来引导 API 用户正确使用。

更好的、行为驱动的设计如下:

input UpdateUserPremiumPreferences {
  alerts: Boolean!
  alertsFrequency: String
}

input UpdateUser {
  name: String
  email: String
  firstName: String
  lastName: String
}

type Mutation {
  updateUser(id: ID!, input: UpdateUser!): User
  updateUserPreferences(id: ID!, input: UpdateUserPremiumPreferences!): User
}

我们现在提供专门的突变,最终用户开发人员不必猜测哪些字段是必需的,并且可以获得更好的突变参数的静态验证。

请记住,GraphQL Schema 既不是后端也不是前端拥有的技术;

无论您是构建产品 API 还是微服务网关,GraphQL API 的目标都是提供一组业务逻辑的简化抽象。

错误 #3:仅通过 Apollo 学习 GraphQL


Apollo 公司一直是 GraphQL 的主要早期贡献者之一,并且仍然是生态系统的重要参与者。

然而,虽然大多数 Apollo 项目仍然在使用率和整体 SEO 存在方面保持良好份额,但值得一提的是,在过去几年中,新的替代方案有所增加。

浏览器 GraphQL 客户端

关于基于 Web 的客户端 GraphQL Client,apollo-client 不再是唯一可行的解​​决方案。

URQL 是对 GraphQL 客户端一个不同实现方式,而不仅仅是作为替代方案,它具有:

  • 灵活的缓存系统
  • 可扩展设计(易于在其之上添加新功能)
  • 轻量级捆绑包(比 Apollo Client 轻约 5 倍)
  • 支持文件上传和离线模式

在 React 生态系统中,React Query(尤其是与 GraphQL 代码生成器一起使用时)为 GraphQL 带来了一个轻量级且不可知的解决方案。

如果您正在寻找一个易于使用且轻量级的多用途客户端(支持 GraphQL),它提供了以下基本功能,那么 React Query 是一个很好的选择:

  • 强大的缓存(后台刷新、窗口焦点刷新)
  • 高级查询模式(并行查询、依赖查询、预取)
  • UX 模式:乐观更新、滚动恢复
  • 很棒的开发工具

最后,在构建可能不需要缓存或乐观 UI 功能的简单应用程序时,著名的 graphql-request 库是完美的伴侣!

这个轻量级库具有查询 GraphQL API 的基本功能:

  • 可变验证
  • 支持文件上传
  • 批量
  • 基于承诺的 API
  • TS支持
服务端 GraphQL 库

同样的趋势也适用于服务器端,现在许多库被广泛使用。

由于 Apollo 支持的技术(例如Federation)的历史存在和领先的解决方案,Apollo 服务器仍然拥有可观的使用份额。

然而,许多新公司和项目更喜欢更自以为是或轻量级的替代方案。

Nest.js 是为 Angular 设计和 DDD 爱好者构建 GraphQL API 的绝佳替代品。 Nest.js 附带:

  • 全类型解析器构建经验
  • 强大的结构化模式,例如存储库、服务等
  • 日志系统
  • 支持Apollo Federation

如果您研究一种自以为是且结构化的方式来构建您的 GraphQL API,Nest.js 是一个不错的选择。

另一方面,GraphQL Yoga 等其他非主流库专注于在构建可扩展的 GraphQL 服务器方面带来最佳的开发人员体验。

GraphQL Yoga 带有一个高性能的 HTTP 服务器,它提供:

  • 通过 HTTP 订阅 (SEE)
  • 支持@defer 和@stream
  • 开箱即用的上传和 CORS 功能
  • 使用 Envelop 插件系统的可扩展设计
  • 支持Apollo Federation
  • 与 Next.js、Svelte Kit、Cloudflare workers 等轻松集成!
新的学习路径

我们可以看到,GraphQL 提供了从前端、后端、数据库到 SaaS 的多种解决方案。

如果您开始学习 GraphQL,一个很好的起点是 howtographql.com,您将能够测试多个框架并找到最适合您的堆栈和产品/项目的框架。

错误 #4:从解析器中抛出错误


对于大多数新采用者(尤其是来自 REST API 的人)来说,让 GraphQL API 在错误时返回 200 OK 似乎是一个缺陷。

然而,虽然这种设计选择是完全合法的(GraphQL 恢复了 REST 劫持的 HTTP 状态代码的真正目的),但 GraphQL 中错误处理的另一个方面并非如此。

大多数 GraphQL 解析器实现处理错误如下:

const resolvers = {
  Query: {
    user: (parent, args, context) => {
      // ...
      throw new Error('User not found')
    }
  }
}

这样做会返回以下响应:

{
  "errors": [
    {
      "message": "User not found",
      "locations": [{ "line": 6, "column": 7 }],
      "path": ["user", 1]
    }
  ]
}

正如本文中 The Guild 的 Laurin 所解释的,使用这种错误模式是一个坏习惯,因为:

  • 错误需要在客户端解析(我们通过解析错误信息来“猜测”错误)
  • 当查询旨在塑造 UI 上的数据时,错误不会位于同一位置

带有联合类型的优雅 GraphQL 错误


在 GraphQL 中发出错误的一种更优雅的方法是使用 Union 类型来表达预期的错误,如下所示:

type User {
  id: ID!
  login: String!
}

type UserNotFoundError {
  message: String!
}

union UserResult = User | UserNotFoundError

type Query {
  user(id: ID!): UserResult!
}

允许我们编写以下查询:

query {
  user(id: 1) {
    ... on User {
      id
      login
    }

    ... on UserNotFoundError {
      message
    }
  }
}

现在,获取的响应以类型安全且位于同一位置的方式反映了正确的错误:

{
  "data": {
    "user": {
      "message": "User not found",
      "__typename": "UserNotFoundError"
    }
  }
}

注意:错误屏蔽也是一种很好的方法,不需要更新架构:https://www.graphql-yoga.com/docs/features/error-masking。

错误 #5:“联合是组合模式的唯一可行方式”

Apollo 的Federation被用作组合模式或使用 GraphQL 构建微服务架构的解决方案。

Stitching 是一种由单个 GraphQL 网关架构组成的设计,该架构由多个底层 GraphQL 服务组成。

它可以通过 3 种方法来实现:Federation、Modern Stitching 和 Schema Extensions。

采用以下子模式:

# Posts subschema
type Post {
  id: ID!
  text: String
  userId: ID!
}

type Query {
  postById(id: ID!): Post
}

# Users subschema
type User {
  id: ID!
  email: String
}

type Query {
  userById(id: ID!): User
}

让我们看看 3 种不同的方法来实现添加 User.posts 子查询:

Federation

Federation架构实际上是在拼接。它通过提供一组合并指令(@requires、@key、@external)来拼接子模式(通过 Apollo 网关)。

每个“实体类型”必须源自单个服务,并且可以通过混合使用模式扩展(扩展类型)和Federation的 SDL 指令进行扩展。

要添加 User.posts 子查询,需要以下模式:

# Posts subschema
type Post {
  id: ID!
  text: String
  userId: ID!
}

extend type User @key(fields: "id") {
	id: ID! @external
	posts: [Post!] @requires("id")
}

extend type Query {
  postById(id: ID!): Post
}

# Users subschema
type User @key(fields: "id") {
  id: ID!
  email: String
}

extend type Query {
  userById(id: ID!): User
}
Modern Stitching

Modern Stitching (GraphQL Tools v7+) 依赖于一种编程方法,其中每个子模式都是完全独立的。

只有网关必须提供类型合并信息以指示谁必须解析跨多个服务共享的字段。

简单地添加帖子后:[发布!]!字段到用户,在“帖子”子模式中:

# Posts subschema
type Post {
  id: ID!
  text: String
  userId: ID!
}

type User {
  id: ID!
  posts: [Post!]!
}

type Query {
  postById(id: ID!): Post
  postsByUserId(userId: ID!): User
}

# Users subschema
type User {
  id: ID!
  email: String
}

type Query {
  userById(id: ID!): User
}

并需要添加以下网关配置:

import { stitchSchemas } from '@graphql-tools/stitch'

const gatewaySchema = stitchSchemas({
  subschemas: [
    {
      schema: postsSchema,
      merge: {
        User: {
          fieldName: 'postUserById',
          selectionSet: '{ id }',
          args: (originalObject) => ({ id: originalObject.id })
        }
      }
    },
    {
      schema: usersSchema,
      merge: {
        User: {
          fieldName: 'userById',
          selectionSet: '{ id }',
          args: (originalObject) => ({ id: originalObject.id })
        }
      }
    }
  ]
})

通过使用类型合并指令(@merge、@canonical),最终的拼接模式也可以与联邦类似地构建:

# Posts subschema
type Post {
  id: ID!
  text: String
  userId: ID!
}

type User {
  id: ID!
  posts: [Post!]!
}

type Query {
  postById(id: ID!): Post
  postsByUserId(userId: ID!): User @merge(keyField: "id")
}

# Users subschema
type User {
  id: ID!
  email: String
}

type Query {
  userById(id: ID!): User @merge(keyField: "id")
}
Schema Extensions

模式扩展,类似于最初的 Stitching 提议,是一种不同的方法,其中各个子模式仅公开它们自己的类型。

网关负责通过使用模式扩展来合并和丰富最终模式:

// gateway.ts

// ...

export const schema = stitchSchemas({
  subschemas: [postsSubschema, usersSubschema],
  typeDefs: /* GraphQL */ `
    extend type Post {
      user: User!
    }
    extend type User {
      posts: [Post!]!
    }
  `,
  resolvers: {
    User: {
      posts: {
        selectionSet: `{ id }`,
        resolve(user, args, context, info) {
          return delegateToSchema({
            schema: postsSubschema,
            operation: 'query',
            fieldName: 'postsByUserId',
            args: { userId: user.id },
            context,
            info
          })
        }
      }
    },
    Post: {
      user: {
        selectionSet: `{ userId }`,
        resolve(post, args, context, info) {
          return delegateToSchema({
            schema: usersSubschema,
            operation: 'query',
            fieldName: 'userById',
            args: { id: post.userId },
            context,
            info
          })
        }
      }
    }
  }
})

让我们回顾一下 Modern Stitching 的主要特点以及它与Federation的区别。

通过查询显式字段解析

Modern Stitching不要求类型具有起源(如Federation实体)。

所有独立的子模式都可以定义可能存在于其他同级服务中的类型。

允许 GraphQL 工具将类型合并在一起的唯一要求是每个子模式必须公开一个查询来解析类型,如下所示:

# Posts subschema
type Post {
  id: ID!
  text: String
  userId: ID!
}

type User {
  id: ID!
  posts: [Post!]!
}

type Query {
  postById(id: ID!): Post
  postsByUserId(userId: ID!): User
}

# Users subschema
type User {
  id: ID!
  email: String
}

type Query {
  userById(id: ID!): User
}

在这里,我们的“帖子”子模式向用户类型 (User.posts) 添加了一个字段。

为了能够在以下查询中解析用户类型:

query UserPosts {
  userById(id: "1") {
    email
    posts {
      id
    }
  }
}

我们需要添加 postsByUserId(userId: ID!) 查询来解析“帖子”服务上的用户类型。

如 GraphQL Tools v7+ 文档中所述,以下是处理 UserPosts 操作时执行的合并流程:

Modern Stitching 的类型合并方法遵循 GraphQL 解析器的初始设计,但用于跨多个子模式共享字段的类型。

这样的设计使您的子模式独立(处理模式只需要本地知识)并使用 vanilla-GraphQL 构建(没有特定于框架的心理模型),而拼接配置是在网关级别完成的。

通过自动批处理提高性能

对旧的 Stitching 方法的主要指责是围绕性能。

Modern Stitching 带来了具有 2 个功能的快速类型合并:

Batching

假设我们对添加到用户服务的新用户查询执行以下操作:

query UsersPosts {
  users {
    email
    posts {
      id
    }
  }
}

为了解析每个用户的帖子,Modern Stitching 必须在 Posts 服务上多次调用 postsByUserId Query。

幸运的是,如果我们通过以下方式转换现有的 postsByUserId:

postUsersByIds(ids: [ID!]!): [User]!

并正确配置我们的网关,Modern Stitching 会将 User.posts 解析分组为对 Posts 服务的单个 postUsersByIds 查询。

Query batching

现代拼接在更高级别上应用了相同的逻辑。

如果一个操作需要多次调用一个服务,所有这些操作将被合并为一个。例如,在假设的 Products 模式中,以下查询:

query {
  products(ids: [1, 2, 5]) {
    name
  }
}
query {
  seller(id: 7) {
    name
  }
}
query {
  seller(id: 8) {
    name
  }
}
query {
  buyer(id: 9) {
    name
  }
}

将转换为以下将发送到 Products 服务的唯一操作:

query {
  products_0: products(ids: [1, 2, 5]) {
    name
  }
  seller_0: seller(id: 7) {
    name
  }
  seller_1: seller(id: 8) {
    name
  }
  buyer_0: buyer(id: 9) {
    name
  }
}

这两个功能(批处理和查询批处理)显着提高了现代拼接架构的性能。

服务器实现的灵活性:没有订阅限制

当与 Modern Stitching 编程 API 一起使用时(没有 @merge 和 @canonical 指令),构建子模式是使用 vanilla GraphQL 实现的。

这意味着每个子模式都可以使用任何技术、框架和库构建,并且不需要兼容性工作(而联邦要求子模式使用与联邦兼容的 GraphQL 服务器)。

网关服务器也是如此。

Modern Stitching 是一组实用程序,可让您选择自己的与 GraphQL 兼容的 HTTP 服务器/库 (JavaScript):

  • GraphQL Yoga
  • Nest.js
  • GraphQL Helix + Express
  • Apollo Server


最后,Modern Stitching 允许您在组合模式中支持 GraphQL 订阅(通过 WS 或 SEE)。

你可能不需要拼接


今天,最流行的组成统一 GraphQL 层的解决方案是通过网络完成的(与远程模式或通过 Apollo 联合网关缝合)。

这样做的主要原因是开发和部署工作流程的分离,并允许团队拥有架构的所有权。

但是,由于网络开销,这样的设置会产生性能问题。

虽然 Apollo 致力于解决网关延迟的软件解决方案,但解决方案可能驻留在组织级别。

JavaScript 生态系统的另一个流行趋势是 monorepo。

由于工具(lerna、yarn 工作空间)和 Github 平台(代码所有者系统)的发展,monorepo 是团队在复杂堆栈上一起工作的一种可行且频繁的模式。

开发和部署工作流程的分离以及允许团队对架构拥有所有权也可以通过在构建时合并子架构并将其部署为运行本地模块的网关来实现,而没有网络开销。

在构建时使用带有模式合并的 monorepo 解决了不可压缩的性能问题,同时保留了子模式的所有权并保证部署的子模式是兼容的。