GraphQL 與 REST - GraphQL 教程
已發表: 2022-03-11你可能聽說過這個街區的新孩子:GraphQL。 如果不是,GraphQL 是一種獲取 API 的新方法,是 REST 的替代方案。 它最初是 Facebook 的一個內部項目,由於它是開源的,因此獲得了很大的關注。
本文的目的是幫助您輕鬆地從 REST 過渡到 GraphQL,無論您是否已經決定使用 GraphQL,或者您只是願意嘗試一下。 不需要 GraphQL 的先驗知識,但需要熟悉 REST API 才能理解本文。
文章的第一部分將首先給出我個人認為 GraphQL 優於 REST 的三個原因。 第二部分是關於如何在後端添加 GraphQL 端點的教程。
Graphql 與 REST:為什麼要放棄 REST?
如果您仍然對 GraphQL 是否適合您的需求猶豫不決,這裡給出了“REST 與 GraphQL”的相當廣泛和客觀的概述。 但是,對於我使用 GraphQL 的三大理由,請繼續閱讀。
原因一:網絡性能
假設您在後端有一個用戶資源,其中包含名字、姓氏、電子郵件和其他 10 個字段。 在客戶端上,您通常只需要其中的幾個。
在/users
端點上進行 REST 調用會返回用戶的所有字段,客戶端只使用它需要的字段。 顯然存在一些數據傳輸浪費,這可能是移動客戶端的考慮因素。
默認情況下,GraphQL 會獲取盡可能少的數據。 如果您只需要用戶的名字和姓氏,請在查詢中指定。
下面的接口叫做 GraphiQL,它就像一個 GraphQL 的 API 瀏覽器。 為了本文的目的,我創建了一個小項目。 代碼託管在 GitHub 上,我們將在第二部分深入研究。
在界面的左側窗格中是查詢。 在這裡,我們正在獲取所有用戶——我們將使用 REST GET /users
users——並且只獲取他們的名字和姓氏。
詢問
query { users { firstname lastname } }
結果
{ "data": { "users": [ { "firstname": "John", "lastname": "Doe" }, { "firstname": "Alicia", "lastname": "Smith" } ] } }
如果我們也想獲取電子郵件,在“姓氏”下方添加“電子郵件”行就可以了。
一些 REST 後端確實提供像/users?fields=firstname,lastname
這樣的選項來返回部分資源。 對於它的價值,谷歌推薦它。 但是,默認情況下它沒有實現,並且它使請求幾乎不可讀,尤其是當您折騰其他查詢參數時:
-
&status=active
過濾活躍用戶 &sort=createdAat
按創建日期對用戶進行排序&sortDirection=desc
因為你顯然需要它&include=projects
包含用戶的項目
這些查詢參數是添加到 REST API 以模仿查詢語言的補丁。 GraphQL 首先是一種查詢語言,它使請求從一開始就簡潔準確。
原因 2:“包含與端點”設計選擇
假設我們要構建一個簡單的項目管理工具。 我們擁有三種資源:用戶、項目和任務。 我們還定義了資源之間的以下關係:
以下是我們向世界公開的一些端點:
端點 | 描述 |
---|---|
GET /users | 列出所有用戶 |
GET /users/:id | 獲取具有 id 的單個用戶:id |
GET /users/:id/projects | 獲取一個用戶的所有項目 |
端點簡單、易讀且組織良好。
當我們的請求變得更加複雜時,事情就會變得更加棘手。 讓我們以GET /users/:id/projects
端點為例:假設我只想在主頁上顯示項目的標題,但在儀表板上顯示項目+任務,而不進行多次 REST 調用。 我會打電話給:
-
GET /users/:id/projects
。 - 在儀表板頁面上
GET /users/:id/projects?include=tasks
(例如),以便後端附加所有相關任務。
通常的做法是添加查詢參數?include=...
以使其工作,甚至被 JSON API 規範推薦。 像?include=tasks
這樣的查詢參數仍然是可讀的,但不久之後,我們將得到?include=tasks,tasks.owner,tasks.comments,tasks.comments.author
。
在這種情況下,創建/projects
端點來執行此操作是否更明智? /projects?userId=:id&include=tasks
之類的東西,因為我們會少包含一層關係? 或者,實際上, /tasks?userId=:id
端點也可以工作。 這可能是一個困難的設計選擇,如果我們有一個多對多的關係,那就更複雜了。
GraphQL 在任何地方都使用include
方法。 這使得獲取關係的語法強大且一致。
這是一個從 id 為 1 的用戶那裡獲取所有項目和任務的示例。
詢問
{ user(id: 1) { projects { name tasks { description } } } }
結果
{ "data": { "user": { "projects": [ { "name": "Migrate from REST to GraphQL", "tasks": [ { "description": "Read tutorial" }, { "description": "Start coding" } ] }, { "name": "Create a blog", "tasks": [ { "description": "Write draft of article" }, { "description": "Set up blog platform" } ] } ] } } }
如您所見,查詢語法很容易閱讀。 如果我們想更深入地包括任務、評論、圖片和作者,我們不會三思而後行如何組織我們的 API。 GraphQL 使獲取複雜對像變得容易。
原因3:管理不同類型的客戶
在構建後端時,我們總是首先嘗試使 API 盡可能地被所有客戶端廣泛使用。 然而,客戶總是希望少打電話,多取錢。 通過深度包含、部分資源和過濾,Web 和移動客戶端發出的請求可能彼此之間存在很大差異。
對於 REST,有幾個解決方案。 我們可以創建自定義端點(即別名端點,例如/mobile_user
)、自定義表示( Content-Type: application/vnd.rest-app-example.com+v1+mobile+json
),甚至是客戶端- 特定的 API(就像 Netflix 曾經做過的那樣)。 這三個都需要後端開發團隊付出額外的努力。
GraphQL 為客戶端提供了更多的權力。 如果客戶端需要復雜的請求,它會自己構建相應的查詢。 每個客戶端可以以不同的方式使用相同的 API。
如何開始使用 GraphQL
在當今大多數關於“GraphQL vs. REST”的辯論中,人們認為他們必須選擇兩者之一。 這是不正確的。
現代應用程序通常使用幾種不同的服務,這些服務公開了幾個 API。 我們實際上可以將 GraphQL 視為所有這些服務的網關或包裝器。 所有客戶端都將訪問 GraphQL 端點,而該端點將訪問數據庫層、外部服務(如 ElasticSearch 或 Sendgrid)或其他 REST 端點。
使用兩者的第二種方法是在您的 REST API 上有一個單獨的/graphql
端點。 如果您已經有大量客戶端訪問您的 REST API,但您想在不損害現有基礎設施的情況下嘗試 GraphQL,這將特別有用。 這就是我們今天正在探索的解決方案。
如前所述,我將通過 GitHub 上提供的一個小示例項目來說明本教程。 它是一個簡化的項目管理工具,包含用戶、項目和任務。
該項目使用的技術是用於 Web 服務器的 Node.js 和 Express,作為關係數據庫的 SQLite,以及作為 ORM 的 Sequelize。 三個模型(用戶、項目和任務)在models
文件夾中定義。 REST 端點/api/users
、 /api/projects
和/api/tasks
向世界公開,並在rest
文件夾中定義。
請注意,GraphQL 可以使用任何編程語言安裝在任何類型的後端和數據庫上。 選擇這裡使用的技術是為了簡單和可讀性。
我們的目標是在不刪除 REST 端點的情況下創建/graphql
端點。 GraphQL 端點將直接訪問數據庫 ORM 以獲取數據,因此它完全獨立於 REST 邏輯。
類型
數據模型在 GraphQL 中由types表示,它們是強類型的。 您的模型和 GraphQL 類型之間應該存在一對一的映射。 我們的User
類型將是:
type User { id: ID! # The "!" means required firstname: String lastname: String email: String projects: [Project] # Project is another GraphQL type }
查詢
查詢定義了可以在 GraphQL API 上運行的查詢。 按照慣例,應該有一個RootQuery
,其中包含所有現有的查詢。 我還指出了每個查詢的 REST 等價物:

type RootQuery { user(id: ID): User # Corresponds to GET /api/users/:id users: [User] # Corresponds to GET /api/users project(id: ID!): Project # Corresponds to GET /api/projects/:id projects: [Project] # Corresponds to GET /api/projects task(id: ID!): Task # Corresponds to GET /api/tasks/:id tasks: [Task] # Corresponds to GET /api/tasks }
突變
如果查詢是GET
請求,突變可以被視為POST
/ PATCH
/ PUT
/ DELETE
請求(儘管它們實際上是查詢的同步版本)。
按照慣例,我們將所有的突變放在一個RootMutation
中:
type RootMutation { createUser(input: UserInput!): User # Corresponds to POST /api/users updateUser(id: ID!, input: UserInput!): User # Corresponds to PATCH /api/users removeUser(id: ID!): User # Corresponds to DELETE /api/users createProject(input: ProjectInput!): Project updateProject(id: ID!, input: ProjectInput!): Project removeProject(id: ID!): Project createTask(input: TaskInput!): Task updateTask(id: ID!, input: TaskInput!): Task removeTask(id: ID!): Task }
請注意,我們在這裡引入了新類型,稱為UserInput
、 ProjectInput
和TaskInput
。 這也是 REST 的常見做法,用於創建用於創建和更新資源的輸入數據模型。 在這裡,我們的UserInput
類型是沒有id
和projects
字段的User
類型,請注意關鍵字input
而不是type
:
input UserInput { firstname: String lastname: String email: String }
架構
通過類型、查詢和突變,我們定義了GraphQL 模式,這是 GraphQL 端點向世界公開的內容:
schema { query: RootQuery mutation: RootMutation }
這個模式是強類型的,這讓我們能夠在 GraphiQL 中擁有那些方便的自動完成功能。
解析器
現在我們有了公共模式,是時候告訴 GraphQL 在請求每個查詢/突變時要做什麼了。 解決者做艱苦的工作; 例如,他們可以:
- 命中內部 REST 端點
- 調用微服務
- 命中數據庫層進行 CRUD 操作
我們在示例應用程序中選擇第三個選項。 讓我們看看我們的解析器文件:
const models = sequelize.models; RootQuery: { user (root, { id }, context) { return models.User.findById(id, context); }, users (root, args, context) { return models.User.findAll({}, context); }, // Resolvers for Project and Task go here }, /* For reminder, our RootQuery type was: type RootQuery { user(id: ID): User users: [User] # Other queries }
這意味著,如果在 GraphQL 上請求user(id: ID!)
查詢,那麼我們從數據庫返回User.findById()
,這是一個 Sequelize ORM 函數。
在請求中加入其他模型怎麼樣? 好吧,我們需要定義更多的解析器:
User: { projects (user) { return user.getProjects(); // getProjects is a function managed by Sequelize ORM } }, /* For reminder, our User type was: type User { projects: [Project] # We defined a resolver above for this field # ...other fields } */
因此,當我們在 GraphQL 中請求User
類型中的projects
字段時,此連接將附加到數據庫查詢中。
最後,突變解析器:
RootMutation: { createUser (root, { input }, context) { return models.User.create(input, context); }, updateUser (root, { id, input }, context) { return models.User.update(input, { ...context, where: { id } }); }, removeUser (root, { id }, context) { return models.User.destroy(input, { ...context, where: { id } }); }, // ... Resolvers for Project and Task go here }
你可以在這裡玩這個。 為了保持服務器上的數據乾淨,我禁用了突變解析器,這意味著突變不會在數據庫中執行任何創建、更新或刪除操作(因此在界面上返回null
)。
詢問
query getUserWithProjects { user(id: 2) { firstname lastname projects { name tasks { description } } } } mutation createProject { createProject(input: {name: "New Project", UserId: 2}) { id name } }
結果
{ "data": { "user": { "firstname": "Alicia", "lastname": "Smith", "projects": [ { "name": "Email Marketing Campaign", "tasks": [ { "description": "Get list of users" }, { "description": "Write email template" } ] }, { "name": "Hire new developer", "tasks": [ { "description": "Find candidates" }, { "description": "Prepare interview" } ] } ] } } }
為現有應用重寫所有類型、查詢和解析器可能需要一些時間。 但是,有很多工具可以幫助您。 例如,有一些工具可以將 SQL 模式轉換為 GraphQL 模式,包括解析器!
把所有東西放在一起
使用定義明確的模式和解析器對模式的每個查詢執行什麼操作,我們可以在後端掛載一個/graphql
端點:
// Mount GraphQL on /graphql const schema = makeExecutableSchema({ typeDefs, // Our RootQuery and RootMutation schema resolvers: resolvers() // Our resolvers }); app.use('/graphql', graphqlExpress({ schema }));
我們可以在後端擁有一個漂亮的 GraphiQL 界面。 要在沒有 GraphiQL 的情況下發出請求,只需複制請求的 URL,然後使用 cURL、AJAX 或直接在瀏覽器中運行它。 當然,有一些 GraphQL 客戶端可以幫助您構建這些查詢。 請參閱下面的一些示例。
下一步是什麼?
本文的目的是讓您了解 GraphQL 的樣子,並向您展示在不丟棄 REST 基礎架構的情況下嘗試 GraphQL 絕對是可能的。 了解 GraphQL 是否適合您的需求的最佳方法是親自嘗試。 我希望這篇文章能讓你潛水。
有很多我們沒有在本文中討論的特性,例如實時更新、服務器端批處理、身份驗證、授權、客戶端緩存、文件上傳等。了解這些特性的絕佳資源是如何使用 GraphQL。
以下是一些其他有用的資源:
服務器端工具 | 描述 |
---|---|
graphql-js | GraphQL 的參考實現。 您可以將它與express-graphql 一起使用來創建服務器。 |
graphql-server | 由 Apollo 團隊創建的一體化 GraphQL 服務器。 |
其他平台的實現 | 紅寶石、PHP 等 |
客戶端工具 | 描述 |
---|---|
中繼 | 用於連接 React 和 GraphQL 的框架。 |
阿波羅客戶端。 | 一個 GraphQL 客戶端,綁定了 React、Angular 2 和其他前端框架。 |
總之,我相信 GraphQL 不僅僅是炒作。 它明天還不會取代 REST,但它確實為真正的問題提供了一個高性能的解決方案。 它相對較新,最佳實踐仍在發展中,但它絕對是我們將在未來幾年內聽到的一項技術。