API 设计
- 可发现性.
- 遵守KISS原则.
- 向后兼容性.
- 避免抽象泄漏: 尽可能减少公开的细节.
- 一致性.
- 由用例驱动的API设计.
浏览器原生支持的数据类型.
优点:
- 可以以人类可读的形式格式化.
强烈建议开启gzip以减少传输数据的大小, 可以将格式化用的空格压缩到可以忽略不计.
在传输数据大小方面压倒性地胜过JSON, 即使在经过gzip压缩后也大幅领先于JSON.
X-Rate-Limit-Limit 当前期间(period)允许的请求数X-Rate-Limit-Remaining 当前期间(period)的剩余请求数X-Rate-Limit-Reset 当前期间(period)剩余的秒数在内部使用 HTTP/2 协议, 但无法直接通过 HTTP 进行访问.
如果需要在 Web 上使用, 则需要在服务器上准备一个用于代理转发请求的 agent.
如果需要在 Web 上使用, 则需要在服务器上准备一个用于代理转发请求的 agent.
优点:
- 基于Proto的类型定义.
- 多语言支持.
- 高性能.
缺点:
- 主要被设计用于服务与服务之间的通信, 而不是客户端到服务.
gRPC对于HTTP的支持是通过一个转发网关来实现的.
重用了 HTTP 协议.
优点:
- 具有类型定义.
- 天生的多语言支持.
- 尽管是 RPC, 却具有 HTTP URL 映射, 因此可以使用海量 HTTP 客户端.
- 定义文件本身即是文档.
使用 HTTP 协议, 一项服务只需一个 URL 作为接口.
实现非常简单, 基本上等于发送 JSON RPC 作为 Payload 的普通 POST 请求.
实现非常简单, 基本上等于发送 JSON RPC 作为 Payload 的普通 POST 请求.
优点:
- 实现简单
- 天生的多语言支持.
缺点:
- 没有类型定义, 但是可以通过共用 TypeScript 接口的方式间接实现类型定义
基于 HTTP 协议, 只有一个
/graphql 作为接口.- 具有类型定义.
- 天生的多语言支持.
- 独有的图联接查询机制, 由客户端决定查询结果的深度与结构,
因此GraphQL可以达到最高的请求利用率,
这对于字段特别多的API特别有用, 能大大减小传输的数据量. - 服务端只需要设计通用的API, 无需专门设计复杂的请求, 客户端可以自行组合出新的请求.
- 服务器负担更重, 为了返回大请求需要消耗更多的内存来存储中间数据.
- 基于图联接的查询机制导致难以控制数据库查询所需的成本.
- 图形式的字段返回方式对于很多项目来说可能是不必要的,
对静态类型语言来说会提升结构体适配的难度. - 无法使用HTTP标准缓存, 因为GraphQL使用POST.
查询请求的动态程度较高, 查询方式符合图数据结构的情况:
要么一次获取多项数据, 要么一次获取很深的数据, 要么两者皆有.
要么一次获取多项数据, 要么一次获取很深的数据, 要么两者皆有.
例如社交网络会遇上需要查询"这些用户评论的评论的评论以及他们的个人信息"的情况,
GraphQL就非常合适.
GraphQL就非常合适.
除了作为RPC接口以外, GraphQL还可以作为API网关使用, 使用GraphQL的目的是减少入站请求数量.
GraphQL常常被形容成与REST API竞争的一种协议, 这种描述本身是过度简化的.
GraphQL与REST API的竞争仅仅在查询具有很强的动态性时成立.
否则只需设计专门的REST API就可以满足需求.
否则只需设计专门的REST API就可以满足需求.
当查询的内容在广度和深度上总是固定时, 使用GraphQL很可能是一个错误的决定:
- 由于引入了GraphQL, 项目增加了多余的抽象和包装, 以及不可控的服务器查询执行成本.
- 对于客户端开发来说, 可能会因为查询的返回值的类型定义过于复杂而变得难以维护.
GraphQL的参考实现, 源于graphql模块.
该模式由JavaScript对象直接定义GraphQL Schema, 模式的可读性比较差.
响应查询时执行广度优先的逐级解析, 先解析外层, 再解析里层.
每层解析都会调用相应字段的resolve方法, 以返回对应的数据.
响应查询时执行广度优先的逐级解析, 先解析外层, 再解析里层.
每层解析都会调用相应字段的resolve方法, 以返回对应的数据.
该模式分别定义GraphQL Schema(字符串, 将被解析为AST)和resolvers(JavaScript对象),
使Schema与解析可以分开编写, 使代码变得清晰.
使Schema与解析可以分开编写, 使代码变得清晰.
此模式是Facebook为解决深度嵌套查询(也称为N+1问题)的性能问题而发明的.
DataLoader的目标是通过制造存活时间非常短的缓冲区将分散的子查询聚合为一个批处理查询.
聚合发生的时间点是由batchScheduleFn这一调度函数决定的,
直到聚合查询返回前, 请求会通过Promise阻塞.
聚合发生的时间点是由batchScheduleFn这一调度函数决定的,
直到聚合查询返回前, 请求会通过Promise阻塞.
默认的调度函数是enqueuePostPromiseJob, 该函数的功能基本上等价于nextTick或setImmediate.
GraphQL的N+1问题在于它图结构中的每一处查询嵌套的子查询都是独立的,
后端将为每个嵌套执行单独的查询, 这必定会严重损害数据库性能.
后端将为每个嵌套执行单独的查询, 这必定会严重损害数据库性能.
DataLoader具有缓存机制(确切地说是memoize), 因此调用
缓存只能被手动清除, 与一般memoize实现不同, DataLoader可以针对特定参数清除缓存, 而无需清除全部缓存.
DataLoader#load 方法会得到相同结果.缓存只能被手动清除, 与一般memoize实现不同, DataLoader可以针对特定参数清除缓存, 而无需清除全部缓存.
通用API的接口设计往往会让人联想到REST语义或/ORM框架.
在设计时往往不考虑查询的通信成本.
在设计时往往不考虑查询的通信成本.
优点:
- 可重用:
从工程角度来看, 只返回ID的API可以与其他通用API相互配合以重用, 可以节省下很多的后台开发成本. - 可缓存:
通用API更容易设计缓存方案.
对前端来说, 通用API会面临与API请求有关的N+1问题(产生瀑布请求),
完成操作所需要经过的时间会显著增加, 对客户端软件而言, 这种用户体验可能是不可接受的.
完成操作所需要经过的时间会显著增加, 对客户端软件而言, 这种用户体验可能是不可接受的.
由于操作需要分解为多个步骤, 对于后端来说, 相关操作不可能成为原子操作, 因此可能出现同步问题.
在获取id到获取id对应的数据之间的空隙, 后端数据可能已经发生根本性变化, 这会带来隐患和麻烦.
在获取id到获取id对应的数据之间的空隙, 后端数据可能已经发生根本性变化, 这会带来隐患和麻烦.
由于请求数量增多, 而每个请求都可能失败, 进而增加使用者编写正确代码的难度.
HTTP/2的多路复用不能解决原子性问题, 也不能保证多次请求不会发生失败.
对后端来说, 通用API会面临与SQL查询有关的N+1问题, 即数据库为了填充查询结果, 不得不进行多次SQL查询.
专用API可以实现特定的执行优化, 这使得专门API一定会比通用API有更好的性能.
专用API可以实现特定的执行优化, 这使得专门API一定会比通用API有更好的性能.
为了解决通用API的后端N+1问题,
添加缓存层(例如Facebook的DataLoader)是一种可以接受的次优解决方案,
但即使这么做, 性能表现仍然远远落后于设计专用API.
添加缓存层(例如Facebook的DataLoader)是一种可以接受的次优解决方案,
但即使这么做, 性能表现仍然远远落后于设计专用API.
通用API很容易在设计时泄漏数据库的模式.
专用API是指那些仅为达到某一特定目标而专门设计出的API.
有一些非常现实的原因迫使人们设计专用的API, 例如性能.
有一些非常现实的原因迫使人们设计专用的API, 例如性能.
优点:
- 易用性:
对于API调用者来说, 专门API可能更容易使用. - 性能优化.
缺点:
- 更高的维护难度:
专用API更难理解和维护, 尤其是在文档不够完善的情况下. - 更高的测试成本:
专用API总是需要专门的测试文件, 这些测试文件通常会很庞大和复杂, 因为专用API的返回数据往往很复杂.