幾年前,我在DocuSign帶領了一個開發團隊,任務是重寫一個有數千萬個用戶在使用的Web應用程序。當時還沒有可以支持前端的API,因爲從一開始,Web應用程序就是一個.NET大單體。西雅圖的API團隊在將拆分單體,並逐步暴露出RESTful API。這個API團隊由兩名工程師組成,發佈週期爲一個月,而我們在舊金山的前端團隊每週都會發布新版本。
API團隊的發佈週期太長,因爲很多(幾乎所有)功能都必須進行手動測試,這是可以理解的。它畢竟是一個單體,而且沒有適當的自動化測試——如果他們修改了一個地方,不知道在應用程序的其他地方會出現什麼問題。
我記得有一次,我們的前端團隊面臨爲某大會交付新版本的壓力,但我們忘記跟進一個重要的API變更,這個變更未被包含在即將發佈的API版本中。我們要麼一直等待,直到錯過截止日期,要麼有人願意放棄優先權,以便讓我們的變更包括在即將發佈的版本中。所幸的是,這個變更最後被包含在新版本中,我們也及時發佈了新的前端版本。我真的希望當時我們已經使用了GraphQL,因爲它可以消除對外部團隊及其發佈週期的重度依賴。
在這篇文章中,我將介紹GraphQL的優勢,以及爲什麼它會變得如此受歡迎。
很多公司已經在內部從RESTful轉向了GraphQL API:IBM、Twitter、Walmart Labs、紐約時報、Intuit、Coursera,等等。
其他一些公司不僅是在內部而且還將外部API也轉爲GraphQL:AWS、Yelp、GitHub、Facebook和Shopify,等等。GitHub甚至打算停止使用REST API,他們的v4版本只使用GraphQL。
GraphQL究竟是一個炒作流行語還是真正會帶來一場變革?有趣的是,我之前列出的大多數從GraphQL獲益的公司都有以下這些共同點。
-
他們擁有包括移動端在內的多個客戶端;
-
他們正在轉向或者已經採用了微服務架構;
-
他們的遺留REST API數量暴增,變得十分複雜;
-
他們希望消除客戶端團隊對API團隊的依賴;
-
他們注重良好的API文檔和開發者體驗。
GitHub工程團隊表明了他們的動機:
“GraphQL彌合了發佈的內容與可以使用的內容之間的差距。我們真的很期待能夠同時發佈它們。GraphQL代表了API開發的巨大飛躍。類型安全、內省、生成文檔和可預測的響應都爲我們平臺的維護者和消費者帶來了好處。我們期待着由GraphQL提供支持的平臺進入新時代,也希望你們也這樣做!”
GraphQL加速了開發速度,提升了開發者體驗,並提供了更好的工具。我並不是說這絕對是這樣的,但我會盡力說明GraphQL與REST之間的爭論點及其原因。
超級數據聚合器
我是Indeed(世界排名第一的求職網站)的軟件工程負責人,所以讓我們先來看看Indeed.com的主頁和職位查詢結果頁面。它們分別發出了10和11個XHR請求。
需要注意的是,在REST中使用POST進行頁面瀏覽並不是很“正規”。
以下是其中的一些調用:
在使用GraphQL時,上面的這些請求可以被包含在單個查詢和單個請求中。
query HomePage {
getConversationCount(...) {
...
}
jobdescs(...) {
...
}
vjslog(...) {
...
}
preccount(...) {
…
}
jobalert(...) {
…
}
count(...) {
…
}
}
響應結果可能是這樣的:
{
"data": {
"getConversationCount": [
{
...
}
],
"vjslog": [...],
"preccount": [...],
"jobalert": [...],
"count": {}
},
"errors": []
}
通常,單個調用比多個調用更方便、更有效,因爲它需要更少的代碼和更少的網絡開銷。來自PayPal過程團隊的開發體驗還證實,很多UI工作實際上不是UI工作,而是其他任務,例如前端和後端之間的通信:
“我們發現,UI開發人員實際用於構建UI的時間不到三分之一,剩下的時間用於確定在何處以及如何獲取數據、過濾/映射數據以及編排API調用,還有一些用於構建和部署。”
需要注意的是,有實時使多個請求也是有必要的,例如多個單獨的請求可以快速且異步獨立地獲取不同的數據,如果採用了微服務架構,它們會增加部署靈活性,而且它們的故障點是多個,而不是一個。
此外,如果頁面是由多個團隊開發的,GraphQL提供了一個功能,可以將查詢分解稱爲片段。稍後我們將詳細介紹這方面的內容。
從更大的角度來看,GraphQL API的主要應用場景是API網關,在客戶端和服務之間提供了一個抽象層。
微服務架構很好,但也存在一些問題,GraphQL可以用來解決這些問題。以下是來自IBM在微服務架構中使用GraphQL的經驗:
“總的來說,GraphQL微服務的開發和部署都非常快。他們5月份開始開發,7月份就進入了生產環境。因爲他們不需要徵得許可,直接開幹。他強烈推薦這個方案,比開會討論好太多了。”
接下來,讓我們逐一討論GraphQL的每一個好處。
提高開發速度
首先,GraphQL有助於減少發出的請求數。通過單個調用來獲取所需的數據比使用多個請求要容易得多。從工程師的角度來看,這加快了開發速度。後面我會解釋更多有關爲什麼會提升開發速度的原因,但現在我想先說明另一個問題。
後端和客戶端團隊需要通過密切合作來定義API、測試它們,並做出更改。前端、移動、物聯網(例如Alexa)等客戶端團隊不斷迭代功能,並嘗試使用新的UX和設計。他們的數據需求經常發生變化,後端團隊必須跟上他們的節奏。如果客戶端和後端代碼由同一團隊負責,那麼問題就沒那麼嚴重了。Indeed的大多數工程團隊都是由全棧工程師組成,但並非全部都是這樣。對於非全棧團隊,客戶端團隊經常因爲依賴了後端團隊開發速度受到影響。
當我轉到Job Seeker API團隊時,移動團隊開始我們的開發進度。我們之間有很多關於參數、響應字段和測試的事情需要溝通。
在使用了GraphQL之後,客戶端工程師就可以完全控制前端,不需要依賴任何人,因爲他們可以告訴後端他們需要什麼以及響應結構應該是怎樣的。他們使用了GraphQL查詢,它們會告訴後端API應該要提供哪些數據。
客戶端工程師不需要花時間讓後端API團隊添加或修改某些內容。GraphQL具有自文檔的特點,所以可以節省一些用於查找文檔以便了解如何使用API的時間。我相信大多數人曾經在找出確切的請求參數方面浪費了很多時間。GraphQL協議本身及其社區在文檔方面爲我們提供了一些有用的工具。在某些情況下,可以從模式自動生成文檔。其他時候,只需使用GraphiQL Web界面就足以編寫一個查詢。
來自紐約時報的工程師表示,他們在轉到GraphQL和Relay之後,在做出變更時不需要改太多的東西:
“當我們想要更新所有產品的設計時,不再需要修改多個代碼庫。這就是我們想要的。我們認爲Relay和GraphQL是幫助我們實現這個偉大目標的完美工具。”
當一家公司已經擁有大量GraphQL API,然後有人想出了一個新的產品創意,這也是我最喜歡GraphQL的應用場景。使用已有的GraphQL API實現原型比調用各種REST端點(將提供太少或太多的數據)或爲新應用程序構建新的REST API要快得多。
開發速度的提升與開發者體驗的提升密切相關。
提升開發者體驗
GraphQL提供了更好的開發者體驗(DX),開發者將花更少的時間思考如何獲取數據。在使用Apollo時,他們只需要在UI中聲明數據。數據和UI放在一起,閱讀代碼和編寫代碼都變得更方便。
通常,在開發UI時需要在UI模板、客戶端代碼和UI樣式之間跳轉。GraphQL允許工程師在客戶端開發UI,減少摩擦,因爲工程師在添加或修改代碼時無需在文件之間切換。如果你熟悉React,這裏有一個很好的比喻:GraphQL之於數據,就像React之於UI。
下面是一個簡單的示例,UI中直接包含了屬性名稱launch.name和launch.rocket.name。
const GET_LAUNCHES = gql`
query launchList($after: String) {
launches(after: $after) {
launches {
id
name
isBooked
rocket {
id
name
}
}
}
}
`;
export default function Launches() {
return (
<Query query={GET_LAUNCHES}>
{({ data, loading, error }) => {
if (loading) return <Loading />;
if (error) return <p>ERROR</p>;
return (
<div>
{data.launches.launches.map(launch => (
<div
key={launch.id}
>{launch.name}<br/>
Rocket: {launch.rocket.name}
</div>
))}
</div>
);
}}
</Query>
);
};
使用這種方法,可以非常容易地修改或向UI或查詢(gql)添加新字段。React組件的可移植性更強了,因爲它們描述了所需的所有數據。
如前所述, GraphQL提供了更好的文檔,而且還有一個叫作GraphiQL的IDE:
前端工程師很喜歡GraphiQL,下面引用Indeed的一位高級工程師說過的話:
“我認爲開發體驗中最好的部分是能夠使用GraphiQL。對我來說,與典型的API文檔相比,這是一種編寫查詢更有效的輔助方法”。
GraphQL的另一個很棒的功能是片段,因爲它允許我們在更高的組件層面重用查詢。
這些功能改善了開發者體驗,讓開發人員更快樂,更不容易出現JavaScript疲勞。
提升性能
工程師並不是唯一從GraphQL中受益的人。用戶也會從中受益,因爲應用程序的性能獲得了提升(可以感知到的):
1.減少了有效載荷(客戶端只需要必要的東西);
2.多個請求合併爲一個請求可減少網絡開銷;
3.使用工具可以更輕鬆地實現客戶端緩存和後端批處理和後端緩存;
4.預取;
5.更快的UI更新。
PayPal使用GraphQL重新設計了他們的結賬流程。下面是來自用戶的反饋:
“REST的原則並沒有爲Web和移動應用及其用戶的需求考慮,這個在結賬優化交易中體現得尤爲明顯。用戶希望能夠儘快完成結賬,如果應用程序使用了很多原子REST API,就需要在客戶端和服務器之間進行多次往返以獲取數據。我們的結賬每次往返網絡時間至少需要700毫秒,這還不包括服務器處理請求的時間。每次往返都會導致渲染變慢,用戶體驗不好,結算轉換率也會降低。”
性能改進中有一項是“多個請求組合成一個請求可以減少網絡開銷”。對於HTTP/1而言,這是非常正確的,因爲它沒有HTTP/2那樣的多路複用機制。但儘管HTTP/2提供的多路複用機制有助於優化單獨的請求,但它對於圖遍歷(獲取相關或嵌套對象)並沒有實際幫助。讓我們來看一看REST和GraphQL是如何處理嵌套對象和其他複雜請求的。
標準化和簡化複雜的API
通常,客戶端會發出複雜的請求來獲取有序、排好序、被過濾過的數據或子集(用於分頁),或者請求嵌套對象。GraphQL支持嵌套數據和其他難以使用標準REST API資源(也叫端點或路由)實現的查詢。
例如,我們假設有三種資源:用戶、訂閱和簡歷。工程師需要按順序進行兩次單獨的調用(這會降低性能)來獲取一個用戶簡歷,首先需要通過調用獲取用戶資源,拿到簡歷ID,然後再使用簡歷ID來獲取簡歷數據。對於訂閱來說也是一樣的。
1.GET /users/123:響應中包含了簡歷ID和工作崗位通知訂閱的ID清單;
2.GET /resumes/ABC:響應中包含了簡歷文本——依賴第一個請求;
3.GET /subscriptions/XYZ:響應中包含了工作崗位通知的內容和地址——依賴第一個請求。
上面的示例很糟糕,原因有很多:客戶端可能會獲得太多數據,並且必須等待相關的請求完成了以後才能繼續。此外,客戶端需要實現如何獲取子資源(例如建立或訂閱)和過濾。
想象一下,一個客戶端可能只需要第一個訂閱的內容和地址以及簡歷中的當前職位,另一個客戶端可能需要所有訂閱和整個簡歷列表。所以,如果使用REST API,對第一個客戶端來說有點不划算。
另一個例子:用戶表裏可能會有用戶的名字和姓氏、電子郵件、簡歷、地址、電話、社會保障號、密碼(當然是經過混淆的)和其他私人信息。並非每個客戶端都需要所有字段,有些應用程序可能只需要用戶電子郵件,所以向這些應用程序發送社會保障號等信息就不太安全。
當然,爲每個客戶端創建不同的端點也是不可行的,例如/api/v1/users和/api/v1/usersMobile。事實上,各種客戶端通常都有不同的數據需求:/api/v1/userPublic、/api/v1/userByName、/api/v1/usersForAdmin,如果這樣的話,端點會呈指數級增長。
GraphQL允許客戶要求API發送他們想要的字段,這將使後端工作變得更加容易:/api/gql——所有客戶端只需要這個端點。
注意:對於REST和GraphQL,後端都需要使用訪問控制級別。
或者可以使用舊REST來實現GraphQL的很多功能。但是這樣要付出什麼代價?後端可以支持複雜的RESTful請求,這樣客戶端就可以使用字段和嵌套對象進行調用:
GET /users/?fields=name,address&include=resumes,subscriptions
上面的請求將比使用多個REST請求更好,但它不是標準化的,不受客戶端庫支持,而且這樣的代碼也更難編寫和維護。對於相對複雜的API,工程師需要在查詢中使用自己的查詢字符串參數約定,最終得到類似GraphQL的東西。既然GraphQL已經提供了標準和庫,爲什麼還要基於REST設計自己的查詢約定呢?
將複雜的REST端點與以下的GraphQL嵌套查詢進行對比,嵌套查詢使用了更多的過濾條件,例如“只要給我前X個對象”和“按時間按升序排列”(可以添加無限制的過濾選項):
{
user (id: 123) {
id
firstName
lastName
address {
city
country
zip
}
resumes (first: 1, orderBy: time_ASC) {
text
title
blob
time
}
subscriptions(first: 10) {
what
where
time
}
}
}
}
在使用GraphQL時,我們可以在查詢中保留嵌套對象,對於每個對象,我們將精確地獲得我們需要的數據,不多也不少。
響應消息的數據格式反映了請求查詢的結構,如下所示:
{
"data": {
"user": {
"id": 123,
"firstName": "Azat",
"lastName": "Mardan",
"address": {
"city": "San Francisco",
"country": "US",
"zip": "94105"
},
"resumes" [
{
"text": "some text here...",
"title": "My Resume",
"blob": "<BLOB>",
"time": "2018-11-13T21:23:16.000Z"
},
],
"subscriptions": [ ]
},
"errors": []
}
相比複雜的REST端點,使用GraphQL的另一個好處是提高了安全性。這是因爲URL經常會被記錄下來,而RESTful GET端點依賴於查詢字符串(是URL的一部分)。這可能會暴露敏感數據,所以RESTful GET請求的安全性低於GraphQL的POST請求。我打賭這就是爲什麼Indeed主頁會使用POST發出“閱讀”頁面請求。
使用GraphQL可有更容易地實現分頁等關鍵功能,這要歸功於查詢以及BaaS提供商提供的標準,以及後端的實現和客戶端庫使用的標準。
改進的安全性、強類型和驗證
GraphQL的schema與語言無關。對前面的示例進行擴展,我們可以在schema中定義Address類型:
type Address {
city: String!
country: String!
zip: Int
}
String和Int是標量類型,!表示字段不可爲空。
schema驗證是GraphQL規範的一部分,因此像這樣的查詢將返回錯誤,因爲name和phone不是Address對象的字段:
{
user (id: 123) {
address {
name
phone
}
}
}
我們可以使用我們的類型構建複雜的GraphQL schema。例如,用戶類型可能會使用我們的地址、簡歷和訂閱類型,如下所示:
type User {
id: ID!
firstName: String!
lastName: String!
address: Address!
resumes: [Resume]
subscriptions: [Subscription]
}
Indeed的大量對象和類型都是使用ProtoBuf定義的。類型化數據並不是什麼新鮮事物,而且類型數據的好處也是衆所周知。與發明新的JSON類型標準相比,GraphQL的優點在於已經存在可以從ProtoBuf自動換換到GraphQL的庫。即使其中一個庫(rejoiner)不能用,也可以開發自己的轉換器。
GraphQL提供了比JSON RESTful API更強的安全性,主要有兩個原因:強類型schema(例如數據驗證和無SQL注入)以及精確定義客戶端所需數據的能力(不會無意泄漏數據)。
靜態驗證是另一個優勢,可以幫助工程師節省時間,並在進行重構時提升工程師的信心。諸如eslint-plugin-graphql之類的工具可以讓工程師知道後端發生的變化,並讓後端工程師確保不會破壞客戶端代碼。
保持前端和後端之間的契約是非常重要的。在使用REST API時,我們要小心不要破壞了客戶端代碼,因爲客戶端無法控制響應消息。相反,GraphQL爲客戶端提供了控制,GraphQL可以頻繁更新,而不會因爲引入了新類型造成重大變更。因爲使用了schema,所以GraphQL是一種無版本的API。
GraphQL的實現
在選擇實現GraphQL API的平臺時,Node是一個候選項,因爲最初GraphQL用於Web應用程序和前端,而Node是開發Web應用程序的首選,因爲它是基於JavaScript的。使用Node可以非常容易地實現GraphQL(假設提供了schema)。事實上,使用Express或Koa來實現只需要幾行代碼:
const Koa = require('koa');
const Router = require('koa-router'); // [email protected]
const graphqlHTTP = require('koa-graphql');
const app = new Koa();
const router = new Router();
router.all('/graphql', graphqlHTTP({
schema: schema,
graphiql: true
}));
app.use(router.routes()).use(router.allowedMethods());
schema是使用npm的graphql中的類型來定義的。Query和Mutation是特殊的schema類型。
GraphQL API的大部分實現都在於schema和解析器。解析器可以包含任意代碼,但最常見的是以下五個主要類別:
-
調用Thrift、gRPC或其他RPC服務;
-
調用HTTP REST API(當優先事項不是重寫現有REST API時);
-
直接調用數據存儲;
-
調用其他GraphQL schema查詢或服務;
-
調用外部API。
這裏有一個示例。
Node很棒,但在Indeed,我們主要使用Java。包括Java在內的很多語言都支持GraphQL,例如https://github.com/graphql-go和https://github.com/graphql-python。
由於Indeed主要使用了Java,因此這裏給出一個使用graphql-java的Java GraphQL示例,完整代碼位於這裏。它定義了/graphql端點:
import com.coxautodev.graphql.tools.SchemaParser;
import javax.servlet.annotation.WebServlet;
import graphql.servlet.SimpleGraphQLServlet;
@WebServlet(urlPatterns = "/graphql")
public class GraphQLEndpoint extends SimpleGraphQLServlet {
public GraphQLEndpoint() {
super(SchemaParser.newParser()
.file("schema.graphqls") //parse the schema file created earlier
.build()
.makeExecutableSchema());
}
}
GraphQL的schema使用POJO來定義。GraphQL端點類使用了LinkRepository POJO。解析器包含了操作的(例如獲取鏈接)實際代碼:
@WebServlet(urlPatterns = "/graphql")
public class GraphQLEndpoint extends SimpleGraphQLServlet {
public GraphQLEndpoint() {
super(buildSchema());
}
private static GraphQLSchema buildSchema() {
LinkRepository linkRepository = new LinkRepository();
return SchemaParser.newParser()
.file("schema.graphqls")
.resolvers(new Query(linkRepository))
.build()
.makeExecutableSchema();
}
}
在很多情況下,GraphQL的schema可以從其他類型的schema自動生成,例如gRPC、Boxcar、ProtoBuf或ORM/ODM。
GraphQL不一定需要客戶端。一個簡單的GraphQL請求就是一個常規的POST HTTP請求,其中包含了查詢內容。我們可以使用任意的HTTP代理庫(如CURL、axios、fetch、superagent等)來生成請求。例如,在終端中使用curl發送請求:
curl \
-X POST \
-H "Content-Type: application/json" \
--data '{ "query": "{ posts { title } }" }' \
https://1jzxrj179.lp.gql.zone/graphql
以下代碼可以在任意一個現代瀏覽器(爲了避免CORS,請訪問launchpad.graphql.com)中運行。
fetch('https://1jzxrj179.lp.gql.zone/graphql', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query: '{ posts { title } }' }),
})
.then(res => res.json())
.then(res => console.log(res.data));
雖然構建GraphQL請求很容易,但是還需要實現很多其他東西,比如緩存,因爲緩存可以極大地改善用戶體驗。構建客戶端緩存不是那麼容易,所幸的是,Apollo和Relay Modern等提供了開箱即用的客戶端緩存。
什麼時候不該使用GraphQL?
當然,完美的解決方案是不存在的(儘管GraphQL接近完美),還有一些問題需要注意,例如:
1.它有單點故障嗎?
2.它可以擴展嗎?
3.誰在使用GraphQL?
最後,以下列出了我們自己的有關GraphQL可能不是一個好選擇的主要原因:
-
當客戶端的需求很簡單時:如果你的API很簡單,例如/users/resumes/123,那麼GraphQL就顯得有點重了;
-
爲了加快加載速度使用了異步資源加載;
-
在開發新產品時使用新的API,而不是基於已有的API;
-
不打算向公衆公開API;
-
不需要更改UI和其他客戶端;
-
產品開發不活躍;
-
使用了其他一些JSON schema或序列化格式。
總結
GraphQL是一種協議和一種查詢語言。GraphQL API可以直接訪問數據存儲,但在大多數情況下,GraphQL API是一個數據聚合器和一個抽象層,一個可以提升開發速度、減少維護工作並讓開發人員更快樂的層。因此,GraphQL比公共API更有意義。很多公司開始採用GraphQL。IBM、PayPal和GitHub聲稱在使用GraphQL方面取得了巨大的成功。如果GraphQL很有前途,我們現在是否可以停止構建過時且笨重的REST API,並擁抱GraphQL?
英文原文:https://webapplog.com/graphql/
更多內容,請關注前端之巔(ID:frontshow)