21 分鐘學 apollo-client 系列:獲取資料

tinkgu發表於2017-09-18

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 資料

相關文章