21 分钟学 apollo-client 是一个系列,简单暴力,包学包会。
搭建 Apollo client 端,集成 redux
使用 apollo-client 来获取数据
修改本地的 apollo store 数据
提供定制方案
使用 Apollo 获取数据
推荐先看:GraphQL 入门: 连接到数据
本文只做补充。
下面编写一个最简单的 Container,观察是否能 query 到数据。
container.jsx
import React, { PureComponent } from 'react';
import { graphql } from 'react-apollo';
import query from './query.gql';
@graphql(query)
export default class ApolloContainer extends PureComponent {
render() {
console.log(this.props);
return <div>Hello Apollo</div>;
}
}
@graphql(query)
是 apollo 提供的高阶组件,以装饰器的形式包裹你的组件。这里是最简单的情况,只传一个 query。
query 语法
基本的 query 语法可以参看官方文档 Queries and Mutations | GraphQL,这里提一下 Apollo 特有的一些语法。
query.gql
#import "../gql/pageInfo.gql"
#import "@/gql/topic/userTopicEntity.gql"
query topic($topicId: Int!, $pageNum: Int = 1) {
community {
topicEntity {
listByTopicId(topicId: $topicId, pageSize: 10, pageNum: $pageNum) {
pageInfo {
...pageInfo
}
edges {
...userTopicEntity
}
}
}
}
}
前两行 import 了其它的 fragment。想必你已经知道,GraphQL 主要通过 fragment 来组合分形 Query。一个好的实践是尽量对业务实体编写 fragment 以便复用。
代码脱敏的关系我就不放详细的 fragment 了。
上一节我们在 webpack 中配置了 graphql-tag/loader,这个 loader 允许你将 query 、fragment 这些 schema 字符串,以 .gql
文件的形式保存,在 import 时转化成 js 代码。
其余部分,基本上和 GraphQL 原生写法是一样的,注意几个点:
- 一次请求只能包含一个 query,而且不能包含未使用的 fragment。
-
#import
语法是 loader 提供的,语法和 js 的 import 差不多,除了不能解构 。
如果你 webpack 配置了 alias 就能使用第二行那种写法。注意,它会把该文件内所有的内容都 import 进来,所以不能在一个gql
文件里写多个query
或fragment
。
对了,为了最小化实践,你可以先写不带参数的 query。也先不要写 union type。
props.data
的数据结构
这样就好了吗,是的。一旦组件挂载后,会自动进行数据请求,前提是客户端提供的 query schema 和后端的相符。
如果请求成功后,会发生什么事情呢?我们可以查看 this.props
打出的 log 来验证:
// this.props
{
// ....
data: {
// ...
community: { ... }, // 这是获取到的数据,结构和你提供的 query schema 一致
loading: false, // 请求过程中为 true
networkStatus: 7, // 从 0-8,具体值的含义看这个文件 https://github.com/apollographql/apollo-client/blob/master/src/queries/networkStatus.ts
variables: { ... }, // 请求时所用的参数
fetchMore, // 一个函数,用于在组件内「继续请求」,一般用于分页请求
refetch, // 函数,用于组件内「强制重新请求」
updateQuery, // 请求成功后立即调用,用于更新本地 store
}
}
高级请求
我们仅改写装饰器部分
@graphql(query, {
skip: props => !isValid(props),
options: props => ({
variables: {
topicId: getIdFromUrl(),
},
}),
})
其中
-
skip
和shouldComponentUpdate
的效果是一样的,决定是否 re-fetch。如果回调返回 false 直接不作请求。 -
options
返回一个函数,用以设置请求的细节,比如variables
用于设置 query 参数
更详细的文档可以查阅
分页请求
如文档 Pagination | Apollo React Docs 所说,Apollo 支持两种分页
offset-based
按条数偏移量来请求分页,请求时提供两个参数
- limit:相当于 pageSize,一页最多取多少个
- offset: 条数偏移量,第 n 页的 offset = limit * n
可见你需要自己维护一个 pageNum: n 来实现按页码分页
cursor-based
这是 Relay 风格的请求,cursor 用于记录下个请求开始时,返回的第一个元素的位置,一般可以用该元素的 id 来标识。
RESTful 风格
我们后端并没有采取上面任何一种,而是提供了一个 pageInfo 对象,由前端传入所需参数,保持和 RESTful api 相似的风格。
query.gql
#import "../gql/pageInfo.gql"
#import "@/gql/topic/userTopic.gql"
query topic($topicId: Int!, $pageNum: Int = 1) {
community {
topicEntity {
listByTopicId(topicId: $topicId, pageSize: 10, pageNum: $pageNum) {
pageInfo {
...pageInfo
}
edges {
...userTopicEntity
}
}
}
}
}
pageInfo.gql
fragment pageInfo on PageInfo {
pageNum # 页码
pageSize # 每页条数
pages # 总页数
total # 总条数
}
声明下,由于我们只使用 GraphQL 的 Query 功能,所以没研究过这种格式是否会影响 Mutation。现在或以后有 Mutation 需求的,尽量采用官方推荐的前两种吧。
在组件内进行分页请求
之前提到了, graphql
这个装饰器为 this.props
添加了 data
对象,其中有个函数为 fetchMore
。
fetchMore 看名字就知道是用来作分页请求的。
下面我们看一个比较真实的例子,许多业务相关的代码都用表示其作用的函数替代了,注意看注释:
import React, { PureComponent } from 'react';
import { graphql } from 'react-apollo';
import { select } from './utils';
// 注意,这里用的 query 是 「RESTful 风格」那一节中贴出的 schema
import query from './query.gql';
@graphql(query, {
skip: props => !isValid(props),
options: props => ({
variables: {
topicId: getIdFromUrl(),
},
}),
})
@select({
// 你可以写一个函数,从 this.props.data 里过滤出当前列表的 pageInfo,直接添加到 this.props.pageInfo
pageInfo: getPathInfoFromProps(props),
})
export default class TopicListContainer extends PureComponent {
hasMore = () => {
const { pageNum = 0, pages = 0 } = this.props.pageInfo || {};
return pageNum < pages;
}
loadNextPage = () => {
const { pageInfo = {}, data } = this.props;
const { pageNum = 1 } = pageInfo;
const fetchMore = data && data.fetchMore;
if (!this.hasMore()) return;
if (!fetchMore) return;
return fetchMore({
variables: {
// 是的,这里不需要把你在 `@graphql` 装饰器中定义的其它 variables 再写一遍
// apollo 会自动 merge
pageNum: pageNum + 1,
},
// 这个回调函数,会在 fetch 成功后自动执行,用于修改本地 apollo store
updateQuery: (prev, { fetchMoreResult }) => {
if (!fetchMoreResult) return prev;
// 尝试 log 下 `fetchMoreResult`,其返回的数据结构,和 query 中的 schmea 是一致的
// parseNextData 返回新数据。
// 新数据的数据结构必须和 query schema 一样
// NOTE: 此处会有大坑,如果你发现最终数据并未改变,请阅读后文
return parseNextData(prev, fetchMoreResult);
}
});
}
render() {
return (
<TopicList
hasMore={this.hasMore()}
// TopicList 里有一个按钮,点击后调用 loadNextPage 进行下一页请求
loadNextPage={this.loadNextPage}
loading={this.props.data && this.props.data.loading}
isError={this.props.data && this.props.data.error}
/>
);
}
}
updateQuery
中,使用 parseNextData
经过一些处理,返回新数据给 apollo,apollo 将把它写入到 apollo store 中。
注意,这里至少会有两处大坑
- 如果写入失败,是会静默失败的,也就是说 没有任何报错提示
- 如果写入数据的结构,和 query schema 不符,就会写入失败。
但写入失败的情况还不止于此!如果你发现最终数据并未改变,可能是中招了,解毒方案 请阅读 写入 store 的失败原因分析和解决方案
这段代码只演示了如何 被动 地去修改本地的 apollo store 数据,要问如何 主动 去修改 apollo store,请看这篇文章: 修改本地的 apollo store 数据