21 分鐘學 apollo-client 系列:獲取數據

21 分鐘學 apollo-client 是一個系列,簡單暴力,包學包會。javascript

搭建 Apollo client 端,集成 redux
使用 apollo-client 來獲取數據
修改本地的 apollo store 數據
提供定製方案html

apollo store 存儲細節
寫入 store 的失敗緣由分析和解決方案前端

使用 Apollo 獲取數據

推薦先看:GraphQL 入門: 鏈接到數據
本文只作補充。java

下面編寫一個最簡單的 Container,觀察是否能 query 到數據。react

container.jsxwebpack

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。git

query 語法

基本的 query 語法能夠參看官方文檔 Queries and Mutations | GraphQL,這裏提一下 Apollo 特有的一些語法。github

query.gqlweb

#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 了。redux

上一節咱們在 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 數據

相關文章
相關標籤/搜索