在 React 应用程序中使用 Apollo-Client 了解客户端 GraphQl
已发表: 2022-03-10根据 State of JavaScript 2019,38.7% 的开发者愿意使用 GraphQL,而 50.8% 的开发者愿意学习 GraphQL。
作为一种查询语言,GraphQL 简化了构建客户端应用程序的工作流程。 它消除了在客户端应用程序中管理 API 端点的复杂性,因为它公开了一个 HTTP 端点来获取所需的数据。 因此,它消除了数据的过度获取和不足,就像在 REST 的情况下一样。
但 GraphQL 只是一种查询语言。 为了轻松使用它,我们需要一个为我们完成繁重工作的平台。 Apollo 就是这样一个平台。
Apollo 平台是 GraphQL 的实现,它在云(服务器)和应用程序的 UI 之间传输数据。 当您使用 Apollo 客户端时,所有用于检索数据、跟踪、加载和更新 UI 的逻辑都由useQuery
钩子封装(就像 React 的情况一样)。 因此,数据获取是声明性的。 它还具有零配置缓存。 只需在您的应用程序中设置 Apollo 客户端,您就可以获得开箱即用的智能缓存,无需额外配置。
Apollo Client 还可以与其他框架互操作,例如 Angular、Vue.js 和 React。
注意:本教程将使那些过去在客户端使用过 RESTful 或其他形式的 API 并希望了解 GraphQL 是否值得一试的人受益。 这意味着您之前应该使用过 API; 只有这样,您才能理解 GraphQL 对您的好处。 虽然我们将介绍 GraphQL 和 Apollo Client 的一些基础知识,但对 JavaScript 和 React Hooks 的深入了解将会派上用场。
GraphQL 基础知识
本文不是对 GraphQL 的完整介绍,但我们将在继续之前定义一些约定。
什么是 GraphQL?
GraphQL 是一种规范,描述了一种声明式查询语言,您的客户可以使用该语言向 API 询问他们想要的确切数据。 这是通过为您的 API 创建一个强大的类型模式来实现的,具有最大的灵活性。 它还确保 API 解析数据,并根据模式验证客户端查询。 这个定义意味着 GraphQL 包含一些规范,使其成为一种声明式查询语言,具有静态类型的 API(围绕 Typescript 构建)并使客户端可以利用这些类型系统向 API 询问它想要的确切数据.
因此,如果我们创建了一些包含一些字段的类型,那么,从客户端,我们可以说,“给我们这些包含这些确切字段的数据”。 然后 API 将以该确切形状响应,就像我们在强类型语言中使用类型系统一样。 您可以在我的 Typescript 文章中了解更多信息。
让我们看看 GraphQl 的一些约定,这些约定将在我们继续进行时对我们有所帮助。
基础
- 运营
在 GraphQL 中,执行的每个操作都称为一个操作。 有几个操作,即:- 询问
此操作与从服务器获取数据有关。 您也可以将其称为只读提取。 - 突变
此操作涉及从服务器创建、更新和删除数据。 它通常被称为 CUD(创建、更新和删除)操作。 - 订阅
GraphQL 中的此操作涉及在发生特定事件时将数据从服务器发送到其客户端。 它们通常使用 WebSockets 实现。
- 询问
在本文中,我们将只处理查询和变异操作。
- 操作名称
您的客户端查询和变异操作有唯一的名称。 - 变量和参数
操作可以定义参数,非常类似于大多数编程语言中的函数。 然后可以将这些变量作为参数传递给操作内的查询或变异调用。 在客户端执行操作期间,预计将在运行时给出变量。 - 别名
这是客户端 GraphQL 中的一种约定,涉及用 UI 的简单易读的字段名称重命名冗长或模糊的字段名称。 在您不希望字段名称冲突的用例中,别名是必要的。
什么是客户端 GraphQL?
当前端工程师使用任何框架(如 Vue.js 或(在我们的例子中)React)构建 UI 组件时,这些组件会根据客户端上的特定模式进行建模和设计,以适应将从服务器获取的数据。
RESTful API 最常见的问题之一是过度获取和获取不足。 发生这种情况是因为客户端下载数据的唯一方法是点击返回固定数据结构的端点。 在这种情况下,过度获取意味着客户端下载的信息多于应用程序所需的信息。
另一方面,在 GraphQL 中,您只需向 GraphQL 服务器发送一个包含所需数据的查询。 然后,服务器将使用您请求的确切数据的 JSON 对象进行响应——因此,不会过度获取。 Sebastian Eschweiler 解释了 RESTful API 和 GraphQL 之间的区别。
客户端 GraphQL 是一个客户端基础设施,它与来自 GraphQL 服务器的数据交互以执行以下功能:
- 它通过发送查询和改变数据来管理数据,而无需您自己构建 HTTP 请求。 您可以花更少的时间来检测数据,而将更多的时间用于构建实际的应用程序。
- 它为您管理缓存的复杂性。 因此,您可以存储和检索从服务器获取的数据,而无需任何第三方干预,并且轻松避免重新获取重复资源。 因此,它可以识别两个资源何时相同,这对于复杂的应用程序来说非常有用。
- 它使您的 UI 与 Optimistic UI 保持一致,Optimistic UI 是一种模拟突变结果(即创建的数据)并在接收到服务器响应之前更新 UI 的约定。 一旦从服务器接收到响应,乐观的结果就会被丢弃并替换为实际结果。
有关客户端 GraphQL 的更多信息,请花一个小时与 GraphQL 的共同创建者和 GraphQL Radio 上的其他酷人交流。
什么是 Apollo 客户端?
Apollo Client 是一个可互操作的、超灵活的、社区驱动的 GraphQL 客户端,适用于 JavaScript 和原生平台。 其令人印象深刻的功能包括强大的状态管理工具(Apollo Link)、零配置缓存系统、获取数据的声明式方法、易于实现的分页以及客户端应用程序的 Optimistic UI。
Apollo 客户端不仅存储从服务器获取的数据的状态,还存储它在客户端本地创建的状态; 因此,它管理 API 数据和本地数据的状态。
同样重要的是要注意,您可以将 Apollo Client 与其他状态管理工具(如 Redux)一起使用,而不会发生冲突。 另外,可以将您的状态管理从 Redux 迁移到 Apollo Client(这超出了本文的范围)。 最终,Apollo Client 的主要目的是使工程师能够无缝地查询 API 中的数据。
阿波罗客户端的特点
Apollo Client 赢得了众多工程师和公司的青睐,因为其非常有用的功能使构建现代强大的应用程序变得轻而易举。 内置以下功能:
- 缓存
Apollo Client 支持动态缓存。 - 乐观的用户界面
Apollo Client 对 Optimistic UI 有很酷的支持。 它涉及在操作进行时临时显示操作的最终状态(突变)。 一旦操作完成,真实数据将取代乐观数据。 - 分页
Apollo Client 具有内置功能,可以很容易地在应用程序中实现分页。 它使用fetchMore
函数解决了大多数技术难题,无论是在补丁中还是一次获取数据列表,该函数带有useQuery
钩子。
在本文中,我们将介绍这些功能的选择。
理论够了。 系好安全带,拿杯咖啡来配煎饼,因为我们的手很脏。
构建我们的 Web 应用程序
这个项目的灵感来自 Scott Moss。
我们将构建一个简单的宠物店网络应用程序,其功能包括:
- 从服务器端获取我们的宠物;
- 创建宠物(包括创建宠物的名称、类型和图像);
- 使用 Optimistic UI;
- 使用分页来分割我们的数据。
首先,克隆存储库,确保starter
分支是您克隆的内容。
入门
- 为 Chrome 安装 Apollo 客户端开发工具扩展。
- 使用命令行界面 (CLI),导航到克隆存储库的目录,然后运行命令以获取所有依赖项:
npm install
。 - 运行命令
npm run app
以启动应用程序。 - 仍在根文件夹中时,运行命令
npm run server
。 这将为我们启动我们的后端服务器,我们将在继续进行时使用它。
该应用程序应在配置的端口中打开。 我的是https://localhost:1234/
; 你的可能是别的东西。
如果一切正常,您的应用程序应如下所示:
您会注意到我们没有可展示的宠物。 那是因为我们还没有创建这样的功能。
如果您已正确安装 Apollo 客户端开发工具,请打开开发工具并单击托盘图标。 你会看到“Apollo”和类似这样的东西:
与 Redux 和 React 开发工具一样,我们将使用 Apollo 客户端开发工具来编写和测试我们的查询和突变。 该扩展随 GraphQL Playground 一起提供。
取宠物
让我们添加获取宠物的功能。 转到client/src/client.js
。 我们将编写 Apollo 客户端,将其链接到 API,将其导出为默认客户端,并编写新查询。
复制以下代码并将其粘贴到client.js
中:
import { ApolloClient } from 'apollo-client' import { InMemoryCache } from 'apollo-cache-inmemory' import { HttpLink } from 'apollo-link-http' const link = new HttpLink({ uri: 'https://localhost:4000/' }) const cache = new InMemoryCache() const client = new ApolloClient({ link, cache }) export default client
这是对上面发生的事情的解释:
-
ApolloClient
这将是包装我们的应用程序的函数,因此,它与 HTTP 接口、缓存数据并更新 UI。 -
InMemoryCache
这是 Apollo 客户端中的规范化数据存储,有助于在我们的应用程序中操作缓存。 -
HttpLink
这是一个标准的网络接口,用于修改 GraphQL 请求的控制流和获取 GraphQL 结果。 它充当中间件,每次触发链接时从 GraphQL 服务器获取结果。 另外,它是其他选项的一个很好的替代品,比如Axios
和window.fetch
。 - 我们声明了一个分配给
HttpLink
实例的链接变量。 它需要一个uri
属性和一个值到我们的服务器,即https://localhost:4000/
。 - 接下来是一个缓存变量,它保存
InMemoryCache
的新实例。 - client 变量还接受
ApolloClient
的实例并包装link
和cache
。 - 最后,我们导出
client
,以便我们可以在整个应用程序中使用它。
在我们看到这一点之前,我们必须确保我们的整个应用程序都暴露给 Apollo,并且我们的应用程序可以接收从服务器获取的数据并且它可以改变这些数据。
为此,让我们转到client/src/index.js
:
import React from 'react' import ReactDOM from 'react-dom' import { BrowserRouter } from 'react-router-dom' import { ApolloProvider } from '@apollo/react-hooks' import App from './components/App' import client from './client' import './index.css' const Root = () => ( <BrowserRouter>
<ApolloProvider client={client}> <App /> </ApolloProvider>
</BrowserRouter> ); ReactDOM.render(<Root />, document.getElementById('app')) if (module.hot) { module.hot.accept() }
正如您将在突出显示的代码中注意到的那样,我们将App
组件包装在ApolloProvider
中,并将客户端作为道具传递给client
。 ApolloProvider
类似于 React 的Context.Provider
。 它包装您的 React 应用程序并将客户端放置在上下文中,这允许您从组件树中的任何位置访问它。
要从服务器获取我们的宠物,我们需要编写查询来请求我们想要的确切字段。 前往client/src/pages/Pets.js
,然后将以下代码复制并粘贴到其中:
import React, {useState} from 'react' import gql from 'graphql-tag' import { useQuery, useMutation } from '@apollo/react-hooks' import PetsList from '../components/PetsList' import NewPetModal from '../components/NewPetModal' import Loader from '../components/Loader'
const GET_PETS = gql` query getPets { pets { id name type img } } `;
export default function Pets () { const [modal, setModal] = useState(false)
const { loading, error, data } = useQuery(GET_PETS); if (loading) return <Loader />; if (error) return <p>An error occured!</p>;
const onSubmit = input => { setModal(false) } if (modal) { return <NewPetModal onSubmit={onSubmit} onCancel={() => setModal(false)} /> } return ( <div className="page pets-page"> <section> <div className="row betwee-xs middle-xs"> <div className="col-xs-10"> <h1>Pets</h1> </div> <div className="col-xs-2"> <button onClick={() => setModal(true)}>new pet</button> </div> </div> </section> <section>
<PetsList pets={data.pets}/>
</section> </div> ) }
使用一些代码,我们就可以从服务器获取宠物。
什么是gql?
需要注意的是,GraphQL 中的操作通常是用graphql-tag
和反引号编写的 JSON 对象。
gql
标签是 JavaScript 模板文字标签,可将 GraphQL 查询字符串解析为 GraphQL AST(抽象语法树)。
- 查询操作
为了从服务器获取我们的宠物,我们需要执行一个查询操作。- 因为我们正在进行
query
操作,所以我们需要在命名之前指定操作的type
。 - 我们的查询名称是
GET_PETS
。 使用 camelCase 作为字段名称是 GraphQL 的命名约定。 - 我们的字段名称是
pets
。 因此,我们从服务器中指定了我们需要的确切字段(id, name, type, img)
。 -
useQuery
是一个 React 钩子,它是在 Apollo 应用程序中执行查询的基础。 为了在我们的 React 组件中执行查询操作,我们调用了useQuery
钩子,它最初是从@apollo/react-hooks
导入的。 接下来,我们向它传递一个 GraphQL 查询字符串,在我们的例子中是GET_PETS
。
- 因为我们正在进行
- 当我们的组件渲染时,
useQuery
从 Apollo 客户端返回一个对象响应,其中包含加载、错误和数据属性。 因此,它们被解构,以便我们可以使用它们来呈现 UI。 -
useQuery
很棒。 我们不必包含async-await
。 它已经在后台处理好了。 很酷,不是吗?-
loading
这个属性帮助我们处理应用程序的加载状态。 在我们的例子中,我们在应用程序加载时返回一个Loader
组件。 默认情况下,加载是false
。 -
error
以防万一,我们使用此属性来处理可能发生的任何错误。 -
data
这包含来自服务器的实际数据。 - 最后,在我们的
PetsList
组件中,我们传递了pets
道具,其中data.pets
作为对象值。
-
至此,我们已经成功查询到了我们的服务器。
要启动我们的应用程序,让我们运行以下命令:
- 启动客户端应用程序。 在 CLI 中运行命令
npm run app
。 - 启动服务器。 在另一个 CLI 中运行命令
npm run server
。
如果一切顺利,您应该会看到:
变异数据
在 Apollo Client 中变异数据或创建数据与查询数据几乎相同,只有非常细微的变化。
仍然在client/src/pages/Pets.js
中,让我们复制并粘贴突出显示的代码:
.... const GET_PETS = gql` query getPets { pets { id name type img } } `;
const NEW_PETS = gql` mutation CreateAPet($newPet: NewPetInput!) { addPet(input: $newPet) { id name type img } } `;
const Pets = () => { const [modal, setModal] = useState(false) const { loading, error, data } = useQuery(GET_PETS);
const [createPet, newPet] = useMutation(NEW_PETS);
const onSubmit = input => { setModal(false)
createPet({ variables: { newPet: input } }); } if (loading || newPet.loading) return <Loader />; if (error || newPet.error) return <p>An error occured</p>;
if (modal) { return <NewPetModal onSubmit={onSubmit} onCancel={() => setModal(false)} /> } return ( <div className="page pets-page"> <section> <div className="row betwee-xs middle-xs"> <div className="col-xs-10"> <h1>Pets</h1> </div> <div className="col-xs-2"> <button onClick={() => setModal(true)}>new pet</button> </div> </div> </section> <section> <PetsList pets={data.pets}/> </section> </div> ) } export default Pets
要创建突变,我们将采取以下步骤。
1. mutation
要创建、更新或删除,我们需要执行mutation
操作。 mutation
操作有一个CreateAPet
名称,带有一个参数。 这个参数有一个$newPet
变量,类型为NewPetInput
。 !
表示需要操作; 因此,除非我们传递一个类型为NewPetInput
的newPet
变量,否则 GraphQL 不会执行该操作。
2. addPet
addPet
函数位于mutation
操作内部,接受一个input
参数并设置为我们的$newPet
变量。 addPet
函数中指定的字段集必须与查询中的字段集相同。 我们操作中的字段集是:
-
id
-
name
-
type
-
img
3. useMutation
useMutation
React 钩子是在 Apollo 应用程序中执行突变的主要 API。 当我们需要改变数据时,我们在 React 组件中调用useMutation
它传递一个 GraphQL 字符串(在我们的例子中是NEW_PETS
)。
当我们的组件渲染useMutation
时,它会在一个数组中返回一个元组(即构成记录的有序数据集),其中包括:
- 一个
mutate
函数,我们可以随时调用它来执行变异; - 具有表示突变执行当前状态的字段的对象。
useMutation
钩子传递了一个 GraphQL 突变字符串(在我们的例子中是NEW_PETS
)。 我们解构了元组,它是一个函数( createPet
),它将改变数据和对象字段( newPets
)。
4. createPet
在我们的onSubmit
函数中,在setModal
状态之后不久,我们定义了我们的createPet
。 该函数接受一个variable
,其对象属性的值设置为{ newPet: input }
。 input
表示我们表单中的各种输入字段(例如名称、类型等)。
完成后,结果应如下所示:
如果您仔细观察 GIF,您会注意到我们创建的宠物不会立即出现,只有在页面刷新时才会出现。 但是,它已在服务器上更新。
最大的问题是,为什么我们的宠物不立即更新? 让我们在下一节中找出答案。
在 Apollo 客户端中缓存
我们的应用没有自动更新的原因是我们新创建的数据与 Apollo Client 中的缓存数据不匹配。 因此,对于需要从缓存中更新的确切内容存在冲突。
简而言之,如果我们执行更新或删除多个条目(一个节点)的突变,那么我们负责更新引用该节点的任何查询,以便它修改我们的缓存数据以匹配突变对我们的后台所做的修改-结束数据。
保持缓存同步
每次执行突变操作时,有几种方法可以使我们的缓存保持同步。
第一种是在突变后重新获取匹配的查询,使用refetchQueries
对象属性(最简单的方法)。
注意:如果我们要使用这个方法,它将在我们的createPet
函数中获取一个名为refetchQueries
的对象属性,并且它将包含一个具有查询值的对象数组: refetchQueries: [{ query: GET_PETS }]
。
因为我们在本节中的重点不仅仅是在 UI 中更新我们创建的宠物,而是为了操作缓存,我们不会使用这种方法。
第二种方法是使用update
功能。 在 Apollo 客户端中,有一个update
助手函数可以帮助修改缓存数据,以便它与突变对我们的后端数据所做的修改同步。 使用这个函数,我们可以读写缓存。
更新缓存
复制以下突出显示的代码,并将其粘贴到client/src/pages/Pets.js
:
...... const Pets = () => { const [modal, setModal] = useState(false) const { loading, error, data } = useQuery(GET_PETS);
const [createPet, newPet] = useMutation(NEW_PETS, { update(cache, { data: { addPet } }) { const data = cache.readQuery({ query: GET_PETS }); cache.writeQuery({ query: GET_PETS, data: { pets: [addPet, ...data.pets] }, }); }, } );
.....
update
函数接收两个参数:
- 第一个参数是来自 Apollo 客户端的缓存。
- 第二个是来自服务器的确切突变响应。 我们解构
data
属性并将其设置为我们的突变(addPet
)。
接下来,要更新函数,我们需要检查需要更新的查询(在我们的例子中是GET_PETS
查询)并读取缓存。
其次,我们需要写入已读取的query
,以便它知道我们将要更新它。 我们通过传递一个包含query
对象属性的对象来做到这一点,该属性的值设置为我们的query
操作( GET_PETS
),以及一个data
属性,其值是一个pet
对象,它具有一个addPet
突变的数组和一个副本宠物的数据。
如果您仔细按照这些步骤操作,您应该会看到您的宠物在创建时自动更新。 让我们来看看变化:
乐观的用户界面
很多人都是装载机和旋转机的忠实粉丝。 使用装载机没有任何问题; 在某些完美的用例中,加载器是最佳选择。 我写过关于加载器与微调器及其最佳用例的文章。
Loaders 和 spinners 确实在 UI 和 UX 设计中扮演着重要的角色,但是 Optimistic UI 的到来已经抢了风头。
什么是乐观 UI?
Optimistic UI 是一种约定,它模拟突变(创建的数据)的结果并在接收到来自服务器的响应之前更新 UI。 一旦从服务器接收到响应,乐观的结果就会被丢弃并替换为实际结果。
最后,乐观的 UI 只不过是一种管理感知性能和避免加载状态的方法。
Apollo Client 有一种非常有趣的方式来集成 Optimistic UI。 它为我们提供了一个简单的钩子,允许我们在突变后写入本地缓存。 让我们看看它是如何工作的!
第1步
转到client/src/client.js
,只添加突出显示的代码。
import { ApolloClient } from 'apollo-client' import { InMemoryCache } from 'apollo-cache-inmemory' import { HttpLink } from 'apollo-link-http'
import { setContext } from 'apollo-link-context' import { ApolloLink } from 'apollo-link' const http = new HttpLink({ uri: "https://localhost:4000/" }); const delay = setContext( request => new Promise((success, fail) => { setTimeout(() => { success() }, 800) }) ) const link = ApolloLink.from([ delay, http ])
const cache = new InMemoryCache() const client = new ApolloClient({ link, cache }) export default client
第一步涉及以下内容:
- 我们从
apollo-link-context
导入setContext
。setContext
函数接受一个回调函数并返回一个setTimeout
设置为800ms
的 promise,以便在执行突变操作时创建延迟。 -
ApolloLink.from
方法确保代表来自HTTP
的链接(我们的 API)的网络活动被延迟。
第2步
下一步是使用 Optimistic UI 挂钩。 滑回client/src/pages/Pets.js
,只添加下面突出显示的代码。
..... const Pets = () => { const [modal, setModal] = useState(false) const { loading, error, data } = useQuery(GET_PETS); const [createPet, newPet] = useMutation(NEW_PETS, { update(cache, { data: { addPet } }) { const data = cache.readQuery({ query: GET_PETS }); cache.writeQuery({ query: GET_PETS, data: { pets: [addPet, ...data.pets] }, }); }, } ); const onSubmit = input => { setModal(false) createPet({ variables: { newPet: input },
optimisticResponse: { __typename: 'Mutation', addPet: { __typename: 'Pet', id: Math.floor(Math.random() * 10000 + ''), name: input.name, type: input.type, img: 'https://via.placeholder.com/200' } }
}); } .....
如果我们希望 UI 在创建宠物时立即更新,而不是等待服务器响应,则使用optimisticResponse
响应对象。
上面的代码片段包括以下内容:
-
__typename
由 Apollo 注入到查询中以获取查询实体的type
。 Apollo 客户端使用这些类型来构建id
属性(这是一个符号),用于apollo-cache
中的缓存目的。 因此,__typename
是查询响应的有效属性。 - 突变被设置为
optimisticResponse
的__typename
。 - 正如前面定义的那样,我们的变异名称是
addPet
,而__typename
是Pet
。 - 接下来是我们希望乐观响应更新的突变字段:
-
id
因为我们不知道来自服务器的 ID 是什么,所以我们使用Math.floor
制作了一个。 -
name
此值设置为input.name
。 -
type
类型的值为input.type
。 -
img
现在,因为我们的服务器为我们生成图像,所以我们使用占位符来模仿来自服务器的图像。
-
这确实是一段漫长的旅程。 如果你到了最后,不要犹豫,从椅子上休息一下,喝杯咖啡。
让我们看看我们的结果。 该项目的支持存储库位于 GitHub 上。 克隆并试验它。
结论
Apollo Client 的惊人功能,例如 Optimistic UI 和分页,使构建客户端应用程序成为现实。
虽然 Apollo Client 可以很好地与其他框架(例如 Vue.js 和 Angular)配合使用,但 React 开发人员拥有 Apollo Client Hooks,因此他们情不自禁地喜欢构建出色的应用程序。
在本文中,我们只触及了表面。 掌握 Apollo Client 需要不断练习。 因此,继续克隆存储库,添加分页,并使用它提供的其他功能。
请在下面的评论部分分享您的反馈和经验。 我们还可以在 Twitter 上讨论您的进展。 干杯!
参考
- “React 中的客户端 GraphQL”,Scott Moss,前端大师
- “文档”,Apollo 客户端
- “使用 React 的乐观 UI”,Patryk Andrzejewski
- “乐观用户界面的真实谎言”,Smashing Magazine