0%

RabbitMQ AMQP

RabbitMQ AMQP 是最流行的開源和跨平台消息代理伺服器。

RabbitMQ 也是一種在應用程式之間交換數據的方式,可在不同平台之間交換訊息。例如 .Net 應用程式發送的消息可以由 Node.js 應用程式或 java 應用程式讀取。

RabbitMQ 被設計為通用消息代理,採用點對點,請求/回覆和 pub-sub 通信模式的多種變體。它使用智能代理/啞消費者模型,專注於向消費者提供一致的消息傳遞,消費者的消費速度與經紀人跟踪消費者狀態的速度大致相似。它是成熟的,在正確配置時表現良好。並且有許多可用的插件可以將它擴展到更多的用例和場景。當應用程式需要訪問流 (Stream)歷史時,RabbitMQ 通常與 Apache Cassandra 一起使用,對於需要 “無限” 佇列(queue)的應用程式,RabbitMQ 通常與 LevelDB 插件一起使用,但這兩個功能都不附帶在 RabbitMQ 本身。

AMQP

一般來說,訊息佇列會被應用在所有無法接受訊息漏失的環境,也就是極為重要的應用程式,例如銀行業務或財務系統。這通常表示,典型的企業級訊息佇列其實是相當複雜的軟體設計,會使用堅固的協定及可靠的存儲,來確保即便有任何故障都能夠完成訊息的交換。因此企業級的訊息中介軟體多年來一直由 Oracle 與 IBM 這類軟體巨人所獨佔,而它們多半是實做了專有的通訊協定,嚴厲的綁住了客戶的選擇。所幸這幾年來,訊息系統已突破這道障礙而蓬勃發展,這歸功於 AMQP、STOMP 與 MQTT 這類的開放協定。為了瞭解訊息佇列系統的運作方式,以下我們會針對 AMQP 進行概述,藉此初步瞭解如何使用基於這類協定的 API。

AMQP(Advanced Message Queuing Protocol)是一項開放標準的協定,許多訊息佇列系統都支援了這項協定。事實上它除了是通訊協定外,還是一個涉及路由、過濾、佇列、可靠性與安全性的模型。

在 AMQP 裡,共有三個基礎元件:

  • 佇列(Queue): 負責儲存訊息的資料架構,裡頭的訊息將由客戶端消費。佇列訊息會被推送(或拉取)至一或多個消費者,也就是我們的應用程式。若有多個消費者連接至相同的佇列,則訊息會是負載平衡的。

佇列可以是以下幾種類型:

  • 可延續性(Durable): 意即若中介者重新啟動,則佇列也會自動建立。可延續性佇列並不表示先前的內容一定會被保留,只有被標示為需要保存的訊息,才會存入磁碟,並於重啟時復原。
  • 專用性(Exclusive): 意即佇列綁定於特定的訂閱者。若彼此的連線關閉,則佇列就會被銷毀。
  • 自動刪除(Auto-delete): 當沒有任何訂閱者連線時,便刪除佇列
  • 交換(Exchange): 訊息發佈之處。依據所實作的演算法,將訊息輸送到一個或多個佇列。
  • 直接交換(Direct exchange): 比對整個路由鍵(例如 chat.msg)是否相符來輸送訊息。
  • 主題交換(Topic exchange): 使用萬用模式比對路由鍵(例如 chat.# 便吻合所有以 chat 為開頭的路由鍵)。
  • 擴散交換(Fanout exchange): 忽略任何的路由鍵,廣播訊息至所有連線的佇列。
  • 綁定(Binding): 交換元件與佇列之間的連結。這裡也定義了路由鍵以過濾來自交換元件的訊息。

以上這些元件是由一個中介者 (broker) 進行管理,它會揭露一個 API,用於相關的建置及處理。當連線到中介者時,客戶端會建立一個抽象化的通道 (channel),用於維護與中介者之間的通訊狀態。而在 AMQP 裡,「專用性」或「自動刪除」以外的佇列都可用於實作可延續性訂閱者。

RabbitMQ with C#

這裡我們會使用 C# 來實作最簡單的發送與接收訊息。我們將使用 Visual Studio Code.Net Code SDK。安裝完成後,開啟 VSCode,打開一個終端視窗或命令提示字元輸入:

1
dotnet --version

如果一卻正常會返回 .NET Code 的版本訊息,那我們就可以開始了。

解決方案與專案設定

我們先在電腦裡面用一個空的資料夾建立 amqp-sample 的 .NET 解決方案。然後使用 VSCode 打開解決方案資料夾 amqp-sample,開啟一個終端視窗,我們需要用 dotnet 初始化解決方案:

1
dotnet new sln

範本 “Solution File” 建立成功後,目錄下會產生一個 amqp-sample.sln 檔案。

接著我們要在這個解決方案目錄下產生兩個專案 Send 與 Receive 分別擺放資料的送出與接收程式。

1
2
3
dotnet new console --name Send

dotnet new console --name Receive

這會在解決方案目錄下產生兩個專案目錄 Send 與 Receive,首先我們來看看 Send 專案。切換到 Send 目錄測試一下專案。

1
2
3
cd Send
move Program.cs Send.cs
dotnet run

切換到 Send 目錄,將 Program.cs 改名為 Send.cs,使用 dotnet run 測試它,你應該會看到 Hello World!。

以同樣的方式測試一下 Receive 專案:

1
2
3
cd ../Receive
move Program.cs Receive.cs
dotnet run

接著我們需要安裝 C# 的 RabbitMQ Client,在兩個專案中分別安裝 RabbitMQ 的客戶端依賴:

1
2
3
4
cd Send
dotnet add package RabbitMQ.Client
cd ../Receive
dotnet add package RabbitMQ.Client

現在我們已經設置好 .NET 專案了,可以開始編寫代碼。

Sending

我們將調用我們的消息發佈者 publisher (sender) Send.cs 和消息消費者 consumer (receiver) Receive.cs。 從消息發佈者開始。

發布者將連接到 RabbitMQ,發送單個消息,然後退出。

在 Send.cs 中,我們需要使用一些命名空間,也將 class 名稱修改為 Send,靜態方法 Main() 是整個專案程式碼的進入點,我們就從這裡開始:

Send.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
using System;
using RabbitMQ.Client;
using System.Text;

namespace Send
{
class Send
{
static void Main(string[] args)
{
Console.WriteLine("Hello World!");
}
}
}

首先我們創建一個 RabbitMQ 伺服器的連接製造工廠:

Send.cs
1
2
3
4
5
6
7
8
9
10
11
12
class Send
{
static void Main(string[] args)
{
var factory = new ConnectionFactory() { HostName = "10.11.xxx.xxx" };
using (var connection = factory.CreateConnection())
using (var channel = connection.CreateModel())
{
...
}
}
}

使用製造工廠創建一個連接到 RabbitMQ 伺服器的連接 (connection),接下來,創建一個頻道 (channel),這是完成任務的 API 所在的通道。

這裡使用的 using 與名稱空間 (namespace) 完全沒有關係。這裡的 using 語句可以確保在實現 IDisposable 介面的物件的引用超出作用域時,在該物件上自動調用 Dispose( ) 方法。using 語句的後面是一對小括號,其中是引用變數的聲明和實例化(connection,channel),該語句使變數的作用域限制在隨後的語句塊中,因此在超出此作用域時就會自動關閉連線 (connection)與頻道 (channel)。另外,在變數超出作用域時,即使出現異常,也會自動調用其 Dispose( ) 方法。這也可省去使用 try/finally 語句處理異常狀況。

這裡我們宣告變數也使用了一個關鍵字 var,這是一種類型推斷關鍵字,使用 var 關鍵字替代實際的類型。編譯器可以根據變數的初始化值推斷變數的類型,但需要遵循以下一些規則:

  • 變數必須初始化。否則,編譯器就沒有推斷變數類型的依據。
  • 初始化器不能為空。
  • 初始化器必須放在表達式中。
  • 不能把初始化器設置為一個物件,除非在初始化器中創建一個新物件。

聲明了變數且推斷出類型後,就不能再改變變數的類型了。變數的類型確定後,對該變數進行任何賦值時,其強類型化規則必須以推斷出的類型為基礎。

初始化 ConnectionFactory 建構式中,我們使用預設的 port 5672 與預設的使用者名稱與密碼 guest, 如果使用的不是預設值,則建構實例時得顯式的加上這些值:

Send.cs
1
2
3
4
5
6
7
var factory = new ConnectionFactory()
{
HostName = "10.11.xx.xxx",
Port = 5672,
UserName = "yourname",
Password = "yourpassword"
};

要發送訊息,我們必須聲明一個佇列(Queue)供我們發送,然後我們就可以向佇列發佈消息,以下是 Send.cs 程式碼:

Send.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
using System;
using RabbitMQ.Client;
using System.Text;

namespace Send
{
class Send
{
static void Main(string[] args)
{
var factory = new ConnectionFactory() { HostName = "10.11.xx.xxx" };
using (var connection = factory.CreateConnection())
using (var channel = connection.CreateModel())
{
channel.QueueDeclare(
queue: "dotnetDemoQueue",
durable: false,
exclusive: false,
autoDelete: false,
arguments: null
);

string message = $"哈囉 from C# send 消息 {DateTime.Now.ToString()}";
var body = Encoding.UTF8.GetBytes(message);
var properties = channel.CreateBasicProperties();

properties.Persistent = true;

channel.BasicPublish(
exchange: "",
routingKey: "dotnetDemoQueue",
basicProperties: properties,
body: body
);
Console.WriteLine($" [x] Send {message}");
}
Console.WriteLine("Press [enter] to exit.");
Console.ReadLine();
}
}
}

QueueDeclare( ) 方法聲明一個 queue 佇列來儲存我們的消息,在這個簡單的範例中,我們會跳過交換器 (exchange) 直接將消息送到 queue 中,其實 RabbitMQ 伺服器會使用一個內建的交換器幫我們把消息直接送到 queue 中。此範例,我們可以省掉交換器的聲明與路由鍵對佇列的綁定 (Binding)。

QueueDeclare( ) 聲明佇列是可冪性的(idempotent),只有在它不存在的情況下才會創建它,否則就使用已存在的佇列。 durable 引數目前設為 false,表示如果 RabbitMQ 伺服器重新啟動或崩潰,這個佇列將不復存在。

消息內容是一個字節陣列 (byte array),因此您可以編碼您喜歡的任何內容。這裡則使用 Encoding.UTF8.GetBytes( ) 將字串序列化。

properties.Persistent 這裡設定為 true,這會指示 RabbitMQ 伺服器將佇列的訊息存入磁碟中,預設值是 false,這只會將佇列訊息存在記憶體中,如果 RabbitMQ 伺服器重新啟動或崩潰,這些訊息將會消失。在這個範例中雖然設定為 true,但它不會有作用,因為我們在佇列聲明時 durable 設定為 false,這會在伺服器重啟時將整個佇列刪除。

BasicPublish( ) 方法讓我們送出消息。引數 exchange 這裡設定為空字串,RabbitMQ 伺服器會使用內建的交換器幫我們送出消息,此時引數 routingKey 可直接設定為佇列名稱。接下來就可以測試送出消息了。

1
2
3
dotnet run
[x] Send 哈囉 from C# send 消息 2019/7/11 上午 08:57:47
Press [enter] to exit.

在我們尚未編寫消費者端時,可以前往 RabbitMQ 的 Management UI 查詢佇列的狀況。

Receiving

消費者,它會監聽來自 RabbitMQ 的消息。 因此,與發佈單個消息的發佈者不同,我們將使消費者保持持續運行以偵聽消息並將其顯示出來。

在 Receive.cs 中,我們需要使用一些命名空間,並將 class 名稱修改為 Receive:

Receive.cs
1
2
3
4
using System;
using System.Text;
using RabbitMQ.Client;
using RabbitMQ.Client.Events;

開始的設置與發佈布者 (Send.cs)相同。我們打開一個連接(connection)和一個通道(channel),並聲明我們要消耗的佇列。請注意,這與 Send 發佈者的佇列要相匹配。

在這裡,我們也在此處聲明佇列。因為我們可能在發佈者之前啟動消費者,所以我們希望在嘗試使用消息之前確保佇列的存在。以下是 Receive.cs 的程式碼:

Receive.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
using System;
using System.Text;
using RabbitMQ.Client;
using RabbitMQ.Client.Events;

namespace Receive
{
class Receive
{
static void Main(string[] args)
{
var factory = new ConnectionFactory() { HostName = "10.11.xx.xxx" };
using (var connection = factory.CreateConnection())
using (var channel = connection.CreateModel())
{
channel.QueueDeclare(
queue: "dotnetDemoQueue",
durable: false,
exclusive: false,
autoDelete: false,
arguments: null
);

var consumer = new EventingBasicConsumer(channel);

consumer.Received += (model, e) =>
{
var body = e.Body;
var message = Encoding.UTF8.GetString(body);

Console.WriteLine($" [x] Received {message}");
};

channel.BasicConsume(
queue: "dotnetDemoQueue",
autoAck: true,
consumer: consumer
);

Console.WriteLine(" Press [enter] to exit.");
Console.ReadLine();
}
}
}
}

RabbitMQ 伺服器是以非同步的方式傳遞消息的,所以我們需要提供回呼函式(callback)處理器。這就是 EventingBasicConsumer.Received 事件處理程序所做的事情。

C# 的事件基於委託(Delegate),為委托提供了一種發佈/訂閱機制。consumer 實例訂閱了 EventingBasicConsumer 發射的事件,因此我們必須註冊一個處理函式(callback),在 C# 即是要註冊一個委托(Delegate),這裡我們使用 “+=” 註冊一個處理器(或稱監聽器)。

事件一般使用的方法有兩個參數: 其中第一個參數是一個物件,包含事件的發送者,第二個參數提供了事件相關的訊息。

我們透過 e.Body 取得發佈的消息,它是一個字節陣列 (byte array),我們使用 Encoding.UTF8.GetString() 作反敘列化,並顯示在終端機上。

BasicConsume() 中的引數 autoAck 設定為 true,在消費成功後會自動通知伺服器將此訊息從佇列中去除,如果沒有送回 Ack 訊息,消息將會保留在佇列中,有可能會被重覆消費。

現在將它們兜在一起,首先啟動消費者 Receive:

1
2
cd Receive
dotnet run

接著從發布者 Send 送出一個訊息。因消費者將透過 RabbitMQ 發佈者處獲得的消息,消費者將會持續運行,等待消息 (使用 Ctrl-C 停止消費),因此請嘗試從另一個終端視窗執行發佈者。

1
2
cd Send
dotnet run

持續運行中的消費者視窗將會即時顯示訊息。

1
2
3
dotnet run
Press [enter] to exit.
[x] Received 哈囉 from C# send 消息 2019/7/11 上午 11:56:27

GraphQL 是一種由 Facebook 創建的開源查詢語言。在 GraphQL 於 2015 年開源之前,Facebook 自 2012 年起在其內部使用它作為移動應用程序(mobile application)常見的 REST 架構的替代方案。它允許對特定數據的請求,使客戶端能夠更好的控制發送的信息。使用 RESTful 架構時這更加困難,因為後端定義了每個 URL 上每個資源可用的數據,而前端始終必須請求資源中的所有信息,即使只需要其中的一部分。此問題稱為過度提取(overfetch)。在最壞的情況下,客戶端應用程序必須通過多個網路請求讀取多個資源。這種過度提取,也增加了對瀑佈網路請求(waterfall network requests)的需求。 GraphQL 允許客戶端通過向服務器發出單個請求來決定所需的數據。因此,Facebook 的移動應用程序的網路使用率大幅下降,因為 GraphQL 使數據傳輸更加有效率。

GraphQL 是供 API 使用的查詢語言。它也是滿足資料查詢的運行環境(runtime)。GraphQL 服務不規定使用那種傳輸協定,但通常是透過 HTTP 來提供的。

為了展示 GraphQL query 與它的回應,請看一下 SWAPI,這是星際大戰(Star Wars) API。SWAPI 是與 GraphQL 包在一起的 REST API。我們可以用來傳送 query 及接收資料。

GraphQL query 只會要求它需要的資料。以下是個 GraphQL query 範例,在 query 區塊,我們索取 Princess Leia 的個人資料。由於我們指明索取第五人的資料(PersonID:5),所以得到 Leia Organa 的紀錄。接下來,我們指定取得資料的三個欄位(field,Google 翻譯為 “字段”): name, birthYear, created。result 區塊則是回應: 它按照 query 的外形(shape)將 JSON 資料格式化了。這個回應只含有我們需要的資料。這與 SQL 很相似,所以同樣都稱為 Query Language。

query
1
2
3
4
5
6
7
query {
person(personID:5) {
name
birthYear
created
}
}
result
1
2
3
4
5
6
7
8
9
{
"data": {
"person": {
"name": "Leia Organa",
"birthYear": "19BBY",
"created": "2014-12-10T15:20:09.791000Z"
}
}
}

因為查詢的動作是互動的,所以我們接下來可以修改它,以查詢新的結果。如果我們加入欄位 filmConnection,就可以索取有 Leia 的電影名稱。

query
1
2
3
4
5
6
7
8
9
10
11
12
query {
person(personID:5) {
name
birthYear
created
filmConnection {
films {
title
}
}
}
}
result
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
{
"data": {
"person": {
"name": "Leia Organa",
"birthYear": "19BBY",
"created": "2014-12-10T15:20:09.791000Z",
"filmConnection": {
"films": [
{
"title": "A New Hope"
},
{
"title": "The Empire Strikes Back"
},
{
"title": "Return of the Jedi"
},
{
"title": "Revenge of the Sith"
},
{
"title": "The Force Awakens"
}
]
}
}
}
}

這個 query 是嵌套狀的,它被執行時可遍歷相關的物件,如此一來,我們就可以用單一 HTTP 請求來取得兩種類型的資料,而不需要為了查詢多個物件而反覆執行多次。我們不會收到與這些類型有關但不想要取得的資料。使用 GraphQL 時,用戶端可以用一個請求指令取得他們需要的所有資料。

當你對 GraphQL 伺服器執行 query 時,它會用一種型態系統(type system)來驗證 query。每一個 GraphQL 服務都會在 GraphQL schema(綱要)裡定義許多型態。你可以將型態系統視為 API 的資料藍圖,這個藍圖的基礎是你所定義的一系列物件。例如,稍早的人物 query 的基礎是 Person 物件:

Person
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
type Person {
id: ID!
name: String
birthYear: String
eyeColor: String
gender: String
hairColor: String
height: Int
mass: Float
skinColor: String
homeworld: Planet
species: Species
filmConnection: PersonFilmsConnection
starshipConnection: PersonStarshipConnection
vehicleConnection: PersonVehiclesConnection
created: String
edited: String
}

Person 型態定義了所有欄位及其型態,讓你可以在查詢 Princess Leia 時取得它們。

GraphQL 通常被稱為宣告式(declarative)資料擷取語言,意思是開發者只要跟據他們需要哪些資料來列出資料需求,而不需要把注意力放在如何取得它。市面上有供各種語言使用的 GraphQL 伺服器程式庫,包括 C#、Clojure、 Elixir、 Erlang、 Go、 Groovy、 Java、 JavaScript、 .NET、 PHP、 Python、 Scala 與 Ruby。

GraphQL 規格

GraphQL 是一種用戶端 / 伺服器通信規格(spec)。規格描述了某種語言的功能與特性。我們受益於語言規格,因為它提供了共通的詞彙與最佳用法,讓語言與使用社群得以依循。

ECMAScript 規格是一種著名的軟體規格。每隔一段時間,一群來自流覽器公司、科技公司與語言社群的代表都會聚在一起,討論應該在 ECMAScript 規格中加入(與移除)哪些東西。GraphQL 也是如此,有一群人會聚在一起討論應納入這種語言(或從中移除)的東西,這份規格是所有 GraphQL 實品的指南。

當規格發表時,GraphQL 的創造者也會分享一個以 JavaScript 寫成的參考成品 graphql.js。你可以將這個參考成品當成藍圖,但它的目的不包括規定你一定要用某種語言來實作服務,它只是個指南。了解這種查詢語言與型態系統之後,你可以用你喜歡的任何一種語言來建立服務。

規格敘述編寫 query 時應使用的語言與語法,也規定一個型態系統及型態系統的執行和驗證引擎。除了以上的規定之外,規格不限制任何東西,GraphQL 未規定該使用哪種語言、如何儲存資料、該支援哪些用戶端。查詢語言有一份指南,但如何實際運用在你的專案完全由你自己決定。

GraphQL 的設計準則

雖然 GraphQL 不限制建構 API 的方式,但它提供了一些構思服務的指南:

  • 階層式

    GraphQL query 是階層式的,query 有一些欄位在其它欄位裡面,且 query 的外型類似它回傳的資料。

  • 以產品為中心

    GraphQL 的目地是提供用戶端需要的資料,以及支援用戶端的語言和 runtime。

  • 強型態

    GraphQL 伺服器採取 GraphQL 型態系統。在 schema 內,每一個資料點都有一種特定的型態,GraphQL 會拿它來驗證資料點。

  • 由用戶端指定的 query

    GraphQL 伺服器提供用戶端可以使用的功能。

  • 自我查詢

    GraphQL 語言可查詢 GraphQL 伺服器的型態系統。

REST 的缺陷

REST 應該是目前應用最普遍的 API 架構。這是一個資源導向的架構,可讓使用者在這個架構上執行諸如 GET、PUT、POST 與 DELETE 等動作來處理網路資源。你可將資源組成的網路當成一種虛擬狀態機,而哪些動作(GET、PUT、POST、DELETE)是這個機制的狀態改變。

在 RESTful 架構中,路由(route)代表資訊。例如,向這些路由請求資訊可得到特定的回應:

1
2
3
/api/food/hot-dog
/api/sport/skiing
/api/city/Lisbon

REST 可讓我們用各種端點來建構資料模型,它提供了複雜網路環境中處理資料的方法。配合 JSON 的應用,REST 變成了廣泛使用的 API,但是隨著行動裝置及網路的發展,REST 在某些狀況下已經顯露了一些疲態了。GraphQL 正是為了緩解這些疲態而造就的。

Overfetch

假如我們要建立一個 app,並讓它使用 REST 版的 SWAPI 提供的資料。我們要先載入 1 號角色 Luke Skywalker 的一些資料,為了取得這項資訊,我們可以向 https://swapi.co/api/people/1/ 發出 GET 請求,得到的回應應該是這筆 JSON 資料:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
{
"name": "Luke Skywalker",
"height": "172",
"mass": "77",
"hair_color": "blond",
"skin_color": "fair",
"eye_color": "blue",
"birth_year": "19BBY",
"gender": "male",
"homeworld": "https://swapi.co/api/planets/1/",
"films": [
"https://swapi.co/api/films/2/",
"https://swapi.co/api/films/6/",
"https://swapi.co/api/films/3/",
"https://swapi.co/api/films/1/",
"https://swapi.co/api/films/7/"
],
"species": [
"https://swapi.co/api/species/1/"
],
"vehicles": [
"https://swapi.co/api/vehicles/14/",
"https://swapi.co/api/vehicles/30/"
],
"starships": [
"https://swapi.co/api/starships/12/",
"https://swapi.co/api/starships/22/"
],
"created": "2014-12-09T13:50:51.644000Z",
"edited": "2014-12-20T21:17:56.891000Z",
"url": "https://swapi.co/api/people/1/"
}

這個龐大的回應,遠比 app 需要的資料還要多。我們只需要 name、mass 與 height 的資訊:

1
2
3
4
5
{
"name": "Luke Skywalker",
"height": 172,
"mass": 77
}

這是明顯的 overfetch (過度擷取)案例 – 取得許多不需要的資料。用戶端只需要三個資料點,卻得到包含 16 個鍵的物件,而且是透過網路傳送不需要的資料。

在 GraphQL 中,這個請求又是如何?

query
1
2
3
4
5
6
7
query {
person(personID:1) {
name
height
mass
}
}
result
1
2
3
4
5
6
7
8
9
{
"data": {
"person": {
"name": "Luke Skywalker",
"height": 172,
"mass": 77
}
}
}

我們索取指定外形的資料,並收到那種外形的資料,不多也不少。這是比較宣告式的作法,因為不會收到無關的額外資料,所以可以更快速的得到回傳。

Underfetch

當我們想在我們的 Star Wars app 中加入另一個功能,除了 name、height 與 mass 之外,也要顯示有 Luke Skywalker 這位角色的電影名稱。當我們向 https://swapi.co/api/people/1/ 請求資料後,仍然需要發出其它的請求來取得更多資料,這代表我們 underfetch (擷取不足)

為了取得每部電影的名稱,我們必須向 films 陣列內的每一個路由索取資料:

1
2
3
4
5
6
7
"films": [
"https://swapi.co/api/films/2/",
"https://swapi.co/api/films/6/",
"https://swapi.co/api/films/3/",
"https://swapi.co/api/films/1/",
"https://swapi.co/api/films/7/"
]

我們要發出一個請求來索取 Luke Skywalker (https://swapi.co/api/people/1/),接著還要發出五個請求來取得每一部電影才能得到這一筆資料。索取每部電影時,我們也會得到一個大型的物件,但我們只想要一個值 title

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
{
"title": "The Empire Strikes Back",
"episode_id": 5,
"opening_crawl": "It is a dark time for the\r\nRebellion. Although the Death\r\nStar has been destroyed,\r\nImperial troops have driven the\r\nRebel forces from their hidden\r\nbase and pursued them across\r\nthe galaxy.\r\n\r\nEvading the dreaded Imperial\r\nStarfleet, a group of freedom\r\nfighters led by Luke Skywalker\r\nhas established a new secret\r\nbase on the remote ice world\r\nof Hoth.\r\n\r\nThe evil lord Darth Vader,\r\nobsessed with finding young\r\nSkywalker, has dispatched\r\nthousands of remote probes into\r\nthe far reaches of space....",
"director": "Irvin Kershner",
"producer": "Gary Kurtz, Rick McCallum",
"release_date": "1980-05-17",
"characters": [
"https://swapi.co/api/people/1/",
"https://swapi.co/api/people/2/",
"https://swapi.co/api/people/3/",
"https://swapi.co/api/people/4/",
"https://swapi.co/api/people/5/",
"https://swapi.co/api/people/10/",
"https://swapi.co/api/people/13/",
"https://swapi.co/api/people/14/",
"https://swapi.co/api/people/18/",
"https://swapi.co/api/people/20/",
"https://swapi.co/api/people/21/",
"https://swapi.co/api/people/22/",
"https://swapi.co/api/people/23/",
"https://swapi.co/api/people/24/",
"https://swapi.co/api/people/25/",
"https://swapi.co/api/people/26/"
],
"planets": [
//... 長串的路由
],
"starships": [
//... 長串的路由
],
"vehicles": [
//... 長串的路由
],
"species": [
//... 長串的路由
],
"created": "2014-12-12T11:26:24.656000Z",
"edited": "2017-04-19T10:57:29.544256Z",
"url": "https://swapi.co/api/films/2/"
}

如果我們想要列出電影的每個角色,就要發出更多的請求,在這個例子中,我們要接觸 16 個路由,並產生 16 次的用戶端往返。每一個 HTTP 請求都會用到用戶端的資源,並過度攫取資料,造成更緩慢的使用者體驗,特別是使用慢速網路或裝置的使用者可能會完全無法看到內容。

GraphQL 處理擷取不足的方法是定義一個嵌套式的 query,接著一次請求所有的資料。

query
1
2
3
4
5
6
7
8
9
10
11
12
query {
person(personID:1) {
name
height
mass
filmConnection {
films {
title
}
}
}
}
result
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
{
"data": {
"person": {
"name": "Luke Skywalker",
"height": 172,
"mass": 77,
"filmConnection": {
"films": [
{
"title": "A New Hope"
},
{
"title": "The Empire Strikes Back"
},
{
"title": "Return of the Jedi"
},
{
"title": "Revenge of the Sith"
},
{
"title": "The Force Awakens"
}
]
}
}
}
}

我們只用一個請求來取得想要的資料。而且一如既往,query 的外形符合所收到資料的外形。

管理 REST 端點

經常有人抱怨 REST API 的另外一個地方是它缺乏彈性。當用戶需求改變時,你通常要建立新的端點,而且新端點可能會開始倍增。套句網路名言:

You get a route!You get a route!Every!Body!Gets!Gets!A!Route!

使用 SWAPI REST API 時,我們必須對許多路由發出請求,大型的 app 通常會使用自訂的端點來盡量減少 HTTP 請求,你可能會開始看到 /api/character-with-movie-title 這類的端點。設置新端點通常意味著前端與後端團隊必須進一步擬定計畫與進行更多的溝通,開發速度或許會因而減緩。

使用 GraphQL 時,典型的架構只有一個端點。單一端點可扮演閘道的角色並協調多個資料來源,但就算只有一個端點,我們仍然可以更輕鬆的組織資料。

在討論 REST 的缺陷時,有一個需要特別注意的地方是許多機構都會同時使用 GraphQL 與 REST。設置 GraphQL 端點來從 REST 端點擷取資料是完全有效的 GraphQL 使用方式。在開始逐漸使用 GraphQL 是一種很好的做法。

現時世界的 GraphQL

許多公司都用 GraphQL 來改善它們的 app、網站與 API,GitHub 是早期就大量採用 GraphQL 的公司之一,它的 REST API 經歷三次變動,在公用 API 第 4 版時開始使用 GraphQL,GitHub 在官網提到,他們發現 “能夠準確定義你需要的資料(而且只有你需要的)是它比 REST API v3 端點還要好的地方”。

其它的公司,例如 The New York Times、IBM、Twitter 與 Yelp 也信任 GraphQL,這些團隊的開發者也經常在會議上宣揚 GraphQL 得好處。

GraphQL 用戶端

GraphQL 只是一種規格。它不在乎與它一起使用的究竟是何種瀏覽器、React、Angular、Vue 或者是純 JavaScript。GraphQL 對一些特定的主題有一些意見,但除此之外,如何設計架構由你自行決定。這導致一些工具出現,讓你在規格之外的領域有一些選擇。

GraphQL 用戶端旨在加速開發團隊的工作流程,並改善 app 的效率與性能。它們可處理諸如網路請求、資料快取,以及將資料注入使用者介面等工作。市面上有許多 GraphQL 用戶端選項,但這個領域的領導者是 RelayApollo

Relay 是 Facebook 製作的用戶端,它是與 React 和 React Native 一起運作的。Relay 是 React 元件與 GraphQL 伺服器回傳的資料之間的連接組織。Facebook、GitHub、Twitch 以及許多其它公司都使用 Relay。

Apollo Client 是社群團體 Meteor Development Group 為了建立更全面的 GraphQL 週邊工具而開發的。Apollo Client 支援所有主要的前端開發平台,例如,Vue ApolloApollo Angular,不限定任何特定的框架。Apollo 開發了一些協助建構 GraphQL 服務(Apollo Server)和改善後端服務效能的開發工具,以及監控 GraphQL API 性能的工具組。Airbnb、CNBC、 The New York Times 與 Ticketmaster 等公司都在產品中使用 Apollo Client。

GraphQL 已經是個相當穩定的標準了,也已經是個龐大且正在持續成長的生態系統。它的概念則來自於圖論(graph theory),要了解關於圖論的點(vertice)與邊(edge)的其他知識,可參考 Vaidehi Joshi 的部落格文章 A Gentle Introduction To Graph Theory

接下來將會是漫長的學習,我們將從 GraphQL 的查詢語言開始,最後會實作 GraphQL API 伺服器。我們將會使用 JavaScript。

哈囉,台南! 就從這裡開始。