Hasura 101:使用 Graphql、Postgres 和 React 构建实时游戏

2025-05-24

Hasura 101:使用 Graphql、Postgres 和 React 构建实时游戏

2019 年我最喜欢的技术发现是Hasura。它让完整的 Postgres + GraphQL 后端的启动和运行变得轻而易举——你几乎只需点击几下,就能拥有一个完全交互式的数据库浏览器和编辑器,以及可以用 GraphIQL 测试的 GraphQL 端点。我想分享一个关于在 Hasura 上构建实时游戏(使用 websockets!!!)的教程,该教程基于我今年早些时候和 Hasura 一起参加的一个研讨会。

我们将讨论什么是 GraphQL,什么是 Hasura,如何设置 Hasura 和数据库,然后在其上构建一个完整的 React 应用。我们将构建一个如下所示的绘图应用程序:

像素艺术应用

是它的部署版本!(注意:它使用的是 Heroku 免费套餐,因此可能需要几秒钟才能启动)

什么是 GraphQl

根据其文档,“GraphQL 是一种 API 查询语言”。传统上,使用 REST API,您可以通过各种端点访问不同的数据或以某种方式更改数据。这很快就会变得非常庞大,如果您的前端和后端团队各自独立工作,甚至可能成为瓶颈。随着我们的应用程序不断发展并需要显示不同的数据,GraphQL 变得非常有用。

Sacha Grief 在他们的文章“那么我一直听到的这个 GraphQL 是什么?”中写了一个很棒的比喻。

旧的 REST 模型就像订披萨,然后叫杂货送货上门,最后打电话给干洗店取衣服。三个商店,打三个电话。

另一方面,GraphQL 就像拥有一个私人助理:一旦你向他们提供了这三个地方的地址,你就可以简单地询问你想要的东西(“给我拿件干洗衣物、一个大披萨和两打鸡蛋”)并等待他们回来。

GraphQL 也与语言无关(即您可以将 GraphQL 与任何编程语言一起使用),并且它位于您的客户端和数据源之间,因此它非常灵活!

Hasura 是什么

Hasura 允许您以闪电般的速度构建 GraphQL 后端——您只需单击按钮即可制作一些非常棒的东西。

哈苏拉:

  • 为您提供新数据库或现有数据库上的即时实时 GraphQL API。
  • 它配有仪表板,可帮助您设置 API 和数据库。
  • 您可以根据需要使用 Web 钩子、外部 API 或无服务器功能对数据库中的变化做出反应。
  • 您还可以将自定义 GraphQL API 和其他数据源拼接成统一的 GraphQL API。

使用 Hasura 启动并运行

  1. 转到此网址
  2. 登录 Heroku(如果还没有,请创建一个帐户,不用担心,它是免费的!)
  3. 为您的应用程序选择一个(唯一)名称
  4. 点击Deploy app
  5. ✨神奇✨!您已部署并运行 Hasura 实例!

用于创建 Hasura 应用程序的 heroku 用户界面

设置数据库

我们的应用程序使用PostgreSQL数据库,但 Hasura 为我们提供了一个与该数据库交互的非常好的界面。

转到您的 Hasura 应用程序,它应该看起来像这样:

Hasura GraphIQL 界面

单击data选项卡,然后单击标题create table旁边的按钮Schema

黄色创建表按钮

我们将pixels在数据库中创建一个表来存储每个像素的坐标和颜色。

我们还将在该表中创建两列:id,这将是 Postgres 为我们处理的自动递增整数,color它将存储每个像素应采用的颜色。

另外,设置id为主键。

您的表单应该是这样的!

表单值

然后,向下滚动到底部并单击add table按钮!

我们现在有一个包含所需列的数据库🎉!

添加初始数据

我们的应用程序第一次加载时,我们希望每个像素只是一个白色框,直到人们开始为它们着色。为了使我们未来的代码更容易,我们将在数据库中植入 400 个具有颜色的值white,因为网格是 20x20 的网格。

注意:您还可以为每个像素存储行、列和颜色,但我发现只需存储所有 400 个像素并在用户界面上按 ID 对它们进行排序就更容易了。

在 Hasura 仪表板中,我们可以通过单击SQL表列表下的链接来运行 SQL 查询。

您可以将 SQL 添加到弹出的文本框中,然后按运行!

是我用来填充初始数据库的查询。您可以复制粘贴相同的查询,以向数据库添加 400 个白色像素!

GraphQL 查询

现在我们已经将数据加载到数据库中,我们可以使用 GraphQL 来查询这些数据。我们不需要再做任何设置!您可以转到GraphIQLHasura 仪表板的选项卡并使用您的数据。

GraphIQL 是一个用于探索 GraphQL 查询的浏览器内置 IDE。它可以帮助你编写查询来获取和操作数据。

GraphQL 查询的语法与您可能习惯的 SQL 查询非常不同——它们看起来更类似于 JavaScript 对象。

例如,要获取所有数据,我们的查询将如下所示:

query GetPixels {
  pixels {
    id
    color
  }
}
Enter fullscreen mode Exit fullscreen mode

首先,我们将查询命名为GetPixels。然后,我们指定要从表中获取数据。我们还指定了要获取和列的pixels数据。如果省略其中一列,则只会获取该列的数据。idcolor

我们还可以更改查询,以便它始终按照像素的 ID 对像素进行排序:

query GetPixels {
  pixels(order_by: { id: asc }) {
    id
    color
  }
}
Enter fullscreen mode Exit fullscreen mode

我们还可以编写subscriptions通过 websockets 订阅数据变化的查询。

将上述示例中的单词更改querysubscription将允许我们在更新时提取新数据。

此外,GraphQL 还mutations允许我们更新数据。

例如,以下查询将允许我们根据像素的 ID 更新其颜色:

mutation changePixelColor($id: Int!, $color: String!) {
  update_pixels(where: { id: { _eq: $id } }, _set: { color: $color }) {
    returning {
      color
      id
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

这个突变名为changePixelColor,就像编程函数一样,突变(和查询)可以接受参数。在本例中,它接受id,它是一个整数,以及 ,color它是一个字符串。我们需要指定要查询的表,在本例中是pixels,我们可以通过 来实现update_pixels。然后,我们添加一个where子句——我们只更新数据库中与id指定项匹配的项目。然后我们指定_set,其中我们表示将行的颜色设置为指定的颜色。

然后,我们添加一个returning数据,其中包含我们想要在查询执行完成后发送回应用程序的数据。

我强烈建议在 GraphIQL 中测试这些查询并使用它来构建自定义查询——它会为您做很多事情!

如果您想深入了解 GraphQL,这里是其文档,非常棒!

与 React Code 集成

由于本教程主要侧重于将 Hasura 和 GraphQL 与现有应用程序集成,因此我们将从一些预先编写的 React 代码开始。这个仓库包含我们将要构建的代码。目前,它是一个静态版本的绘图应用程序。一个人可以创作像素画,但它不连接到后端,因此绘图无法持久化,人们也无法协作绘图。

如果你克隆了代码库,请运行npm install并安装所有依赖项。快速浏览一下代码,看看发生了什么。

Apollo 设置

我们将使用Apollo来更轻松地编写前端 GraphQL 连接。

Connection.js文件中添加以下代码:

import { HttpLink } from "apollo-link-http";
import { ApolloClient } from "apollo-client";
import { InMemoryCache } from "apollo-cache-inmemory";

export default new ApolloClient({
  cache: new InMemoryCache(),
  link: new HttpLink({
    uri: "your-endpoint.herokuapp.com",
  }),
});
Enter fullscreen mode Exit fullscreen mode

对于uri,请使用选项卡顶部的 GraphQL 端点GraphIQL

Hasura 接口上的 GraphQL 端点

这将设置 Apollo 客户端,以便我们的 GraphQL 查询将指向我们创建的端点。

我们还需要添加几行代码index.js

import React from "react";
import ReactDOM from "react-dom";
+import { ApolloProvider } from "@apollo/react-hooks";

import "./index.css";
import App from "./App";
import * as serviceWorker from "./serviceWorker";
import connection from "./Connection";

ReactDOM.render(
+ <ApolloProvider client={connection}>
    <App />
+ </ApolloProvider>,
  document.getElementById("root")
);

// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister();
Enter fullscreen mode Exit fullscreen mode

这使我们的整个应用程序能够访问我们创建的 GraphQL 连接。现在我们的查询将自动转到正确的位置。

查询设置

每次访问应用程序时,我们都需要访问 GraphQL 端点,以获取每个像素应该使用的颜色。我们将在App.js文件中添加一些代码,以使应用程序获取我们创建的数据,而不是现在正在使用的静态数据!

首先,我们将导入gql模板标签。这将允许我们在 JavaScript 代码中编写 GraphQL 查询。我们将使用之前编写的查询来获取所有像素。

const GET_PIXELS = gql`
  query GetPixels {
    pixels(order_by: { id: asc }) {
      color
      id
    }
  }
`;
Enter fullscreen mode Exit fullscreen mode

然后,我们将使用useQueryApollo 提供的钩子来获取我们的数据。

const { loading, error, data } = useQuery(GET_PIXELS);

当我们的组件加载时,这段代码将运行我们的查询。

这个钩子给了我们三个值:查询是否仍在运行(loading)、如果存在错误消息以及从查询返回的数据。

在我们取回数据之前,我们可能需要某种加载指示器,因此我们将向组件添加一个条件:

if (loading) {
  return <h2>Loading...</h2>;
}
Enter fullscreen mode Exit fullscreen mode

我们还将更改map为使用实时数据,而不是我们当前在第 5 行创建的硬编码像素。

data.pixels.map((pixel) => (
  <Pixel {...pixel} key={pixel.id} newColor={color} />
));
Enter fullscreen mode Exit fullscreen mode

总而言之,我们的改变如下App.js

import React, { useState } from "react";
+ import { useQuery } from "@apollo/react-hooks";
+ import gql from "graphql-tag";
import Pixel from "./Pixel";
import ColorPicker from "./ColorPicker";

- const pixels = new Array(400).fill("white");

+ const GET_PIXELS = gql`
+   query GetPixels {
+     pixels(order_by: { id: asc }) {
+      color
+      id
+   }
+ }
+`;

function App() {
+ const { loading, error, data } = useQuery(GET_PIXELS);
  const [color, changeColor] = useState("white");

+ if (loading) {
+   return <h2>Loading...<h2/>;
+ }

  return (
    <div className="content">
      <div className="logo">Draw</div>
      <p>Pick a Color</p>
      <ColorPicker changeColor={changeColor} />
      <p>Click a Pixel</p>
      <div className="container">
+       {data.pixels.map(pixel => (
+         <Pixel {...pixel} key={pixel.id} newColor={color} />
+        ))}
      </div>
    </div>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

变异设置

现在,让我们让Pixel组件在点击像素时运行突变操作,从而改变像素的颜色。这将使我们的更改在各个用户和会话之间保持一致。

我们将再次使用我们的gql模板标签,并将我们的变异放入其中。

const UPDATE_COLOR = gql`
  mutation ChangePixelColor($id: Int!, $color: String!) {
    update_pixels(where: { id: { _eq: $id } }, _set: { color: $color }) {
      returning {
        x
        y
        color
        id
      }
    }
  }
`;
Enter fullscreen mode Exit fullscreen mode

Apollo 也有一个useMutation钩子,所以我们将导入它并使用它。

const [updatePixelColor] = useMutation(UPDATE_COLOR);
Enter fullscreen mode Exit fullscreen mode

onClick当用户点击像素时,我们还将更新我们的处理程序以运行我们的变异。

onClick={() => {
    changeColor(color);
    updatePixelColor({ variables: { id, color: newColor } });
}}
Enter fullscreen mode Exit fullscreen mode

Pixel.js完成转换后,我们的意志将呈现如下面的样子:

import React from "react";
+ import gql from "graphql-tag";
+ import { useMutation } from "@apollo/react-hooks";

+ const UPDATE_COLOR = gql`
+ mutation ChangePixelColor($id: Int!, $color: String!) {
+   update_pixels(where: { id: { _eq: $id } }, _set: { color: $color }) {
+     returning {
+       color
+       id
+     }
+   }
+ }
+ `;

const Pixel = ({ id, color, newColor }) => {
+ const [updatePixelColor] = useMutation(UPDATE_COLOR);

  return (
    <span
      className="pixel"
      onClick={() => {
         changeColor(color);
+        updatePixelColor({ variables: { id, color: newColor } });
      }}
      style={{ backgroundColor: color }}
    ></span>
  );
};

export default Pixel;
Enter fullscreen mode Exit fullscreen mode

哇!🙌🏻 现在我们的应用程序连接到了 GraphQL 端点并拉取了正确的数据。如果你想查看完整的解决方案代码,这里是它!

实现实时

目前,我们的应用程序在页面加载时会从 GraphQL 端点拉取数据,但当其他用户点击像素时,它不会实时更新。我们希望用户能够与朋友实时绘画。您可以尝试在两个标签页中打开已部署的应用程序——如果您在一个标签页中更新像素,另一个标签页也会更新。

我们只需要更新我们的App.js以使用 GraphQL 订阅而不是查询。

我们将使用 Apollo 的useSubscription钩子代替 useQuery,并将query查询中的单词更改为subscription。神奇🧙🏻‍♂️!

这是差异图,显示了哪些地方发生了变化!(注:由于变化不大,因此省略了大部分文件)

import React, { useState } from "react";
+ import { useSubscription } from "@apollo/react-hooks";
import gql from "graphql-tag";

import Pixel from "./Pixel";
import ColorPicker from "./ColorPicker";

const pixels = new Array(400).fill("white");

const GET_PIXELS = gql`
+ subscription GetPixels {
    pixels(order_by: { id: asc }) {
      color
      id
    }
  }
`;

function App() {
  const [color, changeColor] = useState("white");
+ const { loading, error, data } = useSubscription(GET_PIXELS);

...
Enter fullscreen mode Exit fullscreen mode

是包含订阅的完整代码!

后续步骤

结论

使用这个堆栈构建应用程序让我非常享受——它让我可以专注于简单应用程序的前端代码。我可以创建一个允许两个用户实时交互的应用程序,并且几乎没有任何阻碍。传统上,使用 GraphQL 层编写完整的后端是一个相当繁琐的过程,需要大量的维护。有了 Hasura,我们只需点击几下鼠标就能完成。这是我构建快速应用程序的新首选堆栈。

另外,如果您想观看,这里还有视频版本!

迫不及待地想看看您创造了什么!

文章来源:https://dev.to/aspittel/hasura-101-building-a-realtime-game-with-graphql-postgres-and-react-5c3j
PREV
回到老问题:我最终如何编写数独求解算法
NEXT
Storybook 入门:如何在没有应用程序的情况下开发 React 组件