因為 GraphQL Working Group 近期的推動,GraphQL 的 @defer
/@stream
directive 提案又重新回到了開發者的視野。
目前對應的 RFC 仍為 Stage 1,但已經投入相當大量的規格制定工作與討論,應該蠻有機會持續推進。
GraphQL specification RFC 分成四個階段:
- Stage 0: Strawman
- Stage 1: Proposal
- Stage 2: Draft
- Stage 3: Accepted
起源
最早在 2016 時,當時人還在 Facebook 的 Lee Byron 就曾在 Conference 分享過 Facebook 內部正在實驗這兩個 directive:
雖然當時只是在描述概念跟使用方式,而忽略了許多實作上的細節,但已經給了大家很充分的想像。
例如,在欄位上標記 @defer
可以把回應拆成多個部分,被標記的部分會在後續回應中補上。例如以下這個 GraphQL query:
{
feed {
stories {
author { name }
message
comments @defer {
author { name }
message
}
}
}
}
會先拿回不包含 comments
的 stories
:
{
"feed": {
"stories" : [
{
"author": { "name": "Lee Byron" }
"message": "GraphQL est grand!"
}
]
}
}
然後後續會再收到包含 comments
的 patch:
{
"path": ["feed", "stories", 0, "comments"],
"data": [
{
"author": { "name": "Laney Kuenzel" },
"message": "J'aime GraphQL!"
}
}
}
這樣一來,第一包 data 就不會受限於整個 GraphQL 較慢的部分,而可以更快的呈現初始畫面給使用者。
跟 @defer
的概念類似,也可以在 List 上標記 @stream
避免一次拿回整個 List 要花上太多的時間。例如,在上面範例中的 stories
欄位上標記 @stream
:
{
feed {
stories @stream {
author { name }
message
comments @defer {
author { name }
message
}
}
}
}
可以先拿回空的 array:
{
"feed": {
"stories" : []
}
}
再透過後續的 patch 拿到 array 中更多的 item:
{
"path": ["feed", "stories", 0],
"data": [
{
"author": { "name": "Lee Byron" }
"message": "GraphQL est grand!"
}
}
}
甚至是其中使用 @defer
處理的部分:
{
"path": ["feed", "stories", 0, "comments"],
"data": [
{
"author": { "name": "Laney Kuenzel" },
"message": "J'aime GraphQL!"
}
}
}
規格提案
在這許多年之間,雖然有不少 library、framework 有試圖去支援 @defer
/@stream
,但因為這兩個 directive 一直沒出現在 GraphQL specification 裡面,大多只能持續維持實驗性質。
而中間因為 GraphQL 從 Facebook 轉移出來 GraphQL Foundation 等等因素,整個 GraphQL Working Group 延宕直到最近兩年才重新開始活躍。而 @defer
本身也在等待 Facebook 內部以及社群得出正面的實驗結果。
2020 年底,GraphQL 發了「Improving Latency with @defer and @stream Directives」這篇文章,正式開始推動關於草案的回饋,以及推出實驗用的 GraphQL.js 跟 express-graphql 版本,大幅的加速了整個進程。
可以看到目前的草案跟上方提到 2016 年原先的範例,有一些細節上的小差距,這是官方 RFC 上所使用當作範例的 @defer
query:
query {
person(id: "cGVvcGxlOjE=") {
...HomeWorldFragment @defer(label: "homeWorldDefer")
name
films @stream(initialCount: 2, label: "filmsStream") {
title
}
}
}
fragment HomeWorldFragment on Person {
homeworld {
name
}
}
拿回的 Payload 1:
{
"data": {
"person": {
"name": "Luke Skywalker",
"films": [
{ "title": "A New Hope" },
{ "title": "The Empire Strikes Back" }
]
}
},
"hasNext": true
}
拿回的 Payload 2:
{
"label": "homeWorldDefer",
"path": ["person"],
"data": {
"homeworld": {
"name": "Tatooine"
}
},
"hasNext": true
}
很明顯的可以看出,其中多了 label
跟 hasNext
可以幫助 client 去定位以及判斷是否已經拿完所有的 payload。
而在 @stream
的部分,一樣多了 label
跟 hasNext
可以協助 client:
query {
person(id: "cGVvcGxlOjE=") {
...HomeWorldFragment @defer(label: "homeWorldDefer")
name
films @stream(initialCount: 2, label: "filmsStream") {
title
}
}
}
fragment HomeWorldFragment on Person {
homeworld {
name
}
}
拿回的 Payload 1:
{
"data": {
"person": {
"name": "Luke Skywalker",
"films": [
{ "title": "A New Hope" },
{ "title": "The Empire Strikes Back" }
]
}
},
"hasNext": true
}
拿回的 Payload 2:
{
"label": "filmsStream",
"path": ["person", "films", 2],
"data": {
"title": "Return of the Jedi"
},
"hasNext": true
}
拿回的 Payload 3:
{
"label": "filmsStream",
"path": ["person", "films", 3],
"data": {
"title": "Revenge of the Sith"
},
"hasNext": false
}
除此之外有 initialCount
也很方便,可以輕鬆決定第一個 payload 要包含多少個 array item,畢竟第一個畫面通常需要有一定筆數的 data。
Incremental Delivery over HTTP
雖然 GraphQL specification 沒有指定 transport protocols,但「Incremental Delivery over HTTP」這份有描述了接下來大部分 server 都會採用的 transfer-encoding: chunked
。用這個方式可以保持 HTTP 的連線,每個 payload 準備好馬上傳輸,而且從古老的瀏覽器到現代的瀏覽器都有支援。
response body 則是使用 content-type: multipart/mixed
,格式如下,批次中間使用 boundary 隔開:
---
Content-Type: application/json; charset=utf-8
{"data":{"hello":"Hello Rob"},"hasNext":true}
---
Content-Type: application/json; charset=utf-8
{"data":{"test":"Hello World"},"path":[],"hasNext":false}
-----
JavaScript 實作狀況
為了支援 @defer
/@stream
語法以及 HTTP incremental delivery,目前的 library 跟 framework 都需要做一些小調整。
GraphQL.js 的部分,execute
的部分被改成能回傳 AsyncIterable
,詳細可以看這隻 PR。
而 server 部分,已經有實作相關功能的就是 express-graphql 以及 graphql-helix,大致上就是再把 AsyncIterable
處理成 multipart/mixed
的 HTTP 回應。
Apollo 的實作目前還在路上:
- Apollo Client:有一個 open PR,不過在 GraphQL.js 穩定版本支援
@defer
/@stream
之前,應該不會輕易合併釋出。 - Apollo Server:因架構跟多種 HTTP server 耦合,打算在 v4 做大型的 breaking change 來支援
@defer
/@stream
,詳細可以看 roadmap。
Facebook 自家的 Framework — Relay,貌似在更早之前就已經支援 @defer
/@stream
了,最近也試圖相容最新的 proposal,不過並沒有提供任何 server 的方案。
因為 @defer
/@stream
還在 specification draft 的階段,JavaScript 以外其他語言的支援狀況可以想見不會太好,不過後端語言的 GraphQL client 需要 @defer
/@stream
的機會不多,未來社群搞定 GraphQL language 跟 GraphQL HTTP server 的部分就行了。