本节摘要:HATEOAS(超媒体作为应用状态引擎)让服务器在资源表示里附带
_links,用链接引导客户端"下一步能做什么、去哪做",从而把客户端从硬编码 URI 的泥潭里解放出来。本节讲清它的完整机制、业界"半套 vs 全套"的真实落差,并用一个订单状态机案例,判断什么时候值得认真做、做到什么程度。
阅读完本节,你应当能够:
_links 的资源表示,表达 self 与各种关系的链接。大部分团队声称做了 HATEOAS,实际只是"响应里塞了一串 _links"。区别在哪?看一个反例:资源表示里放了个 "self": "/book/42",客户端代码里仍然把"撤销订单那个 URI"写死成字符串。
这是假 HATEOAS——链接只是装饰,客户端根本没靠链接活。真 HATEOAS 的定义是:客户端把链接当应用状态的路标,跟着它走,而不把 URI 硬编码进自己代码。链接不是点缀,是导航,是让客户端"不知道自己下一站在哪、也懒得知道"的机制。
用一个订单案例把机制走通。订单状态可能是:待支付 → 已支付 → 已发货 → 已完成,或中途取消。
服务器对 GET /orders/456(状态为"待支付")返回:
{ "id": 456, "status": "pending", "_links": { "self": "http://api.example.com/orders/456", "pay": "http://api.example.com/orders/456/payments", "cancel": "http://api.example.com/orders/456/cancel" } }
隔天同一个订单已支付,GET /orders/456 现在的表示变成:
{ "id": 456, "status": "paid", "_links": { "self": "http://api.example.com/orders/456", "cancel": "http://api.example.com/orders/456/cancel", "invoice": "http://api.example.com/orders/456/invoice" } }
注意发生了什么:状态从 pending 变 paid 后,pay 链接消失了,出现了 invoice 链接。客户端代码不用改——它读走每一项 _links 里的 href,跟着可用链接导航,状态变了链接集合就变,客户端天然只能做当前状态允许的动作。这就是"应用状态引擎":状态的推进,靠服务器给的链接指引,而不是客户端自己脑补逻辑。
_links 还指向正确的新 URI,客户端无需跟着改。_links 里多暴露一个链接,老客户端忽略它,新客户端直接用上,向后兼容天然成立。链接的"关系名"也别跟闹着玩似的起:self、next、prev、pay、cancel 这类语义明确的关系名,让客户端能按名字而非猜 URI 去找链接;而 link1、foo 这类无意义名字,等于没给导航。关系名本身就是给"应用状态引擎"铺的路标,起得越像是"下一个能做的是什么",客户端越不需要在逻辑里硬塞状态判断。
HATEOAS 的代价也不小,这是谈它时必须摊在明面的部分:
于是业界真实分布是"半套 HATEOAS 远多于全套":绝大多数接口做"链接已暴露、客户端仍硬编码"的折中——效率与演进性的平衡点落在"链接为参考、不强制客户端跟走"。可以这样定位自己:
| 级别 | 做法 | 适合对象 |
|---|---|---|
| 无 | 连 _links 都没有 |
内部一次性接口 |
| 半套 | 有 _links,客户端仍硬编码 |
多数商业 API |
| 全套 | 客户端完全靠链接导航 | 高度进化导向、超媒体成熟团队 |
⚠️ 常见坑:为某个业务一拍脑袋承诺"我们做全套 HATEOAS",却没评估状态机复杂度与客户端迁移成本,最后半途而废,链接成了摆设。先从小范围(订单这种状态明确的资源)试点半套,验证收益再谈升级。
💡 关键直觉:HATEOAS 买到的是"未来改 URI 的自由",付账的是"现在的建模与测试复杂度"。买不买、买到几成,是纯粹的工程取舍,不是宗教立场。
_links 随资源状态变化而增减,驱动客户端动作自适应。导航条款画上句号,最后给这份契约上保险——幂等与安全这两条保证条款,专门管"重试、队列、重复提交"这些出事的时刻。