团队研发文档

开发规范、技术文档等


GraphQL调研

<h3>一种用于 API 的查询语言</h3> <p>GraphQL是Facebook推出的一个查询语言,它并不是一个数据库查询语言,而是你可以用任何其他语言实现的一个用于查询的抽象层。GraphQL 既是一种用于 API 的查询语言,也是一个满足你数据查询的运行时。</p> <h3>Ask exactly what you want.</h3> <p>向API发出一个GraphQL请求,就可以准确获得想要的数据,而且没有任何冗余,不多不少。</p> <pre><code>// 请求 { hero { name height } } // 结果 { "hero" : { "name" : "Luke Skywalker", "height" : 1.72 } }</code></pre> <p>REST 接口返回的数据格式都是后端预先定义好的,达不到接口使用者期望时,不外乎两种方式来解决问题: 1). 和后端沟通,改接口(更改数据源) 2). 自己做一些适配工作(处理数据源)</p> <p><strong>GraphQL请求可以动态设置接口返回值,解耦前后端的关联。</strong></p> <h3>获取多个资源,只用一个请求</h3> <p>GraphQL 查询不仅能够获得资源的属性,还能沿着资源间引用进一步查询。</p> <pre><code>// 请求 { hero { name friends { name } } } // 结果 { "hero" : { "name" : "Luke Skywalker", "friends" : [ { "name" : "Han Solo" }, { "name" : "Leia Organa" }, { "name" : "R2-D2" } ] } }</code></pre> <p>典型的 REST API 请求多个资源时得载入多个 URL,而 GraphQL 可以通过一次请求就获取你应用所需的所有数据。</p> <h3>API 演进,无需划分版本</h3> <p>给 GraphQL API 添加字段和类型而无需影响现有查询。通过使用单一演进版本,GraphQL API 使得应用始终能够使用新的特性,并鼓励使用更加简洁、更好维护的服务端代码。</p> <pre><code>// A type Film { title : String episode: Int releaseDate : String } // B type Film { title : String episode: Int releaseDate : String director : String } // C type Film { title : String episode: Int releaseDate : String director : String @deprecated directedBy : Person } type Person { name : String directed : [Film] actedIn : [Film] }</code></pre> <h3>查询和变更</h3> <h5>字段 Fileds</h5> <p>GraphQL 是关于请求对象上的特定字段</p> <pre><code>// 请求 { hero { name // 字符串类型 friends { // 对象类型,次级选择 name } } } // 结果 { "hero" : { "name" : "Luke Skywalker", "friends" : [ { "name" : "Han Solo" }, { "name" : "Leia Organa" }, { "name" : "R2-D2" } ] } }</code></pre> <h5>参数 Arguments</h5> <p>给字段传递参数</p> <pre><code>// 请求 { human(id: "1000") { name height } } // 结果 { "data": { "human": { "name": "Luke Skywalker", "height": 1.72 } } }</code></pre> <p>给 标量(scalar)字段传递参数,实现服务端的一次转换</p> <pre><code>//请求 { human(id: "1000") { name height(unit: FOOT) } } // 结果 { "data": { "human": { "name": "Luke Skywalker", "height": 5.6430448 } } }</code></pre> <h5>别名 Aliases</h5> <p>下例中,两个 hero 字段将会存在冲突,可以将其另取一个别名,就可以在一次请求中得到两个结果。</p> <pre><code>// 请求 { empireHero: hero(episode: EMPIRE) { name } jediHero: hero(episode: JEDI) { name } } // 结果 { "data": { "empireHero": { "name": "Luke Skywalker" }, "jediHero": { "name": "R2-D2" } } }</code></pre> <h5>片段 Fragments</h5> <p>可复用单元,在需要它们的的地方引入。</p> <pre><code>// 请求 { leftComparison: hero(episode: EMPIRE) { ...comparisonFields } rightComparison: hero(episode: JEDI) { ...comparisonFields } } fragment comparisonFields on Character { name appearsIn friends { name } } // 结果 { "data": { "leftComparison": { "name": "Luke Skywalker", "appearsIn": [ "NEWHOPE", "EMPIRE", "JEDI" ], "friends": [ { "name": "Han Solo" }, { "name": "Leia Organa" }, { "name": "C-3PO" }, { "name": "R2-D2" } ] }, "rightComparison": { "name": "R2-D2", "appearsIn": [ "NEWHOPE", "EMPIRE", "JEDI" ], "friends": [ { "name": "Luke Skywalker" }, { "name": "Han Solo" }, { "name": "Leia Organa" } ] } } }</code></pre> <h6>操作名称 Operation name</h6> <p>这之前,我们使用了简写句法,省略了 query 关键字和查询名称,但是生产中使用这些可以使我们代码减少歧义。</p> <pre><code>// 操作类型的关键字 query 以及操作名称 HeroNameAndFriends query HeroNameAndFriends { hero { name friends { name } } }</code></pre> <h5>变量 Variables</h5> <p>使用变量之前,我们得做三件事: 1). 使用 $variableName 替代查询中的静态值。 2). 声明 $variableName 为查询接受的变量之一。 3). 将 variableName: value 通过传输专用(通常是 JSON)的分离的变量字典中。</p> <pre><code># { "graphiql": true, "variables": { "episode": JEDI } } query HeroNameAndFriends($episode: Episode) { hero(episode: $episode) { name friends { name } } }</code></pre> <h5>变量定义 Variable definitions</h5> <p>上述查询中的 ($episode: Episode)。其工作方式跟类型语言中函数的参数定义一样。它已列出所有变量,变量前缀必须为 $,后跟其类型,本例中为 Episode。</p> <h5>默认变量 Default variables</h5> <p>可以通过在查询中的类型定义后面附带默认值的方式,将默认值赋给变量。</p> <pre><code>query HeroNameAndFriends($episode: Episode = "JEDI") { hero(episode: $episode) { name friends { name } } }</code></pre> <h5>指令 Directives</h5> <p>我们可能也需要一个方式使用变量动态地改变我们查询的结构。譬如我们假设有个 UI 组件,其有概括视图和详情视图,后者比前者拥有更多的字段。</p> <pre><code>// 带指令请求 query Hero($episode: Episode, $withFriends: Boolean!) { hero(episode: $episode) { name friends @include(if: $withFriends) { name } } } // VARIABLES { "episode": "JEDI", "withFriends": false } // 结果 { "data": { "hero": { "name": "R2-D2" } } }</code></pre> <p>@include(if: Boolean) 仅在参数为 true 时,包含此字段。 @skip(if: Boolean) 如果参数为 true,跳过此字段。</p> <h5>变更 Mutations</h5> <p>REST 中,任何请求都可能最后导致一些服务端副作用,但是约定上建议不要使用 GET 请求来修改数据。GraphQL 也是类似 —— 技术上而言,任何查询都可以被实现为导致数据写入。然而,建一个约定来规范任何导致写入的操作都应该显式通过变更(mutation)来发送。</p> <pre><code>mutation CreateReviewForEpisode($ep: Episode!, $review: ReviewInput!) { createReview(episode: $ep, review: $review) { stars commentary } } // 参数 { "ep": "JEDI", "review": { "stars": 5, "commentary": "This is a great movie!" } } // 返回结果 { "data": { "createReview": { "stars": 5, "commentary": "This is a great movie!" } } }</code></pre> <p>我们传递的 review 变量并非标量。它是一个输入对象类型,一种特殊的对象类型,可以作为参数传递。</p> <p><strong>查询字段时,是并行执行,而变更字段时,是线性执行,一个接着一个。</strong></p> <h3>Schema 和类型</h3> <h5>对象类型和字段 Object Types and Fields</h5> <pre><code>type Character { name: String! appearsIn: [Episode]! }</code></pre> <ol> <li>Character 是一个 GraphQL 对象类型,表示其是一个拥有一些字段的类型。你的 schema 中的大多数类型都会是对象类型。</li> <li>name 和 appearsIn 是 Character 类型上的字段。这意味着在一个操作 Character 类型的 GraphQL 查询中的任何部分,都只能出现 name 和 appearsIn 字段。</li> <li>String 是内置的标量类型之一 —— 标量类型是解析到单个标量对象的类型,无法在查询中对它进行次级选择。后面我们将细述标量类型。</li> <li>String! 表示这个字段是非空的,GraphQL 服务保证当你查询这个字段后总会给你返回一个值。在类型语言里面,我们用一个感叹号来表示这个特性。</li> <li>[Episode]! 表示一个 Episode 数组。因为它也是非空的,所以当你查询 appearsIn 字段的时候,你也总能得到一个数组(零个或者多个元素)。</li> </ol> <h5>标量类型 Scalar Types</h5> <p>这些字段必然会解析到具体数据,它们表示对应 GraphQL 查询的叶子节点。</p> <ol> <li>Int:有符号 32 位整数。</li> <li>Float:有符号双精度浮点值。</li> <li>String:UTF‐8 字符序列。</li> <li>Boolean:true 或者 false。</li> <li>ID:ID 标量类型表示一个唯一标识符,通常用以重新获取对象或者作为缓存中的键。ID 类型使用和 String 一样的方式序列化;然而将其定义为 ID 意味着并不需要人类可读型。</li> </ol> <h5>列表和非空 Lists and Non-Null</h5> <ol> <li>Type! 元素非空</li> <li>[Type]! 列表本身为必填项,但其内部元素可以为空</li> <li>[Type!] 列表本身可以为空,但是其内部元素为必填</li> <li>[Type!]! 列表本身和内部元素均为必填</li> </ol> <pre><code>type Comment { id: ID! desc: String, author: User! } type Article { id: ID! text: String isPublished: Boolean author: User! comments: [Comment!] }</code></pre> <p>参考:<a href="http://graphql.cn/" title="GraphQL使用">GraphQL使用</a></p> <h3>REST vs RPC</h3> <h5>REST</h5> <p>rest是一种架构风格,restful是遵循这种架构风格的应用程序或者设计,强调组件交互的无状态性、可伸缩性、接口的通用性、组件的独立部署、以及用来减少交互延迟、增强安全性。</p> <p>操作对象即为资源,对资源的四种操作(post、get、put、delete),并且参数都放在URL上,但是不严格的说Http+json、Http+xml,常见的http api都可以称为Rest接口。</p> <p>REST 的三个要素是 唯一的资源标识, 简单的方法 (此处的方法是个抽象的概念), 一定的表达方式。</p> <p>REST 是以 资源 为中心, 名词即资源的地址, 动词即施加于名词上的一些有限操作, 表达是对各种资源形态的抽象。</p> <p>以HTTP为例, 名词即为URI(统一资源标识), 动词包括POST, GET, PUT, DELETE等(还有其它不常用的2个,所以 整个动词集合是有限的), 资源的形态(如text, html, image, pdf等)</p> <h5>RPC</h5> <p><img src="https://www.showdoc.cc/server/api/common/visitfile/sign/3f15ba1c8dac949ffe4028e811006f2d?showdoc=.jpg" alt="RPC" title="RPC" /></p> <p>远程方法调用,就是像调用本地方法一样调用远程方法,通信协议大多采用二进制方式。</p> <p>本地函数映射到API,也就是说一个API对应的是一个function。</p> <p>RPC 的主要功能目标是让构建分布式计算(应用)更容易,在提供强大的远程调用能力时不损失本地调用的语义简洁性。</p> <p>RPC的核心并不在于使用什么协议。RPC的目的是让你在本地调用远程的方法,而对你来说这个调用是透明的,你并不知道这个调用的方法是部署哪里。通过<strong>RPC能解耦服务</strong>,这才是使用RPC的真正目的。RPC的原理主要用到了动态代理模式,至于http协议,只是传输协议而已。</p> <p><strong>RPC 框架是架构微服务化的首要基础组件 ,它能大大降低架构微服务化的成本,提高调用方与服务提供方的研发效率,屏蔽跨进程调用函数(服务)的各类复杂细节</strong>,服务化的一个好处就是,不限定服务的提供方使用什么技术选型,能够实现大公司跨团队的技术解耦。</p> <p>良好的rpc调用是面向服务的封装,针对服务的可用性和效率等都做了优化(服务发现、错误重试)。</p> <p>如果没有统一的服务框架,RPC框架,各个团队的服务提供方就需要各自实现一套序列化、反序列化、网络框架、连接池、收发线程、超时处理、状态机等“业务之外”的重复技术劳动,造成整体的低效。所以,统一RPC框架把上述“业务之外”的技术劳动统一处理,是服务化首要解决的问题</p> <h4>小结</h4> <ol> <li>REST是一种古老的面向服务端和客户端(CS)的架构风格,并不是一项特定的技术。它定义了一系列严格的构建API的原则,用简单的方式描述资源,并认为大部分时候违背这些原则会让软件的扩展性受限。</li> <li>在多系统(服务)之间采用RPC,对外提供服务使用REST。</li> <li>基于 RPC 的 API 适用于动作(过程、命令等)。 基于 REST 的 API 适用于领域模型(资源或实体),基于数据的 CRUD (create, read, update, delete) 操作。 REST 不 仅 用于 CRUD,但主要是基于 CRUD 的操作。REST 会使用 HTTP 方法,如 GET,POST,PUT,DELETE,OPTIONS 以及很有希望的 PATCH 来从语义上说明动作的意图。然而 RPC 不会,它多数时候只使用 GET 和 POST。GET 用于获取信息,POST 用来干其它事情。RPC API 通常会使用像 POST /deleteFoo 这样的方法,带上内容 { &quot;id&quot;: 1 }。如果用 REST,就会像这样 DELETE /foos/1。</li> </ol> <h3>GraphQL的核心目标就是取代RESTful API</h3> <h5>REST劣势</h5> <ol> <li> <p>资源分类导致性能受限 在前端我们很少遇到运行效率问题,效率问题主要来自网络请求——一次HTTP请求的代价非常高昂,特别是对移动端来说。如果我们遵循REST的风格,我们就要将各种资源分门别类用不同的API来表示。而在客户端中我们经常<strong>需要一次请求多种资源</strong>。这时候我们就要编写许多API来为不同的页面合并这些API。很多时候,我们写的这些API并不是一个特定资源,但我们还得用URL来表示它们。(资源之间有映射关系:比如我们经常要先请求某个user信息,然后等这个API返回之后再渲染这个user名下的articles)</p> </li> <li> <p>在现代场景中难于维护 虽然REST的目标是易于维护和扩展,但在Web前端/客户端领域,它的表现并没有想象得那么好。我们经常说最明显的Code smell就是重复。许多时候,我们要让API适应视图,但我们都知道,这种API仅被客户端消费,与服务端代码耦合是非常不合理的。 随着前端/移动端的兴起,我们经常还要为多种客户端编写多种API。这些API代码既类似又无聊,并且也要在客户端修改时一起修改——仅前端和后端的重复我们可以让IDE查找,然而这种散落在前后端的契约则很容易遗漏。不仅如此,如果这个API是Public API,一点小改动就要修改版本。</p> </li> <li> <p>缺乏约束 RESTful API通过URL表述资源,它本身是无类型的。现在,随着技术的发展,我们已经有许多非常强大的静态类型语言,它们有非常强大的开发工具来帮助我们检查错误。而在我们系统的API边界,这些重量级的强大工具却无能为力。 随着Micro service越来越流行、系统中的边界越来越多,静态类型能捕获的错误则越来越少。 要花费额外的努力来维护契约测试,还要小心翼翼地对应Service之间的版本依赖,因为对REST来说,不同版本之间的兼容能力非常弱小。</p> </li> <li>严格,抽象,但并不能解决客户端问题 付出高昂的性能和开发代价来维护严格的RESTful是不现实的。</li> </ol> <h5>GraphQL优势</h5> <ol> <li> <p>需求驱动 无论进行什么查询,请求都是指向同一个endpoint的POST请求。 GraphQL用来构建客户端API,但它并不关心视图,也不关心服务的到底是什么客户端。 Ask exactly what you want. 对于大部分前后端分离的项目,客户端开发人员可以独立修改页面的展现形式。降低了迭代成本,减少了沟通成本。</p> </li> <li> <p>一次请求复杂数据 <img src="https://www.showdoc.cc/server/api/common/visitfile/sign/81f4a4d8bfeafd372bbd155c551471a9?showdoc=.jpg" alt="" /> 传统方式会导致串行请求,这对性能的损耗是十分严重的。不同于平常的请求,实现GraphQL的服务端接收到请求后,虽然还是HTTP的一次请求,但是会根据查询的结构<strong>递归</strong>地根据查询来调用各项资源的Resolver,最后拼回一张JSON Graph返回给客户端。不仅能够减少多次请求带来的延迟,还能够降低服务器压力,加快前端的渲染速度。</p> </li> <li> <p>静态类型 对于编程语言来说,拥有强大的静态类型是很常见的。对于查询语言,却不是很多见。在这点上GraphQL有点像RPC,可以生成GraphQL Schema来作为服务端和客户端间的契约。</p> </li> <li> <p>兼容多版本 由于客户端可以决定请求的内容,服务端也可以不删除废弃的字段,而仅仅加入@Deprecated注解,这样客户端查询时只会被Warning。不同客户端和不同Service之间的版本依赖也变得非常宽松。</p> </li> <li> <p>Mutation 我们说RESTful API经常说CRUD这几个动作,也就是说光查询不行,还得能向服务端写数据。当然,GraphQL的核心功能之一就是Mutation,也就是实现CUD这些非只读操作。</p> </li> <li>只暴露一个接口 每一个的 GraphQL 服务其实对外只提供了一个用于调用内部接口的端点,所有的请求都访问这个暴露出来的唯一端点。</li> </ol> <h5>GraphQL潜在问题</h5> <ol> <li> <p>服务端优化 由于是查询本身被解析成图,递归地取值,因此可能会存在服务器性能隐患。特别是对SQL来说,非常容易大量出现N+1的情形。</p> </li> <li> <p>安全问题 虽然GraphQL给客户端提供了强大的查询能力,但这也意味着有被客户端滥用的风险。如果不使用某些限制过大的查询,反复请求一条Load出所有Github用户的查询可能会让他们的服务器直接挂掉,GraphQL提高了被DDoS的风险。</p> </li> <li>需要重新思考Cache策略 REST虽然会引起一些性能问题,但它也以HTTP Cache的方式解决了很多性能问题。而对于多变的GraphQL操作来说,Cache就变成一个需要深入讨论的话题了。然而这种Cache策略就要交给客户端来完成了。</li> </ol> <h5>小结</h5> <table> <thead> <tr> <th></th> <th>优点</th> <th>缺点</th> </tr> </thead> <tbody> <tr> <td>GraphQL</td> <td>1). 一次查询得到所有需要的数据,减少网络请求;<br>2). 采用所见即所得模型,客户端代码不易出错;<br>3). 给用户提供了schema(接口描述);<br>4. 更严格、可扩展、可维护的数据查询方式,减少后端代码冗余。</td> <td>1). 没有实现缓存<br> 2). 没有状态码 <br> 3). 需要服务端配合</td> </tr> <tr> <td>RESTful</td> <td>1). 使用状态码,提高了结果的一致性和可预见性;<br>2). 实现了缓存机制</td> <td>1). 多复杂查询,需要发送多次请求,消耗网络资源;<br>2). 对于特殊的数据请求,服务端需要特殊处理,造成代码冗余。</td> </tr> </tbody> </table> <h3>总结</h3> <p>在使用REST有意义时才使用REST;如果RPC更合适,请使用RPC;或者两者结合使用。</p> <ul> <li>如果一个API主要是动作,也许它应该是RPC。</li> <li>如果一个API主要是CRUD和操作相关数据,也许它应该是REST。</li> <li>作为一门中心化的查询语言,GraphQL 在最佳实践中应该只对外暴露一个端点,这个端点会包含当前 Web 服务应该提供的全部资源,并把它们合理的连接成图,但是微服务架构恰恰是相反的思路,它的初衷就是将大服务拆分成独立部署的服务。使用微服务架构对服务进行拆分,通过 GraphQL 对微服务接口进行组合并完成鉴权功能。</li> <li>对外是否使用 GraphQL 其实不是特别的重要,将服务之间的职责进行解耦并对外提供合理的接口才是最关键的,只要架构上的设计合理,我们可以随时引入一个 GraphQL 服务来组合其他服务的功能。</li> </ul>

页面列表

ITEM_HTML