接上篇 —— Apollo 入门引导(六):创建Apollo客户端 —— 继续翻译 Apollo 的官网入门引导。
使用 React 的 Hook:useQuery
Apollo 入门引导 - 目录:
- 介绍
- 构建 schema
- 连接数据源
- 编写查询解析器
- 编写变更解析器
- 连接 Apollo Studio
- 创建 Apollo 客户端
- 通过查询获取数据
- 通过变更修改数据
- 管理本地状态
完成时间:20 分钟
前一节已经设置了 Apollo 客户端,现在可以将其集成到我们的 React 应用中了。可以使用React Hooks将 GraphQL 查询的结果直接绑定到 UI。
与 React 集成
为了将 Apollo 客户端连接到 React,需要将应用程序封装在 @apollo/client
包中的 ApolloProvider
组件中。我们通过 client
属性将客户端实例传递给 ApolloProvider
组件。
打开 src/index.tsx
,更新以下内容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| import { ApolloClient, NormalizedCacheObject, ApolloProvider, } from '@apollo/client'; import { cache } from './cache'; import React from 'react'; import ReactDOM from 'react-dom'; import Pages from './pages'; import injectStyles from './styles';
const client: ApolloClient<NormalizedCacheObject> = new ApolloClient({ cache, uri: 'http://localhost:4000/graphql', });
injectStyles();
ReactDOM.render( <ApolloProvider client={client}> <Pages /> </ApolloProvider>, document.getElementById('root') );
|
ApolloProvider
组件类似于 React 的上下文提供器:它封装了 React 应用,并将 client
放置在上下文中,所以可以从组件树中的任何位置访问它。现在,准备构建执行 GraphQL 查询的 React 组件。
显示发射列表
接下来在应用中构建页面,该页面显示可获得的 SpaceX 发射的列表。打开 src / pages / launches.tsx
,该文件如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| import React, { Fragment, useState } from 'react'; import { RouteComponentProps } from '@reach/router'; import { gql } from '@apollo/client';
export const LAUNCH_TILE_DATA = gql` fragment LaunchTile on Launch { __typename id isBooked rocket { id name } mission { name missionPatch } } `;
interface LaunchesProps extends RouteComponentProps {}
const Launches: React.FC<LaunchesProps> = () => { return <div />; };
export default Launches;
|
定义查询
首先定义查询的格式,该查询将用于获取发射的分页列表。将以下内容粘贴到 LAUNCH_TILE_DATA
声明的下方:
1 2 3 4 5 6 7 8 9 10 11 12
| export const GET_LAUNCHES = gql` query GetLaunchList($after: String) { launches(after: $after) { cursor hasMore launches { ...LaunchTile } } } ${LAUNCH_TILE_DATA} `;
|
使用片段
注意,在定义查询时,在其上方插入了 LAUNCH_TILE_DATA
的定义。 LAUNCH_TILE_DATA
定义了一个 GraphQL 片段(fragment),名为 LaunchTile
。片段对于定义一组字段时很有帮助,无需重写就可以将这组字段包含在多个查询中。
在上面的查询中,通过 ...
加 LaunchTile
方式引入了片段,类似于 JavaScript spread 语法。
分页详细信息
注意,除了获取 launches
列表之外,查询还获取 hasMore
和 cursor
字段。这是因为 launches
查询返回 分页结果 :
hasMore
字段指示服务返回的列表后面是否还有其他的发射信息。
cursor
字段指示客户端在发射列表中的当前位置。可以再次执行查询,并提供最新的 cursor
作为 $after
变量的值,以获取列表中的 下一批 发射的集合。
使用 useQuery
hook
我们将使用 Apollo 客户端的 useQuery
React Hook在 Launches
组件中执行新查询。Hook 的结果对象提供的属性可以在查询执行时填充和呈现组件。
- 修改
@apollo/client
,导入 useQuery
,再导入一些预定义的组件以呈现页面:
1 2
| import { gql, useQuery } from '@apollo/client'; import { LaunchTile, Header, Button, Loading } from '../components';
|
如果使用的是 TypeScript,需要从服务的 schema 定义中导入必需的类型:
1
| import * as GetLaunchListTypes from './__generated__/GetLaunchList';
|
- 将伪声明
const Launches
替换为以下内容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| const Launches: React.FC<LaunchesProps> = () => { const { data, loading, error } = useQuery< GetLaunchListTypes.GetLaunchList, GetLaunchListTypes.GetLaunchListVariables >(GET_LAUNCHES);
if (loading) return <Loading />; if (error) return <p>ERROR</p>; if (!data) return <p>Not found</p>;
return ( <Fragment> <Header /> {data.launches && data.launches.launches && data.launches.launches.map((launch: any) => ( <LaunchTile key={launch.id} launch={launch} /> ))} </Fragment> ); };
|
该组件将 GET_LAUNCHES
查询传给 useQuery
,并从结果中获取 data
、 loading
和 error
属性。根据这些属性的状态来展现发射列表,加载状态和错误信息。
使用 npm start
启动服务和客户端,并访问 localhost:3000
。如果一切都配置正确,将会展现应用主页,并列出 20 次 SpaceX 发射!
但是有一个问题:总共的 SpaceX 发射数超过了 20 个。服务会分页显示其结果,并在一次响应中最多包含 20 次发射。
为了能够获取并存储 全部 启动,需要修改代码以使用查询中包含的 cursor
和 hasMore
字段。接下来学习如何做分页支持。
添加分页支持
本教程中没有展现 Apollo client 3 为基于偏移和Relay 风格的分页助手函数(pagination helper function)。
Apollo 客户端提供了一个 fetchMore
助手函数来协助分页查询。可以用不同值的变量(例如当前游标)来执行相同的查询。
从 useQuery
结果对象中解构的对象列表中添加 fetchMore
,并定义一个 isLoadingMore
状态变量:
1 2 3 4 5 6 7 8 9 10 11 12 13
| const Launches: React.FC<LaunchesProps> = () => { const { data, loading, error, fetchMore, } = useQuery< GetLaunchListTypes.GetLaunchList, GetLaunchListTypes.GetLaunchListVariables >(GET_LAUNCHES); const [isLoadingMore, setIsLoadingMore] = useState(false); };
|
现在将 fetchMore
连接到 Launches
组件中的按钮上,单击该按钮可获取其他发射。
将此代码直接粘贴在 Launches
组件的结束 </ Fragment>
标签上方:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| { data.launches && data.launches.hasMore && (isLoadingMore ? ( <Loading /> ) : ( <Button onClick={async () => { setIsLoadingMore(true); await fetchMore({ variables: { after: data.launches.cursor, }, }); setIsLoadingMore(false); }} > Load More </Button> )); } //</Fragment>
|
单击按钮时,它将调用 fetchMore
(将当前的 cursor
的值传给after
变量),直到查询返回结果前一直显示加载中的状态。
启动所有内容,然后再次访问 localhost:3000
。现在已有的 20 个发射下方会出现一个 Load More 按钮,点击它。查询返回后, _没有其他发射出现_。🤔
如果检查浏览器的网络活动,会发现该按钮确实向服务发送了后续查询,并且服务确实响应了新的发射列表。但是_,Apollo 客户端将这些列表分隔开,因为它们表示带有 _不同变量值 (本例为 after
的值)的查询结果。
我们需要 Apollo 客户端来将 fetchMore
查询中的发射与 之前 查询中的发射进行 合并 。接下来配置该行为。
合并缓存的结果
Apollo 客户端将查询结果存储在内存缓存中。缓存可以智能高效地处理大多数操作,但是并不能自动知道我们是否要合并两个不同的发射列表。为了解决这个问题,为 schema 中的分页字段定义一个 合并函数(merge
function)。
打开 src/cache.ts
,现在初始化的是默认 InMemoryCache
:
1 2 3
| import { InMemoryCache, Reference } from '@apollo/client';
export const cache: InMemoryCache = new InMemoryCache({});
|
服务中分页的 schema 字段是 launches
列表。修改 cache
的初始化过程,为 launches
字段添加一个 merge
函数,如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| export const cache: InMemoryCache = new InMemoryCache({ typePolicies: { Query: { fields: { launches: { keyArgs: false, merge(existing, incoming) { let launches: Reference[] = []; if (existing && existing.launches) { launches = launches.concat(existing.launches); } if (incoming && incoming.launches) { launches = launches.concat(incoming.launches); } return { ...incoming, launches, }; }, }, }, }, }, });
|
这个 merge
方法接受我们现有的缓存发射(existing
)和传入的发射(incoming
),并将它们组合成一个列表并返回。缓存存储了此组合列表,并将其返回给所有使用 launches
字段的查询。
此示例展示字段策略的用法,这是针对 schema 中各个字段的缓存配置选项。
如果现在尝试单击 Load More 按钮,则 UI 将成功将其他发射附加到列表中!
显示单次发射的详情
我们希望能够单击列表中的发射以查看其完整详情。打开 src/pages/launch.tsx
并替换为以下内容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| import { gql } from '@apollo/client'; import { LAUNCH_TILE_DATA } from './launches';
export const GET_LAUNCH_DETAILS = gql` query LaunchDetails($launchId: ID!) { launch(id: $launchId) { site rocket { type } ...LaunchTile } } ${LAUNCH_TILE_DATA} `;
|
该查询包含了所有详情。注意,代码复用了已经在 launches.tsx
中定义的 LAUNCH_TILE_DATA
片段。
再一次将查询传递给 useQuery
hook。这次还需要将对应发射的 launchId
作为变量传给查询。launchId
‘的值可用路由来传递。
现在,将 launch.tsx
的内容替换为以下内容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52
| import React, { Fragment } from 'react'; import { gql, useQuery } from '@apollo/client';
import { LAUNCH_TILE_DATA } from './launches'; import { Loading, Header, LaunchDetail } from '../components'; import { ActionButton } from '../containers'; import { RouteComponentProps } from '@reach/router'; import * as LaunchDetailsTypes from './__generated__/LaunchDetails';
export const GET_LAUNCH_DETAILS = gql` query LaunchDetails($launchId: ID!) { launch(id: $launchId) { site rocket { type } ...LaunchTile } } ${LAUNCH_TILE_DATA} `;
interface LaunchProps extends RouteComponentProps { launchId?: any; }
const Launch: React.FC<LaunchProps> = ({ launchId }) => { const { data, loading, error } = useQuery< LaunchDetailsTypes.LaunchDetails, LaunchDetailsTypes.LaunchDetailsVariables >(GET_LAUNCH_DETAILS, { variables: { launchId } });
if (loading) return <Loading />; if (error) return <p>ERROR: {error.message}</p>; if (!data) return <p>Not found</p>;
return ( <Fragment> <Header image={ data.launch && data.launch.mission && data.launch.mission.missionPatch } > {data && data.launch && data.launch.mission && data.launch.mission.name} </Header> <LaunchDetail {...data.launch} /> <ActionButton {...data.launch} /> </Fragment> ); };
export default Launch;
|
像以前一样,正在查询时呈现 loading
或 error
状态,在查询完成后呈现数据。
回到应用中,单击列表中的发射以查看详情页面。
显示个人资料页面
我们希望用户的个人资料页面显示其已预订的发射列表。打开 src/pages/profile.tsx
并将其内容替换为以下内容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
| import React, { Fragment } from 'react'; import { gql, useQuery } from '@apollo/client';
import { Loading, Header, LaunchTile } from '../components'; import { LAUNCH_TILE_DATA } from './launches'; import { RouteComponentProps } from '@reach/router'; import * as GetMyTripsTypes from './__generated__/GetMyTrips';
export const GET_MY_TRIPS = gql` query GetMyTrips { me { id email trips { ...LaunchTile } } } ${LAUNCH_TILE_DATA} `;
interface ProfileProps extends RouteComponentProps {}
const Profile: React.FC<ProfileProps> = () => { const { data, loading, error } = useQuery<GetMyTripsTypes.GetMyTrips>( GET_MY_TRIPS, { fetchPolicy: 'network-only' } ); if (loading) return <Loading />; if (error) return <p>ERROR: {error.message}</p>; if (data === undefined) return <p>ERROR</p>;
return ( <Fragment> <Header>My Trips</Header> {data.me && data.me.trips.length ? ( data.me.trips.map((launch: any) => ( <LaunchTile key={launch.id} launch={launch} /> )) ) : ( <p>You haven't booked any trips</p> )} </Fragment> ); };
export default Profile;
|
你应该从已经完成的页面中找到上述代码中的所有概念,只有一个例外:正在设置的 fetchPolicy
。
自定义 fetch 策略
如前所述,Apollo 客户端将查询结果存储在其缓存中。如果查询缓存中已存在的数据,则 Apollo 客户端会直接返回该数据,而无需通过网络获取。
_但是_,缓存的数据可能会过时。在大部分情况下,稍微过时的数据是可以接受的,但是用户的预订行程列表应当是时刻保持最新的。为了解决这个问题,专门为 GET_MY_TRIPS
查询指定了fetch 策略。
fetch 策略定义了 Apollo 客户端如何将缓存用于特定查询。默认策略是 cache-first
(缓存优先),这意味着 Apollo 客户端在发出网络请求之前会检查缓存,查看结果是否存在。如果存在结果,则不会发生网络请求。
通过将此查询的 fetch 策略设置为 network-only
(仅限网络来源),就能保证 Apollo 客户端 始终 是从服务中获取用户的最新预订行程列表。
有关所有支持的 fetch 策略的列表,请参阅支持的 fetch 策略。
如果访问应用中的个人资料页面,则会发现查询返回 null。这是因为还未实现登录功能。将在下一节中解决这个问题!
前端记事本,不定期更新,欢迎关注!