为什么我使用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"}]}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章