GraphQL 初学者完整指南
2024 年 6 月 27 日:这篇博文使用 Amplify Gen 1,如果您要启动新的 Amplify 应用程序,我建议您尝试Gen 2!
我第一次使用 GraphQL 是在 2015 年 GraphQL 发布后不久,当时我在一个项目中,说实话,我当时并不明白为什么要用它。多年来,我越来越喜欢 GraphQL——你可以使用 AWS AppSync 和 Hasura 等托管服务快速创建 API,而且它还能减少前端和后端开发之间的摩擦。在这篇文章中,我们将讨论什么是 GraphQL,为什么要使用它,以及什么时候它可能不是最佳选择,然后使用 GraphQL API 创建一个完整的食谱应用程序。
请注意,我是 AWS Amplify 团队的开发倡导者,如果您对此有任何反馈或疑问,请联系我或在我们的 discord 上提问 - discord.gg/amplify!
如果您是 API 新手,我建议您先阅读这篇关于 API 的博客文章!如果您对 REST 的概念还不熟悉,我也建议您先阅读这篇文章;这篇文章经常会将 GraphQL 与 REST 进行比较。我还会使用React来处理前端的一些部分——建议您在阅读这篇文章之前先浏览一下相关的教程。
此外,我们将在本文中使用一些词汇:
- 模式:这是数据结构方式的表示。
- 字段:这些是与数据相关的属性。
什么是 GraphQL?
根据其文档,“GraphQL 是一种用于 API 的查询语言,也是一个服务器端运行时,用于使用您为数据定义的类型系统执行查询。” GraphQL 本身是一种规范,这意味着有一个文档概述了 GraphQL 查询的样子以及客户端-服务器交互如何使用它;但是,它可以与您应用程序的任何编程语言或数据层一起使用。
实际上,这允许前端开发人员向后端发送查询,请求他们需要的数据(包括嵌套数据)。这使得后端开发人员只需创建一个端点,而无需像 REST API 那样创建多个端点。您可以将更改数据的操作和检索数据的查询全部发送到一个地方。
为什么要使用 GraphQL?
GraphQL 受欢迎的原因有很多。首先,它简化了前端和后端开发人员之间的沟通——前端开发人员无需在需求发生变化时请求新的端点,只需更新其 GraphQL 查询即可。如果您有多个前端需要相同的后端数据,这将变得更加有用。前端开发人员可以准确获取所需的数据——不会出现字段或项获取不足或过度的情况。
由于前端开发人员可以使用一个查询来请求嵌套数据,因此网络请求也得到了最小化——例如,如果您查询一篇博客文章,那么您可以在一个查询中同时获取该文章的评论,而无需再次发出请求。这也可以减少所需的前端代码量,并使代码更易于理解。
GraphQL 还强制使用类型化的数据模式,因此每个条目的字段必须与这些类型匹配。这使得数据更加一致且易于管理——无需循环遍历博客文章并确定每个标题是字符串还是布尔值,GraphQL 会强制每个标题都是字符串。
GraphQL 什么时候不那么好?
与软件工程中的任何事物一样,使用 GraphQL 也有缺点。首先,我在 2015 年左右 GraphQL 发布时就开始使用它了,但我讨厌它。我当时是一个小团队的全栈工程师,构建后端的工作量很大,而前端则需要更冗长。GraphQL 查询通常很长,而对于许多 REST API,您只需提供一个 url 即可。此外,与 REST 相比,许多后端框架和语言对 GraphQL API 的支持不太成熟。您可能需要做更多的工作并浏览较少使用的库才能获取 GraphQL Api。如果您是创建和使用端点的人,那么构建 REST API 可能会更快——尤其是当您使用的编程语言或框架对 GraphQL 支持不太成熟时。
GraphQL 在大型团队中大放异彩,这些团队由前端团队开发客户端,并由单独的团队开发服务器端。此外,越来越多的托管 GraphQL 服务出现,例如 Hasura 和 AWS AppSync。这些服务允许你使用它们的服务生成 GraphQL 后端,然后在前端使用它——与从头编写 GraphQL 服务器相比,这通常可以显著加快后端开发的速度。
最后,许多开发人员在职业生涯早期就学习了如何使用和创建 REST API,并且可能对 GraphQL 缺乏系统性的了解。让整个团队快速上手或许是一项值得考虑的投资。
创建 GraphQL API
现在到了最有趣的部分,让我们开始写代码吧!我们将使用 AWS Amplify 来创建一个 GraphQL 后端——这将加快流程,让我们只专注于 GraphQL,而不是后端开发的其他部分。
首先,我将创建一个 React 应用程序——这里没有太多的 React 代码,但设置比使用捆绑器创建 Vanilla JS 应用程序更快。
在你的终端中运行:
npx create-react-app graphql-playground
cd graphql-playground
注意:您需要安装Node才能执行此步骤。
接下来,我们将在我们的项目中初始化 Amplify。
amplify init
注意:您需要安装Amplify才能执行此步骤。
然后,系统会提示您回答几个问题。您可以输入“y”以获取默认的 React 配置,然后选择您的 AWS 配置文件(如果您没有,请参阅上面的教程!)
Project information
| Name: graphqldemo
| Environment: dev
| Default editor: Visual Studio Code
| App type: javascript
| Javascript @framework: react
| Source Directory Path: src
| Distribution Directory Path: dist
| Build Command: npm run-script build
| Start Command: npm run-script start
? Initialize the project with the above configuration? Yes
Using default provider awscloudformation
? Select the authentication method you want to use: AWS profile
For more information on AWS Profiles, see:
https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-profiles.html
? Please choose the profile you want to use default
现在,我们将创建一个 GraphQL API。运行:
amplify add api
系统会再次询问您几个问题!首先,选择 GraphQL,然后为您的 API 命名,例如 graphql demo。然后,您可以按两次 Enter 键接受 API 密钥的默认值。然后,您可以选择 GraphQL API 和 GraphQL 架构的“否”。选择“一对多关系”模板,然后单击“是”以立即编辑架构。
? Please select from one of the below mentioned services: GraphQL
? Provide API name: graphqldemo
? Choose the default authorization type for the API API key
? Enter a description for the API key:
? After how many days from now the API key should expire (1-365): 7
? Do you want to configure advanced settings for the GraphQL API No, I am done.
? Do you have an annotated GraphQL schema? No
? Choose a schema template: One-to-many relationship (e.g., “Blogs” with “Posts” and “Comments”)
? Do you want to edit the schema now? Yes
? Choose your default editor: Visual Studio Code
您会看到一个预先生成的模式弹出,让我们来讨论一下博客模型。
type Blog @model {
id: ID!
name: String!
posts: [Post] @connection(keyName: "byBlog", fields: ["id"])
}
TODO:添加标量类型列表
type
- 该词type
用于表示您可以从 API 中获取的一种对象类型 - 在本例中是博客!
Blog
- 这是类型的名称
@model
- @
GraphQl 中的符号定义了一个指令,这意味着某个字段或类型具有与其关联的自定义逻辑。Amplify提供了许多这样的指令供您使用。该@model
指令使得博客的数据能够存储在我们的数据库中。
id
、、name
和posts
- 这些是每个博客都会有的字段或数据片段
ID
并且String
- 这些是类型,它们定义了id
将是 类型id
,而name
将是字符串。这些字段是,scalar
这意味着它们是单独的数据——一个 ID 和一个名称,而不是每个博客文章都有一个名称集合。
!
- 类型后面的感叹号表示该字段不可为空,或者您始终需要为该字段提供一个值。在这种情况下,每个博客都必须有一个 ID 和名称!
[Post]
- 首先,[]
将其设置为数组字段。每个博客都可以关联一个帖子数组。您也可以使用标量类型来实现这一点,这样[String]
就可以使用字符串数组。在本例中,我们引用的Post
模型也在此文件中声明,因此这两种数据类型是相互关联的。
@connection
- 这是另一个指令,在本例中,它允许我们将一个模型与另一个模型关联起来。你需要向它提供一些数据,在本例中keyName
是fields
keyName
- 这是用于查询相关帖子的索引的名称。您会注意到,模型中Post
有一个@key
指令定义了一个名称。该键的名称将与此处的 匹配。每当您在 Amplify 中有一个一对多字段时,keyName
您都需要定义一个,然后使用来引用它。@key
keyName
fields
- 这是可以查询以获取连接对象的字段。
现在,让我们将其替换为我们的模式。我们将创建一本食谱。首先,让我们创建三个模型,Recipe
、Ingredient
和Instruction
。
type Recipe @model {
}
type Ingredient @model {
}
type Instruction @model {
}
现在,让我们为每个模型添加字段。每个模型都需要一个id
,它将是必填ID
字段。然后,我们将name
s 添加到Recipe
和Ingredient
。Ingredient
也将包含数量,并且Instruction
将包含info
。
type Recipe @model {
id: ID!
name: String!
}
type Ingredient @model {
id: ID!
name: String!
quantity: String!
}
type Instruction @model{
id: ID!
info: String!
}
现在,我们需要连接我们的模型。首先,我们将向@key
两个子模型添加指令——Ingredient
因为sInstruction
会Recipe
同时包含这两个指令!我们希望能够通过它们所属的食谱来访问Ingredient
s 和s。每个模型都有一个,它指向它们所属的食谱。然后,我们将基于该 创建一个与模型的连接。最后,我们将在每个模型上设置,以便我们访问属于某个食谱的配料组或说明。Instruction
recipeID
Recipe
recipeID
@key
type Ingredient @model @key(name: "byRecipe", fields: ["recipeID"]) {
id: ID!
name: String!
quantity: String!
recipeID: ID!
recipe: Recipe @connection(fields: ["recipeID"])
}
type Instruction @model @key(name: "byRecipe", fields: ["recipeID"]) {
id: ID!
info: String!
recipeID: ID!
recipe: Recipe @connection(fields: ["recipeID"])
}
最后,我们将添加从Recipe
模型到每个成分和说明的连接。
type Recipe @model {
id: ID!
name: String!
ingredients: [Ingredient] @connection(keyName: "byRecipe", fields: ["id"])
instructions: [Instruction] @connection(keyName: "byRecipe", fields: ["id"])
}
现在,我们需要部署数据!运行程序amplify push
会在云端为我们创建一个 GraphQL API。
amplify push -y
查询和变异!
好的,我们已经设置好了 GraphQL。现在让我们与它交互!我们将使用 创建数据mutations
。我们也将使用 检索数据queries
。
从命令行运行:
amplify console api
然后选择 graphql。AWS AppSync 的控制台将在浏览器中打开。AppSync 是我们用于创建 GraphQL API 的底层服务,使用其控制台,我们可以通过可视化界面测试查询。
进入 AppSync 界面后,Mutation
在下拉菜单中选择,然后单击加号按钮。
在下面,你会看到一些可供选择的操作。选择“createRecipe”,然后点击name
输入框旁边的复选框。
输入你的食谱名称。我选了mac n cheese
!
按下橙色的运行按钮,你就会得到一个食谱✨!如果你愿意,你可以创建几个不同的食谱——更改食谱名称,然后按下你想要制作的每个食谱对应的橙色按钮。
现在让我们看看我们创建的食谱。将下拉菜单切换回 ,Query
而不是Mutation
。然后选择listRecipes
其下方的 。选择您想要查看的属性,例如name
下方的items
。另请注意,您可以
重复创建食谱的步骤,Recipe
创建一些食材和说明。使用食谱的 ID recipeID
(提示:您可以使用查询获取listRecipes
!),您还可以通过一次更改创建包含食材和说明的食谱,只需选择相应的字段并填充即可!
现在,listRecipes
使用ingredients
and重新运行查询instructions
,您将看到所有内容都已连接。这就是 GraphQL 的魅力所在——您无需更改端点即可获取所需的任何数据,只需更改与之交互的字段即可!
GraphQL 查询的剖析
我们已经使用这个可视化界面编写了 GraphQL 查询和变异,但我们也深入研究它们的语法,以便您可以从头开始编写和理解它们。
以下是我们可以在 API 上使用的示例查询。
query MyQuery {
# This is a comment!
listRecipes {
items {
name
id
createdAt
instructions {
items {
id
info
}
}
ingredients {
items {
id
name
quantity
}
}
}
}
}
query
- 这是我们针对数据执行的操作类型。query
分别是检索数据、mutation
更改数据以及subscription
监听数据变化。在本教程的其余部分,我们将使用这三种操作!
MyQuery
- 这是查询的名称,理想情况下,这些名称应该是描述性的,例如ListRecipes
listRecipes
- AppSync 生成GraphQL 解析器,允许我们获取数据。
items
- 从句法上讲,这表示我们得到了多个食谱
name
,,id
——createdAt
我们想要获取有关我们数据的字段。createdAt
并updatedAt
自动为我们添加。
instructions
并且ingredients
——我们还想获取相关说明和成分的数据!然后,将它们的字段放入查询中即可获取这些数据。
您可以在查询中添加或删除您想要的任何字段!
某些查询也需要arguments
。例如,如果您只想获取一个 Recipe,则可以提供所需 Recipe 的 ID。对于 Mutation 也是如此。
query GetRecipe($id: ID!) {
getRecipe(id: $id) {
id
name
}
}
现在,让我们在应用程序中查询我们新创建的 API!
如何在前端运行这些查询
现在我们已经尝试了变更和查询,如何将它们集成到我们的应用中呢?首先,让我们先尝试一下不使用任何库。我们可以使用用于 REST API 调用的普通 Fetch 请求。
前往您的App.js
组件。首先,从文件导入对象aws-exports.js
。您可以直接查看该文件,但它实际上包含了前端所需的所有 Amplify 生成的后端配置信息。此外,还要useEffect
从 React 导入。
import config from './aws-exports'
import { useEffect } from 'react'
现在,我们将创建一个useEffect钩子,它将在页面加载时发出获取请求(如果您使用的是原始 JavaScript,则很可能会在页面加载事件中编写相同的代码,但不使用 useEffect)。
在获取请求中,我们需要指定端点,以便从aws-exports
对象中获取。然后,我们需要通过添加请求方法来自定义请求POST
。我们还将从aws-exports
文件中提供 API 密钥。之后,请求正文将包含我们之前使用的查询!我们需要使用该JSON.stringify
方法将对象转换为字符串。与其他获取请求一样,我们需要将数据转换为 JSON,然后才能查看!
function App() {
useEffect(() => {
const pullData = async () => {
let data = await fetch(config.aws_appsync_graphqlEndpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
'X-Api-Key': config.aws_appsync_apiKey
},
body: JSON.stringify({
query: `query MyQuery {
listRecipes {
items {
name
id
createdAt
instructions {
items {
id
info
}
}
ingredients {
items {
id
name
quantity
}
}
}
}
}
`
})
})
data = await data.json()
console.log(data)
}
pullData()
}, [])
return <h1>Hello GraphQL!</h1>
}
好的,现在我们可以从 API 中获取数据了,但这有点笨重,代码量也有点大。如果你进入graphql/
Amplify 生成的目录,你会看到一些文件,其中包含所有常见操作的订阅、查询和变更!我们将导入这些文件并在代码中使用它们。此外,Amplify 还提供了辅助函数来抽象 HTTP 请求。
在项目的根目录中,运行:
npm i aws-amplify
这将安装 Amplify 库,这将有助于使 GraphQL 查询更加简洁。
我们将在文件中配置 Amplify,将前端和后端绑定在一起index.js
。在顶部添加以下内容:
// index.js
import { Amplify } from 'aws-amplify'
import config from './aws-exports'
Amplify.configure(config)
现在,回到App.js
文件。我们将从库中导入一些东西aws-amplify
。
import { API } from 'aws-amplify'
我们还将listRecipes
从 Amplify 生成的查询中导入查询。你可以在 graphql/queries.js 文件中查看它的代码。
import { listRecipes } from './graphql/queries'
让我们修改一下useEffect
代码。将pullData
函数替换为以下内容:
useEffect(() => {
const pullData = async () => {
const data = await API.graphql({ query: listRecipes })
console.log(data)
}
pullData()
}, [])
该API.graphql()
方法会向我们应用已配置的 GraphQL API 发起 API 请求。我们将查询结果作为参数传入一个对象中。代码比以前少多了!
现在,我们将运行一个突变,以便在用户点击按钮时创建新的菜谱。我们还会提示用户输入菜谱的名称。将组件return
中的语句替换App.js
为以下内容:一个在点击时运行事件监听器的按钮。
return (
<div className='App'>
<button onClick={createNewRecipe}>create recipe</button>
</div>
)
确保导入我们需要的突变:
import { createRecipe } from './graphql/mutations'
现在,我们将实现该createNewRecipe
函数。将其添加到您的组件中。首先,我们将要求用户命名菜谱。然后,我们将运行一个 GraphQL 请求,这次会使用createRecipe
突变。此突变也接受变量:在本例中是菜谱的名称。我们也将在一个对象中提供该变量!
const createNewRecipe = async () => {
const name = prompt('what is the recipe name?')
const newRecipe = await API.graphql({ query: createRecipe, variables: { input: { name } }}))
console.log(newRecipe)
}
如果你刷新页面,你会看到你的食谱数组里已经有你创建的新食谱了!但是,我们如何才能让查询在每次创建新食谱时自动重新运行呢?订阅!
订阅
订阅允许您通过 GraphQL “订阅” 事件,因此每当数据更新时,您都可以运行代码。在我们的例子中,我们将设置为每当创建新的配方时,都会重新获取所有配方。
首先,导入订阅:
import { onCreateRecipe } from './graphql/subscriptions'
然后,我们将更新我们的useEffect
。保留前几行从 API 中提取配方。在其下方创建一个订阅。这看起来与我们之前发出的其他 API 请求类似,但在本例中,我们将在其上添加方法。我们将传递一个带有和 的.subscribe
对象。如果订阅出现错误, Error 将会运行。将在订阅触发后运行。在我们的例子中,我们希望重新运行!next
error
Next
pullData
最后,确保通过返回清理订阅的函数来取消订阅更新。
useEffect(() => {
const pullData = async () => {
const data = await API.graphql(graphqlOperation(listRecipes))
console.log(data)
}
pullData()
const subscription = API.graphql(
{ query: onCreateRecipe }
).subscribe({
next: (recipeData) => {
pullData()
},
error: (err) => {
console.log(err)
}
})
return () => subscription.unsubscribe()
}, [])
结论
在这篇文章中,我们了解了 GraphQL 及其优势,以及您可能不想使用它的原因!我们还创建了一个 API,并将其用于前端应用。如果您想删除 API,可以amplify delete
从 CLI 运行,您的代码将保留在本地,但不会再部署到云端!