使用 AWS AppSync 入门 GraphQL

Avatar of Nader Dabit
Nader Dabit

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

GraphQL 越来越受欢迎。问题是,如果您是前端开发人员,那么您只完成了一半的工作。GraphQL 不仅仅是一项客户端技术。服务器也必须根据规范进行实现。这意味着为了将 GraphQL 应用到您的应用程序中,您不仅需要学习前端的 GraphQL,还需要学习 GraphQL 最佳实践、服务器端开发以及后端相关的所有内容。

您将来还会遇到诸如扩展服务器、复杂的授权方案、恶意查询以及更多需要更多专业知识甚至更深入了解传统上归类为后端开发的内容等问题。

值得庆幸的是,我们今天拥有各种托管后端服务提供商,使前端开发人员只需关注前端功能的实现,而无需处理所有传统的后端工作。

诸如 Firebase(API)/ AWS AppSync(数据库)、Cloudinary(媒体)、Algolia(搜索)和 Auth0(身份验证)等服务使我们能够将复杂的底层架构卸载到第三方提供商,而是专注于以新功能的形式为最终用户提供价值。

在本教程中,我们将学习如何利用 AWS AppSync(一种托管的 GraphQL 服务)来构建一个完整堆栈应用程序,而无需编写任何后端代码。

虽然我们使用的框架是 React,但我们将使用的概念和 API 调用与框架无关,并且在 Angular、Vue、React Native、Ionic 或任何其他 JavaScript 框架或应用程序中都能以相同的方式工作。

我们将构建一个餐厅评论应用程序。在这个应用程序中,我们将能够创建餐厅、查看餐厅、为餐厅创建评论以及查看餐厅的评论。

An image showing four different screenshots of the restaurant app in mobile view.

我们将使用的工具和框架是 React、AWS Amplify 和 AWS AppSync。

AWS Amplify 是一个框架,允许我们创建和连接到云服务,例如身份验证、GraphQL API 和 Lambda 函数等。AWS AppSync 是一种托管的 GraphQL 服务。

我们将使用 Amplify 创建并连接到 AppSync API,然后编写客户端 React 代码来与 API 交互。

查看代码库

开始

首先,我们将创建一个 React 项目并进入新目录。

npx create-react-app ReactRestaurants

cd ReactRestaurants

接下来,我们将安装此项目中将使用的依赖项。AWS Amplify 是我们将用于连接到 API 的 JavaScript 库,我们将使用 Glamor 进行样式设置。

yarn add aws-amplify glamor

接下来我们需要做的是安装和配置 Amplify CLI。

npm install -g @aws-amplify/cli

amplify configure

Amplify 的配置将引导您完成在您的帐户中开始创建 AWS 服务所需的步骤。有关如何执行此操作的分步指南,请查看 此视频

现在应用程序已创建并且 Amplify 已准备好使用,我们可以初始化一个新的 Amplify 项目。

amplify init

Amplify init 将引导您完成初始化新 Amplify 项目的步骤。它将提示您输入所需的项目名称、环境名称和选择的文本编辑器。CLI 将自动检测您的 React 环境并为其余选项选择智能默认值。

创建 GraphQL API

初始化了一个新的 Amplify 项目后,我们现在可以添加 Restaurant Review GraphQL API。要添加新服务,我们可以运行 amplify add 命令。

amplify add api

这将引导我们完成以下步骤,以帮助我们设置 API。

? Please select from one of the below mentioned services GraphQL
? Provide API name bigeats
? Choose an authorization type for the API API key
? Do you have an annotated GraphQL schema? N
? Do you want a guided schema creation? Y
? What best describes your project: Single object with fields
? Do you want to edit the schema now? Y

CLI 现在应该在文本编辑器中打开一个基本模式。这将是我们 GraphQL API 的模式。

粘贴以下模式并保存。

// amplify/backend/api/bigeats/schema.graphql

type Restaurant @model {
  id: ID!
  city: String!
  name: String!
  numRatings: Int
  photo: String!
  reviews: [Review] @connection(name: "RestaurantReview")
}
type Review @model {
  rating: Int!
  text: String!
  createdAt: String
  restaurant: Restaurant! @connection(name: "RestaurantReview")
}

在此模式中,我们创建了两种主要类型:**Restaurant** 和 **Review**。请注意,我们的模式中包含 **@model** 和 **@connection** 指令。

这些指令是 Amplify CLI 中内置的 GraphQL Transform 工具的一部分。GraphQL Transform 将获取用指令修饰的基本模式,并将我们的代码转换为实现基本数据模型的完全功能的 API。

如果我们自己启动 GraphQL API,那么我们将不得不手动执行所有这些操作。

  1. 定义模式
  2. 定义针对模式的操作(查询、更改和订阅)
  3. 创建数据源
  4. 编写解析器,将模式操作与数据源映射起来。

使用 **@model** 指令,GraphQL Transform 工具将构建所有模式操作、解析器和数据源,因此我们只需定义基本模式(步骤 1)。**@connection** 指令将使我们能够对模型之间的关系进行建模,并为关系构建相应的解析器。

在我们的模式中,我们使用 **@connection** 来定义 Restaurant 和 Reviews 之间的关系。这将在最终生成的模式中为评论创建餐厅 ID 的唯一标识符。

现在我们已经创建了基本模式,我们可以在我们的帐户中创建 API 了。

amplify push
? Are you sure you want to continue? Yes
? Do you want to generate code for your newly created GraphQL API Yes
? Choose the code generation language target javascript
? Enter the file name pattern of graphql queries, mutations and subscriptions src/graphql/**/*.js
? Do you want to generate/update all possible GraphQL operations - queries, mutations and subscriptions Yes

因为我们正在创建 GraphQL 应用程序,所以我们通常需要从头开始编写所有本地 GraphQL 查询、更改和订阅。相反,CLI 将检查我们的 GraphQL 模式,然后为我们生成所有定义,并将它们保存在本地供我们使用。

完成后,后端已创建,我们可以开始从我们的 React 应用程序访问它。

如果您想在 AWS 控制台中查看您的 AppSync API,请访问 https://console.aws.amazon.com/appsync 并点击您的 API。在仪表板中,您可以查看模式、数据源和解析器。您还可以使用内置的 GraphQL 编辑器执行查询和更改。

构建 React 客户端

现在 API 已创建,我们可以开始查询和在我们的 API 中创建数据了。我们将使用三个操作来与我们的 API 交互。

  1. 创建新的餐厅
  2. 查询餐厅及其评论
  3. 为餐厅创建评论

在我们开始构建应用程序之前,让我们看看这些操作将如何显示和工作。

与 AppSync GraphQL API 交互

在使用 GraphQL API 时,可以使用许多 GraphQL 客户端。

我们可以使用任何我们想与 AppSync GraphQL API 交互的 GraphQL 客户端,但有两个客户端是专门配置为最容易工作的。它们是 Amplify 客户端(我们将使用它)和 AWS AppSync JS SDK(与 Apollo 客户端类似的 API)。

Amplify 客户端类似于 fetch API,因为它基于 Promise 并且易于理解。Amplify 客户端默认不支持离线。AppSync SDK 更复杂,但默认支持离线。

要使用 Amplify 调用 AppSync API,我们使用 API 类别。以下是如何调用 **查询** 的示例。

import { API, graphqlOperation } from 'aws-amplify'
import * as queries from './graphql/queries'

const data = await API.graphql(graphqlOperation(queries.listRestaurants))

对于 **更改**,它非常相似。唯一的区别是我们需要为更改中发送的数据传递第二个参数。

import { API, graphqlOperation } from 'aws-amplify'
import * as mutations from './graphql/mutations'

const restaurant = { name: "Babalu", city: "Jackson" }
const data = await API.graphql(graphqlOperation(
  mutations.createRestaurant,
  { input: restaurant }
))

我们使用 API 类别中的 graphql 方法来调用操作,将其包装在 graphqlOperation 中,后者将 GraphQL 查询字符串解析为标准 GraphQL AST。

我们将在应用程序中的所有 GraphQL 操作中使用此 API 类别。

这是包含此项目最终代码的代码库

使用 Amplify 配置 React 应用程序

在我们的应用程序中,首先需要做的就是配置它以识别我们的 Amplify 凭据。当我们创建 API 时,CLI 会在我们的 src 文件夹中创建一个名为 aws-exports.js 的新文件。

当我们创建、更新和删除服务时,CLI 会为我们创建和更新此文件。我们将使用此文件来配置 React 应用程序,使其了解我们的服务。

要配置应用程序,请打开 src/index.js 并添加以下代码

import Amplify from 'aws-amplify'
import config from './aws-exports'
Amplify.configure(config)

接下来,我们将创建组件所需的文件。在 src 目录中,创建以下文件

  • Header.js
  • Restaurant.js
  • Review.js
  • CreateRestaurant.js
  • CreateReview.js

创建组件

虽然样式在下面的代码片段中引用,但为了使代码片段更简洁,已省略了样式定义。有关样式定义,请参阅 最终项目仓库

接下来,我们将通过更新 src/Header.js 来创建 Header 组件。

// src/Header.js

import React from 'react'
import { css } from 'glamor'
const Header = ({ showCreateRestaurant }) => (
  <div {...css(styles.header)}>
    <p {...css(styles.title)}>BigEats</p>
    <div {...css(styles.iconContainer)}>
      <p {...css(styles.icon)} onClick={showCreateRestaurant}>+</p>
    </div>
  </div>
)

export default Header

现在我们的 Header 已创建,我们将更新 src/App.js。此文件将包含与 API 的所有交互,因此它相当大。我们将定义方法并将它们作为 props 传递给将调用它们的组件。

// src/App.js

import React, { Component } from 'react'
import { API, graphqlOperation } from 'aws-amplify'

import Header from './Header'
import Restaurants from './Restaurants'
import CreateRestaurant from './CreateRestaurant'
import CreateReview from './CreateReview'
import Reviews from './Reviews'
import * as queries from './graphql/queries'
import * as mutations from './graphql/mutations'

class App extends Component {
  state = {
    restaurants: [],
    selectedRestaurant: {},
    showCreateRestaurant: false,
    showCreateReview: false,
    showReviews: false
  }
  async componentDidMount() {
    try {
      const rdata = await API.graphql(graphqlOperation(queries.listRestaurants))
      const { data: { listRestaurants: { items }}} = rdata
      this.setState({ restaurants: items })
    } catch(err) {
      console.log('error: ', err)
    }
  }
  viewReviews = (r) => {
    this.setState({ showReviews: true, selectedRestaurant: r })
  }
  createRestaurant = async(restaurant) => {
    this.setState({
      restaurants: [...this.state.restaurants, restaurant]
    })
    try {
      await API.graphql(graphqlOperation(
        mutations.createRestaurant,
        {input: restaurant}
      ))
    } catch(err) {
      console.log('error creating restaurant: ', err)
    }
  }
  createReview = async(id, input) => {
    const restaurants = this.state.restaurants
    const index = restaurants.findIndex(r => r.id === id)
    restaurants[index].reviews.items.push(input)
    this.setState({ restaurants })
    await API.graphql(graphqlOperation(mutations.createReview, {input}))
  }
  closeModal = () => {
    this.setState({
      showCreateRestaurant: false,
      showCreateReview: false,
      showReviews: false,
      selectedRestaurant: {}
    })
  }
  showCreateRestaurant = () => {
    this.setState({ showCreateRestaurant: true })
  }
  showCreateReview = r => {
    this.setState({ selectedRestaurant: r, showCreateReview: true })
  }
  render() {
    return (
      <div>
        <Header showCreateRestaurant={this.showCreateRestaurant} />
        <Restaurants
          restaurants={this.state.restaurants}
          showCreateReview={this.showCreateReview}
          viewReviews={this.viewReviews}
        />
        {
          this.state.showCreateRestaurant && (
            <CreateRestaurant
              createRestaurant={this.createRestaurant}
              closeModal={this.closeModal}   
            />
          )
        }
        {
          this.state.showCreateReview && (
            <CreateReview
              createReview={this.createReview}
              closeModal={this.closeModal}   
              restaurant={this.state.selectedRestaurant}
            />
          )
        }
        {
          this.state.showReviews && (
            <Reviews
              selectedRestaurant={this.state.selectedRestaurant}
              closeModal={this.closeModal}
              restaurant={this.state.selectedRestaurant}
            />
          )
        }
      </div>
    );
  }
}
export default App

我们首先创建一些初始状态来保存我们将从 API 中获取的餐厅数组。我们还创建布尔值来控制我们的 UI 和一个 selectedRestaurant 对象。

componentDidMount 中,我们查询餐厅并更新状态以保存从 API 中检索到的餐厅。

createRestaurantcreateReview 中,我们向 API 发送 mutation。还要注意,我们通过立即更新状态来提供乐观更新,以便在响应返回之前更新 UI,从而使我们的 UI 更流畅。

接下来,我们将创建 Restaurants 组件(src/Restaurants.js)。

// src/Restaurants.js

import React, { Component } from 'react';
import { css } from 'glamor'

class Restaurants extends Component {
  render() {
    const { restaurants, viewReviews } = this.props
    return (
      <div {...css(styles.container)}>
        {
          restaurants.length === Number(0) && (
            <h1
              {...css(styles.h1)}
            >Create your first restaurant by clicking +</h1>
          )
        }
        {
          restaurants.map((r, i) => (
            <div key={i}>
              <img
                src={r.photo}
                {...css(styles.image)}
              />
              <p {...css(styles.title)}>{r.name}</p>
              <p {...css(styles.subtitle)}>{r.city}</p>
              <p
                onClick={() => viewReviews(r)}
                {...css(styles.viewReviews)}
              >View Reviews</p>
              <p
                onClick={() => this.props.showCreateReview(r)}
                {...css(styles.createReview)}
              >Create Review</p>
            </div>
          ))
        }
      </div>
    );
  }
}

export default Restaurants

此组件是应用程序的主要视图。我们遍历餐厅列表并显示餐厅图片、名称和位置,以及将打开覆盖层以显示评论和创建新评论的链接。

接下来,我们将查看 Reviews 组件(src/Reviews.js)。在此组件中,我们遍历所选餐厅的评论列表。

// src/Reviews.js

import React from 'react'
import { css } from 'glamor'

class Reviews extends React.Component {
  render() {
    const { closeModal, restaurant } = this.props
    return (
      <div {...css(styles.overlay)}>
        <div {...css(styles.container)}>
          <h1>{restaurant.name}</h1>
          {
            restaurant.reviews.items.map((r, i) => (
              <div {...css(styles.review)} key={i}>
                <p {...css(styles.text)}>{r.text}</p>
                <p {...css(styles.rating)}>Stars: {r.rating}</p>
              </div>
            ))
          }
          <p onClick={closeModal}>Close</p>
        </div>
      </div>
    )
  }
}

export default Reviews

接下来,我们将查看 CreateRestaurant 组件(src/CreateRestaurant.js)。此组件包含一个表单,用于跟踪表单状态。createRestaurant 类方法将调用 this.props.createRestaurant,并将表单状态作为参数传递。

// src/CreateRestaurant.js

import React from 'react'
import { css } from 'glamor';

class CreateRestaurant extends React.Component {
  state = {
    name: '', city: '', photo: ''
  }
  createRestaurant = () => {
    if (
      this.state.city === '' || this.state.name === '' || this.state.photo === ''
    ) return
    this.props.createRestaurant(this.state)
    this.props.closeModal()
  }
  onChange = ({ target }) => {
    this.setState({ [target.name]: target.value })
  }
  render() {
    const { closeModal } = this.props
    return (
      <div {...css(styles.overlay)}>
        <div {...css(styles.form)}>
          <input
            placeholder='Restaurant name'
            {...css(styles.input)}
            name='name'
            onChange={this.onChange}
          />
          <input
            placeholder='City'
            {...css(styles.input)}
            name='city'
            onChange={this.onChange}
          />
          <input
            placeholder='Photo'
            {...css(styles.input)}
            name='photo'
            onChange={this.onChange}
          />
          <div
            onClick={this.createRestaurant}
            {...css(styles.button)}
          >
            <p
              {...css(styles.buttonText)}
            >Submit</p>
          </div>
          <div
            {...css([styles.button, { backgroundColor: '#555'}])}
            onClick={closeModal}
          >
            <p
              {...css(styles.buttonText)}
            >Cancel</p>
          </div>
        </div>
      </div>
    )
  }
}

export default CreateRestaurant

接下来,我们将查看 CreateReview 组件(src/CreateReview.js)。此组件包含一个表单,用于跟踪表单状态。createReview 类方法将调用 this.props.createReview,并将餐厅 ID 和表单状态作为参数传递。

// src/CreateReview.js

import React from 'react'
import { css } from 'glamor';
const stars = [1, 2, 3, 4, 5]
class CreateReview extends React.Component {
  state = {
    review: '', selectedIndex: null
  }
  onChange = ({ target }) => {
    this.setState({ [target.name]: target.value })
  }
  createReview = async() => {
    const { restaurant } = this.props
    const input = {
      text: this.state.review,
      rating: this.state.selectedIndex + 1,
      reviewRestaurantId: restaurant.id
    }
    try {
      this.props.createReview(restaurant.id, input)
      this.props.closeModal()
    } catch(err) {
      console.log('error creating restaurant: ', err)
    }
  }
  render() {
    const { selectedIndex } = this.state
    const { closeModal } = this.props
    return (
      <div {...css(styles.overlay)}>
        <div {...css(styles.form)}>
          <div {...css(styles.stars)}>
            {
              stars.map((s, i) => (
                <p
                  key={i}
                  onClick={() => this.setState({ selectedIndex: i })}
                  {...css([styles.star, selectedIndex === i && { backgroundColor: 'gold' }])}
                >{s} star</p>
              ))
            }
          </div>
          <input
            placeholder='Review'
            {...css(styles.input)}
            name='review'
            onChange={this.onChange}
          />
          <div
            onClick={this.createReview}
            {...css(styles.button)}
          >
            <p
              {...css(styles.buttonText)}
            >Submit</p>
          </div>
          <div
            {...css([styles.button, { backgroundColor: '#555'}])}
            onClick={closeModal}
          >
            <p
              {...css(styles.buttonText)}
            >Cancel</p>
          </div>
        </div>
      </div>
    )
  }
}

export default CreateReview

运行应用程序

现在我们已经构建了后端,配置了应用程序并创建了组件,我们准备对其进行测试。

npm start

现在,导航到 https://#:3000。恭喜,您刚刚构建了一个完整的无服务器 GraphQL 应用程序!

结论

对于许多应用程序来说,下一步是应用额外的安全功能,例如身份验证、授权和细粒度访问控制。所有这些功能都内置在服务中。要了解有关 AWS AppSync 安全性的更多信息,请 查看文档

如果您想为您的应用程序添加托管和持续集成/持续部署管道,请查看 Amplify 控制台

我还维护了一些存储库,其中包含有关 Amplify 和 AppSync 的其他资源:Awesome AWS AmplifyAwesome AWS AppSync

如果您想了解有关使用托管服务构建应用程序的这种理念的更多信息,请查看我的帖子,标题为 “无服务器计算时代中的全栈开发”。