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