前后端交互-一些关于接口设计的思考

接口设计的考虑点

我从下面几个方面考虑接口的设计:

  • 1 接口文档
  • 2 接口安全
  • 3 一些基本原则
  • 4 瘦客户端

1 关于接口文档

1.1 接口设计必须提供接口文档

无论项目团队的大小,在遇到接口问题的时候单纯的从代码出发,而不是从接口文档出发,对于整个项目团队的维护简直就是耍流氓。

1.2 文档也应纳入版本控制

使用markdown,wiki 做文本类型的文档,使用svn,git等做为版本工具 可以很清晰的看到接口文档的改动人和改动时间,同样是方便维护工作。

1.3 文档类型选择markdown,wiki等

使用文本类型的文档(比如markdown, wiki等格式),一则方便比较版本间改动,二则可以生成html, word, pdf等多种美观格式。我见过有好多团队是使用word来写文档的,由于是二进制格式,不利于版本比较,也不专业。

1.4 文档- 简洁

档不应浮于形式,而是力求只写最有价值的内容。做好这一点的关键是作者与读者要有足够的约定,比如蚕茧法就能很好的帮助简化类型定义的描述。

1.5 应有机制保证接口文档与代码的一致

一些团队在文档上应付差事浮于形式,在代码写完后,补一个word文档应付。在更新代码时,文档没有及时更新,导致文档都是错误没法看。好的做法都应先有设计再写代码,比如架构师或主程先设计好接口,然后再开始编程实现,在实现中发现问题再修正接口,更新设计文档,而不应是写完代码再补个设计。而在文档更新的具体做法上,也流行一种做法即文档以注释的方式内嵌于代码中,我称之为“格式化注释”,这样做到设计与代码在一起,更新也就更自然的同步了。之后再通过工具将注释抽出来美化给读者看。

1.6 接口应当包含内容

接口地址、请求方法、请求参数、返回内容、错误代码
- 以下是一个用户信息接口的文档示例,包含接口描述,请求参数,响应参数,json示例等。

接口描述:用户登陆成功后,或进入个人中心时会获取一次用户信息

1
2
3
4
5
6
7
8
9
10
{
"code":200,
"msg":"成功",
"time":"1482213602000",
"data": {
"id":"1001",
"name":"张三",
"age":"20"
}
}

2 接口安全

当我们面对很多外部接口的时候,我们需要考虑数据的安全性。

分为请求参数和响应参数:

  • 请求参数中包含用户隐私的字段参数,如:登陆接口的密码字段,需要进行加密传输,避免被代理捕捉请求后获取明文密码。
  • 响应参数中包含用户隐私的字段数据,需要加*号。如:手机号,身份证,用户邮箱,支付账号,邮寄地址等。
1
2
3
"phone":"150*****000",
"idCard":"3500**********0555",
"email":"40*****00@qq.com"

客户端和服务器通过约定的算法,对传递的参数值进行签名匹配,防止参数在请求过程中被抓取篡改

保护接口的方式最基本的是SSL/TLS,其次是:

  • 对称加密的方式
  • 非对称加密的方式
  • 动态秘钥

具体可以看 像架构师一样来思考微服务接口设计

3 接口的一些原则

  1. 原则一:一个页面尽量只有一个拉取接口

    主要考虑的是尽量减少请求链接数,请求链接数越多,由于网络原因,出现异常的可能性越大。

  2. 原则二:打破规则一,当请求需要缓存并且有需要及时更新的情况

    为了更好的打开速度,对于不经常变化的数据,往往需要做数据缓存以及请求缓存。但有些信息,比如预约时间,又需要做到及时,则应该分多个请求。

  3. 原则三:如果返回数据中某个字段的数据没有,返回该字段比不返回该字段要好。

    JSON格式的好处在于灵活性,但没有校验机制。所以定义协议时规定了有哪些字段,最好这些字段都返回。我的意思是比如返回一个列表,大多数场景是返回一个数组,但如果没有数据,返回一个空数组比不返回该字段要好。当然前端也有必要做自己的容错考虑。

  4. 返回格式:比较常见的返回数据的格式,经验有限,也不清楚这是不是最优的。

1
2
3
4
5
6
7
8
{
code: "",
msg: "",
time: "",
data: {

}
}
命名规范

统一命名:与后端约定好即可(php和js在命名时一般采用下划线风格,而Java中一般采用的是驼峰法),无绝对标准,不要同时存在驼峰”userName”,下划线”phone_number”两种形式就可以了。

避免冗余字段:每次在新增接口字段时,注意是否已经存在同一个含义的字段,保持命名一致,不要同时存在”userName”,”username”,”uName”多种同义字段。

注释清晰(重要):每个接口/字段都需要有详细的描述信息,很多时候接口体现业务逻辑,是团队中很重要的文档沉淀,同时,详细的接口文档,可以帮助新人快速熟悉业务。

3.5 将APP接收数据的类型定义为容错能力更强的String(推荐)

容错性强,规避因脏数据引起的数据解析失败。

4 瘦客户端

众所周知,客户端任何的修改都是需要发版的,特别是IOS需要走AppStore的审核流程。为了修一个bug,仅仅改几行代码,而重新走一轮发版流程,是很劳民伤财的。所以在接口设计的时候,也需要适当考虑这点,将业务重心交由后端,客户端保持逻辑简单。后端一天可以发n个版,客户端一个版本却只能发一次,有些团队一开始并没意识到这点,总觉后端就是重度业务逻辑的所在,管那么多前端的展示,真正到了出问题(bug或需求变更)需要发版的时候,虽然70%的锅是客户端背,但是,剩余30%也会对当初重客户端的选择而后悔,不过重点不是谁背锅,而是产品不出问题。so,为了大局,后端的RD们,我们得聊聊。

4.1 客户端尽量只负责展示逻辑,不处理业务逻辑

例如:客户端有个TextView,后端只给个status字段,status=1时,展示文案1;status=2时,展示文案2;这样设计的缺点是,如果以后要修改status=3时,展示文案1,那么这个status判断逻辑时写死在客户端,就没办法支持这种修改,且这种设计限定死了TextView只能展示2种文案。推荐方案是后端直接将TextView需要展示的文案下发,这样不管是status的判断,还是文案的展示,后期都是可变的。

4.2 客户端不处理金额的计算

例如:外卖APP,用户在下单的时候,需要选择收货地址,支付类型,优惠券等,任何一个选项的修改,都可能影响用户最后需要支付的金额。所以这里比较常见的接口设计是在每次选择完回到订单支付页面后,再发送一次请求,后端根据当前选项重新计算金额。金额永远是一款产品最重要,最敏感的信息,如果交由客户端计算,万一出错,即使少1分,都是毁灭性的,所以,关于金额,展示就好。

4.3 客户端少处理请求参数的校验与约束提示

例如:修改密码功能,密码规则”6-12字母,数字,下划线”,有3种做法:

  • 1 在发送请求前,客户端校验密码规则,如果不符合,则不发送请求。优点:规则不满足时,可以减少不必要的请求。缺点:客户端写死校验逻辑,密码规则变化时,客户端需要发版。

  • 2 客户端只判断null,和最短位数限制,其他校验规则交由后端处理。优点:灵活性最好。缺点:后端压力大,校验请求多。

  • 3 后端在通用配置的接口返回正则表达式,客户端获取后进行正则校验。优点:具有一定灵活性。缺点:开发,调试成本较高。(推荐:即使出问题,也可以清除配置,回退到第2个方案)

HTTP API设计指南

翻译自 HTTP API Design Guide https://github.com/interagent/http-api-design

目录

  • 基础
    • 强制使用安全连接(Secure Connections)
    • 强制头信息 Accept 中提供版本号
    • 支持Etag缓存
    • 为内省而提供 Request-Id
    • 通过请求中的范围(Range)拆分大的响应
  • 请求(Requests)
    • 在请求的body体使用JSON格式数据
    • 使用统一的资源路径格式
    • 路径和属性要小写
    • 支持方便的无id间接引用
    • 最小化路径嵌套
  • 响应(Responses)
    • 返回合适的状态码
    • 提供全部可用的资源
    • 提供资源的(UU)ID
    • 提供标准的时间戳
    • 使用UTC(世界标准时间)时间,用ISO8601进行格式化
    • 嵌套外键关系
    • 生成结构化的错误
    • 显示频率限制状态
    • 保证响应JSON最小化
  • 工件(Artifacts)
    • 提供机器可读的JSON模式
    • 提供人类可读的文档
    • 提供可执行的例子
    • 描述稳定性
  • 译者注

基础

隔离关注点

设计时通过将请求和响应之间的不同部分隔离来让事情变得简单。保持简单的规则让我们能更关注在一些更大的更困难的问题上。

请求和响应将解决一个特定的资源或集合。使用路径(path)来表明身份,body来传输内容(content)还有头信息(header)来传递元数据(metadata)。查询参数同样可以用来传递头信息的内容,但头信息是首选,因为他们更灵活、更能传达不同的信息。

强制使用安全连接(Secure Connections)

所有的访问API行为,都需要用 TLS 通过安全连接来访问。没有必要搞清或解释什么情况需要 TLS 什么情况不需要 TLS,直接强制任何访问都要通过 TLS。

理想状态下,通过拒绝所有非 TLS 请求,不响应 http 或80端口的请求以避免任何不安全的数据交换。如果现实情况中无法这样做,可以返回403 Forbidden响应。

把非 TLS 的请求重定向(Redirect)至 TLS 连接是不明智的,这种含混/不好的客户端行为不会带来明显好处。依赖于重定向的客户端访问不仅会导致双倍的服务器负载,还会使 TLS 加密失去意义,因为在首次非 TLS 调用时,敏感信息就已经暴露出去了。

强制头信息 Accept 中提供版本号

制定版本并在版本之间平缓过渡对于设计和维护一套API是个巨大的挑战。所以,最好在设计之初就使用一些方法来预防可能会遇到的问题。

为了避免API的变动导致用户使用中产生意外结果或调用失败,最好强制要求所有访问都需要指定版本号。请避免提供默认版本号,一旦提供,日后想要修改它会相当困难。

最适合放置版本号的位置是头信息(HTTP Headers),在 Accept 段中使用自定义类型(content type)与其他元数据(metadata)一起提交。例如:

1
Accept: application/vnd.heroku+json; version=3

支持Etag缓存

在所有返回的响应中包含ETag头信息,用来标识资源的版本。这让用户对资源进行缓存处理成为可能,在后续的访问请求中把If-None-Match头信息设置为之前得到的ETag值,就可以侦测到已缓存的资源是否需要更新。

为内省而提供 Request-Id

为每一个请求响应包含一个Request-Id头,并使用UUID作为该值。通过在客户端、服务器或任何支持服务上记录该值,它能为我们提供一种机制来跟踪、诊断和调试请求。

通过请求中的范围(Range)拆分大的响应

一个大的响应应该通过多个请求使用Range头信息来拆分,并指定如何取得。详细的请求和响应的头信息(header),状态码(status code),范围(limit),排序(ordering)和迭代(iteration)等,参考Heroku Platform API discussion of Ranges.

请求(Requests)

在请求的body体使用JSON格式数据

PUT/PATCH/POST 请求的正文(request bodies)中使用JSON格式数据,而不是使用 form 表单形式的数据。这与我们使用JSON格式返回请求相对应,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
$ curl -X POST https://service.com/apps \
-H "Content-Type: application/json" \
-d '{"name": "demoapp"}'

{
"id": "01234567-89ab-cdef-0123-456789abcdef",
"name": "demoapp",
"owner": {
"email": "username@example.com",
"id": "01234567-89ab-cdef-0123-456789abcdef"
},
...
}

使用统一的资源路径格式

资源名(Resource names)

使用复数形式为资源命名,除非这个资源在系统中是单例的 (例如,在大多数系统中,给定的用户帐户只有一个)。 这种方式保持了特定资源的统一性。

行为(Actions)

好的末尾不需要为资源指定特殊的行为,但在特殊情况下,为某些资源指定行为却是必要的。为了描述清楚,在行为前加上一个标准的actions

1
/resources/:resource/actions/:action

例如:

1
/runs/{run_id}/actions/stop

路径和属性要小写

为了和域名命名规则保持一致,使用小写字母并用-分割路径名字,例如:

1
2
service-api.com/users
service-api.com/app-setups

属性也使用小写字母,但是属性名要用下划线_分割,以便在Javascript中省略引号。 例如:

1
service_class: "first"

支持方便的无id间接引用

在某些情况下,让用户提供ID去定位资源是不方便的。例如,一个用户想取得他在Heroku平台app信息,但是这个app的唯一标识是UUID。这种情况下,你应该支持接口通过名字和ID都能访问,例如:

1
2
3
$ curl https://service.com/apps/{app_id_or_name}
$ curl https://service.com/apps/97addcf0-c182
$ curl https://service.com/apps/www-prod

不要只接受使用名字而放弃了使用id。

最小化路径嵌套

在一些有父路径/子路径嵌套关系的资源数据模块中,路径可能有非常深的嵌套关系,例如:

1
/orgs/{org_id}/apps/{app_id}/dynos/{dyno_id}

推荐在根(root)路径下指定资源来限制路径的嵌套深度。使用嵌套指定范围的资源。在上述例子中,dyno属于app,app属于org可以表示为:

1
2
3
4
5
/orgs/{org_id}
/orgs/{org_id}/apps
/apps/{app_id}
/apps/{app_id}/dynos
/dynos/{dyno_id}

响应(Responses)

返回合适的状态码

为每一次的响应返回合适的HTTP状态码。 好的响应应该使用如下的状态码:

  • 200: GET请求成功,及DELETEPATCH同步请求完成,或者PUT同步更新一个已存在的资源
  • 201: POST 同步请求完成,或者PUT同步创建一个新的资源
  • 202: POSTPUTDELETE,或PATCH请求接收,将被异步处理
  • 206: GET 请求成功,但是只返回一部分,参考:上文中范围分页

使用身份认证(authentication)和授权(authorization)错误码时需要注意:

  • 401 Unauthorized: 用户未认证,请求失败
  • 403 Forbidden: 用户无权限访问该资源,请求失败

当用户请求错误时,提供合适的状态码可以提供额外的信息:

  • 422 Unprocessable Entity: 请求被服务器正确解析,但是包含无效字段
  • 429 Too Many Requests: 因为访问频繁,你已经被限制访问,稍后重试
  • 500 Internal Server Error: 服务器错误,确认状态并报告问题

对于用户错误和服务器错误情况状态码,参考: HTTP response code spec

提供全部可用的资源

提供全部可显现的资源表述 (例如: 这个对象的所有属性) ,当响应码为200或是201时返回所有可用资源,包含 PUT/PATCHDELETE
请求,例如:

1
2
3
4
5
6
7
8
9
10
11
12
$ curl -X DELETE \  
https://service.com/apps/1f9b/domains/0fd4

HTTP/1.1 200 OK
Content-Type: application/json;charset=utf-8
...
{
"created_at": "2012-01-01T12:00:00Z",
"hostname": "subdomain.example.com",
"id": "01234567-89ab-cdef-0123-456789abcdef",
"updated_at": "2012-01-01T12:00:00Z"
}

当请求状态码为202时,不返回所有可用资源,例如:

1
2
3
4
5
6
7
$ curl -X DELETE \  
https://service.com/apps/1f9b/dynos/05bd

HTTP/1.1 202 Accepted
Content-Type: application/json;charset=utf-8
...
{}

提供资源的(UU)ID

在默认情况给每一个资源一个id属性。除非有更好的理由,否则请使用UUID。不要使用那种在服务器上或是资源中不是全局唯一的标识,尤其是自动增长的id。

生成小写的UUID格式 8-4-4-4-12,例如:

1
"id": "01234567-89ab-cdef-0123-456789abcdef"

提供标准的时间戳

为资源提供默认的创建时间 created_at 和更新时间 updated_at,例如:

1
2
3
4
5
6
{
...
"created_at": "2012-01-01T12:00:00Z",
"updated_at": "2012-01-01T13:00:00Z",
...
}

有些资源不需要使用时间戳那么就忽略这两个字段。

使用UTC(世界标准时间)时间,用ISO8601进行格式化

仅接受和返回UTC格式的时间。ISO8601格式的数据,例如:

1
"finished_at": "2012-01-01T12:00:00Z"

嵌套外键关系

使用嵌套对象序列化外键关联,例如:

1
2
3
4
5
6
7
{
"name": "service-production",
"owner": {
"id": "5d8201b0..."
},
// ...
}

而不是像这样:

1
2
3
4
5
{
"name": "service-production",
"owner_id": "5d8201b0...",
...
}

这种方式尽可能的把相关联的资源信息内联在一起,而不用改变资源的结构,或者引入更多的顶层字段,例如:

1
2
3
4
5
6
7
8
9
{
"name": "service-production",
"owner": {
"id": "5d8201b0...",
"name": "Alice",
"email": "alice@heroku.com"
},
...
}

生成结构化的错误

响应错误的时,生成统一的、结构化的错误信息。包含一个机器可读的错误 id,一个人类可读的错误信息(message),根据情况可以添加一个url来告诉客户端关于这个错误的更多信息以及如何去解决它,例如:

1
HTTP/1.1 429 Too Many Requests
1
2
3
4
5
{
"id": "rate_limit",
"message": "Account reached its API rate limit.",
"url": "https://docs.service.com/rate-limits"
}

文档化错误信息格式,以及客户端可能遇到的错误信息id

显示频率限制状态

客户端的访问速度限制可以维护服务器的良好状态,保证为其他客户端请求提供高性的服务。你可以使用token bucket algorithm技术量化请求限制。

为每一个带有RateLimit-Remaining响应头的请求,返回预留的请求tokens。

保证响应JSON最小化

请求中多余的空格会增加响应大小,而且现在很多的HTTP客户端都会自己输出可读格式(”prettify”)的JSON。所以最好保证响应JSON最小化,例如:

1
{"beta":false,"email":"alice@heroku.com","id":"01234567-89ab-cdef-0123-456789abcdef","last_login":"2012-01-01T12:00:00Z","created_at":"2012-01-01T12:00:00Z","updated_at":"2012-01-01T12:00:00Z"}

而不是这样:

1
2
3
4
5
6
7
8
{
"beta": false,
"email": "alice@heroku.com",
"id": "01234567-89ab-cdef-0123-456789abcdef",
"last_login": "2012-01-01T12:00:00Z",
"created_at": "2012-01-01T12:00:00Z",
"updated_at": "2012-01-01T12:00:00Z"
}

你可以提供可选的方式为客户端提供更详细可读的响应,使用查询参数(例如:?pretty=true)或者通过Accept头信息参数(例如:Accept: application/vnd.heroku+json; version=3; indent=4;)。

工件(Artifacts)

提供机器可读的JSON模式

提供一个机器可读的模式来恰当的表现你的API。使用
prmd管理你的模式,并且确保用prmd verify验证是有效的。

提供人类可读的文档

提供人类可读的文档让客户端开发人员可以理解你的API。

如果你用prmd创建了一个概要并且按上述要求描述,你可以为所有节点很容易的使用prmd doc生成Markdown文档。

除了节点信息,提供一个API概述信息:

  • 验证授权,包含如何取得和如何使用token。
  • API稳定及版本管理,包含如何选择所需要的版本。
  • 一般情况下的请求和响应的头信息。
  • 错误的序列化格式。
  • 不同编程语言客户端使用API的例子。

提供可执行的例子

提供可执行的示例让用户可以直接在终端里面看到API的调用情况,最大程度的让这些示例可以简单的使用,以减少用户尝试使用API的工作量。例如:

1
2
$ export TOKEN=... # acquire from dashboard
$ curl -is https://$TOKEN@service.com/users

如果你使用prmd生成Markdown文档,每个节点都会自动获取一些示例。

描述稳定性

描述您的API的稳定性或是它在各种各样节点环境中的完备性和稳定性,例如:加上 原型版(prototype)/开发版(development)/产品版(production)等标记。

更多关于可能的稳定性和改变管理的方式,查看 Heroku API compatibility policy

一旦你的API宣布产品正式版本及稳定版本时,不要在当前API版本中做一些不兼容的改变。如果你需要,请创建一个新的版本的API。


前后端交互-一些关于接口设计的思考
https://flepeng.github.io/021-frontend-前后端交互-一些关于接口设计的思考/
作者
Lepeng
发布于
2021年3月8日
许可协议