2

21 分钟学 apollo-client 是一个系列,简单暴力,包学包会。

搭建 Apollo client 端,集成 redux
使用 apollo-client 来获取数据
修改本地的 apollo store 数据
提供定制方案

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 文件里写多个 queryfragment

对了,为了最小化实践,你可以先写不带参数的 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(),
        },
    }),
})

其中

  • skipshouldComponentUpdate 的效果是一样的,决定是否 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 中。
注意,这里至少会有两处大坑

  1. 如果写入失败,是会静默失败的,也就是说 没有任何报错提示
  2. 如果写入数据的结构,和 query schema 不符,就会写入失败。

但写入失败的情况还不止于此!如果你发现最终数据并未改变,可能是中招了,解毒方案 请阅读 写入 store 的失败原因分析和解决方案

这段代码只演示了如何 被动 地去修改本地的 apollo store 数据,要问如何 主动 去修改 apollo store,请看这篇文章: 修改本地的 apollo store 数据









tinkgu
503 声望20 粉丝

{{user.signature}}