前后端交互-一些关于接口设计的思考
接口设计的考虑点
我从下面几个方面考虑接口的设计:
- 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 接口安全
当我们面对很多外部接口的时候,我们需要考虑数据的安全性。
分为请求参数和响应参数:
- 请求参数中包含用户隐私的字段参数,如:登陆接口的密码字段,需要进行加密传输,避免被代理捕捉请求后获取明文密码。
- 响应参数中包含用户隐私的字段数据,需要加*号。如:手机号,身份证,用户邮箱,支付账号,邮寄地址等。
1 |
|
客户端和服务器通过约定的算法,对传递的参数值进行签名匹配,防止参数在请求过程中被抓取篡改
保护接口的方式最基本的是SSL/TLS,其次是:
- 对称加密的方式
- 非对称加密的方式
- 动态秘钥
具体可以看 像架构师一样来思考微服务接口设计
3 接口的一些原则
原则一:一个页面尽量只有一个拉取接口
主要考虑的是尽量减少请求链接数,请求链接数越多,由于网络原因,出现异常的可能性越大。
原则二:打破规则一,当请求需要缓存并且有需要及时更新的情况
为了更好的打开速度,对于不经常变化的数据,往往需要做数据缓存以及请求缓存。但有些信息,比如预约时间,又需要做到及时,则应该分多个请求。
原则三:如果返回数据中某个字段的数据没有,返回该字段比不返回该字段要好。
JSON格式的好处在于灵活性,但没有校验机制。所以定义协议时规定了有哪些字段,最好这些字段都返回。我的意思是比如返回一个列表,大多数场景是返回一个数组,但如果没有数据,返回一个空数组比不返回该字段要好。当然前端也有必要做自己的容错考虑。
返回格式:比较常见的返回数据的格式,经验有限,也不清楚这是不是最优的。
1 |
|
命名规范
统一命名:与后端约定好即可(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 |
|
支持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 |
|
使用统一的资源路径格式
资源名(Resource names)
使用复数形式为资源命名,除非这个资源在系统中是单例的 (例如,在大多数系统中,给定的用户帐户只有一个)。 这种方式保持了特定资源的统一性。
行为(Actions)
好的末尾不需要为资源指定特殊的行为,但在特殊情况下,为某些资源指定行为却是必要的。为了描述清楚,在行为前加上一个标准的actions
:
1 |
|
例如:
1 |
|
路径和属性要小写
为了和域名命名规则保持一致,使用小写字母并用-
分割路径名字,例如:
1 |
|
属性也使用小写字母,但是属性名要用下划线_
分割,以便在Javascript中省略引号。 例如:
1 |
|
支持方便的无id间接引用
在某些情况下,让用户提供ID去定位资源是不方便的。例如,一个用户想取得他在Heroku平台app信息,但是这个app的唯一标识是UUID。这种情况下,你应该支持接口通过名字和ID都能访问,例如:
1 |
|
不要只接受使用名字而放弃了使用id。
最小化路径嵌套
在一些有父路径/子路径嵌套关系的资源数据模块中,路径可能有非常深的嵌套关系,例如:
1 |
|
推荐在根(root)路径下指定资源来限制路径的嵌套深度。使用嵌套指定范围的资源。在上述例子中,dyno属于app,app属于org可以表示为:
1 |
|
响应(Responses)
返回合适的状态码
为每一次的响应返回合适的HTTP状态码。 好的响应应该使用如下的状态码:
200
:GET
请求成功,及DELETE
或PATCH
同步请求完成,或者PUT
同步更新一个已存在的资源201
:POST
同步请求完成,或者PUT
同步创建一个新的资源202
:POST
,PUT
,DELETE
,或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
/PATCH
和 DELETE
请求,例如:
1 |
|
当请求状态码为202时,不返回所有可用资源,例如:
1 |
|
提供资源的(UU)ID
在默认情况给每一个资源一个id
属性。除非有更好的理由,否则请使用UUID。不要使用那种在服务器上或是资源中不是全局唯一的标识,尤其是自动增长的id。
生成小写的UUID格式 8-4-4-4-12
,例如:
1 |
|
提供标准的时间戳
为资源提供默认的创建时间 created_at
和更新时间 updated_at
,例如:
1 |
|
有些资源不需要使用时间戳那么就忽略这两个字段。
使用UTC(世界标准时间)时间,用ISO8601进行格式化
仅接受和返回UTC格式的时间。ISO8601格式的数据,例如:
1 |
|
嵌套外键关系
使用嵌套对象序列化外键关联,例如:
1 |
|
而不是像这样:
1 |
|
这种方式尽可能的把相关联的资源信息内联在一起,而不用改变资源的结构,或者引入更多的顶层字段,例如:
1 |
|
生成结构化的错误
响应错误的时,生成统一的、结构化的错误信息。包含一个机器可读的错误 id
,一个人类可读的错误信息(message
),根据情况可以添加一个url
来告诉客户端关于这个错误的更多信息以及如何去解决它,例如:
1 |
|
1 |
|
文档化错误信息格式,以及客户端可能遇到的错误信息id
。
显示频率限制状态
客户端的访问速度限制可以维护服务器的良好状态,保证为其他客户端请求提供高性的服务。你可以使用token bucket algorithm技术量化请求限制。
为每一个带有RateLimit-Remaining
响应头的请求,返回预留的请求tokens。
保证响应JSON最小化
请求中多余的空格会增加响应大小,而且现在很多的HTTP客户端都会自己输出可读格式(”prettify”)的JSON。所以最好保证响应JSON最小化,例如:
1 |
|
而不是这样:
1 |
|
你可以提供可选的方式为客户端提供更详细可读的响应,使用查询参数(例如:?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 |
|
如果你使用prmd生成Markdown文档,每个节点都会自动获取一些示例。
描述稳定性
描述您的API的稳定性或是它在各种各样节点环境中的完备性和稳定性,例如:加上 原型版(prototype)/开发版(development)/产品版(production)等标记。
更多关于可能的稳定性和改变管理的方式,查看 Heroku API compatibility policy
一旦你的API宣布产品正式版本及稳定版本时,不要在当前API版本中做一些不兼容的改变。如果你需要,请创建一个新的版本的API。