使用 GraphQL 的多人井字棋

Avatar of Rishichandra Wawhal
Rishichandra Wawhal

DigitalOcean 为您旅程的每个阶段提供云产品。 立即开始使用 200 美元的免费积分!

GraphQL 是一种用于 API 的查询语言,对前端开发人员来说非常强大。 正如 GraphQL 网站 所解释的那样,您可以描述您的数据,请求您想要的内容,并获得可预测的结果。

如果您以前没有使用过它,GraphQL 乍看起来可能有点难以理解。 因此,让我们使用它构建一个多人井字棋游戏,以演示它是如何使用的以及我们可以用它做什么。

首先我们需要一个 API 的后端。 在本教程中,我们将使用 Hasura GraphQL 引擎 以及自定义 GraphQL 服务器。 我们将查看客户端构建游戏所需的查询和变异。 您可以使用您想要的任何框架实现这种事情,但我们将在本教程中使用 React 和 Apollo。

以下是我们要制作的东西

GitHub 仓库

GraphQL 简介

GraphQL 是一种用于 API 的查询语言;一种具有定义从服务器获取数据的语法的语言。 它适用于任何由强大的系统支持的 API,该系统使您的代码库具有弹性。 GraphQL 的一些主要特征是

  • 客户端可以向服务器询问它支持哪些查询(查看 自省 以了解更多信息)。
  • 客户端必须向服务器请求它想要的确切内容。 它不能请求类似通配符(*)的内容,而应该请求确切的字段。 例如,要获取用户的 ID 和姓名,GraphQL 查询将类似于
    query {
      user {
        id
        name
      }
    }
  • 每个查询都发送到单个端点,并且每个请求都是 POST 请求。
  • 给定查询,响应的结构将被强制执行。 例如,对于上面获取 user 对象的 idname 的查询,成功响应将类似于
    {
      "data": {
        "user": {
          "id": "AUTH:482919",
          "name": "Jon Doe"
        }
      }
    }

如果您想了解有关 GraphQL 的更多信息,此 系列文章 是一个很好的起点。

我们为什么要使用 GraphQL?

我们刚刚讨论了 GraphQL 如何要求客户端必须向服务器请求它想要的确切内容。 这意味着不会从服务器检索不必要的数据,例如在 REST 的情况下,即使您只需要一个字段,您也会收到一个巨大的响应。 获取我们所需的新内容以及我们仅需的内容会优化响应,使其快速且可预测。

客户端可以通过自省向服务器询问其模式。 此模式可用于使用 GraphiQL 等 API 资源管理器动态构建查询。 它还支持代码检查和自动完成,因为每个查询都可以使用 GraphQL 模式构建并与之交叉检查。 作为前端开发人员,这极大地增强了 DX,因为人为错误更少了。

由于只有一个端点,并且每个请求都是 POST 请求,因此 GraphQL 可以避免许多样板代码,因为它不必跟踪端点、请求有效负载和响应类型。 响应缓存更加容易,因为可以预期每个查询响应都具有特定结构。

此外,GraphQL 具有一个定义明确的规范,用于实现实时订阅。 您不必为构建实时服务器想出自己的实现细节。 构建一个符合 GraphQL 实时规范的服务器,任何客户端都可以轻松地开始使用实时 GraphQL API

GraphQL 术语

我将在本文中使用一些 GraphQL 术语,因此提前了解其中一些术语是值得的。

  • 查询:GraphQL 查询只是从服务器获取数据的查询。
  • 变异:这是一个更改服务器上的某些内容并获取一些数据的 GraphQL 查询。
  • 订阅:这是一个将客户端订阅服务器上某些更改的 GraphQL 查询。
  • 查询变量:这些变量允许我们在 GraphQL 查询中添加参数。

回到后端

现在我们对 GraphQL 有了初步的了解,让我们开始对后端进行建模。 我们的 GraphQL 后端将是 Hasura GraphQL 引擎和自定义 GraphQL 服务器的组合。 在这种情况下,我们将不会深入探讨代码的细微差别。

由于井字棋是一个多人游戏,因此需要在数据库中存储状态。 我们将使用 Postgres 作为我们的数据库,并使用 Hasura GraphQL 引擎 作为 GraphQL 服务器,该服务器允许我们通过 GraphQL 对 Postgres 中的数据执行 CRUD 操作。

除了对数据库的 CRUD 操作外,我们还希望以 GraphQL 变异的形式运行一些自定义逻辑。 我们将使用自定义 GraphQL 服务器来实现这一点。

Hasura 在其 自述文件 中很好地描述了自己

GraphQL 引擎是一个速度极快的 GraphQL 服务器,它可以让您在 Postgres 上创建即时、实时的 GraphQL API,并带有对数据库事件的 Webhook 触发器,以及用于业务逻辑的 远程模式

更深入地说,Hasura GraphQL 引擎是一个开源服务器,它位于 Postgres 数据库之上,允许您通过实时 GraphQL 对数据执行 CRUD 操作。 它适用于新的和现有的 Postgres 数据库。 它还具有一个访问控制层,您可以将其与任何身份验证提供者集成。 不过,在本篇文章中,为了简洁起见,我们将不实现身份验证。

让我们从将 Hasura GraphQL 引擎的一个实例部署到 Heroku 的免费层开始,该层附带一个新的 Postgres 数据库。 继续,动手操作; 它是免费的,您无需输入您的信用卡信息:)

部署完成后,您将进入 Hasura 控制台,这是管理您的后端的管理 UI。 请注意您所在的 URL 是您的 GraphQL 引擎 URL。 让我们从创建所需的表开始。

user

user 表将存储我们的用户。 要创建表,请转到顶部的“数据”选项卡,然后单击“创建表”按钮。

此表有一个 id 列,它是每个用户的唯一标识符,以及一个 name 列,用于存储用户的姓名。

board

board 表将存储所有动作发生的棋盘。 我们将在每个开始的游戏中启动一个新的棋盘。

让我们看看此表的列

  • id:每个棋盘的唯一 UUID,自动生成
  • user_1_id:第一个用户的 user_id。 此用户默认在游戏中使用 X
  • user_2_id:第二个用户的 user_id。 此用户默认使用 O
  • winner:这是一个文本字段,在决定获胜者后设置为 XO
  • turn:这是一个文本字段,可以是 XO,它存储当前回合。 它从 X 开始。

由于 user_1_iduser_2_id 存储用户 ID,让我们在 board 表上添加一个约束,以确保 user_1_iduser_2_id 存在于表 user 中。

转到 Hasura board 表中的“修改”选项卡,并添加外键。

user_1_id 上添加外键。 我们还需要在 user_2_id 上添加一个新的外键。

现在,根据这些关系,我们需要在这些表之间创建连接,以便我们可以在查询棋盘时查询 user 信息。

转到 Hasura 中的“关系”选项卡,并根据建议的关系为 user_1_iduser_2_id 创建名为 user1user2 的关系。

move

最后,我们需要一个move 表来存储用户在棋盘上进行的移动。

让我们看一下列

  • id: 每个移动的唯一标识符,自动生成
  • user_id: 执行移动的用户的 ID
  • board_id: 进行移动的棋盘的 ID
  • position: 进行移动的位置(例如 1-9)

由于user_idboard_id 分别是userboard 表的外键,因此我们必须创建这些外键约束,就像我们在上面所做的那样。接下来,创建关系作为user 用于user_id 上的外键,以及board 用于board_id 上的外键。然后,我们将返回到board 表的“关系”选项卡,并创建对move 表的建议关系。将其命名为moves

我们需要一个自定义服务器

除了将数据存储在数据库中,我们还希望执行自定义逻辑。逻辑是什么?每当用户进行移动时,必须验证移动,并在轮到下一位玩家之前进行移动。

为了做到这一点,我们必须在数据库上运行一个 SQL 事务。我已经编写了一个 GraphQL 服务器,它可以精确地做到这一点,我已经 部署在 Glitch 上。

现在我们有两个 GraphQL 服务器。但是 GraphQL 规范强制执行必须只有一个端点。为此,Hasura 支持远程模式,即您可以向 Hasura 提供一个外部 GraphQL 端点,它将此 GraphQL 服务器与自身合并并在单个端点下提供组合的模式。让我们将此自定义 GraphQL 服务器添加到我们的 Hasura Engine 实例中

  1. 分叉 GraphQL 服务器.
  2. 添加一个环境变量,它是到您的 Postgres 数据库的连接。为此,请访问https://dashboard.heroku.com,选择您的应用程序,转到“设置”,然后显示配置变量。

再进行几步

  1. 复制DATABASE_URL 的值。
  2. 转到您分叉的 GraphQL 服务器,并将该值粘贴到.env 文件(POSTGRES_CONNECTION_STRING=<value>)中。
  3. 单击顶部的“显示直播”按钮,然后复制打开的 URL

我们将通过将其添加为远程模式来将此 URL 添加到 GraphQL 引擎。转到顶部的“远程模式”选项卡,然后单击“添加”选项。

我们已经完成了后端设置!

让我们开始前端开发

我不会深入前端实现的细节,因为你们可以选择在您选择的框架中实现它。我将提供构建游戏所需的所有必要查询和变异。使用这些查询和变异以及您选择的任何前端框架,您就可以构建一个功能齐全的多人井字棋游戏。

设置

Apollo Client 是客户端 GraphQL 的首选库。他们为 React、Vue、Angular、iOS、Android 等提供了出色的抽象。它可以帮助您节省大量样板代码,并且 DX 很流畅。您可能希望考虑使用 Apollo 客户端而不是从头开始构建所有内容。

让我们讨论客户端此游戏所需的查询和变异。

插入用户

在我的应用程序中,我为每个用户生成了一个随机用户名,并在他们打开应用程序时将此名称插入数据库。我还将名称存储在本地存储中并生成了一个用户 ID,以确保同一用户不会拥有不同的用户名。我使用的变异是

mutation ($name:String) {
  insert_user (
    objects: {
      name: $name
    }
  ) {
    returning {
      id
    }
  }
}

此变异在user 表中插入一个条目并返回生成的id。如果您仔细观察变异,它使用$name。这被称为查询变量。当您将此变异连同变量{ "name": "bazooka"} 发送到服务器时,GraphQL 服务器将从查询变量中替换$name,在本例中为“bazooka”。

如果您愿意,您可以实现身份验证并将用户名或昵称插入此表中。

加载所有棋盘

要加载所有棋盘,我们运行一个 GraphQL 订阅

subscription {
  board (
    where: {
      _and: {
        winner: {
          _is_null: true
        },
        user_2_id: {
          _is_null: true
        }
      }
    }
    order_by: {
      created_at: asc }
  ) {
    id
    user1 {
      id
      name
    }
    user_2_id
    created_at
    winner
  }
}

此订阅是一个实时查询,它返回iduser1 及其idname(来自关系)、user_2_idwinnercreated_at。我们设置了一个where 过滤器,它只获取没有有效获胜者的棋盘,并且user_2_idnull,这意味着棋盘对玩家开放加入。最后,我们按created_at 时间戳对这些棋盘进行排序。

创建棋盘

用户可以创建棋盘供其他人加入。为此,他们必须在boards 表中插入一个条目。

mutation ($user_id: Int) {
  insert_board (
    objects: [{
      user_1_id: $user_id,
      turn: "x",
    }]
  ) {
    returning {
      id
    }
  }
}

加入棋盘

要加入棋盘,用户需要将棋盘的user_2_id 更新为他们自己的user_id。变异如下所示

mutation ($user_id: Int, $board_id: uuid!) {
  update_board (
    _set: {
      user_2_id: $user_id
    },
    where: {
      _and: {
        id: {
          _eq: $board_id
        },
        user_2_id: {
          _is_null: true
        },
        user_1_id: {
          _neq: $user_id
        }
      }
    }
  ) {
    affected_rows
    returning {
      id
    }
  }
}

在上面的 GraphQL 变异中,我们将棋盘的user_2_id 设置为user_id。我们还添加了额外的检查,以确保只有在加入的玩家不是创建者并且棋盘尚未满员的情况下才能执行此操作。变异后,我们询问受影响的行数。

在我的应用程序中,加入棋盘后,我将用户重定向到/play?board_id=<board_id>

订阅棋盘

当两个玩家都加入游戏时,我们需要关于每个玩家移动的实时更新。因此,我们必须订阅正在进行的棋盘以及移动(通过关系)。

subscription($board_id: uuid!) {
  board: board_by_pk (id: $board_id) {
    id
    moves (order_by: { id: desc}) {
      id
      position
      user {
        id
        name
      }
      user_id
    }
    user1 {
      id
      name
    }
    user2 {
      id
      name
    }
    turn
    winner
  }
}

上面的查询将客户端订阅到正在进行的棋盘。每当进行新的移动时,客户端都会收到更新。

进行移动

要进行移动,我们将使用自定义 GraphQL 服务器中的make_move 变异。

mutation (
  $board_id: String!,
  $position: Int!,
  $user_id: Int!
) {
  make_move (
    board_id: $board_id,
    position: $position,
    user_id: $user_id
  ) {
    success
  }
}

此变异从查询变量中获取board_idpositionuser_id。它验证移动,进行移动,并切换回合。最后,它返回此事务是否成功。

井字棋!

现在您就拥有了一个可以正常运行的井字棋游戏!您可以使用我们介绍的概念实现任何实时多人游戏。如果您有任何问题,请告诉我,我很乐意为您解答。