XieYang-blog

React + GraphQL + apollo-client技术栈简要介绍(基于官方文档v2.5)

本文档诸多例子来源于官方文档,但也在编写过程中添加了我对这套技术栈的一些理解,如果你更喜欢看官方文档,请移步官网/官方文档

为什么要使用apollo?
没有redux繁琐的action、reducer、dispatch……让全局管理store变得简单、直白!

使用redux管理状态,重心是放在如何去拿数据上;而apollo把重心放在需要什么数据上。理解这一点非常重要!

好了,废话不多说,我们立即开始!

准备工作

创建React项目

  1. 你可以使用create-react-app快速创建一个React应用,不熟悉create-react-app的小伙伴可以先行了解。
1
2
3
4
5
6
7
npm i create-react-app -g

create-react-app react-apollo-client-demo --typescript

cd react-apollo-client-demo

npm start
  1. 也可以在codesandbox上在线搭建React项目。方便快捷!

搭建GraphQL服务

  1. 你可以在github上fork graphpack项目,然后使用github账号登录codesandbox并导入该项目,即可零配置搭建一个在线的GraphQL服务。
    本文档在编写时在codesandbox上搭建了一个服务,可供参考:https://kdvmr.sse.codesandbox.io/
  2. 也可以在本地搭建自己的GraphQL服务,因不在本文档讨论范围,所以暂不提供搭建步骤。

安装需要的包

既然本文讲的是graphql + react + apollo开发React App,所以需要安装以下包来支撑,以前使用的redux、react-redux等包可以丢到一边了。

PS:在apollo 1.0时代,本地状态管理功能(本文档后面作了介绍)还依赖于redux等相关技术。但现在apollo已经升级到2.0时代,已经完全抛弃了redux的依赖。

1
npm install apollo-boost react-apollo graphql --save

我们来看一下这三个包的作用:

  • apollo-boost:包含设置Apollo Client所需的核心包。如果你需要按自己的意愿定制化项目,可以自行选择安装单独的包:
    apollo-client:apollo客户端包
    • apollo-cache-inmemory:官方推荐的缓存包
    • apollo-link-http:用于获取远程数据的包
    • apollo-link-error:用于处理错误的包
    • apollo-link-state:本地状态管理的包(2.5版本已集成到apollo-client
  • react-apollo:react的图层集成(用react的组件方式来使用apollo)
  • graphql:解析GraphQL查询

实例化Apollo客户端

需要注意一点的是apollo-boostapollo-client都提供了ApolloClient,但是两者需要的参数有一点差别。具体见各自API:

  • apollo-boost导出的Apollo Client对象(详细API):集成官方核心功能的一个大集合对象
  • apollo-client导出的Apollo Client对象(详细API):默认为App所在的同一主机上的GraphQL端点
    要自定义uri还需引入apollo-link-http包。如果你使用的是apollo v1.x,可直接从apollo-client包内导出createNetworkInterface方法,用法请见1.x迁移至2.x指南

我们看一下使用apollo-boost实例化Apollo客户端:

1
2
3
4
5
6
import ApolloClient from 'apollo-boost'

const client = new ApolloClient({
// 如果你实在找不到现成的服务端,可以使用apollo官网提供的:https://48p1r2roz4.sse.codesandbox.io或者本教程的服务:https://kdvmr.sse.codesandbox.io/
uri: '你的GraphQL服务链接'
})

以及使用apollo-client实例化Apollo客户端:

1
2
3
4
5
6
7
8
import ApolloClient from 'apollo-client'
import { createHttpLink } from 'apollo-link-http'

const client = new ApolloClient({
link: createHttpLink({
uri: '你的GraphQL服务链接'
})
})

编写GraphQL查询语句

如果对GraphQL语法不是很了解,请先移步graphQL基础实践

为了演示GraphQL查询,我们暂且使用普通的请求看一段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
import { gql } from 'apollo-boost'

// 实例化 Apollo 客户端

client.query({
query: gql`
{
rates(currency: "CNY") {
currency
}
}
`
}).then(result => console.log(result));

除了从apollo-boost导入gql,你还可以从graphql-tag这个包导入:

1
2
3
import gql from 'graphql-tag';

gql`...`

显而易见,gql()的作用是把查询字符串解析成查询文档。

连接Apollo客户端到React

jsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// ...
import React from 'react'
import { ApolloProvider } from 'react-apollo'

const App: React = () => {
// ...

return (
<ApolloProvider client={client}>
<div>App content</div>
</ApolloProvider>
)
}

export default App

ApolloProvider(详细API)有一个必需参数client

和redux一样(redux使用<Provider/>组件包裹React App),react-apollo需要<ApolloProvider />组件来包裹整个React App,以便将实例化的client放到上下文中,就可以在组件树的任何位置访问到它。
另外,还可以使用withApollo来包裹组件,以在组件内部获取到client实例(还有很多获取实例的方法,文档后面有介绍),
详情请参考withApollo()

Query组件与Mutation组件

在graphql中,query操作代表查询,mutation操作代表增、删和改,他们对应REST API的GET与POST请求,但要注意在实际的请求过程中Query或许并不是GET请求,这里只是为了方便大家理解做的假设!

获取数据——Query组件

jsx
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
import { Query } from "react-apollo";
import { gql } from "apollo-boost";

const ExchangeRates = () => (
<Query
query={gql`
{
rates(currency: "USD") {
currency
rate
}
}
`}
>
{
({ loading, error, data }) => {
if (loading) return <p>Loading...</p>;
if (error) return <p>Error :(</p>;

return data.rates.map(({ currency, rate }) => (
<div key={currency}>
<p>{currency}: {rate}</p>
</div>
));
}
}
</Query>
);

恭喜,你刚刚创建了第一个React Query组件!🎉🎉🎉

代码解析:

可以看到,在Query组件内,有一个匿名函数,这个匿名函数有一些参数,最常用的有:loadingerrordata
它们分别代表组件的加载状态、组件的加载错误提示、以及组件加载到的数据。

Query组件是从react-apollo导出的React组件,它使用render prop模式与UI共享GraphQL数据。(即我们可以从组件的props获取到GraphQL查询返回的数据)
Query组件还有很多其他props,上面就展示了一个query属性,其他的如:

  • children(根据查询结果显示要渲染的UI)
  • variables(用来传递查询参数到gql())
  • skip(跳过这个查询,比如登录时,验证失败,我们使用skip跳过这个查询,则登录失败)

更多props详见Query API

更新数据——Mutation组件

更新数据包括新增、修改和删除,这些操作统一使用Mutation组件。
Mutation组件和Query组件一样,使用render prop模式,但props有差别,Mutation API

jsx
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
import gql from 'graphql-tag';
import { Mutation } from "react-apollo";

const ADD_TODO = gql`
mutation AddTodo($type: String!) {
addTodo(type: $type) {
id
type
}
}
`;

const AddTodo = () => {
let input;

return (
<Mutation mutation={ADD_TODO}>
{
(addTodo, { data }) => (
<div>
<form
onSubmit={e => {
e.preventDefault();
addTodo({ variables: { type: input.value } });
input.value = "";
}}
>
<input
ref={node => {
input = node;
}}
/>
<button type="submit">Add Todo</button>
</form>
</div>
)
}
</Mutation>
);
};

我们来梳理一下代码:

  • 首先,创建用于mutation(突变)的GraphQL,mutation需要一个字符串类型的参数type。它将用于mutation的GraphQL语句包装在gql方法中,并将其传递给Mutation组件props
  • Mutation组件内需要一个匿名函数作为子函数(也称为render prop函数),同Query组件,但参数有差异。
  • render prop函数的第一个参数是Mutation组件内部定义的mutate()函数。为了提高代码可读性,这里取名为addTodo
    也可以直接用“mutate”表示mutate函数,通过调用它来告诉Apollo Client,接下来要触发mutation(即触发提交表单的POST请求,在onSubmit事件里面可以看见addTodo函数被调用了)。
  • render prop函数的第二个参数是一个对象,这个对象有多个属性,包括data(mutation的结果,POST请求的返回值)、loading(加载状态)和error(加载过程中的错误信息),同Query组件

mutate函数(也就是上面命名的addTodo函数)可选地接受变量,如:

  • optimisticResponse
  • refetchQueries和update(这些函数就是后面用来更新缓存的)
  • ignoreResults:忽略mutation操作返回的结果(即忽略POST请求的返回值)

你也可以将这些值作为props传递给Mutation组件。详细的介绍请移步mutate函数 API

到这里,我们能发出客户端请求,也能得到服务器返回的结果,那接下来就着手怎么处理这些数据,然后渲染到UI上。我们看一下redux在这一步是怎么处理的:

  • dispatch触发数据请求
  • reducer根据之前定义的action处理得到的新数据,把数据保存到store中
  • react-redux的connect连接store与React组件
  • mapStateToProps/mapDisToProps完成render prop。

以上步骤,全靠一行一行的代码手动实现,我们再来看一下apollo是怎么处理的:

  • cache.writeQuery()

没错,你没看错,就是这一个API,搞定以上redux需要一大堆代码才能完成的数据更新!writeQuery相当于通过一种方式来告诉Apollo Client:
我们已经成功发出POST请求并得到了返回的结果了,现在把结果给你,你更新一下本地的缓存吧!
并且如果你的数据写得很规范(呃,其实它叫范式化,不要急,后面有介绍),甚至连这一句话都不用写,当你执行query或mutation后,UI便会自动根据新的数据更新UI!!

更新缓存——mutation后内部自动query

有时,当执行mutation时,GraphQL服务器和Apollo缓存会变得不同步。当执行的更新取决于本地缓存中已有的数据时,会发生这种情况。例如,本地缓存了一张列表,
当删除列表中的一项或添加一项新的数据,当我们执行mutation后,graphql服务端和本地缓存不一致,我们需要一种方法来告诉Apollo Client去更新项目列表的查询,
以获取我们mutation后新的项目列表数据;又或者我们仅仅使用mutation提交一张表单,本地并没有缓存这张表单的数据,所以我们并不需要新的查询来更新本地缓存。

下面来看一段代码:

jsx
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
53
import gql from 'graphql-tag';
import { Mutation } from "react-apollo";

const ADD_TODO = gql`
mutation AddTodo($type: String!) {
addTodo(type: $type) {
id
type
}
}
`;

const GET_TODOS = gql`
query GetTodos {
todos
}
`;

const AddTodo = () => {
let input;

return (
<Mutation
mutation={ADD_TODO}
update={(cache, { data: { addTodo } }) => {
const { todos } = cache.readQuery({ query: GET_TODOS });
cache.writeQuery({
query: GET_TODOS,
data: { todos: todos.concat([addTodo]) },
});
}}
>
{addTodo => (
<div>
<form
onSubmit={e => {
e.preventDefault();
addTodo({ variables: { type: input.value } });
input.value = "";
}}
>
<input
ref={node => {
input = node;
}}
/>
<button type="submit">Add Todo</button>
</form>
</div>
)}
</Mutation>
);
};

通过这段代码可以看见,update()函数可以作为props传递给Mutation组件,但它也可以作为prop传递给mutate函数,即:

1
2
3
4
5
6
7
// 借用上面的mutate(重命名为addTodo)函数来举例
addTodo({
variables: { type: input.value },
update: (cache, data: { addTodo }) => {
// ...
}
})

update: (cache: DataProxy, mutationResult: FetchResult):用于在发生突变(mutation)后更新缓存

参数:

  • cache,这个参数详细讲又可以讲几节课,所以这里只简单介绍一下,详细API
    • cache通常是InMemoryCache的一个实例,在创建Apollo Client时提供给Apollo Client的构造函数(怎么创建的Apollo Client?请返回创建一个apollo客户端复习一下)
    • InMemoryCache来自于一个单独的包apollo-cache-inmemory。如果你使用apollo-boost,这个包已经被包含在里面了,无需重复安装。
    • cache有几个实用函数,例如cache.readQuerycache.writeQuery,它们允许您使用GraphQL读取和写入缓存。
    • 另外还有其他的方法,例如cache.readFragmentcache.writeFragmentcache.writeData,详细API)。
  • mutationResult,一个对象,对象里面的data属性保存着执行mutation后的结果(POST请求后得到的数据),详细API
    • 如果指定乐观响应,则会更新两次update函数:一次是乐观结果,另一次是实际结果。
    • 您可以使用您的变异结果来使用cache.writeQuery更新缓存。

对于update函数,当你在其内部调用cache.writeQuery时,更新操作会触发Apollo内部的广播查询(broadcastQueries),而广播查询又会触发缓存中与本次mutation相关的数据的自动更新——自动使用受影响组件的GraphQL进行查询并更新UI。
因此当执行mutation后,我们不必手动去执行相关组件的查询,Apollo Client在内部已经做好了所有工作,这区别于redux在dispatch后所做的一切处理数据的工作。

有时,update函数不需要为所有mutation更新缓存(比如提交了一张表单)。所以,Apollo提供单独的cache.writeQuery方法,来触发相关缓存的查询,以更新本地缓存。
所以需要注意:仅仅只在update函数内部调用cache.writeQuery()才会触发广播行为。在其他任何地方,cache.writeQuery只会写入缓存,并且所做的更改不会广播到视图层。
为了避免给代码造成混淆,推荐在未使用update函数时,使用Apollo Client实例对象clientclient.writeQuery方法将数据写入缓存。

解析代码:

由于我们需要更新显示TODOS列表的查询,因此首先使用cache.readQuery从缓存中读取数据。
然后,我们将mutation后得到的新todo与现有todo列表合并起来,并使用cache.writeQuery将查询到的数据写回缓存。
既然我们已经指定了一个update函数,那么一旦新的todo从服务器返回,我们的用户界面就会用它进行响应性更新(广播给其他与此缓存数据有关的组件的GraphQL查询,让他们及时更新更新相关缓存到UI上)。

Apollo还提供一种的方法来及时地修改本地缓存以快速渲染UI并触发相关缓存的查询,待查询返回新的数据后再真正更新本地缓存,详见乐观更新

基于乐观UI,如果您运行相同的查询两次,则不会看到加载指示符(Apollo Client返回的loading字段)。apollo会检测当前的请求参数是否变化,然后判断是否向服务器发送新的请求。

Apollo范式化缓存 API

jsx
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
53
54
55
56
import gql from 'graphql-tag';
import { Mutation, Query } from "react-apollo";

const UPDATE_TODO = gql`
mutation UpdateTodo($id: String!, $type: String!) {
updateTodo(id: $id, type: $type) {
id
type
}
}
`;

// 注意:这里通过graphql得到的todos数据是一个包含id和type字段的对象的数组,与 UPDATE_TODO 里面的字段(主要是id)对应
const GET_TODOS = gql`
query GetTodos {
todos
}
`;

const Todos = () => (
<Query query={GET_TODOS}>
{({ loading, error, data }) => {
if (loading) return <p>Loading...</p>;
if (error) return <p>Error :(</p>;

return data.todos.map(({ id, type }) => {
let input;

return (
<Mutation mutation={UPDATE_TODO} key={id}>
{updateTodo => (
<div>
<p>{type}</p>
<form
onSubmit={e => {
e.preventDefault();
updateTodo({ variables: { id, type: input.value } });

input.value = "";
}}
>
<input
ref={node => {
input = node;
}}
/>
<button type="submit">Update Todo</button>
</form>
</div>
)}
</Mutation>
);
});
}}
</Query>
);

注意:这一次在mutate函数(这里命名为updateTodo)里并没有调用update函数,在也没有传递update函数给Mutation组件,但是UI会立即更新。这就是范式化缓存的魅力了。

范式化缓存——InMemoryCache在将数据保存到存储之前对数据进行范式化,方法是将结果拆分为单个对象,为每个对象创建唯一标识符,并将这些对象存储在展平的数据结构中(创建的唯一标识符为这些对象的键,成为缓存键)。
默认情况下,InMemoryCache将尝试使用常见的id_id的主键作为唯一标识符(如果它们与对象上的__typename字段一起存在)。

如果未指定id_id,或者未指定__typename,则InMemoryCache将按照查询到对象的层级关系依次回退到根查询为止。

例如ROOT_QUERY.allPeople.0将作为数据中allPeople[0]对象的缓存键(cache key)被存储到缓存的根查询(ROOT_QUERY)下。(在展平的数据结构中,所有对象都在ROOT_QUERY下):

即使我们不打算在我们的UI中使用mutation返回的结果,我们仍然需要返回更新的ID和属性,以便我们的UI进行自动更新。

以上代码中,我们不需要指定update函数,因为TODOS查询将使用缓存中更新的TODO数据自动重建查询结果。

结合上一节介绍的update函数那样——并非每次mutation都需要使用update函数——其原因就是依据Apollo Cache的范式化数据结构,
在尽量减少手动操作数据的情况下自动更新UI,当前后端都规范化数据后(特别是唯一标识符id的统一, __typename字段的定义),
querymutation操作后,我们几乎不用手动处理数据,就能实现UI的自动更新。

例如:如果只需要更新缓存里面的单条数据,只需要返回这条数据的ID和要更新的属性即可,这种情况下通常不需要使用update函数。

如果想要自定义唯一标识符,即不用默认的ID来生成缓存键,可以使用InMemoryCache构造函数的dataIdFromObject函数:

1
2
3
const cache = new InMemoryCache({
dataIdFromObject: object => object.key || null
});

在指定自定义dataIdFromObject时,Apollo Client不会将类型名称添加到缓存键,因此,如果您的ID在所有对象中不唯一,则可能需要在dataIdFromObject中包含__typename

在谷歌浏览器中安装apollo devtools扩展(需要科学上网),可以清晰看到这种范式化缓存的存储状态。

中场休息

使用redux管理状态,重心是放在如何去拿数据上;而apollo把重心放在需要什么数据上。理解这一点非常重要!

还记得这句话吗?我们在本文档开篇的时候介绍过。现在理解了吗?现在,我们回过头来梳理一下自己学到的知识点:

  • 当学习了怎样去获取数据(query)以及更新数据和修改数据(mutation)后,原来Apollo和React结合,原来组件可以这么简单的与数据交互!
  • 当学习了Apollo缓存后,我们对Apollo数据存储的理解又上升了一个台阶,把所有查询回来的对象一一拆分,通过唯一标识符的形式把一个深层级的对象展平,直观展现在缓存的根查询中。
  • 当学习了Apollo的范式化缓存后,我们才知道,原来自动更新UI可以如此优雅!我们甚至不需要管理数据,只需按照规范传递数据即可!

本地状态管理 详情

上半场我们接触了本地与服务端的远程数据交互,接下来,我们将进入本地的状态管理

Apollo Client在2.5版本具有内置的本地状态处理功能,允许将本地数据与远程数据一起存储在Apollo缓存中。要访问本地数据,只需使用GraphQL查询即可。

而在2.5版本之前,如果想要使用本地状态管理,必须引入已经废弃的一个包apollo-link-state(API),
这个包在2.5版本已被废弃,因为从2.5版本开始,这个包的功能已经集成到apollo的核心之中,不再额外维护一个单独的包。而在apollo的1.x版本,如果要实现本地状态管理,依然得引入redux。

Apollo Client有两种主要方法可以执行局部状态突变:

  • 第一种方法是通过调用cache.writeData直接写入缓存。
    更新缓存那一节,我们已经详细介绍过cache.writeData的用法,以及其余update函数的搭配使用。
  • 第二种方法是创建一个带有GraphQL突变(mutation)的Mutation组件,该组件调用本地客户端解析器(resolvers)。
    如果mutation依赖于缓存中的现有值,我们建议使用解析器(resolvers,后面两节将介绍,目前只需知道它的存在,它和apollo-server端的resolver完全相同)。

直接写入缓存

jsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import React from 'react';
import { ApolloConsumer } from 'react-apollo';

import Link from './Link';

const FilterLink = ({ filter, children }) => (
<ApolloConsumer>
{client => (
<Link
onClick={() => client.writeData({ data: { visibilityFilter: filter } })}
>
{children}
</Link>
)}
</ApolloConsumer>
);

Apollo在ApolloConsumer组件(API)或Query组件的render prop中注入了Apollo Client实例,
所以当使用这些组件时,我们可以直接从组件的props中拿到client实例

直接写入缓存不需要GraphQLmutate函数或resolvers函数。因此我们在上面的代码中没有使用它们,我们直接在onClick事件函数里面调用client.writeData来写入缓存。

但是只建议将直接写入缓存用于简单写入,例如写入字符串或一次性写入。
重要的是要注意直接写入并不是作为GraphQL突变实现的,因此不应将它们包含在复杂的开发模式之中。
它也不会验证你写入缓存的数据是否为有效GraphQL数据的结构。
如果以上提到的任何一点对您很重要,则应选择使用本地resolvers

@client 指令

上一节提到过,Query组件的render prop同样包含client实例。所以配合@client指令,我们可以在Query组件中轻松地从cacheresolvers获取本地状态。
或许换个方式介绍大家能理解得更透彻:配合@client指令,我们可以在Query组件中轻松地从cache获取本地状态,或者通过resolverscache获取本地状态。

jsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import React from 'react';
import { Query } from 'react-apollo';
import gql from 'graphql-tag';

import Link from './Link';

const GET_VISIBILITY_FILTER = gql`
{
visibilityFilter @client
}
`;

const FilterLink = ({ filter, children }) => (
<Query query={GET_VISIBILITY_FILTER}>
{({ data, client }) => (
<Link
onClick={() => client.writeData({ data: { visibilityFilter: filter } })}
active={data.visibilityFilter === filter}
>
{children}
</Link>
)}
</Query>
);

代码解读:

@client指令告诉Apollo Client在本地获取数据(cache或resolvers),而不是将其发送到graphql服务器。
在调用client.writeData后,render prop函数上的查询结果将自动更新。同时所有缓存的写入和读取都是同步的,所以不必担心加载状态(loading)。

本地解析器——resolvers

终于见到了你——resolvers!前面几节都一笔带过了resolvers(解析器),现在,我们来了解它到底有什么强大的功能。

如果要依赖本地状态实现GraphQL的突变,我们只需要在本地resolvers对象中指定一个与之对应的函数即可。
在Apollo Client的实例化中,resolvers映射为一个对象,这个对象中保存着每一个用于本地突变的resolver函数
当在GraphQL的字段上找到@client指令时,Apollo Client会在resolvers对象中寻找与之对应的resolver函数,这个对应关系是通过resolvers的键来关联的。

即:当执行没有加@client指令的查询或突变时,GraphQL文档中的字段已预定义在了服务端,所以我们只需在查询或突变时按照服务端定义的字段编写GraphQL文档即可;
当加上@client指令后,Apollo Client不会向服务端发送请求,转而在自己内部寻找GraphQL文档内指定的字段。但是,我们怎么去访问本地的这些字段呢?或许他们根本就不存在。
(关于@client的运作方式,请参考官方文档中关于——本地数据查询流程的部分,由于篇幅原因,本文档不再详细介绍。)
这时,我们就需要自己定义可以访问这些字段的方式——resolver,在解析器对象(resolvers)中定义一个解析函数(resolver),以供GraphQL查询或突变在使用了@client指令时调用,
这样就建立了GraphQL查询或突变与Apollo Client之间的联系,通过这个函数可以解析有@client指令控制的查询或突变,因此这个函数被命名为解析函数,意指从本地解析函数中寻找GraphQL文档中指定的字段的值。

那解析函数定义在哪里呢?

其实它在ApolloClient的构造函数中,也就是说我们实例化Apollo Client时,需要传递resolvers给它。

解析器,它和client-server的resolver函数完全相同:

1
2
>   fieldName: (obj, args, context, info) => result;
>

obj {object}: 包含父字段上resolver函数返回的结果的对象,或者为DOM树最顶层的查询或突变的ROOT_QUERY对象
args {object}: 包含传递到GraphQL文档中的所有参数的对象。例如,如果使用updateNetworkStatus(isConnected:true)触发查询或突变,则args{isConnected:true}
context {object}: React组件与Apollo Client网络堆栈之间共享的上下文信息的对象。除了可能存在的任何自定义context属性外,本地resolvers始终会收到以下内容:

  • context.client: Apollo Client的实例
  • context.cache: Apollo Cache的实例
    context.cache.readQuery, .writeQuery, .readFragment, .writeFragment, and .writeData: 一系列用于操作cache的API
  • context.getCacheKey: 使用__typenameid从cache中获取key
    info {object}: 有关查询执行状态的信息。实际中,你可能永远也不会使用到这个参数。
jsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import { ApolloClient } from 'apollo-client';
import { InMemoryCache } from 'apollo-cache-inmemory';

const client = new ApolloClient({
cache: new InMemoryCache(),
resolvers: {
Mutation: {
toggleTodo: (_root, variables, { cache, getCacheKey }) => {
const id = getCacheKey({ __typename: 'TodoItem', id: variables.id })
const fragment = gql`
fragment completeTodo on TodoItem {
completed
}
`;
const todo = cache.readFragment({ fragment, id });
const data = { ...todo, completed: !todo.completed };
cache.writeData({ id, data });
return null;
},
},
},
});

代码解析:

为了切换todo的状态,首先需要查询缓存以找出todo当前状态的内容,然后通过使用cache.readFragment从缓存中读取片段来实现此目的。
此函数采用fragment和id,它对应于item的缓存键(cache key)。我们通过调用context中的getCacheKey并传入项目的__typenameid来获取缓存键。

一旦读取了fragment,就可以切换todo的已完成状态并将更新的数据写回缓存。由于我们不打算在UI中使用mutation的返回结果,因此我们返回null,因为默认情况下所有GraphQL类型都可以为空。

下面,我们来看一下怎么调用这个toggleTodo解析函数(触发toggleTodo突变):

jsx
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
import React from 'react';
import { Mutation } from 'react-apollo';
import gql from 'graphql-tag';

const TOGGLE_TODO = gql`
mutation ToggleTodo($id: Int!) {
toggleTodo(id: $id) @client
}
`;

const Todo = ({ id, completed, text }) => (
<Mutation mutation={TOGGLE_TODO} variables={{ id }}>
// 特别注意,此toggleTodo非解析器里面的toggleTodo,这个toggleTodo是我们之前介绍过的mutate函数,这里被更名为‘toggleTodo’而已,不要混淆了
{toggleTodo => (
<li
onClick={toggleTodo}
style={{
textDecoration: completed ? 'line-through' : 'none',
}}
>
{text}
</li>
)}
</Mutation>
);

代码解析:

首先,我们创建一个GraphQL突变文档,它将我们想要切换的item的id作为唯一的参数。我们通过使用@client指令标记GraphQLtoggleTodo字段来指示这是一个本地突变。
这将告诉Apollo Client调用我们本地突变解析器(resolvers)里面的toggleTodo解析函数来解析该字段。然后,我们创建一个Mutation组件,就像我们操作远程突变一样。
最后,将GraphQL突变传递给组件,并在render prop函数的UI中触发它。

查询本地状态

查询本地数据与查询GraphQL服务器非常相似。唯一的区别是本地查询在字段上添加了@client指令,以指示它们应该从Apollo Client cache或resolvers中解析。

我们来看一个例子:

jsx
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
import React from 'react';
import { Query } from 'react-apollo';
import gql from 'graphql-tag';

import Todo from './Todo';

const GET_TODOS = gql`
{
todos @client {
id
completed
text
}
visibilityFilter @client
}
`;

const TodoList = () => (
<Query query={GET_TODOS}>
{
({ data: { todos, visibilityFilter } }) => (
<ul>
{
getVisibleTodos(todos, visibilityFilter).map(todo => (
<Todo key={todo.id} {...todo} />
))
}
</ul>
)
}
</Query>
);

代码解析:

创建GraphQL查询并将@client指令添加到GraphQL文档的todos和visibilityFilter字段。
然后,我们将查询传递给Query组件@client指令让Query组件知道应该从Apollo Client缓存中提取todos和visibilityFilter,或者使用预定义的本地resolver解析。

由于上面的查询在安装组件后立即运行,如果cache中没有item或者没有定义任何本地resolver,我们该怎么办?

我们需要在运行查询之前将初始状态写入缓存,以防止错误输出。

初始化本地状态

通常,我们需要将初始状态写入缓存,以便在触发mutation之前查询数据的所有组件都不会出错。
要实现此目的,可以使用cache.writeData为初始值准备缓存。

jsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import { ApolloClient } from 'apollo-client';
import { InMemoryCache } from 'apollo-cache-inmemory';

const cache = new InMemoryCache();
const client = new ApolloClient({
cache,
resolvers: { /* ... */ },
});

cache.writeData({
data: {
todos: [],
visibilityFilter: 'SHOW_ALL',
networkStatus: {
__typename: 'NetworkStatus',
isConnected: false,
},
},
});

注意:Apollo v2.4和v2.5写入初始本地状态的方式不一样,详情参考官方API

重置本地状态/缓存

jsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import { ApolloClient } from 'apollo-client';
import { InMemoryCache } from 'apollo-cache-inmemory';

const cache = new InMemoryCache();
const client = new ApolloClient({
cache,
resolvers: { /* ... */ },
});

const data = {
todos: [],
visibilityFilter: 'SHOW_ALL',
networkStatus: {
__typename: 'NetworkStatus',
isConnected: false,
},
};

cache.writeData({ data });

client.onResetStore(() => cache.writeData({ data }));

使用client.onResetStore方法可以重置缓存。

同时请求本地状态和远程数据

1
2
3
4
5
6
7
mutation ToggleTodo($id: Int!) {
toggleTodo(id: $id) @client
getData(id: $id) {
id,
name
}
}

只需在需要从本地查询的字段后面加上@client指令即可。

使用@client字段作为变量

在同一个graphql语句中,还可以将从本地查到的状态用于下一个查询,通过@export指令

jsx
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 } from 'apollo-client';
import { InMemoryCache } from 'apollo-cache-inmemory';
import { HttpLink } from 'apollo-link-http';
import gql from 'graphql-tag';

const query = gql`
query currentAuthorPostCount($authorId: Int!) {
currentAuthorId @client @export(as: "authorId")
postCount(authorId: $authorId)
}
`;

const cache = new InMemoryCache();
const client = new ApolloClient({
link: new HttpLink({ uri: 'http://localhost:4000/graphql' }),
cache,
resolvers: {},
});

cache.writeData({
data: {
currentAuthorId: 12345,
},
});

// ... run the query using client.query, the <Query /> component, etc.

在上面的示例中,currentAuthorId首先从缓存加载,然后作为authorId变量(由@export(as:“authorId”)指令指定)传递到后续postCount字段中。
@export指令也可用于选择集中的特定字段,如:

@export指令还可以用于选择集中的特定字段

jsx
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
import { ApolloClient } from 'apollo-client';
import { InMemoryCache } from 'apollo-cache-inmemory';
import { HttpLink } from 'apollo-link-http';
import gql from 'graphql-tag';

const query = gql`
query currentAuthorPostCount($authorId: Int!) {
currentAuthor @client {
name
authorId @export(as: "authorId")
}
postCount(authorId: $authorId)
}
`;

const cache = new InMemoryCache();
const client = new ApolloClient({
link: new HttpLink({ uri: 'http://localhost:4000/graphql' }),
cache,
resolvers: {},
});

cache.writeData({
data: {
currentAuthor: {
__typename: 'Author',
name: 'John Smith',
authorId: 12345,
},
},
});

// ... run the query using client.query, the <Query /> component, etc.

@export指令使用不仅限于远程查询;它还可以用于为其他@client字段或选择集定义变量:(注意以下代码中GraphQL文档的currentAuthorIdpostCount字段之后都有@client指令)

jsx
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
import { ApolloClient } from 'apollo-client';
import { InMemoryCache } from 'apollo-cache-inmemory';
import gql from 'graphql-tag';

const query = gql`
query currentAuthorPostCount($authorId: Int!) {
currentAuthorId @client @export(as: "authorId")
postCount(authorId: $authorId) @client
}
`;

const cache = new InMemoryCache();
const client = new ApolloClient({
cache,
resolvers: {
Query: {
postCount(_, { authorId }) {
return authorId === 12345 ? 100 : 0;
},
},
},
});

cache.writeData({
data: {
currentAuthorId: 12345,
},
});

// ... run the query using client.query, the <Query /> component, etc.

动态注入resolver

有时,当我们在APP中使用了代码拆分,如使用react-loadable时,我们并不是很希望所有的resolver都在初始化Apollo客户端的统一写在一起,而是希望单独拆分到各自的模块中,这样在APP编译后,
每个模块各自resolver将包含在自己的包中,这样也有助于减少入口文件的大小,使用addResolverssetResolvers即可办到(API),
例如以下代码:

jsx
1
2
3
4
5
6
7
8
import Loadable from 'react-loadable';

import Loading from './components/Loading';

export const Stats = Loadable({
loader: () => import('./components/stats/Stats'),
loading: Loading,
});
jsx
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
import React from 'react';
import { ApolloConsumer, Query } from 'react-apollo';
import gql from 'graphql-tag';

const GET_MESSAGE_COUNT = gql`
{
messageCount @client {
total
}
}
`;

const resolvers = {
Query: {
messageCount: (_, args, { cache }) => {
// ... calculate and return the number of messages in
// the cache ...
return {
total: 123,
__typename: 'MessageCount',
};
},
},
};

const MessageCount = () => {
return (
<ApolloConsumer>
{(client) => {
client.addResolvers(resolvers);
return (
<Query query={GET_MESSAGE_COUNT}>
{({ loading, data: { messageCount } }) => {
if (loading) return 'Loading ...';
return (
<p>
Total number of messages: {messageCount.total}
</p>
);
}}
</Query>
);
}}
</ApolloConsumer>
);
};

export default MessageCount;

脱离React标签的写法

由于编程习惯的不同,有些人(比如我),并不是很喜欢(或者说成习惯)把逻辑代码React标签混合写在一起,就如我们从本文档开始一路看来所有的示例代码那样!
个人觉得在一个大型的项目中把Query标签Mutation标签以及其他的各种标签层层嵌套,再加上各种逻辑实现代码,全部挤在React组件中,真的是一件糟糕的事情。
虽然官方推崇这种写法(以上绝大部分代码是官方文档的示例),他们给出的理由是这样写更方便,更简单!

因人而异吧!

我个人更倾向于把GraphQL和Apollo的逻辑部分React组件分离开,我们可以使用react-apollo库提供的graphqlcompose方法做到分离。
当然,在react-apollo这个包中并不止这两个方法,还有其他的方法,请参考React Apollo API

下面展示一段分离开的代码示例:

jsx
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
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
import { Avatar, Card, Icon } from 'antd'
import gql from 'graphql-tag'
import * as React from 'react'
import { useEffect, useRef } from 'react'
import { compose, graphql } from 'react-apollo'
import { QueryState, Typename } from 'src/config/clientState'
import { GET_COMPONENTS_LIST, GET_QUERY_STATE } from 'src/graphql'
import { getCorrectQueryState } from 'src/util'
import Pagination from '../pagination'
import './index.scss'

const { Meta } = Card

type Type = {
id: number,
name: string
}

type Author = {
username: string,
email: string,
avatar: string
}

type Component = {
id: number,
name: string,
version: string,
chineseName: string,
description: string,
type: Type,
url: string,
author: Author,
previewUrl: string,
isOwn: boolean,
isStored: boolean
}

type ListComponent = {
data: {
components: Component[],
compCount: number,
},
componentsCollect: ([id, isCollect]: [number, boolean]) => void,
queryState: QueryState
}

const ListComponent = (props: ListComponent) => {
const cardList: React.MutableRefObject<HTMLDivElement | null> = useRef(null)
const { data, componentsCollect, queryState } = props

// 加载时组件卡片的动画效果,配合React的ref和key属性使用
useEffect(() => {
if(cardList.current) {
cardList.current.childNodes.forEach((element: HTMLElement, index: number) => {
setInterval(() => {
element.classList.add('card-load-anim')
}, index * 40)
})
}
})

return (
<div className='m-list'>
<div className='m-cards' ref={cardList} key={Math.random()}>
{
!data || !data.components
? null
: data.components.map((o: Components, i: number) => {
return (
<Card
key={`m-list-btn-${i}`}
hoverable={true}
cover={<img alt='example' src='https://gw.alipayobjects.com/zos/rmsportal/JiqGstEfoWAOHiTxclqi.png' />}
actions={
[
<a
className={`${o.isOwn ? 'm-list-btn-text-disabled' : 'm-list-btn-text'}`}
key={`m-list-btn-${i}-1`}
href='javascript:void(0)'
onClick={!o.isOwn ? componentsCollect.bind(null, [o.id, !o.isStored]) : null}
>
<Icon type='copy' />
{o.isStored ? '取消收藏' : '收藏'}
</a>,
<a
className='m-list-btn-text'
key='m-list-btn-2'
href='javascript:void(0)'
>
<Icon type='file-search' />
文档
</a>
]
}
>
<Meta
avatar={<Avatar src='https://zos.alipayobjects.com/rmsportal/ODTLcjxAfvqbxHnVXCYX.png' />}
title={o.name}
description={o.description}
/>
</Card>
)
})
}
</div>
<Pagination totalPages={Math.ceil(data.compCount / queryState.pagination.size)} total={data.compCount} />
</div>
)
}

export default compose(
graphql(
gql(GET_QUERY_STATE),
{
props: ({ data: { queryState } }: any) => ({ queryState })
}
),
graphql(
gql(GET_COMPONENTS_LIST),
{
options: ({ queryState }: any) => ({
variables: {
...getCorrectQueryState(queryState)
}
})
}
),
graphql(gql`
mutation ($id: Int!, $isCollect: Boolean!){
storeComponent(id: $id, isStore: $isCollect)
}
`, {
props: ({ mutate }: any) => ({
componentsCollect: ([id, isCollect]: [number, boolean]) => {
debugger
mutate({
variables: { id, isCollect },
optimisticResponse: {
__typename: Typename.Mutation,
storeComponent: {
__typename: Typename.Component,
id,
isStored: isCollect
}
}
})
}
})
})
)(ListComponent)

写在最后:

如果认真学习完这扁文档,相信你对Apollo技术栈开发React应用已经算是入门了,今后开发时遇到问题,多看一看官方文档,相信你会很快掌握它。

由于水平有限,这篇文章是我自己一边翻译一边加入自己的理解而写成的,其中肯定少不了一些不妥或错误的地方,欢迎大家指正!

🐶 您的支持将鼓励我继续创作 🐶