0%

Apollo入门引导(七):通过查询获取数据

接上篇 —— Apollo 入门引导(六):创建Apollo客户端 —— 继续翻译 Apollo 的官网入门引导

使用 React 的 Hook:useQuery

Apollo 入门引导 - 目录:

  1. 介绍
  2. 构建 schema
  3. 连接数据源
  4. 编写查询解析器
  5. 编写变更解析器
  6. 连接 Apollo Studio
  7. 创建 Apollo 客户端
  8. 通过查询获取数据
  9. 通过变更修改数据
  10. 管理本地状态

完成时间: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';

// 初始化Apollo 客户端
const client: ApolloClient<NormalizedCacheObject> = new ApolloClient({
cache,
uri: 'http://localhost:4000/graphql',
});

injectStyles();

// 传递ApolloClient的实例给ApolloProvider组件
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 列表之外,查询还获取 hasMorecursor 字段。这是因为 launches 查询返回 分页结果

  • hasMore 字段指示服务返回的列表后面是否还有其他的发射信息。
  • cursor 字段指示客户端在发射列表中的当前位置。可以再次执行查询,并提供最新的 cursor 作为 $after 变量的值,以获取列表中的 下一批 发射的集合。

使用 useQuery hook

我们将使用 Apollo 客户端的 useQuery React HookLaunches 组件中执行新查询。Hook 的结果对象提供的属性可以在查询执行时填充和呈现组件。

  1. 修改 @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';
  1. 将伪声明 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 ,并从结果中获取 dataloadingerror 属性。根据这些属性的状态来展现发射列表,加载状态和错误信息。

使用 npm start 启动服务和客户端,并访问 localhost:3000。如果一切都配置正确,将会展现应用主页,并列出 20 次 SpaceX 发射!

但是有一个问题:总共的 SpaceX 发射数超过了 20 个。服务会分页显示其结果,并在一次响应中最多包含 20 次发射。

为了能够获取并存储 全部 启动,需要修改代码以使用查询中包含的 cursorhasMore 字段。接下来学习如何做分页支持。

添加分页支持

本教程中没有展现 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, // highlight-line
} = useQuery<
GetLaunchListTypes.GetLaunchList,
GetLaunchListTypes.GetLaunchListVariables
>(GET_LAUNCHES);
const [isLoadingMore, setIsLoadingMore] = useState(false); //highlight-line
// ...
};

现在将 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'; // preserve-line
import { gql, useQuery } from '@apollo/client'; // preserve-line

import { LAUNCH_TILE_DATA } from './launches';
import { Loading, Header, LaunchDetail } from '../components'; // preserve-line
import { ActionButton } from '../containers'; // preserve-line
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;

像以前一样,正在查询时呈现 loadingerror 状态,在查询完成后呈现数据。

回到应用中,单击列表中的发射以查看详情页面。

显示个人资料页面

我们希望用户的个人资料页面显示其已预订的发射列表。打开 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'; // preserve-line
import { gql, useQuery } from '@apollo/client'; // preserve-line

import { Loading, Header, LaunchTile } from '../components'; // preserve-line
import { LAUNCH_TILE_DATA } from './launches'; // preserve-line
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' } // highlight-line
);
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。这是因为还未实现登录功能。将在下一节中解决这个问题!


前端记事本,不定期更新,欢迎关注!


👆 全文结束,棒槌时间到 👇