# 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 當中,可以分為兩種操作:querymutation。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 來簡化資料讀寫的複雜度。

除了用 GraphQL 從後端操作資料外,還有一個很酷的想法,將 local state 也一起塞到 apollo 裡,大家都用 GraphQL 語法統一操作就對了。

這樣做的好處在於我們可以使用 apollo 的快取機制跟統一介面,也可以讓後端回傳的資料跟 local 狀態整合在一起,但設定 apollo-link-state 也挺麻煩的,究竟有沒有必要將 local state 也塞到 apollo 裡呢?或許就見仁見智了。

# 小結

GraphQL 在前端是個蠻好用的利器,雖然對後端來說要實作整個架構並不是件容易的事,如果要整合不同的來源的話就更麻煩了,還要考慮函式庫的支援度(雖然各大語言都有支援啦),但能夠享受的好處也相當明顯,一旦定義好型別與實作,就讓前端自己組合想要的資料。

GraphQL 的概念非常值得學習,雖然背後仍然是老派的語法解析 + 抽象語法樹,搭配函式庫,你也可以透過 GraphQL 語法來查詢報表、檔案等等的操作,甚至可以讓營運人員學習簡單的 GraphQL 讓他們自己搜尋對應資料。