GraphQL & Relay 初探

LiuRhoRamen發表於2018-03-16

距離Facebook釋出新版Relay(Relay Modern)已經快一年時間了,但相關的中文資料與實踐案例依然不是很多。究其原因,可能和官方文件不夠詳細有關。本文通過對GraphQL與Relay的淺析,希望能降低其上手難度,同時也便於判斷,自己的業務是否適合使用Relay框架。
什麼?直接上程式碼?猴~可以Github克隆Relay應用模版,裡面整合了前後端和路由,基本能滿足常見App的需求。

GraphQL

GraphQL是一套獨立的資料查詢系統,關於它的介紹與使用,官方網站已有比較詳細的介紹,同時,現在已有中文版可以參考。對於基本概念,建議直接閱讀官網,本文不做詳細介紹。

設計思想

之所以名稱中包含有Graph,是因為GraphQL採用了圖結構的查詢方式。以一個例子來看:
我們想要設計一個公司的內部人員管理系統,假設一種最簡單的場景,至少會包含部門和員工兩大資訊。以圖的結構來表示他們的關係的話,可能會是這樣:

GraphQL & Relay 初探
回顧常用的系統會發現,基本都可以通過圖來描述角色關係。這裡,我們可以類比圖資料庫的概念。由於圖的結構更接近於自然世界,相比關係型資料庫,在設計圖資料庫時,會省去一個圖結構向關係型結構的轉化工作。關於圖資料庫的更多介紹,可以參考neo4j的介紹
回到上圖,假設我們現在要查詢員工L某的詳細資訊,用GraphQL,可以這樣來請求(為更加直觀,這裡以中文來表示):

{
  員工(ID: "022") {
    姓名
    職位
    所屬部門 {
      名稱
    }
    同事 {
      ID
      姓名
      職位
    }
  }
}
複製程式碼

正常情況下,服務端返回的結果是:

{
  員工 {
    姓名: "L某",
    職位: "員工",
    所屬部門:{
      名稱: "前端部"
    }
    同事:[
      {
        ID: "022",
        姓名: "S某",
        職位: "經理"
      },
      {
        ID: "033",
        姓名: "Y某",
        職位: "總監"
      }
    ]
  }
}
複製程式碼

可以看到,根據查詢請求,員工的資訊以Json的格式全部返回出來了。對應到圖中,其實是提取了藍色的這部分資訊:

GraphQL & Relay 初探
查詢以員工(L某)為起點,沿著邊,將所需的關聯資料提取出來,形成了最終的返回的結果。由此可以看出,GraphQL所做的,其實是將圖結構的資料提取出成為一個樹狀結構。為了更清晰的體現這一點,我們將藍色部分單獨取出來,並稍稍換一下節點的位置:
GraphQL & Relay 初探
這裡,可能會有個疑問:這樣查詢出的樹狀結構,必然要求涉及的節點之間有邊。如果我想要同時查詢兩個節點的資訊,但它們間沒有邊,那該怎麼辦呢?
仍以之前的例子來看,如果我想查詢員工L某和設計部的資訊,但它們之間沒有邊。這種時候,可以構建一個虛擬的節點,並以它為起點,連線起其他需要查詢的節點。大概會是這樣的一種結構:
GraphQL & Relay 初探
查詢結構:

{
  員工(ID: "022") {
    姓名
    職位
    ...
  }
  部門(名稱: "設計部") {
    名稱
  }
}
複製程式碼

返回結果:

{
  員工 {
    姓名: "L某",
    職位: "員工",
    ...
  }
  部門 {
    名稱: "設計部"
  }
}
複製程式碼

在實際的應用開發中,也通常採用以上這樣的設計結構。
到此,我們來總結一下。GraphQL通過查詢語句,將圖結構的資料,提取成了樹狀結構作為結果返回。這裡的圖結構的資料,對應的便是GraphQL的型別系統。而如何將圖中的節點,也就是定義的各個型別相互關聯,需要通過具體的邏輯程式碼來實現。官方和社群也提供了各種語言的GraphQL庫
那麼還有個問題,如果我想修改資料該怎麼辦呢?
為解決這個問題,GraphQL引入了Mutation的概念。我們可以把Mutation看作是一種特殊的查詢,你需要為它定義名稱、引數、返回資料,並在具體的程式碼邏輯中,完成它的具體資料操作。詳細可以參考官方文件

優勢

官方網站簡單介紹了GraphQL的一些優點,不過,你可能更想知道,相比其他的API設計模式,GraphQL有什麼優勢呢?
根據我實踐下來的理解,GraphQL主要解決了面向前端的API在開發和後期維護中,常會遇到的一些矛盾點。下面,我們以RESTful API為參照,來具體看一下。

靈活性

再來看上一節的例子,像獲取員工全部資訊這樣的場景在應用開發中還是比較常見的,如果換成RESTful的API,會是怎麼樣的呢?我想,一般會有兩種做法:

  • 1、單獨的API來拉取全部資訊;
  • 2、將“同事”、“部門”這些資訊作為獨立的API,在前端通過多條API來組合。

我們先看第一種做法。這種方式有幾點明顯的弊端:
首先是資訊的冗餘。如果我在其他地方只需要顯示員工基礎資訊,不包含具體的部門或同事資訊,那就存在了資料的冗餘。
其次,從後端角度,會帶來維護上的成本。由於前端的展示需求相對多變,很可能會造成許多不再使用的API,而這些API又往往不敢輕易移除。又或者,在新需求中,很可能會新增與現有API重複度較高的API,造成後端業務程式碼的冗餘。
再者,即使考慮通過引數的方式,能使得返回值有可選擇性,確實可以增加一定的靈活度,但又不可避免的增加了引數的複雜性。
再看第二種做法。通過這種細粒度的模組劃分方式,相對第一種來說,減輕了後端程式碼的維護成本,但卻對前端極不友好。比如,一個列表中的某個欄位,後端只提供了單獨查詢的方式,當列表資料量特別大的時候,請求數也大大增加,將直接影響前端效能與使用者體驗。此外,大量的非同步請求無疑增加了前端程式碼複雜度,從而提高了前端的維護成本。
而GraphQL只需要定義好型別及對應的資料處理方式,暴露給查詢根節點,前端可以隨意按需請求,很好的解決了以上的矛盾。

前端友好性

GraphQL用來作為BFF層(Backend For Frontend)有其先天的優勢,最主要在於其面向前端的友好性設計。
除了上面提到的,在查詢請求中,請求數的減少對前端體驗上的提升之外,Mutation同樣減少了請求次數。在Restful API中,除了GET請求,其它的請求完成後,前端通常還需要再發出一個GET請求,來拉取變更後的資料。如果在返回結果中,為前端的顯示介面而增加了一部分資料,又會破壞後端程式碼的可複用性。而在GraphQL的Mutation中,返回的資料完全可以根據前端介面的資料需求來決定,而且只需一次請求即可。結合Relay框架,還可以定義理想化更新來減少Loading介面出現的次數,進一步提高使用者體驗。(這一點會在下一節具體展開)

降低溝通成本

在當下流行的前後端分離開發模式下,前後端開發者的溝通成本也是影響專案進度的一大要素。通常,為了降低溝通成本,後端開發者需要提前定義API文件,前端會根據API文件來MOCK資料以開發前端介面。後端開發者還需要通過各種工具,如PostMan來進行測試。在前後端完成開發後,還需要做聯調對接。
對於GraphQL來說,schema自身就是很好的文件。同時,官方還提供了一個類似於PostMan的工具GraphiQL,可以有助於開發中的除錯。

其他

GraphQL與Relay框架結合後,還能發揮出更大優勢,比如前後端一致的型別校驗、前端快取等,具體請看下一節內容。

Relay

Relay是一套基於GraphQL和React的框架,它將這兩者結合,在原來React元件的基礎上,進一步將請求封裝進元件。
官方提供了一個TodoMVC的demo可以參考,基本涵蓋了CRUD操作。

QueryRenderer

Relay框架提供了QueryRenderer這樣一個高階元件(HOC)來封裝React元件和GraphQL請求。這個元件接受四個Props:environment、query、variables以及render。 environment需要配置網路請求Store;query接受的便是GraphQL請求;variables接受GraphQL請求中需要的變數,最後render用來定義元件的渲染。
假設我們要開發一個顯示員工基本資訊的Relay元件,那麼它可能會是這樣的:

<QueryRenderer 
  environment={environment}
  query={graphql`
    query StaffQuery($id: ID!) {
      員工(ID: $id) {
        ID
        姓名
        職位
      }
    }
  `}
  variables={{ id: '011' }}
  render={({error, props}) => {
    if (error) {
      return <div>{error.message}</div>;
    } else if (props) {
      return <div>工號:{props["員工"]["ID"]};姓名:{props["員工"]["姓名"]};職位:{props["員工"]["職位"]};</div>;
    }
      return <div>Loading...</div>;
    }
  }
/>
複製程式碼

Fragment

現在我們已有了一個展示員工基本資訊的元件,如果我們現在要在這個元件的基礎上,進一步封裝出一個員工列表的元件,該怎麼辦呢?
參照React元件的方式,可以建立一個新的元件,接收一個包含員工ID陣列的props,在這個新的元件內部,根據ID陣列Map多個員工資訊的Relay元件。
這樣似乎可以,但問題是,如果有10個ID,那這樣一個元件也就會發出10個GraphQL請求,顯然違背了GraphQL的設計理念。
當然也可以建立一個新的Relay元件:query中直接請求一組員工資料,渲染出列表。但這樣就失去了元件的複用性,因為很顯然,這個新元件中,顯示每條員工資訊的邏輯和樣式,跟單個員工資訊的元件是一致的。
這裡,Relay提供了一個Fragment的HOC元件,它接受兩個Props:component和fragmentSpec。
component接受React元件,用來處理具體的元件檢視和邏輯;fragmentSpec則是接受一段GraphQL Fragment。所謂Fragment,對應到上一節的圖中,就是節點的某一部分。比如:

fragment 員工資訊 on 員工 {
  ID
  姓名
  職位
}
複製程式碼

在請求中,就可以這樣引入Fragment:

{
  員工(ID: "022") {
    ...員工資訊
  }
}
複製程式碼

那麼回到Relay中,可以這樣建立一個員工資訊的Fragment元件:

createFragmentContainer(
  class 員工資訊 extends React.Component {
    render() {
      return <div>工號:{this.props.data["員工"]["ID"]};姓名:{this.props.data["員工"]["姓名"]};職位:{this.props.data["員工"]["職位"]};</div>;
    }
  },
  graphql`
    fragment 員工資訊 on 員工 {
      ID
      姓名
      職位
    }
  `,
)
複製程式碼

有了這樣一個員工資訊的Fragment元件後,我們可以再建立員工資訊列表的元件:

<QueryRenderer 
  environment={environment}
  query={graphql`
    query StaffListQuery($ids: [ID]!) {
      員工(IDs: $ids) {
        ...員工資訊
      }
    }
  `}
  variables={{ id: ['011', '022', '033'] }}
  render={({error, props}) => {
    if (error) {
      return <div>{error.message}</div>;
    } else if (props) {
      return <員工資訊 data={this.props.data} />;
    }
      return <div>Loading...</div>;
    }
  }
/>
複製程式碼

這樣一來,元件實際的請求還是隻有一條,但員工資訊的元件得到了成功複用,如果在其他元件中,需要顯示員工資訊,也同樣只需要將該Fragment元件引入即可。
除了基本的Fragment Container,Relay還提供了Refetch ContainerPagination Container元件,前者在原Fragment元件的基礎上,注入了refetch方法,以便滿足元件需要更新資料的場景(如:使用者主動點選資料列表的重新整理按鈕);而後者,則新增了若干分頁的操作,這裡就不具體展開了。

Relay Store

在QueryRenderer中配置的environment裡面,主要包含的是網路請求和Store。這裡的Store於Redux的Store不太一致。Redux中主要用來統一管理元件的State,而Relay Store則記錄的是Record。這裡的Record,其實就是GraphQL的每個Type,或者對應於上一節的圖中的每個節點。
當Relay框架收到GraphQL返回的資料後,會為每一個節點資料記錄一個ID,並在Relay Store中存為一個Record。同時,Relay也為這些Record提供了CRUD的方法。具體可以參考官方文件

Mutations

為了便於在元件中發起GraphQL Mutation操作,Relay提供了commitMutation方法。除了發起Mutation之外,利用Relay Store,可以方便的定位頁面資料並進行更新,還能夠實現理想更新,進一步提升使用者體驗。

優勢

到這裡,基本已經涵蓋了Relay的全部功能。從上文可以看出,在GraphQL原有的優勢基礎上,Relay還帶來了以下兩點優勢:

  • 1、實現了資料查詢與元件的結合,進一步提高了前端模組化程度,提高元件複用性。
  • 2、優秀的客戶端快取,提升使用者體驗。

除此之外,結合Flow框架的型別檢測,Relay可以很好地根據後端提供的schema做型別校驗,避免一些潛在的Bug。

總結

通過以上的介紹和分析,相信你對GraphQL與Relay已經有了大致的瞭解,我認為,Relay比較適合的場景,是那種前端資料展示類別眾多,且變化較大的應用,比如社交網站。但具體是否在專案中應用,還是需要結合需求實際來決定。

參考資料

GraphQL Concepts Visualized
GraphQL and Relay 淺析

相關文章