前端er瞭解GraphQL,看這篇就夠了

快狗叫車前端團隊發表於2019-04-01

image

GraphQL在近幾年被提到的次數越來越多,最近參加過的幾次技術大會前端分會場均提到過。對於這種光看名字並不容易想到它是什麼的東西,還是存在些神祕感的。於是,打算去了解一下GraphQL到底是什麼。

什麼是GraphQL?

首先,GraphQL來自Facebook,如果你也跟我一樣完全沒了解過它,不知道它到底是幹什麼的,那麼你一定聽說過另一個叫做 Structured QL的東西。WHAT? 其實就是SQL了。

  • 嗯,和SQL一樣,GraphQL是一門查詢語言(Query Language
  • 同樣和SQL一樣的是,GraphQL也是一套規範,就像MySQLSQL的一套實現一樣,Apollo, Relay...也是GraphQL規範的實現
  • SQL不同的是,SQL的資料來源是資料庫,而GraphQL的資料來源可以是各種各樣的REST API,可以是各種服務/微服務,甚至可以是資料庫

這裡借Apollo官網的一張圖來說明GraphQL所處的位置

這裡借Apollo官網的一張圖來說明GraphQL在網際網路應用架構中所處的位置

幾個時間點

  • GraphQL規範於2015年開源
  • Subscriptions操作於2017年被加入到規範中

那麼為什麼叫 Graph呢?

Graph是圖的意思,在GraphQL的世界裡,萬物皆為圖,也就是說你可以把你的業務模型建模為圖。

圖是將很多真實世界現象變成模型的強大工具,因為它們和我們自然的心智模型和基本過程的口頭描述很相似。

這裡就涉及到了圖論,Graph Database之類的知識,感興趣可以某歌學習一下。

這樣我們可以對GraphQL大體有了一個概念了。那麼我們來大概瞭解一下GraphQL。

GraphQL為我們解決了什麼問題呢?

簡單的說,站在前端角度,很多文章都會提到過為了取代Restful API,稍微具體點:

  • API欄位的定製化,按需取欄位
  • API的聚合,一次請求拿到所有的資料
  • 後端不再需要維護介面的版本號了
  • 完備的型別校驗機制,提供了更健壯的介面
  • 。。。

在知道以上這些優點是如何做到的之前,我們先來對GraphQL做一個簡單的學習

前端學習GraphQL

做為一名前端開發人員,只站在前端的角度上來說,更多的時候,我們需要關心的只有兩個操作:

  • query: 在GraphQL中這個關鍵字屬於schema(可以理解為協議)中的一種,代表你要執行的是查詢動作,即增刪改查中的查

  • mutation: 代表你要執行的動作是增刪改

querymutation 統稱為schema。其實還有一個subscriptions在2017年被加入到規範(spec)中,讓我們可以更輕鬆的實現推送功能

這裡我們以一個公司內部的分享平臺的兩個場景為例,來介紹一下這兩個操作如何使用。

query操作

首先,最基礎的一個場景,分享平臺首頁需要調一個介面,獲取全部的分享列表,目前這個介面的呼叫方式是:

GET /api/share/allShares
複製程式碼

返回

{
    "shares": [
        {
            "shareId": 1238272,
            "title": "分享一下Vue3.0",
            "desc": "Vue3.0就要釋出了,帶來了哪些新功能呢?",
            "where": "6F-19會議室",
            "startTime": 1548842400
        },
        {
            "shareId": 1238272,
            "title": "用flutter寫app頁面是一種什麼樣的體驗",
            "desc": "用跨平臺框架flutter來寫app頁面的初體驗", 
            "where": "6F-17會議室",
            "startTime": 1548842400
        },
        {
            "shareId": 1238272,
            "title": "Cordova原理",
            "desc": "一起來了解一下Cordova",
            "where": "6F-19會議室",
            "startTime": 1548842400
        }
    ]
}
複製程式碼

那麼換成GraphQL的方式,我們可以這麼寫

query {
	shares {
		title
		desc
		where
		startTime
	}
}
複製程式碼

咦?發現漏掉了一個欄位,如果我們要跳至詳情頁,需要知道分享的id,改造一下

# 給一個查詢起一個名字是一個好習慣
query findAllShares {
	shares {
		# 為id起了一個別名,叫shareId
		shareId: id
		title
		desc
		where
		startTime
	}
}
複製程式碼

到此,一個基礎的查詢操作就完成了。

分頁

通常,如果列表類資料量比較大的話,我們會採用分頁的方式獲取資料,而非一次性獲取全部資料,依然以剛才的分享列表獲取為例,如果用傳統的介面呼叫的方式,通常是要這樣去調介面:

GET /api/share/allShares?star=0&limit=10
複製程式碼

返回

{
    "allShares": {
        "totalCount": 3,
        "shares": [
            {
                "shareId": 1238272,
                "title": "分享一下Vue3.0",
                "desc": "Vue3.0就要釋出了,帶來了哪些新功能呢?",
                "where": "6F-19會議室",
                "startTime": 1548842400
            },
            {
                "shareId": 1238273,
                "title": "用flutter寫app頁面是一種什麼樣的體驗",
                "desc": "用跨平臺框架flutter來寫app頁面的初體驗",
                "where": "6F-17會議室",
                "startTime": 1548842400
            },
            {
                "shareId": 1238274,
                "title": "Cordova原理",
                "desc": "一起來了解一下Cordova",
                "where": "6F-19會議室",
                "startTime": 1548842400
            }
        ]
    }
}
複製程式碼

我們來繼續改造GraphQL的方式,分頁的方式:

# 分頁方式
query findAllShares($start: Int!, $limit: Int = 10) {
	allShares (start: $start, limit: $limit) {
		totalCount
		shares {
		    shareId: id
		    title
		    desc
		    where
	    	startTime
		}
	 }
}
複製程式碼

GraphQL提供了完備的分頁解決方案,可參考 Pagination

下一場景,得到了所有的分享列表,可以進入詳情頁了。目前詳情頁有三個主要的查詢介面:獲取分享詳情,獲取分享的評論列表和獲取分享者所有的分享列表。如果是傳統的方式,我們需要調三個介面:

// 獲取分享的詳情
GET /api/share/:shareId
複製程式碼
// 分享詳情返回
{
    "shareDetail": {
        "shareId": 1238274,
        "title": "Cordova原理",
        "desc": "一起來了解一下Cordova",
        "where": "6F-19會議室",
        "startTime": 1548842400,
        "attchments": "",
        "creatorId": 321,
        "lastUpdateTime": 1548842400,
        "logoUrl": "",
        ...
    }
}
複製程式碼
// 獲取分享的評論列表
GET /api/share/comments/:shareId
複製程式碼
// 分享評論列表返回
{
    "commentInfo": {
        "totalCount": 5,
        "comments": [
            {
                "id": 1,
                "content": "非常不錯",
                "userId": 213,
                "commentTime": 1548842400,
            },
            {
                "id": 2,
                "content": "很好",
                "userId": 214,
                "commentTime": 1548842400,
            },
            {
                "id": 3,
                "content": "不錯",
                "userId": 216,
                "commentTime": 1548842400,
            },
            {
                "id": 4,
                "content": "Very GOOD!",
                "userId": 2313,
                "commentTime": 1548842400,
            }
        ]
    }
}
複製程式碼
// 分享的建立者的建立的全部分享列表
GET /api/share/shares/:creatorId
複製程式碼
// 分享建立者的全部分享返回
{
    "hisShares": [
        {
            "shareId": 1238272,
            "title": "分享一下Vue3.0",
            "desc": "Vue3.0就要釋出了,帶來了哪些新功能呢?",
            "where": "6F-19會議室",
            "startTime": 1548842400
        },
        {
            "shareId": 1238273,
            "title": "用flutter寫app頁面是一種什麼樣的體驗",
            "desc":     "用跨平臺框架flutter來寫app頁面的初體驗", 
            "where": "6F-17會議室",
            "startTime": 1548842400
        },
        {
            "shareId": 1238274,
            "title": "Cordova原理",
            "desc": "一起來了解一下Cordova",
            "where": "6F-19會議室",
            "startTime": 1548842400
        }
    ]
}
複製程式碼

那麼如果用GraphQL的方式呢?

query shareDetailPage($shareId: Int!, $creatorId:ID!, $start: Int!, $limit: Int = 10) {
    # 分享詳情
    shareDetail: share (shareId: $shareId) {
        shareId: id
        title
        desc
        where
        logoUrl
        attchments
    }
    
    # 評論資訊
    commentInfo(shareId: $shareId, start: $start, limit: $limit) {
        totalCount
        comments {
            id
            userId
            content
            commentTime
        }
    }
    
    # 他的分享
    hisShares (creatorId: $creatorId) {
        shares {
            title
            desc
            where
            startTime
        }
    }
}
複製程式碼

一個查詢即可搞定。

mutation操作

變更操作,這裡只介紹一種場景。到了分享詳情頁,我們可能會需要編輯這個分享,在傳統的方式中,需要調一個更新操作的介面:

POST /api/share/update/:shareId
FormData:
title=xxx&desc=xxx&where=xxx
複製程式碼

調完此介面後為了確認確實已經更新成功了,我們可能還會調一次獲取分享詳情介面:

GET /api/share/:shareId
複製程式碼

接下來我們換成GraphQL的方式:

mutation editShareInfo($shareObj: ShareInput!) {
    editShareInfo(shareInfo: $shareObj) {
    shareId: id
    title
    desc
    where
    logoUrl
    attchments
  }
}
複製程式碼

這樣,便可以直接將分享內容修改並返回修改後的分享詳情

其他的功能

為了我們寫查詢語句部分程式碼能有更好的可複用性,GraphQL還提供了Fragments(片段), Inline Fragments(內聯片段)和Directives(指令)功能。前兩者可以類比為JavaScript中的function(函式)和anonymous function(匿名函式),Directives(指令)可以根據我們傳的引數來決定某些欄位是否需要返回。這裡就不做過多介紹了。

以上的功能如何實現?

schema

通過上面的例子,肯定會產生些疑問,我們要如何知道可以查詢哪些欄位?使用哪些引數?這就需要引入schema了。

通俗點說,schema就是協議,規範,或者可以當他是介面文件。

GraphQL規定,每一個schema有一個根(root)query和根(root)mutation。

我們先來看Root Query怎麼寫,依然是上面的查詢的例子

# 定義一個根查詢
type Query {
    # 可以查詢的欄位和引數
    shares(start: Int = 0, limit: Int = 10, creatorId: ID): [Share!]!
    share(shareId: ID!): Share!
    commentInfo(shareId: ID!, start: Int = 0, limit: Int = 10): CommentInfo!
}
複製程式碼

資料型別

如果你熟悉TypeScript或Flow的話可能會發現上面的寫法似曾相識,是的,裡面的含義就是你想的那樣。每一個可以查詢的欄位的引數後面會跟標明這個引數的型別,!用來表示這個引數不可以是空的。[]表示查詢這個欄位返回的是陣列,[]裡面是陣列的型別。

上面我們還看到了一些在TypeScript中不存在的型別,比如ID,ID我們暫且把他當成字串String型別就可以了。類似我們熟悉的JavaScrpit或TypeScript,GraphQL也有幾個基礎型別,在GraphQL中他們統稱叫標量型別(Scalar Type),主要包括:Int(整型), Float(浮點型), String(字串), Boolean(布林型)和ID(唯一識別符號型別)。同時,GraphQL也允許你去自定義標量型別,例如:Date型別,只需實現相關的序列化,反序列化和驗證的功能即可。

物件型別

上面的根查詢定義中,我們還看到了一些與業務相關的型別,比如Share, Comment,這些統稱為物件型別。物件型別也是GraphQL中的schema的基本元件,他可以告訴我們在服務上可以獲得到哪些物件,以及這個物件有哪些欄位。接下來我們要做的就是定義這些物件型別,直到全部為基礎型別。

# 定義Share的物件型別
type Share {
  id: ID!
  title: String!
  desc: String!
  startTime: Int!
  where: String
  attchments: String
  logoUrl: String
  creatorId: ID!
  lastUpdateTime: Int
  is_delete: Int
  score: Int
  createTime: Int!
}

# 定義評論資訊物件型別
type CommentInfo {
  totalCount: Int!
  comments: [Comment!]!
}

# 定義評論物件型別
type Comment {
  id: ID!
  content: String!
  commentTime: Int!
  userId: ID!
  shareId: ID!
}

複製程式碼

這樣,我們就完成了schema的定義。

其他型別和功能

GraphQL其實還有Enumeration types(列舉型別),Union types(聯合型別)。同時,為了程式碼能更好的複用,GraphQL還提供了 Interface(介面)功能。這裡就不做過多介紹了。

實現執行

GraphQL約定,我們需要為Root Query(根查詢)和Root Mutation(根變更)裡面的每一個欄位提供一個resolver的函式。幷包裝成一個物件暴露出去,就像這樣:

const resolvers = {
    // 這裡面寫查詢操作欄位的resolver函式
    Query: {},
    // 這裡面寫變更操作欄位的resolver函式
    Mutation: {},
}

export default resolvers
複製程式碼

讓我們繼續寫完整:

// 一些載入資料的async function
import { loadSharesFromDB, loadShareById, loadCommentsByShareId } from './datasource'

const resolvers = {
    // 這裡面寫查詢操作欄位的resolver函式
    Query: {
        shares: (parent, { start, limit, creatorId }, context, info) => {
            return loadSharesFromDB(start, limit, creatorId)
                    .then(...)
        },

        share: (parent, { shareId }, context, info) => {
            return loadShareById(shareId)
                    .then(...)
        },

        commentInfo: (parent, { shareId, start, limit }, context, info) => {
            return loadCommentsByShareId(shareId, start, limit)
                    .then(...)
        },
    },
    // 這裡面寫變更操作欄位的resolver函式
    Mutation: {
         // ...
    },
}

複製程式碼

同樣的,對於mutation(變更)操作,我們也是先把schema完成:

# 定義Mutation根入口
type Mutation {
  editShareInfo(shareInfo: ShareInput!): Share! 
}

input ShareInput {
  id: ID!
  title: String!
  desc: String!
  where: String
}
複製程式碼

然後,補全resolver函式:

import { updateShareInfo, loadShareById } from './datasource'

const resolvers = {
    Query: {
        // ...
    },
    
    Mutation: {
        editShareInfo: (parent, { shareInfo }, context, info) => {
            // 更新分享詳情,then獲取更新後的分享詳情
            return updateShareInfo(shareInfo.id, shareInfo)
                    .then(loadShareById(shareInfo.id))
        },  
    },
}

export default resolvers
複製程式碼

到此,我們就實現了這個簡單的GraphQL的Server了。

結語

GraphQL還有很多內容可以探索,使用。比如,如果用Schema構建函式來生成物件型別,可以標記某一個欄位為廢棄,並給出廢棄原因。這樣,在版本迭代時,就可以友好的提示到舊版本的使用者,促使其升級到最新的介面,通過某些檢測手段,我們也能很輕鬆的知道舊版本的使用頻率,從而方便的讓我們在某一個時間徹底刪掉這個欄位。

如果說GraphQL有什麼缺點,那可能就是上手確實沒那麼容易,而且對於後端同學來說,還是有很多坑要踩的,比如快取,效能問題等。好在目前的GraphQL的資料已經不像幾年前那樣的匱乏,不管是官方還是社群,GraphQL可以參考的資源和解決方案都越來越多了。

不管怎樣,單純的對於前端er來說,如果說上一次前端的技術變革是SPA的普及的話,相信當下一次變革到來時,一定有GraphQL的影子。

一些連結

相關文章