個人前端故事----我爲何用GraphQL

背景

今年我在作一個有關商戶的app,這是一個包含商戶從入網到審覈、從駁回提交到入網維護的完整的生命週期線下推廣人員使用的客戶端軟件,但故事並無這麼簡單。。。前端

疑問

隨着app的逐漸完善,遇到的問題也漸漸多了起來,界面加載太久,初始化頁面請求次數過多等各類各樣的小毛病開始凸顯了出來。因而我開始了優化之路,第一步即是從api請求入手,仔細查看了每一個api返回的內容,一直奇怪爲何接口老是返回不少的數據回來,好比我須要一個商戶的詳細信息,可接口卻會把這個商戶相關的門店信息、全部人信息等其它各類各樣的信息一塊兒返回回來,若是是例如商戶詳情頁面也就罷了,但是在商戶列表這個接口下依舊返回如此之多的數據,可想而知這個列表有多大多複雜了。node

後來問事後端的同窗才知道是爲了兼容 web 端的需求,一個接口須要同時爲多個平臺提供內容,這大大增長了接口的返回內容和處理邏輯,並且需求也常常改動,因此還不如把能用到的字段全都輸出出來,省得每次改需求都要先後端一塊兒聯動。web

反思

得知告終果,這確實是一個有「充分」理由的處理結果,但是真的只能這樣了嘛?有沒有什麼更好的解決辦法呢?咱們先來總結一下如今遇到的問題:數據庫

  • 兼容多平臺致使字段冗餘
  • 一個頁面須要屢次調用 API 聚合數據
  • 需求常常改動致使接口很難爲單一接口精簡邏輯

以上三個問題看起來並不複雜,按照以往的邏輯其實也是很好解決的,就拿第一個來講,遇到多平臺須要兼容時其實能夠經過提供不一樣平臺的接口來解決,例如這樣:npm

http://api.xxx.com/web/getUserInfo/:uid
http://api.xxx.com/app/getUserInfo/:uid
http://api.xxx.com/mobile/getUserInfo/:uid

又或者是經過不一樣的參數去控制:後端

http://api.xxx.com/getUserInfo/:uid?platfrom=web

雖然這是一個方便的解決方案,但帶來的實際上是後端邏輯的增長,須要爲不一樣平臺維護不一樣的邏輯代碼。api

再說第二個問題,一個頁面須要屢次調用接口來聚合數據這塊也能夠經過多加接口的方式來解決:restful

http://api.xxx.com/getIndexInfo

或者是經過 http2 來複用請求,但這些方法不是增長工做量就是有兼容性問題,那麼還有沒有其餘的方法呢?數據結構

若是你們還記得數據庫的知識的話,就發現其實咱們能夠用 SQL 的思路去解決這些事情,那若是把後端抽象成一個數據庫會怎麼樣呢?app

我想要什麼字段就 SELECT 什麼字段就行咯,若是一個頁面須要多個數據源的內容來填充那不就是組合 SQL 語句嘛。這樣不就解決了上面提出的三個問題了嘛,並且不管前端需求如何變動,只要咱們維持一個數據的超集,那麼每次只要讓前端改動查詢語句就能夠了,後端這裏也不須要同步的去給某個接口增長字段什麼的了,那麼解決方案有了,那該怎麼把後端抽象成一個數據庫呢?

解決

既然思路有了,那麼辦法也會有的~這就是 Facebook 在2015年開源的 GraphQL

這又是一個什麼東西呢?具體的介紹直接看它的官網就行了,我在這裏就很少說了,直接來看看如何使用吧。

因爲個人中間層是基於 Koa2 的,因此就在 koa2 上面作演示了,手寫咱們先安裝依賴:

npm install graphql koa-graphql --save

這樣咱們就能夠在 koa 中使用 graphql 了,而後就是配置路由了,按照文檔上面的例子,咱們能夠這樣寫:

"use strict";
const router = require('koa-router')();
const graphqlHTTP = require('koa-graphql');
const GraphQLSchema = require('./graphql');
const renderGraphiQL = require('../utils/render_graphiQL');
const graphqlModule = graphqlHTTP((request) => ({
    schema: GraphQLSchema,
    graphiql: false,
    context: {token: request.header.authorization, platform: request.query.platform},
    formatError: error => ({
        type: 'graphql',
        path: error.path,
        message: error.message,
        locations: error.locations ? error.locations[0] : null
    })
}));
router.all('/graphql', graphqlModule);

咱們來看看 graphqlModule 對象中都包含寫什麼吧,首先是 schema,這個是咱們主要的解析邏輯,全部經過 graphql 的請求都會被傳入這裏進行解析和處理,graphiql 這個是 koa-graphql 自帶的一個圖形界面版的測試地址,後面我再單獨介紹這個插件,context 是咱們的上下文,若是咱們須要在每一個解析函數內獲取到例如用戶 token時就能夠在這裏賦值,需求說明的是這個必須是一個 object 對象,而且若是咱們不指定的話會默認傳整個 request 對象,接下來就是最後一個經常使用的屬性了,formatError 是格式化錯誤的屬性,咱們能夠根據業務的需求自定義咱們的錯誤返回。

接下來去看看從最簡單的 hello world 開始,而後完成一個最基礎的 demo。

首先咱們在客戶端發起一個 post 請求,而後在請求 body 中帶上咱們的查詢語句,在 Graphql 中有兩種類型的查詢,一直是 query 開頭的查詢操做,一種是 mutation 開頭的修改操做。

query {hello}

這是一個最簡單的查詢,那麼這個查詢是如何經過解析的呢?上文說到所有的 Graphql 查詢都會經過 schema 來進行解析,那咱們看看上面定義的GraphQLSchema對象是個什麼吧。

module.exports = new GraphQLSchema({
    query: new GraphQLObjectType({
        name: 'rootQueryType',
        description: '查詢操做',
        fields: {
            hello: {
                type: GraphQLString,
                description: '演示 demo',
                args: {
                    name: {type: GraphQLString, description: '演示參數'}
                },
                resolve(it, args, context) {
                    return args.name;
                }
            }
        }
    }),
    mutation: new GraphQLObjectType({
        name: 'rootMutationType',
        description: '新增或修改操做',
        fields: {}
    })
});

咱們一步步來講,首先在這個文件中導出的是一個 GraphQLSchema 對象,這是 Graphql 的基礎對象,裏面包含了咱們須要的兩種類型,而後看 query 屬性,它返回的是一個GraphQLObjectType對象,這是 Graphql 中對於 object 的基本類型,這個對象中包含 name:名稱(全局惟一),description: 描述(這會自動的顯示在文檔中,雖然是非必須的,可是我仍是強烈建議每一個 Graphql 節點都寫上,這樣在後面的維護和查詢中都很是有利),最後就是fields屬性了,一個 Graphql 語句能查到什麼就全靠這裏寫了什麼了,在開始的例句中咱們查詢 query{hello},其實就是說咱們要查根節點下的 hello 屬性,因此這裏咱們就須要在 query 的 fields 中寫上 hello 屬性了,不然這條查詢語句就沒法生效了。

接下來咱們看看這個 hello 屬性中又包含了什麼呢?首先咱們須要指定它的類型,這很關鍵,這個類型是 hello 的返回類型,在這裏我指定它返回的是一個字符串,除此以外還有 Int,Boolean 等 js 的基礎類型可供選擇,具體可去查看文檔,固然了,在複雜狀況下也能夠返回 GraphQLObjectType 這種對象類型,而後就是對 hello 的描述字段description,接下來是args 屬性,若是咱們須要給此次查詢傳入參數的話就靠這個了,接下來就是最關鍵的 resolve 函數了,這個函數接受三個參數,第一個是上層的返回值,這在循環嵌套的狀況下會常用,好比說若是 hello 還有子屬性的話,那麼子屬性的這個參數就會是 args.name,第二個參數即是查詢屬性,第三個是咱們一開始說的貫穿整個請求的上下文。下面是一個完整的例子:

Request: query{hello(name: "world")}

Response: {"hello": "world"}

講完了 query 操做,其實 mutation 操做也是相似的,我就再也不展開說了。

總結

最後來總結一下這個解決方案吧,其實這個方案你說是否是最佳解呢?也未必,仍是要看具體的業務場景的,在我遇到的場景中各類數據的關係是明確的,或者說是能夠抽象成模型的,我認爲這是可否使用 Graphql 的關鍵,從上面的實例中咱們其實能夠發現經過 Graphql 咱們把每一個數據都規範了起來,指定了類型,肯定了嵌套關係,這在以 JavaScript 爲基礎的 node 環境中顯得那麼格格不入,自己 JavaScript 是弱類型的,基於此咱們能夠在 node 服務中靈活的修改數據,從而不須要關心返回值和參數值,可是 Graphql 用一種強類型的觀念來強制咱們設計每一個數據,也許會有些前端的同窗接受不了,可是我我的認爲這種思路實際上是很是合理的,而且 Graphql 這種還支持嵌套查詢,只須要 fields 屬性中有這個對象就好了,所以咱們能夠把每一個數據類型儘量的抽象和分離出來,舉個例子,店長這個角色不就是用戶對象加上商戶對象的組合嘛,這不只從關係上明確了邏輯,也方便了更多可能性的組合條件。

對應我一開始遇到的那幾個問題,Graphql 看上去彷佛完美的解決了個人問題,可是對於更加複雜的場景呢?或者說對於老項目的改形成本是否划算呢?雖然我沒有遇到,可是我以爲只要認真梳理數據結構,最終均可以的,但那個時候是否還須要 Graphql 呢?那就不知道了,這篇博客不是介紹 Graphql 如何使用的中文文檔,我想表達的是這種思路對於這種場景下的一個解決思路,如今它只是解決了個人這些問題,那麼從這個思路的身上能不能挖掘出更多的驚喜呢?它仍是太新了,也許過幾年回頭再看,它說不定就和 restful 同樣是 API 的標配了也說不定呢,畢竟 GitHub 今年也推出了他們的 Graphql API 了呢。

相關文章
相關標籤/搜索