1. <strong id="7actg"></strong>
    2. <table id="7actg"></table>

    3. <address id="7actg"></address>
      <address id="7actg"></address>
      1. <object id="7actg"><tt id="7actg"></tt></object>

        為什么我使用 GraphQL 而放棄 REST API?

        共 9532字,需瀏覽 20分鐘

         ·

        2021-05-22 00:40

        點擊上方 前端Q,關注公眾號

        回復加群,加入前端Q技術交流群




        作者 | Max Desiatov
        譯者 | 平川
        策劃 | 萬佳

        本文最初發(fā)布于 Max Desiatov 的個人博客,經原作者授權由 InfoQ 中文站翻譯并分享。

        在大多數移動和 Web 應用中,服務器交互需要花費開發(fā)人員大量時間和精力來開發(fā)和測試。

        在我所開發(fā)的那些擁有最復雜 API 應用程序中,網絡層設計和維護占去高達 40% 的開發(fā)時間,特別是由于我在本文中提到的一些邊緣情況。這樣實現過幾次后,很容易就會發(fā)現,有一些不同的模式、工具和框架可以帶來幫助。雖然我們很幸運,不必再關心 SOAP,但 REST 也不是歷史的終結。

        最近,我有機會為自己的項目和客戶開發(fā)和運行一些使用 GraphQL API 構建的移動和 Web 應用程序。這真是一個很好的體驗,尤其要感謝令人驚嘆的 PostGraphile 和 Apollo。至此,我再也無法回過頭來享受使用 REST 的工作了。

        REST 有什么問題嗎?
         每個 REST API 都是獨特的

        公平地說,REST 甚至不是一個標準。維基百科將其定義為:

        一種架構風格,基于 HTTP 定義了一組約束和屬性。

        雖然確實存在像 JSON API 規(guī)范這樣的東西,但在實踐中,我們很少看到有 RESTful 后端實現它。在最好的情況下,你可能會偶然發(fā)現一些使用 OpenAPI/Swagger 的東西。即使這樣,OpenAPI 也沒有指定 API 的形狀或格式,它只是一個機器可讀的規(guī)范,允許(但不是要求)你對 API 運行自動化測試、自動生成文檔等。

        主要問題仍然存在。你可能會說你的 API 是 RESTful 的,但是對于如何安排端點或是否應該(例如)使用 HTTP 方法PATCH進行對象更新,一般沒有嚴格的規(guī)則。

        還有一些東西乍一看是 RESTful 的,但如果你仔細看,就不是那么像了:Dropbox HTTP API。

        端點接受請求體中的文件內容,因此,它們的參數將以 JSON 的形式在Dropbox-API-Arg請求頭或 arg URL 參數中傳遞。

        JSON 在請求頭中?

        沒錯,Dropbox API 端點要求你將請求正文留空,并將有效載荷序列化為 JSON,放到一個自定義的 HTTP 頭中。為這種特殊情況編寫客戶端代碼很有趣。我們不能抱怨,因為畢竟沒有廣泛使用的標準。

        事實上,下面提到的大多數注意事項都是由于缺乏標準造成的,但是我想強調一下在實踐中經??吹降那闆r。

        在一個有經驗的團隊中,你可以避免這些問題,但是你難道不希望一些問題已經在軟件方面得到解決嗎?

         沒有靜態(tài)類型意味著要注意類型驗證

        無論如何努力避免這種情況,你遲早會遇到 JSON 屬性拼寫錯誤、發(fā)送或接收的數據類型錯誤、字段丟失等問題。如果你的客戶端和 / 或服務器編程語言是靜態(tài)類型的,并且你不能用錯誤的字段名或類型構造對象,那可能沒問題。如果你的 API 是版本化的,舊 API 的 URL 為/API/v1,新版本的 URL 為/API/v2,那么你可能做得很好。如果有一個 OpenAPI 規(guī)范,可以為你生成客戶端 / 服務器類型聲明,那就更好了。

        但你真能負擔得起在所有項目中都做到這樣嗎?當你的團隊在沖刺期間決定重命名或重新安排對象字段時,你能負擔得起上線/api/v1.99端點的成本嗎?即使完成了,團隊會不會忘記更新規(guī)范并通知客戶端開發(fā)人員更新內容?

        在客戶端或服務器上的所有驗證邏輯,你確定都是正確的嗎?理想情況下,你希望它在兩邊都得到驗證,對吧?維護所有這些自定義代碼非常有趣?;蛘弑3?API JSON 模式是最新的。

         分頁和過濾并不簡單

        大多數 API 都使用對象集合。在待辦事項列表應用中,列表本身就是一個集合。大多數集合都可以包含 100 多個項。對于大多數服務器來說,在一次響應的一個集合中返回所有項是一個繁重的操作。如果再乘以在線用戶的數量,就會產生很大的 AWS 賬單。顯而易見的解決方案:只返回集合的子集。

        分頁相對簡單。在查詢參數中傳遞類似offsetlimit這樣的值:/todos?Limit =10&offset=20以獲得從 20 開始的 10 個對象。每個人對這些參數的命名都不一樣,有些人喜歡countskip,而我喜歡offsetlimit,因為它們直接對應于 SQL 修飾符。

        一些后端數據庫會暴露要傳遞給下一頁查詢的游標或標記。請查看 Elasticsearch API,該 API 建議在需要依次瀏覽大量結果文檔時使用scroll調用。還有一些 API 在頭中傳遞相關信息。參見 GitHub REST API(至少不是在頭中傳遞 JSON)。

        說到過濾,就有趣多了……需要按一個字段過濾嗎?沒問題,可能是/todos?filter=key%3Dvalue,也可能是可讀性更好的/todos?filterKey=key&filterValue=value。那么按兩個值過濾呢?這應該很簡單,對吧?使用 URL 編碼,查詢看起來是這個樣子:/todos?filterKeys=key1%2Ckey2&filterValue=value。但通常,我們沒有辦法阻止特性蔓延,可能會出現使用AND/OR操作符進行高級過濾的需求。或者復雜的全文搜索查詢和復雜的過濾。遲早你會看到一些 API 發(fā)明了自己的過濾 DSL。URL 查詢組件已經不夠用了,但是GET請求中的請求體也不太好,這意味著你最終要在POST請求中發(fā)送非可變查詢(Elasticsearch 就是這樣做的)。至此,API 還是 RESTful 的嗎?

        無論哪種方式,客戶端和服務器都需要特別注意解析、格式化和驗證所有這些參數。如此多的樂趣!舉例來說,如果沒有恰當的驗證且存在未初始化的變量,你就很容易地得到類似這樣的東西:/todos?offset=undefined。

         不容易記錄和測試

        上面提到的 Swagger 可能是目前最好的工具,但其應用還不夠廣泛。根據我的觀察,更常見的情況是,API 文檔單獨維護。對一個穩(wěn)定且廣泛使用的 API 來說,這沒什么大不了的,但是在敏捷流程的開發(fā)過程中,這就比較糟糕了。文檔單獨存儲意味著,它經常不會更新,特別是當更改是一個小的、但會破壞客戶端的更改時。

        如果你不使用 Swagger,這可能意味著你需要維護專門的測試基礎設施。與單元測試相比,你對集成測試(即同時測試客戶端和服務器端代碼)的需求會更多。

         關系查詢和批量查詢會讓人更加沮喪

        對于比較大的 API,這就成了一個問題,因為你可能有許多相關的集合。讓我們進一步來看一個待辦事項列表應用程序的例子:假設每個待辦事項也可以屬于一個項目。你是否總是希望一次獲取所有相關的項目?可能不需要,但是還需要添加更多的查詢參數。也許你不想一次獲取所有對象字段。如果應用程序需要項目有所有者,并且除了每個集合有單獨的視圖顯示外,還有一個視圖顯示所有這些數據的聚合?它要么是三個獨立的 HTTP 請求,要么是一個復雜的請求,同時獲取所有數據用于聚合。

        無論哪種方式,都存在復雜性和性能上的權衡,在不斷發(fā)展的應用程序中維護這些請求會帶來更多令人頭痛的問題。

         你需要同時在服務器和客戶端上實現每個端點

        還有大量的庫可以在 ORM 或直接數據庫自省的幫助下自動生成 REST 端點。即使使用了這樣的庫,它們通常也不是很靈活或可擴展的。也就是說,如果需要自定義參數、高級過濾行為或對請求 / 響應有效負載的一些更智能的處理,就需要從頭重新實現端點。

        另一項任務是在客戶端代碼中使用這些端點。如果有的話,最好使用代碼生成,但是它似乎不夠靈活。即使是使用像 Moya 這樣的輔助庫,也會遇到同樣障礙:有許多自定義行為需要處理,這是由前面提到的邊緣情況引起的。

        如果開發(fā)團隊不是全棧的,那么服務器和客戶端團隊之間的溝通就至關重要,在沒有機器可讀的 API 規(guī)范的情況下更是如此。

        GraphQL 如何做得更好?

        對于所有討論過的問題,我傾向于認為,在 CRUD 應用程序中,有一種標準方式來生成和使用 API 會非常棒。通用的工具和模式、集成測試和文檔基礎設施將有助于解決技術和組織問題。

        GraphQL 有一個 RFC 規(guī)范草案 和一個參考實現。此外,請參閱 GraphQL 教程,它描述了你需要了解的大多數概念。有針對不同平臺的實現,也有許多可用的開發(fā)工具,其中最著名的是 GraphiQL,它捆綁了一個很好的、具有自動完成功能的 API 瀏覽器,以及一個文檔瀏覽器,可以瀏覽從 GraphQL 模式自動生成的文檔。

        事實上,我發(fā)現 GraphiQL 是不可或缺的。它可以幫助解決我前面提到的客戶端和服務器團隊之間的溝通問題。只要 GraphQL 模式中有任何更改,你就可以在 GraphQL 瀏覽器中看到它,就像嵌入式 API 文檔?,F在,客戶端和服務器團隊可以以一種更好的方式在 API 設計上開展合作,縮短迭代時間,共享自動生成的文檔,它們讓每次 API 更新對每個人都可見。要了解這些工具是如何工作的,請查看 Star Wars API 示例,它可以作為 GraphiQL 的在線演示。

        能指定從服務器請求的對象字段讓客戶端可以根據需要只獲取需要的數據。不再有多個重量級的查詢發(fā)送到一個剛性的 REST API,為了讓客戶端可以在應用程序 UI 中一次性顯示它。你不再受限于一組端點,而是有一個可以查詢和修改的模式,能夠挑選客戶端指定的字段和對象。服務器只需以這種方式實現頂級模式對象。

         一個簡單的例子

        GraphQL 模式定義了可用于在服務器和客戶端之間通信的類型。有兩種特殊類型,它們同時也是 GraphQL 的核心概念:QueryMutation。在大多數情況下,向 GraphQL API 發(fā)出的每個請求要么是沒有副作用的Query實例,要么是會修改存儲在服務器上的對象的Mutation實例。

        現在,繼續(xù)我們待辦事項列表應用程序的例子,考慮下面這個 GraphQL 模式:
        type Project {
          id: ID
          name: String!
        }
        type TodoItem {
          id: ID
          description: String!
          isCompleted: Boolean!
          dueDate: Date
          project: Project
        }
        type TodoList {
          totalCount: Int!
          items: [TodoItem]!
        }
        type Query {
          allTodos(limit: Int, offset: Int): TodoList!
          todoByID(id: ID!): TodoItem
        }
        type Mutation {
          createTodo(item: TodoItem!): TodoItem
          deleteTodo(id: ID!): TodoItem
          updateTodo(id: ID!, newItem: TodoItem!): TodoItem
        }
        schema {
          query: Query
          mutation: Mutation
        }
        底部的schema塊是特定的,定義了前面描述的根類型QueryMutation。此外,它非常簡單:type塊定義新的類型,每個塊包含具有自己類型的字段定義。類型可以是非可選的,例如String!字段不能有空值,而String可以。字段也可以有命名參數,所以TodoList!類型的字段allTodos(limit: Int, offset: Int): TodoList!接受兩個可選參數,而其本身的值是非可選的,這意味著它將始終返回一個不能為空的TodoList實例。然后,要查詢所有待辦事項的id和名稱,你可以編寫這樣一個查詢:
        query {
          allTodos(limit: 5) {
            totalCount
            items {
              id
              description
              isCompleted
            }
          }
        }
        GraphQL 客戶端庫根據模式自動解析和驗證查詢,然后將其發(fā)送到 GraphQL 服務器。請注意,allTodos字段的offset參數是缺失的。作為可選項,它的缺失意味著它有null值。如果服務器提供這種模式,文檔中可能會聲明,null偏移量意味著默認情況下應該返回第一頁。響應可能是這樣的:
        {
          "data": {
            "allTodos": {
              "totalCount": 42,
              "items": [
                {
                  "id": 1,
                  "description": "write a blogpost",
                  "isCompleted": true
                },
                {
                  "id": 2,
                  "description": "edit until looks good",
                  "isCompleted": true
                },
                {
                  "id": 2,
                  "description": "proofread",
                  "isCompleted": false
                },
                {
                  "id": 4,
                  "description": "publish on the website",
                  "isCompleted": false
                },
                {
                  "id": 5,
                  "description": "share",
                  "isCompleted": false
                }
              ]
            }
          }
        }

        如果你從查詢中刪除isCompleted字段,它將從結果中消失?;蛘吣憧梢蕴砑?code>project字段,用其idname來遍歷關系。將offset參數添加到allTodos字段進行分頁,這樣allTodos(count: 5, offset: 5)將返回第二頁。結果中提供了totalCount字段,這很有用,因為現在你知道總共有42 / 5 = 9頁。但顯然,如果不需要totalCount,你可以忽略它。查詢可以完全控制將要接收的實際信息,但是底層的 GraphQL 基礎設施還必須確保所有必需的字段和參數都在那里。如果你的 GraphQL 服務器足夠聰明,它將不會對你不需要的字段運行數據庫查詢,而且有些庫好到免費提供這種查詢。此模式中的其他變體和查詢也是如此:對輸入進行類型檢查和驗證,并且基于查詢,GraphQL 服務器知道期望的結果形狀。本質上,所有通信都通過服務器上一個預定義的 URL(通常是/graphql)運行,借助一個簡單的POST請求,其中包含序列化為 JSON 有效負載的查詢。但是,你幾乎從來都不需要接觸如此低的抽象層。

        總體來說還不錯:我們已經解決了類型級別的驗證問題,分頁看起來也不錯,并且在需要時可以輕松地遍歷實體關系。如果使用一些現成的 GraphQL->數據庫查詢翻譯庫,你甚至不需要在服務器上編寫大多數數據庫查詢。客戶端庫可以很容易地將 GraphQL 響應自動解包為所需類型的對象實例,因為從模式和查詢可以提前知道響應形狀。

         GraphQL 是個時髦的東西,是一種時尚,對嗎?

        雖然 Netflix falcor 似乎在解決類似問題,它比 GraphQL 早幾個月發(fā)布在 GitHub 上,也更早地引起我的注意,但很明顯,似乎 GraphQL 贏了。良好的工具和強大的行業(yè)支持使其非常有吸引力。

        除了一些客戶端庫中存在的一些小問題(現在已經解決了)之外,我強烈推薦你仔細看看 GraphQL 在你的技術棧中可以提供什么。它已經出技術預覽四年多了,而且這個生態(tài)系統(tǒng)正在變得更加強大。在 Facebook 設計 GraphQL 的同時,我們也看到越來越多的大公司在他們的產品中使用它:GitHub、Shopify、Khan Academy、Coursera,而且 這個列表還在不斷增長。

        有很多流行的開源項目都在使用 GraphQL:這個博客是基于靜態(tài)站點生成器 Gatsby,它將 GraphQL 查詢的結果轉換成數據,然后呈現到 HTML 文件中。如果你使用的是 WordPress,也有 GraphQL API 可以使用。Reaction Commerce 是 Shopify 的開源替代方案,同樣是基于 GraphQL。

        另外值得一提的兩個 GraphQL 庫是 PostGraphile 和 Apollo。

        如果你使用 PostgreSQL 作為后端數據庫,PostGraphile 能夠掃描 SQL 模式并自動生成一個帶有實現的 GraphQL 模式。你可以將所有常見的 CRUD 操作暴露為所有表的查詢和修改。它可能看起來像 ORM,但它不是:你可以完全控制如何設計數據庫模式,以及使用什么索引。

        最妙的是,PostGraphile 還以查詢和修改的方式暴露視圖和函數,所以如果有特別復雜的 SQL 查詢需要映射到 GraphQL 字段,只需創(chuàng)建 SQL 視圖或函數,它就會自動出現在 GraphQL 模式中。通過像行級安全這樣的高級 Postgres 特性,你可以通過編寫少量 SQL 策略實現復雜的訪問控制邏輯。PostGraphile 甚至還有模式文檔這樣的東西,可以從 Postgres 注釋自動生成。

        相應地,Apollo 提供了多個平臺的客戶端庫,以及在最流行的編程語言(包括 TypeScript 和 Swift)中生成類型定義的代碼生成器。

        總的來說,我發(fā)現,Apollo 比 Relay 等更簡單和易于使用。由于 Apollo 客戶端庫架構簡單,我能夠將一個使用 React.js 與 Redux 的應用慢慢過渡到 React Apollo,一個組件一個組件的,只在有意義的時候才這樣做。與原生 iOS 應用一樣,Apollo iOS 是一個相對輕量級的、易于使用的庫。

         延伸閱讀

        https://desiatov.com/why-graphql/?fileGuid=cGOKAr3CJtY4Y9Rh



        內推社群


        我組建了一個氛圍特別好的騰訊內推社群,如果你對加入騰訊感興趣的話(后續(xù)有計劃也可以),我們可以一起進行面試相關的答疑、聊聊面試的故事、并且在你準備好的時候隨時幫你內推。下方加 winty 好友回復「面試」即可。


        瀏覽 41
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

        分享
        舉報
        評論
        圖片
        表情
        推薦
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

        分享
        舉報
        1. <strong id="7actg"></strong>
        2. <table id="7actg"></table>

        3. <address id="7actg"></address>
          <address id="7actg"></address>
          1. <object id="7actg"><tt id="7actg"></tt></object>
            天堂中文在线资源 | 天天日天天日天天干 | 国产又黄又爽免费视频 | 亚洲成人三级视频 | 淫荡的骚货 | 天天尻逼 | 8050午夜 | 在线观看淫色视频 | 中文字幕第二页精品 | 99国产精品久久久久久久久久 |