爲什麼我使用GraphQL而放棄REST API?

{"type":"doc","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"italic"},{"type":"strong"}],"text":"本文最初發佈於Max Desiatov的個人博客,經原作者授權由InfoQ中文站翻譯並分享。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在大多數移動和Web應用中,服務器交互需要花費開發人員大量時間和精力來開發和測試。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在我所開發的那些擁有最複雜API應用程序中,網絡層設計和維護佔去高達40%的開發時間,特別是由於我在本文中提到的一些邊緣情況。這樣實現過幾次後,很容易就會發現,有一些不同的模式、工具和框架可以帶來幫助。雖然我們很幸運,不必再關心"},{"type":"link","attrs":{"href":"https:\/\/en.wikipedia.org\/wiki\/SOAP","title":"","type":null},"content":[{"type":"text","text":"SOAP"}]},{"type":"text","text":",但"},{"type":"link","attrs":{"href":"https:\/\/en.wikipedia.org\/wiki\/Representational_state_transfer","title":"","type":null},"content":[{"type":"text","text":"REST"}]},{"type":"text","text":"也不是歷史的終結。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"最近,我有機會爲自己的項目和客戶開發和運行一些使用GraphQL API構建的移動和Web應用程序。這真是一個很好的體驗,尤其要感謝令人驚歎的 "},{"type":"link","attrs":{"href":"https:\/\/www.graphile.org\/postgraphile\/","title":"","type":null},"content":[{"type":"text","text":"PostGraphile"}]},{"type":"text","text":" 和 "},{"type":"link","attrs":{"href":"https:\/\/www.apollographql.com\/client","title":"","type":null},"content":[{"type":"text","text":"Apollo"}]},{"type":"text","text":"。至此,我再也無法回過頭來享受使用REST的工作了。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"REST有什麼問題嗎?"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"每個REST API都是獨特的"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"公平地說,REST甚至不是一個標準。維基百科將其"},{"type":"link","attrs":{"href":"https:\/\/en.wikipedia.org\/wiki\/Representational_state_transfer","title":"","type":null},"content":[{"type":"text","text":"定義"}]},{"type":"text","text":"爲:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"一種架構風格,基於HTTP定義了一組約束和屬性。"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"雖然確實存在像JSON API規範這樣的東西,但在實踐中,我們很少看到有RESTful後端實現它。在最好的情況下,你可能會偶然發現一些使用 "},{"type":"link","attrs":{"href":"OpenAPI\/Swagger","title":"","type":null},"content":[{"type":"text","text":"OpenAPI\/Swagger"}]},{"type":"text","text":" 的東西。即使這樣,OpenAPI 也沒有指定API的形狀或格式,它只是一個機器可讀的規範,允許(但不是要求)你對API運行自動化測試、自動生成文檔等。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"主要問題仍然存在。你可能會說你的API是RESTful的,但是對於如何安排端點或是否應該(例如)使用HTTP方法"},{"type":"codeinline","content":[{"type":"text","text":"PATCH"}]},{"type":"text","text":"進行對象更新,一般沒有嚴格的規則。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"還有一些東西乍一看是RESTful的,但如果你仔細看,就不是那麼像了:"},{"type":"link","attrs":{"href":"https:\/\/www.dropbox.com\/developers\/documentation\/http\/documentation","title":"","type":null},"content":[{"type":"text","text":"Dropbox HTTP API"}]},{"type":"text","text":"。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"端點接受請求體中的文件內容,因此,它們的參數將以JSON的形式在"},{"type":"codeinline","content":[{"type":"text","text":"Dropbox-API-Arg"}]},{"type":"text","text":"請求頭或arg URL參數中傳遞。"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"JSON在請求頭中?"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"沒錯,Dropbox API端點要求你將請求正文留空,並將有效載荷序列化爲JSON,放到一個自定義的HTTP頭中。爲這種特殊情況編寫客戶端代碼很有趣。我們不能抱怨,因爲畢竟沒有廣泛使用的標準。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"事實上,下面提到的大多數注意事項都是由於缺乏標準造成的,但是我想強調一下在實踐中經常看到的情況。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在一個有經驗的團隊中,你可以避免這些問題,但是你難道不希望一些問題已經在軟件方面得到解決嗎?"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"沒有靜態類型意味着要注意類型驗證"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"無論如何努力避免這種情況,你遲早會遇到JSON屬性拼寫錯誤、發送或接收的數據類型錯誤、字段丟失等問題。如果你的客戶端和\/或服務器編程語言是靜態類型的,並且你不能用錯誤的字段名或類型構造對象,那可能沒問題。如果你的API是版本化的,舊API的URL爲"},{"type":"codeinline","content":[{"type":"text","text":"\/API\/v1"}]},{"type":"text","text":",新版本的URL爲"},{"type":"codeinline","content":[{"type":"text","text":"\/API\/v2"}]},{"type":"text","text":",那麼你可能做得很好。如果有一個OpenAPI規範,可以爲你生成客戶端\/服務器類型聲明,那就更好了。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"但你真能負擔得起在所有項目中都做到這樣嗎?當你的團隊在衝刺期間決定重命名或重新安排對象字段時,你能負擔得起上線"},{"type":"codeinline","content":[{"type":"text","text":"\/api\/v1.99"}]},{"type":"text","text":"端點的成本嗎?即使完成了,團隊會不會忘記更新規範並通知客戶端開發人員更新內容?"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在客戶端或服務器上的所有驗證邏輯,你確定都是正確的嗎?理想情況下,你希望它在兩邊都得到驗證,對吧?維護所有這些自定義代碼非常有趣。或者保持API JSON模式是最新的。"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"分頁和過濾並不簡單"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"大多數API都使用對象集合。在待辦事項列表應用中,列表本身就是一個集合。大多數集合都可以包含100多個項。對於大多數服務器來說,在一次響應的一個集合中返回所有項是一個繁重的操作。如果再乘以在線用戶的數量,就會產生很大的AWS賬單。顯而易見的解決方案:只返回集合的子集。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"分頁相對簡單。在查詢參數中傳遞類似"},{"type":"codeinline","content":[{"type":"text","text":"offset"}]},{"type":"text","text":"和"},{"type":"codeinline","content":[{"type":"text","text":"limit"}]},{"type":"text","text":"這樣的值:"},{"type":"codeinline","content":[{"type":"text","text":"\/todos?Limit =10&offset=20"}]},{"type":"text","text":"以獲得從20開始的10個對象。每個人對這些參數的命名都不一樣,有些人喜歡"},{"type":"codeinline","content":[{"type":"text","text":"count"}]},{"type":"text","text":"和"},{"type":"codeinline","content":[{"type":"text","text":"skip"}]},{"type":"text","text":",而我喜歡"},{"type":"codeinline","content":[{"type":"text","text":"offset"}]},{"type":"text","text":"和"},{"type":"codeinline","content":[{"type":"text","text":"limit"}]},{"type":"text","text":",因爲它們直接對應於SQL修飾符。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"一些後端數據庫會暴露要傳遞給下一頁查詢的遊標或標記。請查看"},{"type":"link","attrs":{"href":"https:\/\/www.elastic.co\/guide\/en\/elasticsearch\/reference\/current\/search-request-scroll.html","title":"","type":null},"content":[{"type":"text","text":"Elasticsearch API"}]},{"type":"text","text":",該API建議在需要依次瀏覽大量結果文檔時使用"},{"type":"codeinline","content":[{"type":"text","text":"scroll"}]},{"type":"text","text":"調用。還有一些API在頭中傳遞相關信息。參見"},{"type":"link","attrs":{"href":"https:\/\/developer.github.com\/v3\/guides\/traversing-with-pagination\/","title":"","type":null},"content":[{"type":"text","text":"GitHub REST API"}]},{"type":"text","text":"(至少不是在頭中傳遞JSON)。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"說到過濾,就有趣多了……需要按一個字段過濾嗎?沒問題,可能是"},{"type":"codeinline","content":[{"type":"text","text":"\/todos?filter=key%3Dvalue"}]},{"type":"text","text":",也可能是可讀性更好的"},{"type":"codeinline","content":[{"type":"text","text":"\/todos?filterKey=key&filterValue=value"}]},{"type":"text","text":"。那麼按兩個值過濾呢?這應該很簡單,對吧?使用"},{"type":"link","attrs":{"href":"https:\/\/en.wikipedia.org\/wiki\/Percent-encoding","title":"","type":null},"content":[{"type":"text","text":"URL編碼"}]},{"type":"text","text":",查詢看起來是這個樣子:"},{"type":"codeinline","content":[{"type":"text","text":"\/todos?filterKeys=key1%2Ckey2&filterValue=value"}]},{"type":"text","text":"。但通常,我們沒有辦法阻止特性蔓延,可能會出現使用"},{"type":"codeinline","content":[{"type":"text","text":"AND"}]},{"type":"text","text":"\/"},{"type":"codeinline","content":[{"type":"text","text":"OR"}]},{"type":"text","text":"操作符進行高級過濾的需求。或者複雜的全文搜索查詢和複雜的過濾。遲早你會看到一些API發明了自己的過濾 "},{"type":"link","attrs":{"href":"https:\/\/en.wikipedia.org\/wiki\/Domain-specific_language","title":"","type":null},"content":[{"type":"text","text":"DSL"}]},{"type":"text","text":"。URL查詢組件已經不夠用了,但是"},{"type":"codeinline","content":[{"type":"text","text":"GET"}]},{"type":"text","text":"請求中的請求體也不太好,這意味着你最終要在"},{"type":"codeinline","content":[{"type":"text","text":"POST"}]},{"type":"text","text":"請求中發送非可變查詢(Elasticsearch就是這樣做的)。至此,API 還是RESTful的嗎?"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"無論哪種方式,客戶端和服務器都需要特別注意解析、格式化和驗證所有這些參數。如此多的樂趣!舉例來說,如果沒有恰當的驗證且存在未初始化的變量,你就很容易地得到類似這樣的東西:"},{"type":"codeinline","content":[{"type":"text","text":"\/todos?offset=undefined"}]},{"type":"text","text":"。"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"不容易記錄和測試"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"上面提到的 "},{"type":"link","attrs":{"href":"https:\/\/swagger.io\/","title":"","type":null},"content":[{"type":"text","text":"Swagger"}]},{"type":"text","text":" 可能是目前最好的工具,但其應用還不夠廣泛。根據我的觀察,更常見的情況是,API文檔單獨維護。對一個穩定且廣泛使用的API來說,這沒什麼大不了的,但是在敏捷流程的開發過程中,這就比較糟糕了。文檔單獨存儲意味着,它經常不會更新,特別是當更改是一個小的、但會破壞客戶端的更改時。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果你不使用Swagger,這可能意味着你需要維護專門的測試基礎設施。與單元測試相比,你對集成測試(即同時測試客戶端和服務器端代碼)的需求會更多。"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"關係查詢和批量查詢會讓人更加沮喪"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"對於比較大的API,這就成了一個問題,因爲你可能有許多相關的集合。讓我們進一步來看一個待辦事項列表應用程序的例子:假設每個待辦事項也可以屬於一個項目。你是否總是希望一次獲取所有相關的項目?可能不需要,但是還需要添加更多的查詢參數。也許你不想一次獲取所有對象字段。如果應用程序需要項目有所有者,並且除了每個集合有單獨的視圖顯示外,還有一個視圖顯示所有這些數據的聚合?它要麼是三個獨立的HTTP請求,要麼是一個複雜的請求,同時獲取所有數據用於聚合。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"無論哪種方式,都存在複雜性和性能上的權衡,在不斷髮展的應用程序中維護這些請求會帶來更多令人頭痛的問題。"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"你需要同時在服務器和客戶端上實現每個端點"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"還有大量的庫可以在 "},{"type":"link","attrs":{"href":"https:\/\/en.wikipedia.org\/wiki\/Object-relational_mapping","title":"","type":null},"content":[{"type":"text","text":"ORM"}]},{"type":"text","text":" 或直接數據庫自省的幫助下自動生成REST端點。即使使用了這樣的庫,它們通常也不是很靈活或可擴展的。也就是說,如果需要自定義參數、高級過濾行爲或對請求\/響應有效負載的一些更智能的處理,就需要從頭重新實現端點。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"另一項任務是在客戶端代碼中使用這些端點。如果有的話,最好使用代碼生成,但是它似乎不夠靈活。即使是使用像 "},{"type":"link","attrs":{"href":"https:\/\/github.com\/moya\/moya","title":"","type":null},"content":[{"type":"text","text":"Moya"}]},{"type":"text","text":" 這樣的輔助庫,也會遇到同樣障礙:有許多自定義行爲需要處理,這是由前面提到的邊緣情況引起的。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果開發團隊不是全棧的,那麼服務器和客戶端團隊之間的溝通就至關重要,在沒有機器可讀的API規範的情況下更是如此。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"GraphQL如何做得更好?"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"對於所有討論過的問題,我傾向於認爲,在 CRUD 應用程序中,有一種標準方式來生成和使用 API 會非常棒。通用的工具和模式、集成測試和文檔基礎設施將有助於解決技術和組織問題。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"GraphQL 有一個 RFC規範草案 和一個參考實現。此外,請參閱 "},{"type":"link","attrs":{"href":"http:\/\/graphql.org\/learn\/","title":"","type":null},"content":[{"type":"text","text":"GraphQL教程"}]},{"type":"text","text":",它描述了你需要了解的大多數概念。有針對不同平臺的實現,也有許多可用的開發工具,其中最著名的是 GraphiQL,它捆綁了一個很好的、具有自動完成功能的API瀏覽器,以及一個文檔瀏覽器,可以瀏覽從GraphQL模式自動生成的文檔。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"事實上,我發現GraphiQL是不可或缺的。它可以幫助解決我前面提到的客戶端和服務器團隊之間的溝通問題。只要GraphQL模式中有任何更改,你就可以在GraphQL瀏覽器中看到它,就像嵌入式API文檔。現在,客戶端和服務器團隊可以以一種更好的方式在API設計上開展合作,縮短迭代時間,共享自動生成的文檔,它們讓每次API更新對每個人都可見。要了解這些工具是如何工作的,請查看Star Wars API示例,它可以作爲"},{"type":"link","attrs":{"href":"http:\/\/graphql.org\/swapi-graphql\/","title":"","type":null},"content":[{"type":"text","text":"GraphiQL的在線演示"}]},{"type":"text","text":"。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"能指定從服務器請求的對象字段讓客戶端可以根據需要只獲取需要的數據。不再有多個重量級的查詢發送到一個剛性的REST API,爲了讓客戶端可以在應用程序UI中一次性顯示它。你不再受限於一組端點,而是有一個可以查詢和修改的模式,能夠挑選客戶端指定的字段和對象。服務器只需以這種方式實現頂級模式對象。"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"一個簡單的例子"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"GraphQL模式定義了可用於在服務器和客戶端之間通信的類型。有兩種特殊類型,它們同時也是GraphQL的核心概念:"},{"type":"codeinline","content":[{"type":"text","text":"Query"}]},{"type":"text","text":"和"},{"type":"codeinline","content":[{"type":"text","text":"Mutation"}]},{"type":"text","text":"。在大多數情況下,向GraphQL API發出的每個請求要麼是沒有副作用的"},{"type":"codeinline","content":[{"type":"text","text":"Query"}]},{"type":"text","text":"實例,要麼是會修改存儲在服務器上的對象的"},{"type":"codeinline","content":[{"type":"text","text":"Mutation"}]},{"type":"text","text":"實例。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"現在,繼續我們待辦事項列表應用程序的例子,考慮下面這個GraphQL模式:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"plain"},"content":[{"type":"text","text":"type Project {\n id: ID\n name: String!\n}\ntype TodoItem {\n id: ID\n description: String!\n isCompleted: Boolean!\n dueDate: Date\n project: Project\n}\ntype TodoList {\n totalCount: Int!\n items: [TodoItem]!\n}\ntype Query {\n allTodos(limit: Int, offset: Int): TodoList!\n todoByID(id: ID!): TodoItem\n}\ntype Mutation {\n createTodo(item: TodoItem!): TodoItem\n deleteTodo(id: ID!): TodoItem\n updateTodo(id: ID!, newItem: TodoItem!): TodoItem\n}\nschema {\n query: Query\n mutation: Mutation\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"底部的"},{"type":"codeinline","content":[{"type":"text","text":"schema"}]},{"type":"text","text":"塊是特定的,定義了前面描述的根類型"},{"type":"codeinline","content":[{"type":"text","text":"Query"}]},{"type":"text","text":"和"},{"type":"codeinline","content":[{"type":"text","text":"Mutation"}]},{"type":"text","text":"。此外,它非常簡單:"},{"type":"codeinline","content":[{"type":"text","text":"type"}]},{"type":"text","text":"塊定義新的類型,每個塊包含具有自己類型的字段定義。類型可以是非可選的,例如"},{"type":"codeinline","content":[{"type":"text","text":"String!"}]},{"type":"text","text":"字段不能有空值,而"},{"type":"codeinline","content":[{"type":"text","text":"String"}]},{"type":"text","text":"可以。字段也可以有命名參數,所以"},{"type":"codeinline","content":[{"type":"text","text":"TodoList!"}]},{"type":"text","text":"類型的字段"},{"type":"codeinline","content":[{"type":"text","text":"allTodos(limit: Int, offset: Int): TodoList!"}]},{"type":"text","text":"接受兩個可選參數,而其本身的值是非可選的,這意味着它將始終返回一個不能爲空的"},{"type":"codeinline","content":[{"type":"text","text":"TodoList"}]},{"type":"text","text":"實例。然後,要查詢所有待辦事項的"},{"type":"codeinline","content":[{"type":"text","text":"id"}]},{"type":"text","text":"和名稱,你可以編寫這樣一個查詢:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"plain"},"content":[{"type":"text","text":"query {\n allTodos(limit: 5) {\n totalCount\n items {\n id\n description\n isCompleted\n }\n }\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"GraphQL客戶端庫根據模式自動解析和驗證查詢,然後將其發送到GraphQL服務器。請注意,"},{"type":"codeinline","content":[{"type":"text","text":"allTodos"}]},{"type":"text","text":"字段的"},{"type":"codeinline","content":[{"type":"text","text":"offset"}]},{"type":"text","text":"參數是缺失的。作爲可選項,它的缺失意味着它有"},{"type":"codeinline","content":[{"type":"text","text":"null"}]},{"type":"text","text":"值。如果服務器提供這種模式,文檔中可能會聲明,"},{"type":"codeinline","content":[{"type":"text","text":"null"}]},{"type":"text","text":"偏移量意味着默認情況下應該返回第一頁。響應可能是這樣的:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"plain"},"content":[{"type":"text","text":"{\n \"data\": {\n \"allTodos\": {\n \"totalCount\": 42,\n \"items\": [\n {\n \"id\": 1,\n \"description\": \"write a blogpost\",\n \"isCompleted\": true\n },\n {\n \"id\": 2,\n \"description\": \"edit until looks good\",\n \"isCompleted\": true\n },\n {\n \"id\": 2,\n \"description\": \"proofread\",\n \"isCompleted\": false\n },\n {\n \"id\": 4,\n \"description\": \"publish on the website\",\n \"isCompleted\": false\n },\n {\n \"id\": 5,\n \"description\": \"share\",\n \"isCompleted\": false\n }\n ]\n }\n }\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果你從查詢中刪除"},{"type":"codeinline","content":[{"type":"text","text":"isCompleted"}]},{"type":"text","text":"字段,它將從結果中消失。或者你可以添加"},{"type":"codeinline","content":[{"type":"text","text":"project"}]},{"type":"text","text":"字段,用其"},{"type":"codeinline","content":[{"type":"text","text":"id"}]},{"type":"text","text":"和"},{"type":"codeinline","content":[{"type":"text","text":"name"}]},{"type":"text","text":"來遍歷關係。將"},{"type":"codeinline","content":[{"type":"text","text":"offset"}]},{"type":"text","text":"參數添加到"},{"type":"codeinline","content":[{"type":"text","text":"allTodos"}]},{"type":"text","text":"字段進行分頁,這樣"},{"type":"codeinline","content":[{"type":"text","text":"allTodos(count: 5, offset: 5)"}]},{"type":"text","text":"將返回第二頁。結果中提供了"},{"type":"codeinline","content":[{"type":"text","text":"totalCount"}]},{"type":"text","text":"字段,這很有用,因爲現在你知道總共有"},{"type":"codeinline","content":[{"type":"text","text":"42 \/ 5 = 9"}]},{"type":"text","text":"頁。但顯然,如果不需要"},{"type":"codeinline","content":[{"type":"text","text":"totalCount"}]},{"type":"text","text":",你可以忽略它。查詢可以完全控制將要接收的實際信息,但是底層的GraphQL基礎設施還必須確保所有必需的字段和參數都在那裏。如果你的GraphQL服務器足夠聰明,它將不會對你不需要的字段運行數據庫查詢,而且有些庫好到免費提供這種查詢。此模式中的其他變體和查詢也是如此:對輸入進行類型檢查和驗證,並且基於查詢,GraphQL服務器知道期望的結果形狀。本質上,所有通信都通過服務器上一個預定義的URL(通常是"},{"type":"codeinline","content":[{"type":"text","text":"\/graphql"}]},{"type":"text","text":")運行,藉助一個簡單的"},{"type":"codeinline","content":[{"type":"text","text":"POST"}]},{"type":"text","text":"請求,其中包含序列化爲JSON有效負載的查詢。但是,你幾乎從來都不需要接觸如此低的抽象層。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"總體來說還不錯:我們已經解決了類型級別的驗證問題,分頁看起來也不錯,並且在需要時可以輕鬆地遍歷實體關係。如果使用一些現成的GraphQL->數據庫查詢翻譯庫,你甚至不需要在服務器上編寫大多數數據庫查詢。客戶端庫可以很容易地將GraphQL響應自動解包爲所需類型的對象實例,因爲從模式和查詢可以提前知道響應形狀。"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"GraphQL是個時髦的東西,是一種時尚,對嗎?"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"雖然 "},{"type":"link","attrs":{"href":"https:\/\/github.com\/Netflix\/falcor","title":"","type":null},"content":[{"type":"text","text":"Netflix falcor"}]},{"type":"text","text":"似乎在解決類似問題,它比GraphQL早幾個月發佈在GitHub上,也更早地引起我的注意,但很明顯,似乎GraphQL贏了。良好的工具和強大的行業支持使其非常有吸引力。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"除了一些客戶端庫中存在的一些小問題(現在已經解決了)之外,我強烈推薦你仔細看看GraphQL在你的技術棧中可以提供什麼。它已經出技術預覽四年多了,而且這個生態系統正在變得更加強大。在Facebook設計GraphQL的同時,我們也看到越來越多的大公司在他們的產品中使用它:GitHub、Shopify、Khan Academy、Coursera,而且"},{"type":"link","attrs":{"href":"http:\/\/graphql.org\/users\/","title":"","type":null},"content":[{"type":"text","text":"這個列表還在不斷增長"}]},{"type":"text","text":"。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"有很多流行的開源項目都在使用GraphQL:這個博客是基於靜態站點生成器Gatsby,它將GraphQL查詢的結果轉換成數據,然後呈現到HTML文件中。如果你使用的是WordPress,也有 GraphQL API 可以使用。Reaction Commerce 是Shopify的開源替代方案,同樣是基於GraphQL。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"另外值得一提的兩個GraphQL庫是PostGraphile 和 Apollo。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果你使用PostgreSQL作爲後端數據庫,PostGraphile能夠掃描SQL模式並自動生成一個帶有實現的GraphQL模式。你可以將所有常見的CRUD操作暴露爲所有表的查詢和修改。它可能看起來像ORM,但它不是:你可以完全控制如何設計數據庫模式,以及使用什麼索引。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"最妙的是,PostGraphile還以查詢和修改的方式暴露視圖和函數,所以如果有特別複雜的SQL查詢需要映射到GraphQL字段,只需創建SQL視圖或函數,它就會自動出現在GraphQL模式中。通過像行級安全這樣的高級Postgres特性,你可以通過編寫少量SQL策略實現複雜的訪問控制邏輯。PostGraphile甚至還有模式文檔這樣的東西,可以從Postgres註釋自動生成。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"相應地,Apollo提供了多個平臺的客戶端庫,以及在最流行的編程語言(包括TypeScript和Swift)中生成類型定義的代碼生成器。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"總的來說,我發現,Apollo比Relay等更簡單和易於使用。由於Apollo客戶端庫架構簡單,我能夠將一個使用React.js與Redux的應用慢慢過渡到React Apollo,一個組件一個組件的,只在有意義的時候才這樣做。與原生iOS應用一樣,Apollo iOS是一個相對輕量級的、易於使用的庫。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"原文鏈接:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"https:\/\/desiatov.com\/why-graphql\/?fileGuid=cGOKAr3CJtY4Y9Rh"}]}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章