Web API设计其实是一个挺重要的设计话题,许多公司都会有公司层面的Web API设计规范,几乎所有的项目在详细设计阶段都会进行API设计,项目开发后都会有一份API文档供测试和联调。本文尝试根据自己的理解总结一下目前常见的四种API设计风格以及设计考虑点。
这是最常见的方式,RPC说的是本地调用远程的方法,面向的是过程。
这里就不贴例子了,估计超过50%的API是这种分格的。
是一种架构风格,有四个级别的成熟度:
级别0其实就是类RPC的风格,级别3是真正的REST,大多数号称REST的API在级别2。REST实现一些要点包括:
{ "content": [ { "price": 499.00, "description": "Apple tablet device", "name": "iPad", "links": [ { "rel": "self", "href": "http://localhost:8080/product/1" } ], "attributes": { "connector": "socket" } }, { "price": 49.00, "description": "Dock for iPhone/iPad", "name": "Dock", "links": [ { "rel": "self", "href": "http://localhost:8080/product/3" } ], "attributes": { "connector": "plug" } } ], "links": [ { "rel": "product.search", "href": "http://localhost:8080/product/search" } ] }
Spring框架也提供了相应的支持:https://spring.io/projects/spring-hateoas,比如如下的代码:
@RestController public class GreetingController { private static final String TEMPLATE = "Hello, %s!"; @RequestMapping("/greeting") public HttpEntity<Greeting> greeting( @RequestParam(value = "name", required = false, defaultValue = "World") String name) { Greeting greeting = new Greeting(String.format(TEMPLATE, name)); greeting.add(linkTo(methodOn(GreetingController.class).greeting(name)).withSelfRel()); return new ResponseEntity<>(greeting, HttpStatus.OK); } }
产生如下的结果:
可以说REST的API设计是需要设计感的,需要仔细来思考API的资源,资源之间的关系和导航,URI的定义等等。对于一套设计精良的REST API,其实客户端只要知道可用资源清单,往往就可以轻易根据约定俗成的规范以及导航探索出大部分API。比较讽刺的是,有很多网站给前端和客户端的接口是REST的,爬虫开发者可以轻易探索到所有接口,甚至一些内部接口,毕竟猜一下REST的接口比RPC的接口容易的多。
作为补充,下面再列几个有关REST API设计大家争议讨论纠结的比较多的几个方面。
比如 https://stackoverflow.com/questions/630453/put-vs-post-in-rest ,总的来说大家基本认同微软提到的三个方面:
当然,有些公司的规范是创建资源仅仅是POST,不支持PUT
看到过许多文章都在说,REST还是建议返回的数据本身就是实体信息(或列表信息),而不建议把数据进行一层包装(Result)。如果需要有更多的信息来补充的话,可以放到HTTP Header中,比如https://developer.github.com/v3/projects/cards/的API:
GET /projects/columns/:column_id/cards Status: 200 OK Link: <https://api.github.com/resource?page=2>; rel="next", <https://api.github.com/resource?page=5>; rel="last" [ { "url": "https://api.github.com/projects/columns/cards/1478", "id": 1478, "node_id": "MDExOlByb2plY3RDYXJkMTQ3OA==", "note": "Add payload for delete Project column", "created_at": "2016-09-05T14:21:06Z", "updated_at": "2016-09-05T14:20:22Z", "archived": false, "column_url": "https://api.github.com/projects/columns/367", "content_url": "https://api.github.com/repos/api-playground/projects-test/issues/3", "project_url": "https://api.github.com/projects/120" } ]
之前我们给出的HATEOAS的例子是在响应体中有"content"和"links"的层级,也就是响应体并不是资源本身,是有包装的,除了links,很多时候我们会直接以统一的格式来定义API响应结构体,比如:
{ "code" : "", "message" : "", "path" : "" "time" : "", "data" : {}, "links": [] }
我个人比较喜欢这种方式,不喜欢使用HTTP头,原因还是因为多变的部署和网络环境下,如果某些环节请求头被修改了或丢弃了会很麻烦(还有麻烦的Header Key大小写问题),响应体一般所有的代理都不会去动。
微软的API设计指南(文末有贴地址)中指出避免太复杂的层级资源,比如/customers/1/orders/99/products过于复杂,可以退化为/customers/1/orders和/orders/99/products,不URI的复杂度不应该超过collection/item/collection,Google的一些API会层级比较多,比如:
API service: spanner.googleapis.com A collection of instances: projects/*/instances/*. A collection of instance operations: projects/*/instances/*/operations/*. A collection of databases: projects/*/instances/*/databases/*. A collection of database operations: projects/*/instances/*/databases/*/operations/*. A collection of database sessions: projects/*/instances/*/databases/*/sessions/*
这点我比较赞同微软的规范,太深的层级在实现起来也不方便。
如果说RPC面向过程,REST面向资源,那么GraphQL就是面向数据查询了。“GraphQL 既是一种用于 API 的查询语言也是一个满足你数据查询的运行时。 GraphQL 对你的 API 中的数据提供了一套易于理解的完整描述,使得客户端能够准确地获得它需要的数据,而且没有任何冗余,也让 API 更容易地随着时间推移而演进,还能用于构建强大的开发者工具。”
采用GraphQL,甚至不需要有任何的接口文档,在定义了Schema之后,服务端实现Schema,客户端可以查看Schema,然后构建出自己需要的查询请求来获得自己需要的数据。
比如定义如下的Schema:
# # Schemas must have at least a query root type # schema { query: Query } type Query { characters( episode: Episode ) : [Character] human( # The id of the human you are interested in id : ID! ) : Human droid( # The non null id of the droid you are interested in id: ID! ): Droid } # One of the films in the Star Wars Trilogy enum Episode { # Released in 1977 NEWHOPE # Released in 1980. EMPIRE # Released in 1983. JEDI } # A character in the Star Wars Trilogy interface Character { # The id of the character. id: ID! # The name of the character. name: String! # The friends of the character, or an empty list if they # have none. friends: [Character] # Which movies they appear in. appearsIn: [Episode]! # All secrets about their past. secretBackstory : String @deprecated(reason : "We have decided that this is not canon") } # A humanoid creature in the Star Wars universe. type Human implements Character { # The id of the human. id: ID! # The name of the human. name: String! # The friends of the human, or an empty list if they have none. friends: [Character] # Which movies they appear in. appearsIn: [Episode]! # The home planet of the human, or null if unknown. homePlanet: String # Where are they from and how they came to be who they are. secretBackstory : String @deprecated(reason : "We have decided that this is not canon") } # A mechanical creature in the Star Wars universe. type Droid implements Character { # The id of the droid. id: ID! # The name of the droid. name: String! # The friends of the droid, or an empty list if they have none. friends: [Character] # Which movies they appear in. appearsIn: [Episode]! # The primary function of the droid. primaryFunction: String # Construction date and the name of the designer. secretBackstory : String @deprecated(reason : "We have decided that this is not canon") }
采用GraphQL Playground(https://github.com/prisma/graphql-playground)来查看graphql端点可以看到所有支持的查询:
其实就是__schema:
然后我们可以根据客户端的UI需要自己来定义查询请求,服务端会根据客户端给的结构来返回数据:
再来看看Github提供的GraphQL(更多参考https://developer.github.com/v4/guides/):
查询出了最后的三个我的repo:
GraphQL就是通过Schema来明确数据的能力,服务端提供统一的唯一的API入口,然后客户端来告诉服务端我要的具体数据结构(基本可以说不需要有API文档),有点客户端驱动服务端的意思。虽然客户端灵活了,但是GraphQL服务端的实现比较复杂和痛苦的,GraphQL不能替代其它几种设计风格,并不是传说中的REST 2.0。更多信息参见 https://github.com/chentsulin/awesome-graphql 。
没有高大上的英文缩写,因为这种模式或风格是我自己想出来的,那就是通过API让服务端来驱动客户端,在之前的一些项目中也有过实践。说白了,就是在API的返回结果中包含驱动客户端去怎么做的信息,两个层次:
之前有两个这样的项目采用了类似的API设计方式:
一般而言,对外的Web API是不会采用这种服务端驱动客户端的方式来设计API的。对于某些特殊类型的项目,我们可以考虑采用这种服务端驱动的方式来设计API,让客户端变为一个不含逻辑的执行者,执行的是UI和交互。
https://user-gold-cdn.xitu.io/2019/2/15/168eff296f015115 此文给出了一个有关RPC、REST、GRAPHQL选择的决策方式可以参考,见上图。
我觉得:
很多API设计指南都提到了下面这些设计考量点,也需要在设计的时候进行考虑:
HEAD https://adventure-works.com/products/10?fields=productImage HTTP/1.1 HTTP/1.1 200 OK Accept-Ranges: bytes Content-Type: image/jpeg Content-Length: 4580
然后提供资源分段下载功能:
GET https://adventure-works.com/products/10?fields=productImage HTTP/1.1 Range: bytes=0-2499 HTTP/1.1 206 Partial Content Accept-Ranges: bytes Content-Type: image/jpeg Content-Length: 2500 Content-Range: bytes 0-2499/4580 [...]
评论(
0)