我的前端故事----我為什么用GraphQL


背景

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

疑問

隨着app的逐漸完善,遇到的問題也漸漸多了起來,界面加載過久,初始化頁面請求次數過多等各種各樣的小毛病開始凸顯了出來。於是我開始了優化之路,第一步便是從api請求入手,仔細查看了每個api返回的內容,一直奇怪為什么接口總是返回很多的數據回來,比如我需要一個商戶的詳細信息,可接口卻會把這個商戶相關的門店信息、所有人信息等其它各種各樣的信息一起返回回來,如果是例如商戶詳情頁面也就罷了,可是在商戶列表這個接口下依舊返回如此之多的數據,可想而知這個列表有多大多復雜了。

后來問過后端的同學才知道是為了兼容 web 端的需求,一個接口需要同時為多個平台提供內容,這大大增加了接口的返回內容和處理邏輯,而且需求也經常改動,所以還不如把能用到的字段全都輸出出來,免得每次改需求都要前后端一起聯動。

反思

得知了結果,這確實是一個有“充分”理由的處理結果,可是真的只能這樣了嘛?有沒有什么更好的解決辦法呢?我們先來總結一下現在遇到的問題:

  • 兼容多平台導致字段冗余
  • 一個頁面需要多次調用 API 聚合數據
  • 需求經常改動導致接口很難為單一接口精簡邏輯

以上三個問題看起來並不復雜,按照以往的邏輯其實也是很好解決的,就拿第一個來說,遇到多平台需要兼容時其實可以通過提供不同平台的接口來解決,例如這樣:

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

雖然這是一個方便的解決方案,但帶來的其實是后端邏輯的增加,需要為不同平台維護不同的邏輯代碼。

再說第二個問題,一個頁面需要多次調用接口來聚合數據這塊也可以通過多加接口的方式來解決:

http://api.xxx.com/getIndexInfo

或者是通過 http2 來復用請求,但這些方法不是增加工作量就是有兼容性問題,那么還有沒有其他的方法呢?

如果大家還記得數據庫的知識的話,就發現其實我們可以用 SQL 的思路去解決這些事情,那如果把后端抽象成一個數據庫會怎么樣呢?

我想要什么字段就 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 了呢。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM