GraphQL 從入門到實踐

2021-02-20 前端早讀課

前言

信天翁計劃第三期來自@SHERlocked93投稿。

正文從這開始~~

本文首先介紹了 GraphQL,再通過 MongoDB + graphql + graph-pack 的組合實戰應用 GraphQL,詳細闡述如何使用 GraphQL 來進行增刪改查和數據訂閱推送,並附有使用示例,邊用邊學印象深刻~

0. 什麼是 GraphQL

GraphQL 是一種面向數據的 API 查詢風格。

傳統的 API 拿到的是前後端約定好的數據格式,GraphQL 對 API 中的數據提供了一套易於理解的完整描述,客戶端能夠準確地獲得它需要的數據,沒有任何冗餘,也讓 API 更容易地隨著時間推移而演進,還能用於構建強大的開發者工具。

1. 概述

前端的開發隨著 SPA 框架全面普及,組件化開發也隨之成為大勢所趨,各個組件分別管理著各自的狀態,組件化給前端仔帶來便利的同時也帶來了一些煩惱。比如,組件需要負責把異步請求的狀態分發給子組件或通知給父組件,這個過程中,由組件間通信帶來的結構複雜度、來源不明的數據源、不知從何訂閱的數據響應會使得數據流變得雜亂無章,也使得代碼可讀性變差,以及可維護性的降低,為以後項目的迭代帶來極大困難。

試想一下你都開發完了,產品告訴你要大改一番,從接口到組件結構都得改,後端也罵罵咧咧不願配合讓你從好幾個 API 裡取數據自己組合,這酸爽 😅

在一些產品鏈複雜的場景,後端需要提供對應 WebApp、WebPC、APP、小程序、快應用等各端 API,此時 API 的粒度大小就顯得格外重要,粗粒度會導致移動端不必要的流量損耗,細粒度則會造成函數爆炸 (Function Explosion);在此情景下 Facebook 的工程師於 2015 年開源了 GraphQL 規範,讓前端自己描述自己希望的數據形式,服務端則返回前端所描述的數據結構。

簡單使用可以參照下面這個圖:

比如前端希望返回一個 ID 為 233 的用戶的名稱和性別,並查找這個用戶的前十個僱員的名字和 Email,再找到這個人父親的電話,和這個父親的狗的名字(別問我為什麼有這麼奇怪的查找 🤪),那麼我們可以通過 GraphQL 的一次 query 拿到全部信息,無需從好幾個異步 API 裡面來回找:

query {
user (id : "233") {
name
gender
employee (first: 10) {
name
email
}
father {
telephone
dog {
name
}
}
}
}

返回的數據格式則剛好是前端提供的數據格式,不多不少,是不是心動了 😏

2. 幾個重要概念

這裡先介紹幾個對理解 GraphQL 比較重要的概念,其他類似於指令、聯合類型、內聯片段等更複雜的用法,參考 GraphQL 官網文檔 ~

2.1 操作類型 Operation Type

GraphQL 的操作類型可以是 query、mutation 或 subscription,描述客戶端希望進行什麼樣的操作

query 查詢:獲取數據,比如查找,CRUD 中的 R

mutation 變更:對數據進行變更,比如增加、刪除、修改,CRUD 中的 CUD

substription 訂閱:當數據發生更改,進行消息推送

這些操作類型都將在後文實際用到,比如這裡進行一個查詢操作

query {
user { id }
}

2.2 對象類型和標量類型 Object Type & Scalar Type

如果一個 GraphQL 服務接受到了一個 query,那麼這個 query 將從 Root Query 開始查找,找到對象類型(Object Type)時則使用它的解析函數 Resolver 來獲取內容,如果返回的是對象類型則繼續使用解析函數獲取內容,如果返回的是標量類型(Scalar Type)則結束獲取,直到找到最後一個標量類型。

比如在 Schema 中聲明

type User {
name: String!
age: Int
}

這個 User 對象類型有兩個欄位,name 欄位是一個為 String 的非空標量,age 欄位為一個 Int 的可空標量。

2.3 模式 Schema

如果你用過 MongoOSE,那你應該對 Schema 這個概念很熟悉,翻譯過來是『模式』。

它定義了欄位的類型、數據的結構,描述了接口數據請求的規則,當我們進行一些錯誤的查詢的時候 GraphQL 引擎會負責告訴我們哪裡有問題,和詳細的錯誤信息,對開發調試十分友好。

Schema 使用一個簡單的強類型模式語法,稱為模式描述語言(Schema Definition Language, SDL),我們可以用一個真實的例子來展示一下一個真實的 Schema 文件是怎麼用 SDL 編寫的:

# src/schema.graphql

# Query 入口
type Query {
hello: String
users: [User]!
user(id: String): [User]!
}

# Mutation 入口
type Mutation {
createUser(id: ID!, name: String!, email: String!, age: Int,gender: Gender): User!
updateUser(id: ID!, name: String, email: String, age: Int, gender: Gender): User!
deleteUser(id: ID!): User
}

# Subscription 入口
type Subscription {
subsUser(id: ID!): User
}

type User implements UserInterface {
id: ID!
name: String!
age: Int
gender: Gender
email: String!
}

# 枚舉類型
enum Gender {
MAN
WOMAN
}

# 接口類型
interface UserInterface {
id: ID!
name: String!
age: Int
gender: Gender
}

這個簡單的 Schema 文件從 Query、Mutation、Subscription 入口開始定義了各個對象類型或標量類型,這些欄位的類型也可能是其他的對象類型或標量類型,組成一個樹形的結構,而用戶在向服務端發送請求的時候,沿著這個樹選擇一個或多個分支就可以獲取多組信息。

注意:在 Query 查詢欄位時,是並行執行的,而在 Mutation 變更的時候,是線性執行,一個接著一個,防止同時變更帶來的競態問題,比如說我們在一個請求中發送了兩個 Mutation,那麼前一個將始終在後一個之前執行。

2.4 解析函數 Resolver

前端請求信息到達後端之後,需要由解析函數 Resolver 來提供數據,比如這樣一個 Query:

query {
hello
}

那麼同名的解析函數應該是這樣的

Query: {
hello (parent, args, context, info) {
return ...
}
}

解析函數接受四個參數,分別為

解析函數的返回值可以是一個具體的值,也可以是 Promise 或 Promise 數組。

一些常用的解決方案如 Apollo 可以幫省略一些簡單的解析函數,比如一個欄位沒有提供對應的解析函數時,會從上層返回對象中讀取和返回與這個欄位同名的屬性。

2.5 請求格式

GraphQL 最常見的是通過 HTTP 來發送請求,那麼如何通過 HTTP 來進行 GraphQL 通信呢

舉個慄子,如何通過 Get/Post 方式來執行下面的 GraphQL 查詢呢

query {
me {
name
}
}

Get 是將請求內容放在 URL 中,Post 是在 content-type: application/json 情況下,將 JSON 格式的內容放在請求體裡

# Get 方式
http://myapi/graphql?query={me{name}}

# Post 方式的請求體
{
"query": "...",
"operationName": "...",
"variables": { "myVariable": "someValue", ... }
}

返回的格式一般也是 JSON 體

# 正確返回
{
"data": { ... }
}

# 執行時發生錯誤
{
"errors": [ ... ]
}

如果執行時發生錯誤,則 errors 數組裡有詳細的錯誤信息,比如錯誤信息、錯誤位置、拋錯現場的調用堆棧等信息,方便進行定位。

3. 實戰

這裡使用 MongoDB + graph-pack 進行一下簡單的實戰,並在實戰中一起學習一下,詳細代碼參見 Github ~

MongoDB 是一個使用的比較多的 NoSQL,可以方便的在社區找到很多現成的解決方案,報錯了也容易找到解決方法。

graph-pack 是集成了 Webpack + Express + Prisma + Babel + Apollo-server + Websocket 的支持熱更新的零配置 GraphQL 服務環境,這裡將其用來演示 GraphQL 的使用。

3.1 環境部署

首先我們把 MongoDB 啟起來,這個過程就不贅述了,網上很多教程;

搭一下 graph-pack 的環境

npm i -S graphpack

在 package.json 的 scripts 欄位加上:

"scripts": {
"dev": "graphpack",
"build": "graphpack build"
}

創建文件結構:

.
├── src
│ ├── db // 資料庫操作相關
│ │ ├── connect.js // 資料庫操作封裝
│ │ ├── index.js // DAO 層
│ │ └── setting.js // 配置
│ ├── resolvers // resolvers
│ │ └── index.js
│ └── schema.graphql // schema
└── package.json

這裡的 schema.graphql 是 2.3 節的示例代碼,其他實現參見 Github,主要關注 src/db 、src/resolvers、src/schema.graphql 這三個地方

src/db:資料庫操作層,包括 DAO 層和 Service 層(如果對分層不太了解可以看一下最後一章)

src/resolvers:Resolver 解析函數層,給 GraphQL 的 Query、Mutation、Subscription 請求提供 resolver 解析函數

src/schema.graphql:Schema 層

然後 npm run dev ,瀏覽器打開 http://localhost:4000/ 就可以使用 GraphQL Playground 開始調試了,左邊是請求信息欄,左下是請求參數欄和請求頭設置欄,右邊是返回參數欄,詳細用法可以參考 Prisma 文檔

3.2 Query

首先我們來試試 hello world,我們在 schema.graphql 中寫上 Query 的一個入口 hello,它接受 String 類型的返回值

# src/schema.graphql

# Query 入口
type Query {
hello: String
}

在 src/resolvers/index.js 中補充對應的 Resolver,這個 Resolver 比較簡單,直接返回的 String

// src/resolvers/index.js

export default {
Query: {
hello: () => 'Hello world!'
}
}

我們在 Playground 中進行 Query

# 請求
query {
hello
}

# 返回值
{
"data": {
"hello": "Hello world!"
}
}

Hello world 總是如此愉快,下面我們來進行稍微複雜一點的查詢

查詢入口 users 查找所有用戶列表,返回一個不可空但長度可以為 0 的數組,數組中如果有元素,則必須為 User 類型;另一個查詢入口 user 接受一個字符串,查找 ID 為這個字符串的用戶,並返回一個 User 類型的可空欄位

# src/schema.graphql

# Query 入口
type Query {
user(id: String): User
users: [User]!
}

type User {
id: ID!
name: String!
age: Int
email: String!
}

增加對應的 Resolver

// src/resolvers/index.js

import Db from '../db'

export default {
Query: {
user: (parent, { id }) => Db.user({ id }),
users: (parent, args) => Db.users({})
}
}

這裡的兩個方法 Db.user、Db.users 分別是查找對應數據的函數,返回的是 Promise,如果這個 Promise 被 resolve,那麼傳給 resolve 的數據將被作為結果返回。

然後進行一次查詢就可以查找我們所希望的所有信息

# 請求
query {
user(id: "2") {
id
name
email
age
}
users {
id
name
}
}

# 返回值
{
"data": {
"user": {
"id": "2",
"name": "李四",
"email": "mmmmm@qq.com",
"age": 18
},
"users": [{
"id": "1",
"name": "張三"
},{
"id": "2",
"name": "李四"
}]
}
}

注意這裡,返回的數組只希望拿到 id、name 這兩個欄位,因此 GraphQL 並沒有返回多餘的數據,怎麼樣,是不是很貼心呢

3.3 Mutation

知道如何查詢數據,還得了解增加、刪除、修改,畢竟這是 CRUD 工程師必備的幾板斧,不過這裡只介紹比較複雜的修改,另外兩個方法可以看一下 Github 上。

# src/schema.graphql

# Mutation 入口
type Mutation {
updateUser(id: ID!, name: String, email: String, age: Int): User!
}

type User {
id: ID!
name: String!
age: Int
email: String!
}

同理,Mutation 也需要 Resolver 來處理請求

// src/resolvers/index.js

import Db from '../db'

export default {
Mutation: {
updateUser: (parent, { id, name, email, age }) => Db.user({ id })
.then(existUser => {
if (!existUser)
throw new Error('沒有這個id的人')
return existUser
})
.then(() => Db.updateUser({ id, name, email, age }))
}
}

Mutation 入口 updateUser 拿到參數之後首先進行一次用戶查詢,如果沒找到則拋錯,這個錯將作為 error 信息返回給用戶,Db.updateUser 這個函數返回的也是 Promise,不過是將改變之後的信息返回

# 請求
mutation UpdataUser ($id: ID!, $name: String!, $email: String!, $age: Int) {
updateUser(id: $id, name: $name, email: $email, age: $age) {
id
name
age
}
}

# 參數
{"id": "2", "name": "王五", "email": "xxxx@qq.com", "age": 19}

# 返回值
{
"data": {
"updateUser": {
"id": "2",
"name": "王五",
"age": 19
}
}
}

這樣完成了對數據的更改,且拿到了更改後的數據,並給定希望的欄位。

3.4 Subscription

GraphQL 還有一個有意思的地方就是它可以進行數據訂閱,當前端發起訂閱請求之後,如果後端發現數據改變,可以給前端推送實時信息,我們用一下看看。

照例,在 Schema 中定義 Subscription 的入口

# src/schema.graphql

# Subscription 入口
type Subscription {
subsUser(id: ID!): User
}

type User {
id: ID!
name: String!
age: Int
email: String!
}

補充上它的 Resolver

// src/resolvers/index.js

import Db from '../db'

const { PubSub, withFilter } = require('apollo-server')
const pubsub = new PubSub()
const USER_UPDATE_CHANNEL = 'USER_UPDATE'

export default {
Mutation: {
updateUser: (parent, { id, name, email, age }) => Db.user({ id })
.then(existUser => {
if (!existUser)
throw new Error('沒有這個id的人')
return existUser
})
.then(() => Db.updateUser({ id, name, email, age }))
.then(user => {
pubsub.publish(USER_UPDATE_CHANNEL, { subsUser: user })
return user
})
},
Subscription: {
subsUser: {
subscribe: withFilter(
(parent, { id }) => pubsub.asyncIterator(USER_UPDATE_CHANNEL),
(payload, variables) => payload.subsUser.id === variables.id
),
resolve: (payload, variables) => {
console.log('🚢 接收到數據: ', payload)
}
}
}
}

這裡的 pubsub 是 apollo-server 裡負責訂閱和發布的類,它在接受訂閱時提供一個異步迭代器,在後端覺得需要發布訂閱的時候向前端發布 payload。withFilter 的作用是過濾掉不需要的訂閱消息,詳細用法參照訂閱過濾器。

首先我們發布一個訂閱請求

# 請求
subscription subsUser($id: ID!) {
subsUser(id: $id) {
id
name
age
email
}
}

# 參數
{ "id": "2" }

我們用剛剛的數據更新操作來進行一次數據的更改,然後我們將獲取到並列印出 pubsub.publish 發布的 payload,這樣就完成了數據訂閱。
在 graph-pack 中數據推送是基於 websocket 來實現的,可以在通信的時候打開 Chrome DevTools 看一下。

4. 總結

目前前後端的結構大概如下圖。後端通過 DAO 層與資料庫連接實現數據持久化,服務於處理業務邏輯的 Service 層,Controller 層接受 API 請求調用 Service 層處理並返回;前端通過瀏覽器 URL 進行路由命中獲取目標視圖狀態,而頁面視圖是由組件嵌套組成,每個組件維護著各自的組件級狀態,一些稍微複雜的應用還會使用集中式狀態管理的工具,比如 Vuex、Redux、Mobx 等。前後端只通過 API 來交流,這也是現在前後端分離開發的基礎。

如果使用 GraphQL,那麼後端將不再產出 API,而是將 Controller 層維護為 Resolver,和前端約定一套 Schema,這個 Schema 將用來生成接口文檔,前端直接通過 Schema 或生成的接口文檔來進行自己期望的請求。

經過幾年一線開發者的填坑,已經有一些不錯的工具鏈可以使用於開發與生產,很多語言也提供了對 GraphQL 的支持,比如 JavaScript/Nodejs、Java、PHP、Ruby、Python、Go、C# 等。

一些比較有名的公司比如 Twitter、IBM、Coursera、Airbnb、Facebook、Github、攜程等,內部或外部 API 從 RESTful 轉為了 GraphQL 風格,特別是 Github,它的 v4 版外部 API 只使用 GraphQL。據一位在 Twitter 工作的大佬說矽谷不少一線二線的公司都在想辦法轉到 GraphQL 上,但是同時也說了 GraphQL 還需要時間發展,因為將它使用到生產環境需要前後端大量的重構,這無疑需要高層的推動和決心。
正如尤雨溪所說,為什麼 GraphQL 兩三年前沒有廣泛使用起來呢,可能有下面兩個原因:

GraphQL 的 field resolve 如果按照 naive 的方式來寫,每一個 field 都對資料庫直接跑一個 query,會產生大量冗餘 query,雖然網絡層面的請求數被優化了,但資料庫查詢可能會成為性能瓶頸,這裡面有很大的優化空間,但並不是那麼容易做。FB 本身沒有這個問題,因為他們內部資料庫這一層也是抽象掉的,寫 GraphQL 接口的人不需要顧慮 query 優化的問題。

GraphQL 的利好主要是在於前端的開發效率,但落地卻需要服務端的全力配合。如果是小公司或者整個公司都是全棧,那可能可以做,但在很多前後端分工比較明確的團隊裡,要推動 GraphQL 還是會遇到各種協作上的阻力。

大約可以概括為性能瓶頸和團隊分工的原因,希望隨著社區的發展,基礎設施的完善,會漸漸有完善的解決方案提出,讓廣大前後端開發者們可以早日用上此利器。

關於本文
作者:@SHERlocked93
原文:https://juejin.im/post/5c87b1776fb9a049ac7a0247

最後,為你推薦

【第1345期】解析 GraphQL 的查詢語法

【第1343期】GraphQL 在微服務架構中的實踐

【第1125期】GraphQL 技術棧揭秘

其他信天翁推薦

一個很有意思的hook庫:react-hanger

京東開源Vue移動端組件庫NutUI2.0發布

相關焦點

  • GraphQL入門指南
    還是需要強調一點,引入GraphQL並不意味著要像之前從Struts遷移到SpringBoot一樣需要去修改你的真實的後端代碼,因此GraphQL可以看做一個業務邏輯層靈活有效地輔助工具。');var graphqlHTTP = require('express-graphql');var express = require('express');// Import the data you created abovevar data = require('.
  • GraphQL 基礎實踐
    ThinkJS 中配置中間件有三個關鍵參數:match: 用於匹配 URL,我們想讓我們的請求發送到 /graphql 中進行處理,那麼我們對這個路徑進行 match 後進行處理;handle:中間件的處理函數,當 match 到時,此處理函數會被調用執行,我們的解析任務也在這裡進行,並將解析結果返回;options:options 時傳給中間件的參數
  • GraphQL 入門看這篇就夠了
    graphql + graph-pack 的組合實戰應用 GraphQL,詳細闡述如何使用 GraphQL 來進行增刪改查和數據訂閱推送,並附有使用示例,邊用邊學印象深刻~如果希望將 GraphQL 應用到前後端分離的生產環境,請期待後續文章。
  • GraphQL 簡介:原理及其使用
    我們將使用以下命令安裝 GraphQL 和 GraphQL for Express:npm install express-graphql graphqlNodeJS 代碼在項目中創建一個名叫 server.js 的文件,並將以下代碼複製到其中:const express = require("express")
  • GraphQL|一種配得上凡爾賽的API框架
    在知道答案之前,我們先來了解以下graphql。以下是graphql的官方站:https://graphql.cn/通過官方的實例,我們可以知道,graphql的主要功能,是進行API測試,與其他的API測試工具相比,graphql有幾個優勢:1、graphql可以通過圖形化界面的方式,在同一個界面上交互式地進行API數據測試,
  • GraphQL 實戰篇之前端Vue+後端
    ',})// 創建訂閱的 websocket 連接const wsLink = new WebSocketLink({    uri: 'ws://localhost:3001/graphql',    options: {        reconnect: true,    }})// 使用分割連接的功能// 你可以根據發送的操作類型將數據發送到不同的連接
  • GraphQL 概念與測試(下):graphy測試框架
    graphql測試思路於是我們可以歸納graphql接口的測試思路: sgqlc:官方的python客戶端sgqlc是graphql推薦的官方python客戶端,它的github地址是:https://github.com/profusion/sgqlc。
  • 【超詳細教程】如何使用TypeScript和GraphQL開發應用
    fantingshengdeMacBook-Pro:graphql-typescript fantingsheng$ yarn inityarn init v1.12.3question name (graphql-typescript):question version (1.0.0):question description: for study
  • GraphQL 值得了解一下
    本文參考資料:https://medium.com/javarevisited/basic-functionalities-of-graphql-every-developer-should-know-71347d7fab08
  • 如何使用GraphQL-進階教程:客戶端
    在 Xcode 中,可以使用 助理編輯器(Assistant Editor) 來同時處理視圖控制器和 graphql 代碼。前端記事本,不定期更新,歡迎關注!
  • Netflix 聯邦 GraphQL 平臺的實現過程及經驗教訓
    在這篇文章中,我們會將注意力轉移到成功運行聯邦 GraphQL 平臺所需的內容上——從它實現的過程到我們汲取的經驗教訓等方面。在過去的一年中,我們已經實現了聯邦 GraphQL 架構所需的核心基礎架構,正如我們前一篇文章所描述的那樣:
  • 如何使用GraphQL-基礎教程:介紹
    但是中文目前沒有很好的成套的入門教程,官網中文文檔看的我頭大,自學的過程中發現—— How to GraphQL 系列教程 ——寫的很明白,對於入門者來說比官網強得多,現將它基礎系列、進階系列和客戶端系列翻譯成中文輸出型學習一波。
  • Awesome Knowledge Graph : 知識圖譜資源匯總
    項目地址訪問 https://github.com/husthuke/awesome-knowledge-graph 可以獲取到最新版本的這個倉庫。如果想要開展研究和實踐,數據集必不可少,這一部分整理了目前常見的圖譜和數據集資料,可以拿來做分析使用。
  • 【歸納綜述】Graph Neural Network: An Introduction Ⅰ
    在這個19年都是老研究的AI領域,2020年年中的我還對已經火到巔峰的圖神經網絡一知半解就說不過去了。作者:劉浚嘉地址:https://www.zhihu.com/people/JunjiaLiu本文根據以下三篇綜述,旨在入門 Graph Neural Networks:A Comprehensive Survey on Graph Neural Networks | 2019 Janhttps://arxiv.org/pdf/1901.00596
  • 早餐 | 第二十八期 · Graph Transformation(一)
    ●force_clean_up屬性指定在該Transformation之後是否立即清理graph。清理graph會移除graph中所有從model inputs到達不了的節點。默認值為False。●graph_condition 屬性指定一組函數,改組函數都只有一個參數 - Graph對象。只有所有的函數都返回True以後該Transformation才會執行。如果這個屬性沒有設置值,則不做任何check。●run_before()方法返回一組該Transformation必須在其之前執行Transformations。
  • 深度學習中不得不學的Graph Embedding方法
    在這樣的背景下,對圖結構中間的節點進行表達的graph embedding成為了新的研究方向,並逐漸在深度學習推薦系統領域流行起來。圖5顯示了node2vec算法從節點t跳轉到節點v後,下一步從節點v跳轉到周圍各點的跳轉概率。
  • 圖論Graph theory
    一個頂點的度或價是連接到它的邊的數量,其中連接一個頂點到它自身的邊(循環)被計數兩次。例如,一個網站的連結結構可以用一個有向圖表示,其中頂點表示網頁,有向邊表示從一個頁面到另一個頁面的連結。類似的方法可以應用到社交媒體、旅遊、生物、計算機晶片設計和許多其他領域。因此,開發處理圖形的算法是計算機科學的主要興趣所在。圖的變換通常是形式化的,並由圖形重寫系統表示。
  • 資料|Graph embedding|Graph Neural Network 學習資料匯總
    Graph embedding techniques, applications, and performance: A surveyDeep Learning on Graphs: A Survey. arxiv 2018相關的TutorialKDD 2018 Tutorial2018年KDD上的Tutorial從數據中推斷圖(graph
  • 如何使用GraphQL-基礎教程:架構
    具有一個已連接到資料庫的 GraphQL 服務端的標準 greenfield 體系結構。2. 集成現有系統的 GraphQL 層GraphQL 的另一個主要用法是在一致的 GraphQL API 之後集成多個現有系統。
  • Tensorflow 全網最全學習資料匯總之Tensorflow 的入門與安裝【2】
    《TensorFlow極速入門》連結:http://www.leiphone.com/news/201702/vJpJqREn7EyoAd09.html本文介紹了 graph 與 session 等基本組件,解釋了 rank 和 shape 等基礎數據結構概念,講解了一些 variable 需要注意的地方並介紹了