# GraphQL
GraphQL 是個查詢語言,可以透過各個程式語言的實作來定義型別與 data schema,而在前端則可以用 graphQL 這套查詢語言來查詢想要的資料。
在以往 RESTful API 當中,時常會有幾個問題:
- 資料有相依性,導致一個頁面要發 5 ~ 6 個 API 拿資料
- 拿到 user profile
- 根據 userId 打 API 拿 posts
- 每個 posts 打 API 拿 comments
- ...
- 每次新增欄位時後端就要再重新部署一次 API
以下是 GraphQL 的語法:
query list($page: Number!) {
postList(page: $page) {
id
title
content
createdAt
}
}
query 的名字叫做 list,可以傳入 page(型別為數字)當作參數,並且傳給 postList
,回傳的欄位會有 id, title, content, createdAt。
一目了然對吧!透過這種方式除了不用每次新增欄位都要部署一次 API 之外,前端多了一份彈性,可以自行定義想要拿取的資料型別,提供了相當大的彈性。後端實作時也可以將不同的資料來源整合成 graphQL API 提供。
除此之外,GraphQL 本身因為是強型別的查詢語言,所以可以自動產生文件(幾乎各大程式語言都有實作)。
Github 也有提供 GraphQL 的 API 使用,可以參考 (opens new window)看看。
# Query 與 Mutation
在 GraphQL 當中,可以分為兩種操作:query
與 mutation
。query 指的是查詢資料;而 mutation 則是任何會對資料做更動(增、刪、改)。雖然 resolver 在實作上可以自己定義,如果你想要用 query 來完成所有事情也不是不可能,但實務上通常會根據是否有更動資料來選擇要用 query 還是 mutation。
# GraphQL 的限制
儘管如此,在實作上或許還是要考慮一些事情:
- GraphQL 只有一個 endpoint:雖然回傳的狀態碼都是 200(除非前面還有做驗證),但這也代表我們沒辦法仰賴 status code 來判斷 API 的回應
- permission 的控管只能在 API endpoint 實作,如果要做更細膩的權限控管就相對麻煩一些。(雖然大多數是後端要煩惱的就是了)
- GraphQL 的 errors 會統一搜集,query 全部結束後才回傳
- 需要考慮 N + 1 的問題
# GraphQL 的原理
GraphQL 會將查詢的語法透過解析器轉換成語法樹,再遞迴呼叫每個欄位對應的 resolver。
因為有解析這個步驟,所以儘管只是純字串,如果語法有錯誤還是可以直接偵測錯誤。另外因為 GraphQL 內建的強型別系統,每個欄位都需要定義型別,近一步讓欄位的存取更加安全。
# apollo-client 與 react-apollo
在前端實作的時候,雖然可以直接用 graphQL 語法,直接用 graphql 字串當作 post body 送過去後端,不過在實務上常常使用 apollo-client (opens new window) 來簡化操作。
這個函式庫提供了相當完善的功能,像是 Observable Query,在資料有變化的時候會自動通知有使用此資料的 query 來更新資料;還有 cache 功能,如果 query 不變,或是資料有重複的話會自動從 cache 拿資料回傳;另外還有 refecth, pagination, polling 等等常見的功能。
透過 graphql-tag (opens new window) 可以進一步幫你做解析成與法樹,也可以在解析階段找語法錯誤。
另外如果想介紹一下 react-apollo
,react-apollo 是以 apollo-client
當作基礎,搭配 React 的特性再做一層包裝。你也可以只用 apollo-client
來下 query 及 mutation 等等,但 react-apollo 裡頭有許多實用的功能。
const GET_ACCOUNT_QUERY = gql`
query getAccount(
$cursor: String!
$filter: MyFilter
) {
accountList(cursor: $cursor, filter: $filter) {
user {
userId
name
avatarURL
}
...
}
}
`;
function Account({ cursor, filter }) {
const { loading, error, data } = useQuery(GET_ACCOUNT_QUERY, {
variables: { cursor, filter }
});
if (loading) return <p>Loading...</p>;
if (error) return <p>Error :(</p>;
return data.accountList.map(...)
}
在 React hooks 還沒發佈之前,是用 render props 的方式來拿資料(更早以前是用 <Query>
component),不過有了 hooks 之後寫起來相當簡潔,而且還解決了最麻煩的 loading 跟 error。在官方文件 (opens new window)上有更多詳盡的用法,如果後端實作 GraphQL 的話,可以考慮使用 apollo
來簡化資料讀寫的複雜度。
# apollo-link-state
除了用 GraphQL 從後端操作資料外,還有一個很酷的想法,將 local state 也一起塞到 apollo 裡,大家都用 GraphQL 語法統一操作就對了。
這樣做的好處在於我們可以使用 apollo 的快取機制跟統一介面,也可以讓後端回傳的資料跟 local 狀態整合在一起,但設定 apollo-link-state 也挺麻煩的,究竟有沒有必要將 local state 也塞到 apollo 裡呢?或許就見仁見智了。
# 小結
GraphQL 在前端是個蠻好用的利器,雖然對後端來說要實作整個架構並不是件容易的事,如果要整合不同的來源的話就更麻煩了,還要考慮函式庫的支援度(雖然各大語言都有支援啦),但能夠享受的好處也相當明顯,一旦定義好型別與實作,就讓前端自己組合想要的資料。
GraphQL 的概念非常值得學習,雖然背後仍然是老派的語法解析 + 抽象語法樹,搭配函式庫,你也可以透過 GraphQL 語法來查詢報表、檔案等等的操作,甚至可以讓營運人員學習簡單的 GraphQL 讓他們自己搜尋對應資料。