21 分钟学 apollo-client 系列:获取数据
21 分钟学 apollo-client 是一个系列,简单暴力,包学包会。
搭建 Apollo client 端,集成 redux
使用 apollo-client 来获取数据
修改本地的 apollo store 数据
提供定制方案
- 请求拦截
- 封装修改 client 的 api
apollo store 存储细节
写入 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 参数
更详细的文档可以查阅
- API: graphql container with queries | Apollo React Docs
- GraphQL 入门: Apollo Client - 连接到数据
分页请求
如文档 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 数据