简历回答逐字稿:HATEOAS 资源动作契约消费层设计
关联:简历追问:HATEOAS 资源动作契约消费层设计、15分钟:HATEOAS 资源契约架构。
30 秒开场
我当时做这块,不是为了追求 HATEOAS 这个概念,而是为了解决多端状态机漂移。以前很多业务动作是前端根据 status、role、permission 自己判断,比如审批、撤回、重新提交。后端状态机一改,Web、小程序、App 都要跟着改,而且很容易漏。
所以我的思路是把“当前资源现在能做什么”放回资源表达本身。后端返回资源数据时,同时返回 _links 和 _templates。前端不再解释 pending 到底能不能审批,而是消费 relation,比如有没有 approve 这个 action;有就按模板提交,没有就不展示或展示不可操作状态。
如果面试官问:你具体怎么设计 _links 和 _templates?
我会这样说:
_links 主要表达资源导航和动作入口,比如 self、detail、approve、reject 这类 relation;_templates 更偏动作提交契约,里面会有 method、content type、字段列表、必填、枚举、默认值这些信息。
我没有把它设计成简单的“按钮数组”,因为按钮只是 UI 表现。真正稳定的是业务 relation。比如审批动作可以在 Web 上表现成按钮,在移动端表现成底部操作栏,对 Agent 来说则可以变成 tool。只要 relation 和 template 稳定,不同消费端可以有不同 UI。
一个典型资源大概是这样:
{
"id": "order-1",
"status": "pending",
"_links": {
"self": { "href": "/orders/order-1" },
"approve": { "href": "/orders/order-1/approve", "method": "POST" }
},
"_templates": {
"approve": {
"method": "POST",
"properties": [
{ "name": "comment", "type": "text", "required": false }
]
}
}
}前端看到 approve 就知道当前上下文允许审批,同时根据 template 生成或校验提交参数。前端不需要知道为什么允许,原因可能是状态、角色、租户权限、数据归属,这些权威规则仍然在后端。
如果面试官追:没有返回某个动作,前端是隐藏还是禁用?
我会先说我们不会让业务页面自己猜,而是做一层统一策略。
一般动作缺失有几种语义:第一,业务上不可执行,比如订单已经撤回;第二,用户无权限;第三,契约异常,比如后端漏返回;第四,资源还没加载完成。不同语义 UI 不一样。
如果是列表里的非主操作,通常缺失就不展示,避免用户误点。如果是详情页里的关键主操作,我们更倾向于展示禁用态或解释入口,比如“当前状态不可审批”或者“无审批权限”。但这个解释不能靠前端猜 status,而是最好由后端模板或错误原因提供。
这里的原则是:前端可以决定交互呈现,但不要重新实现业务状态机。
如果面试官追:列表 100 条资源,每条都算动作,会不会很慢?
我会这样回答:
这确实是 HATEOAS 落地时最容易踩的坑,不能在循环里对每条资源单独查权限。后端组装动作时要把它当成资源查询的一部分,而不是额外的 N 次查询。
我们的思路是先批量加载资源状态和必要的关联信息,比如用 IN 查询一次拿到这些单据的状态、归属、租户信息;当前用户的角色权限矩阵一般已经在鉴权上下文或缓存里。接下来动作判断大部分应该是内存里的状态机计算。
如果是特别复杂的首页列表,还可以把 available_actions 做成读模型或物化视图,通过领域事件异步更新。这样牺牲一点点实时性,换取列表查询性能。关键是不能让 _links 组装变成物理数据库 N+1。
如果面试官追:这个改造有什么收益?
我会尽量不说空话:
收益主要有三个。第一,多端一致性更好,因为按钮和动作是否可用都来自后端资源契约。第二,接口路径演进更容易,前端依赖 relation,不到处硬编码 URL。第三,测试范围可以收敛,重点测不同角色、状态、租户下后端返回的动作契约,以及 SDK 是否正确消费契约。
如果要量化,我会先限定口径:不是所有前端逻辑都消失,而是原来散在多个端的状态机判断减少了,回归重点从“每个端都重新跑按钮规则”收敛到“契约矩阵 + 消费层”。
收尾句
所以我理解的 HATEOAS,不是少写几个 URL,而是把“资源当前能做什么”变成 API 层的稳定契约,让前端、测试、移动端,甚至后续 Agent,都消费同一份业务动作事实。