资源表述 — 返回的数据长什么样:可消费、可组合、可适配三级台阶
资源表述 — 返回的数据长什么样
这一章讲什么: 前几章管「地址怎么起、动作用哪个」,这一章管交换的实质: 服务器把资源当前状态的快照发给客户端,这段快照(书里叫资源表述,representation)写得好坏,直接决定客户端是「拿来就能用」还是「对着文档猜半天」。 原书的方法论很具体:好坏有一条滑动标尺,三个品质(可消费、可组合、可适配)各占一条,每条从最坏的样子开始,分四步改到最好——每一版都指名上一版的病。 读完你拿到一套「给返回数据体检」的眼力。
1. 顶层全景:三条滑动标尺
可消费: 不可用 ──→ 不透明 ──→ 可读 ──→ 可消费
可组合: 割裂 ──→ 模块化 ──→ 结构化 ──→ 可组合
可适配: 静态 ──→ 可变 ──→ 动态 ──→ 可适配
图说:每条标尺从最坏到最好走四站。本章就是把三条标尺各走一遍,
每站配一段具体的 JSON、指名上一站的病。先解释本章的专用工具:一套描述性词与评价性词的词汇法。描述性词只说「我们看到了什么」,不带判断——cryptic(隐晦的)、opaque(不透明的)、linked(带链接的);评价性词负责下判断——unusable(没法用)、deficient(有缺陷的)、holistic(成体系的)。原书说得很直白:描述性词帮你无偏见地讲清一段数据的样子,评价性词帮你批评它、决定改不改;两张词汇表轮着用,就是这一章的分析方法1。
为什么资源表述值得单独立章? 书里给的理由:客户端是拿这些返回搭建它对应用状态的模型的——表述就是建筑砌块。砌块有问题,上面盖的所有客户端代码都要跟着病:不可用的数据制造「不可用、割裂、僵硬」的集成,这是全章反复出现的一组评价性词2。
本章的主线例子:四版「书」,一条标尺走到底
这一章没有贯穿所有节的单一输入,而是每条标尺各挑一个资源走全程。我们把三条线并排摆出来,后面逐节展开:
标尺一(书): {"a1":"x94",…} → 键能读懂 → 值是真的 → 加上文与链接
标尺二(病历): 只有个保险号 → 嵌套成小对象 → 加地址和链接 → 整体拼装
标尺三(商品): 写死的大图 URL → 图片对象可调 → 按客户端分版 → 参数化自选
图说:三条线是同一件事的三张脸——数据从「死物」变成「能融入系统的活件」。2. 标尺一:可消费(以「书」为例)
起点:不可用
最坏的样子,原书给的正是这一段:
{ "a1": "x94", "b2": "y32", "c3": "z28" }评价性诊断:不可用、无效、有缺陷。描述性诊断:键是隐晦的、值是没有意义的、结构是无法辨认的。技术上它确实是个数据结构,但它不承载任何信息——客户端要做一堆假设才敢用,误读的机会极高3。注意书里点破的一层:数据不等于信息——合格的比特堆不等于能指导使用者的内容。
第一站:不透明 → 键能读懂
{ "title": "a94", "author": "b32", "isbn": "c28" }键现在是人话:能看出这是一本书的轮廓。但值还是占位码——评价:不完整、像骨架、有歧义。改进是真的(你知道该往哪个槽填什么了),但数据还不能当动作的依据4。
第二站:可读 → 值是真的
{ "title": "The Great Gatsby", "author": "F. Scott Fitzgerald", "isbn": "9780743273565" }键值都清楚了,两段小改动走完——评价升级为可用、有效、可访问。ISBN 还能拿去和别的系统交叉引用。但书里立刻指出下一站:它还是一座孤岛——没有链接,切断了和更大系统的关系,集成的潜力被自己的封闭性掐住了5。
终点:可消费 → 有来龙去脉,能直接行动
{
"title": "The Great Gatsby",
"author": "F. Scott Fitzgerald",
"isbn": "9780743273565",
"genres": ["Classic", "Fiction"],
"availability": "In Stock",
"links": { "author": "/authors/156", "purchase": "/books/the-great-gatsby/purchase" }
}加了元数据(genres、availability)和指向相关资源的链接(作者详情、购买端点)。这一版发生了什么质变?客户端不用查文档就能行动:按流派过滤、看库存、顺链接去买。书里给可消费状态下的定义:键值人读得懂、为资源的意义服务;元数据和链接提供来龙去脉的线索,引导客户端在应用空间里导航6。
这一站的机制小结
| 站 | 描述性词(我们看到) | 评价性词(我们判断) | 治的病 |
|---|---|---|---|
| 起点 | 隐晦的键、无意义的值 | 不可用 | 客户端无从下口 |
| ① | 可读的键、占位的值 | 不完整 | 结构清楚了、内容没有 |
| ② | 可读的键值 | 可用但孤立 | 能读不能连 |
| 终点 | 带元数据与链接 | 可消费 | 直接可用、可导航 |
3. 标尺二:可组合(以「病历」为例)
解决什么问题
可组合(composable)的意思是:资源能轻易和别的数据拼成更大的结构。它的反义词是割裂(disjointed):数据碎成拼不起来的片,客户端要做额外请求、解开莫名其妙的 ID,才能凑齐一幅完整的图7。
从割裂走到可组合的三步
起点,一个病人资源只有碎片:
{ "id": "12345", "name": "John Doe", "age": 45,
"insurance_id": "5678-1234-A",
"health_conditions": ["allergies", "headaches"] }诊断:只有保险号、没有保险本身——完整保单得去别处查;健康状况只是几个词,看不出治疗方案。碎片化的结构让客户端既要多发请求,又难以把数据揉进自己的模型;而且将来想加「多种保险」「多条记录」时,这种平铺结构根本长不了,一改就是破坏性改动8。
第一步,模块化——把零散的字段嵌套成小对象:
{ "id": "12345", "name": "John Doe", "age": 45,
"insurance": { "id": "5678-1234-A", "provider": "HealthFirst" },
"health_conditions": [
{ "condition": "allergies", "treatment": "Antihistamines", "frequency": "During pollen season" },
{ "condition": "headaches", "treatment": "Ibuprofen", "frequency": "Once every two weeks" } ] }每个对象都很轻,保险和健康记录各自成团。评价:有实质了,但还不完整、有割裂感——和联系方式、保险细节还没接通9。
第二步,结构化——补上背景、加链接:
{ "id": "12345", "name": "John Doe", "age": 45,
"address": { "street": "123 Main St", "city": "Sample City", "state": "SC", "zip": "12345" },
"insurance": { "id": "5678-1234-A", "provider": "HealthFirst", "coverage": "Basic",
"links": { "self": "/patients/12345/insurance" } },
"health_conditions": { "allergies": { "treatment": "Antihistamines", "frequency": "During pollen season" },
"headaches": { "treatment": "Painkillers", "frequency": "Once every two weeks" } } }有了邮寄地址(社区与位置),保险对象里有了 self 链接。评价:分段但连通——能看懂,全貌还差一点10。
第三步,可组合——嵌套 + 链接 + 多层次结构齐活:
{ "id": "12345", "name": "John Doe", "age": 45,
"contact": { "phone": "+1234567890", "email": "john.doe@example.com",
"address": [ { "type": "home", "street": "123 Main St", "city": "Sample City", "state": "SC", "zip": "12345" } ] },
"insurance": { "id": "5678-1234-A", "provider": "HealthFirst", "coverage": "Basic",
"links": { "self": "/patients/12345/insurance", "provider": "/providers/HealthFirst" } },
"health_conditions": [
{ "condition": "allergies", "treatment": "Antihistamines", "frequency": "During pollen season",
"symptoms": [ { "type": "sneezing", "frequency": "daily" } ] },
{ "condition": "headaches", "treatment": "Aspirin", "frequency": "Once every two weeks" } ] }到达可组合状态的标志:症状有了层级(病种 → 症状),保险既有 self 又指向提供方;客户端拿到一个病人,就等于拿到了一座能自主导航的小站。组合的收益是分析能力:原书接着举例,手握几千个这样的病人资源,客户端就能自己分年龄段分组、看地理上的集中与分散、算保险构成、找症状共现——「过敏 + 头痛」的共现规律、某地区某病种的聚集,都从组合视图里浮现,不需要服务器再加一个分析端点11。
4. 标尺三:可适配(以「商品」为例)
解决什么问题
可适配(adaptable)的意思是:同一份资源能按客户端的条件调整自己的形态。反义词是静态(static):一份数据打天下,Web 端嫌简陋、手机端嫌臃肿。原书用的冲突场景:Web 客户端要高清大图和十条评论;移动客户端要小图省流量、只要三条评论12。
从静态走到可适配的三步
起点,静态:
{ "product_id": "12345", "name": "Wireless Headphones", "price": 99.99,
"description": "High-quality wireless headphones",
"image_url": "example.com/images/headphones.jpg" } (示例省略协议头)诊断:一个尺寸适配所有客户端,谁都不满意。而且 image_url 里写死了域名,带来三个具体麻烦:换域名要全 API 改;指向生产环境让开发/测试没法做;暴露内部域名有安全风险13。
第一步,可变——把图片字段改成一个可调的小对象,URL 换成相对的:
{ "product_id": "12345", "name": "Wireless Headphones", "price": 99.99,
"description": "High-quality wireless headphones",
"image": { "size": "default", "url": "/images/headphones.jpg" } }从平的一个字段,变成带两个属性的小对象。评价:更可移植(相对路径跨环境通用)、更可扩展(以后要加分辨率、格式,往对象里加属性就行),但仍是一种尺寸——默认图加三个尺寸,没照顾 Web 与移动的不同需求14。
第二步,动态——按客户端分版本:
{ "product_id": "12345", "name": "Wireless Headphones", "price": 99.99,
"description": "High-quality wireless headphones",
"image": { "web": "/images/headphones-large.jpg", "mobile": "/images/headphones-small.jpg" },
"reviews": { "mobile": "/products/12345/reviews?limit=3", "web": "/products/12345/reviews?limit=10" },
"links": { "self": "/products/12345", "purchase": "/products/12345/purchase" } }Web 和移动各拿各的图、各看各的评论数,还配上了 self 与购买链接。评价:真正适配了多客户端——但书里诚实地记下代价:Web 客户端会收到移动端的评论链接这种它用不上的数据,传输量在某些情况下反而多了15。
第三步,可适配——把选择权参数化地交还给客户端:
{ "product_id": "12345", "name": "Wireless Headphones", "price": 99.99,
"description": "High-quality wireless headphones",
"image_options": { "available_sizes": [
{ "size": "large", "url": "/images/headphones-large.jpg", "resolution": "1024x1024" },
{ "size": "small", "url": "/images/headphones-small.jpg", "resolution": "256x256" } ],
"default": "/images/headphones-large.jpg" },
"links": { "self": "/products/12345", "purchase": "/products/12345/purchase",
"reviews": "/products/12345/reviews?limit={n}" } }到了终点,资源自己配置自己:列出所有可选图片(带分辨率),默认大图;评论端点带个 {n} 参数,客户端想要几条就取几条。评价性总结:可适配的资源随语境变格式,对还没出现的未来用法也留好了口子;同一个端点同时服务需求不同的多类客户端,且不破兼容16。
三条标尺走完,一张总表
| 品质 | 起点(反模式) | 终点(目标) | 关键动作 |
|---|---|---|---|
| 可消费 | 乱码键、占位值 | 键值可读 + 元数据 + 链接 | 补内容、补来龙去脉 |
| 可组合 | 平铺碎片、引用不明 ID | 嵌套结构 + self/邻居链接 | 聚合成对象、接上链接 |
| 可适配 | 写死域名、一个尺寸 | 参数化选项、默认 + 自选 | 去掉写死的值(硬编码:把值直接写死在程序里)、把选择权交给客户端 |
5. 作者的判断与证据
- 本章几乎没有外部证据,全部方法是内省式的:三张标尺上的每一步都是「展示一段 JSON → 用词汇法诊断 → 改一步」的示范,没有用户实验、没有案例研究。它的说服力来自可复现——你把自家 API 的任何一段返回放进这张表,都能立刻指认出它在哪一站。
- 作者的判断(值得单独标出):「别追求一步到位,一小步一小步地改」——加一个好读的键、嵌套一组相关数据、插一个链接,每步都立刻改善可用性。原书把这称为迭代式设计路径,并说这是「改善当下可用性、又能扩到未来」的折中17。
- 一个容易滑过去的立场:书里把「静态资源写死生产域名」同时标成工程问题(不可移植、难测试)和安全问题(暴露基础设施)。安全这条是书里少有的把表述设计与安全挂钩的地方,值得记住。
6. 边界与局限
- 没有量出来的及格线。 「可组合」到什么程度算够?嵌套多深开始反噬(过大的响应、过深的层级)?原书给了方向没给尺子——实践中要靠第 07 章的性能工具(载荷大小、over-fetching 检查)来收口。
- 三条标尺之间有张力。 可组合(多嵌套、多内联)会把响应做大,可适配(多选项)也是;而性能要求响应小。原书把「动态版会多传用不上的数据」点出来了,但没给「什么时候该收手」的判据。
- 词汇法的作用域有限。 描述性/评价性词汇是教新手设计师建立品味的工具;对资深者,它更像一份检查表而非方法。
- 链接的设计归属问题。 本章里的
links块直接引到了第 05 章 HATEOAS 的地盘(链接放哪、rel 怎么写、要不要标准格式),本章没展开——这是刻意的章节分工,读的时候别把两章的链接约定混为一谈(第 05 章的链接带method与rel,本章的没有)。
7. 可带走的
本章三趟改造一行走完: 书从 {"a1":"x94"} 改到带流派、库存和购买链接;病历从裸保险号改到病种-症状两层的自导航小站;商品从写死的大图 URL 改到「列全选项、客户端自选」。共同动作只有两个:把死值换成活结构(嵌套/对象),把孤岛接上链接。
- 先诊断、后动手:用「描述性词说样子、评价性词下判断」两张表,把「这段数据哪里坏」说具体了再改。
- 键值人读得懂是可消费的地板,不是天花板;天花板是「客户端不查文档就能行动」。
- 链接是表述的一部分:没有链接的资源是孤岛,有链接的资源是地图(第 05 章会把地图升级成导航)。
- 嵌套为组合服务,不是为嵌套而嵌套:判断标准永远是「客户端要拼更大视图时,还需不需要额外请求」。
- 写死域名是三重债:换域名要全改、没法测、暴露内部结构;相对路径一次还清。
- 适配的终点是参数化,不是「为每类客户端各建一个端点」——同一个端点 + 自选参数,才能同时服务现在和未来的客户端。
- 一步一改:加一个好读的键、嵌一组数据、插一个链接,每次发布改善一点,比憋大招可靠。
8. 原文地图
| 主题 | 原书章 | 原文位置 |
|---|---|---|
| 描述性与评价性词汇法 | Resource Representation | text/11-fm-introduction.txt:61(搜「descriptive and evaluative」) |
| 滑动标尺方法论 | Resource Representation | text/11-fm-introduction.txt:64(搜「sliding scale」) |
| 不可用起点 | Resource Representation | text/11-fm-introduction.txt:76(搜「cryptic keys」) · :94(搜「data and information」) |
| 不透明站(键可读) | Resource Representation | text/11-fm-introduction.txt:121(搜「barebones」) |
| 可读站(值真实) | Resource Representation | text/11-fm-introduction.txt:148(搜「cross-referenced」) |
| 可消费终点(链接与元数据) | Resource Representation | text/11-fm-introduction.txt:154(搜「purchase endpoint」) · :193(搜「contextual clues」) |
| 可组合:割裂起点 | Resource Representation | text/11-fm-introduction.txt:212(搜「Disjointed」) · :248(搜「inefficient」) |
| 可组合:模块化站 | Resource Representation | text/11-fm-introduction.txt:257(搜「nesting smaller structures」) |
| 可组合:结构化站 | Resource Representation | text/11-fm-introduction.txt:329(搜「adds links」) |
| 可组合终点与聚合分析 | Resource Representation | text/11-fm-introduction.txt:425(搜「holistic view」) · :566(搜「20-30, 30-40」) |
| 可适配:静态起点与写死域名 | Resource Representation | text/11-fm-introduction.txt:588(搜「one-size-fits-all」) · :612(搜「hardcoded」) |
| 可适配:可变站 | Resource Representation | text/11-fm-introduction.txt:618(搜「image size adjustments」) |
| 可适配:动态站 | Resource Representation | text/11-fm-introduction.txt:657(搜「based on the client」) · :717(搜「verbose」) |
| 可适配终点(参数化) | Resource Representation | text/11-fm-introduction.txt:723(搜「bend for web clients」) · :804(搜「configures itself」) |
| 结论:三个品质与迭代 | Resource Representation | text/11-fm-introduction.txt:820(搜「three traits」) |
Footnotes
-
出处:「CHAPTER 4 Resource Representation」第 61 段(
text/11-fm-introduction.txt:61,搜「descriptive and evaluative」)。原文:「The first set helps define resource representation patterns without passing judgement... The second set of terms relates resources to their surroundings, puts them in a specific context, and criticizes them.」 ↩ -
出处:「CHAPTER 4 Resource Representation」第 38 段(
text/11-fm-introduction.txt:38,搜「building blocks」)。原文:「For clients to build a model of the application's state, resource representations provide the building blocks... Conversely, poor resource design contributes to unusable, disjointed, and inflexible output」。 ↩ -
出处:「CHAPTER 4 Resource Representation」第 76 段(
text/11-fm-introduction.txt:76,搜「cryptic keys」)与第 94 段(text/11-fm-introduction.txt:94,搜「a burden than an asset」)。原文:「This example highlights a critical gap between data and information. While it qualifies, technically, as a data structure, it fails to inform or guide its consumer.」 ↩ -
出处:「CHAPTER 4 Resource Representation」第 103 段(
text/11-fm-introduction.txt:103,搜「human readable」)与第 121、124 段(text/11-fm-introduction.txt:121,搜「barebones」)。原文:「the result is still incomplete, barebones, and somewhat ambiguous」。 ↩ -
出处:「CHAPTER 4 Resource Representation」第 148 段(
text/11-fm-introduction.txt:148,搜「The structure is uncomplicated」)与第 148 段(text/11-fm-introduction.txt:148,搜「cut off from a broader web」)。原文:「Without links it remains cut off from a broader web... its potential to integrate with related resources is effectively stunted.」 ↩ -
出处:「CHAPTER 4 Resource Representation」第 154 段(
text/11-fm-introduction.txt:154,搜「metadata」)与第 193 段(text/11-fm-introduction.txt:193,搜「contextual clues」)。原文:「A consumable resource embodies clarity, structure, and function... Through links, a consumable resource guides the client in navigating to other relevant parts of the application space.」 ↩ -
出处:「CHAPTER 4 Resource Representation」第 209 段(
text/11-fm-introduction.txt:209,搜「composable」)。原文:「A composable resource representation combines easily with other data to generate more complex structures... a disjointed resource lacks the structural elements needed for integration into a larger data model.」 ↩ -
出处:「CHAPTER 4 Resource Representation」第 248 段(
text/11-fm-introduction.txt:248,搜「additional requests」)与第 251 段(text/11-fm-introduction.txt:251,搜「restructured」)。原文:碎片化使数据难以组合;「if we need to add multiple health conditions or other insurance types as arrays, the resource will need to be restructured... affecting its compatibility with clients」。 ↩ -
出处:「CHAPTER 4 Resource Representation」第 257 段(
text/11-fm-introduction.txt:257,搜「nesting smaller structures」)与第 323 段(text/11-fm-introduction.txt:323,搜「giant gap」)。 ↩ -
出处:「CHAPTER 4 Resource Representation」第 329 段(
text/11-fm-introduction.txt:329,搜「adds links」)。原文:「the resource adds links, thereby connecting itself to related entities, such as the full insurance record」。 ↩ -
出处:「CHAPTER 4 Resource Representation」第 425 段(
text/11-fm-introduction.txt:425,搜「holistic view」)与第 566 段(text/11-fm-introduction.txt:566,搜「geographic distributions」)。原文:聚合后「clients can easily see which insurance providers dominate the patient pool」、「A grouping may highlight healthcare coverage gaps in underserved areas」。 ↩ -
出处:「CHAPTER 4 Resource Representation」第 582 段(
text/11-fm-introduction.txt:582,搜「static resource」)。原文:「a static resource lacks the variability required to engage a broad range of clients running in different environments」;Web/移动的不同需求见第 612 段。 ↩ -
出处:「CHAPTER 4 Resource Representation」第 612 段(
text/11-fm-introduction.txt:612,搜「hardcoded」)。原文:写死 example.com 的三个后果——换域名全 API 要改、难在开发/测试环境验证、暴露服务基础设施细节构成安全风险。 ↩ -
出处:「CHAPTER 4 Resource Representation」第 618 段(
text/11-fm-introduction.txt:618,搜「default image size」)与第 651 段(text/11-fm-introduction.txt:651,搜「portable across environments」)。原文:相对 URL 让资源「portable across environments」;但「the form is still somewhat restrictive」。 ↩ -
出处:「CHAPTER 4 Resource Representation」第 657 段(
text/11-fm-introduction.txt:657,搜「based on the client」)与第 717 段(text/11-fm-introduction.txt:717,搜「verbose」)。原文:「web clients receive unnecessary data like mobile review data... The system transmits more data than required in certain cases」。 ↩ -
出处:「CHAPTER 4 Resource Representation」第 723 段(
text/11-fm-introduction.txt:723,搜「bend for web clients」)与第 804 段(text/11-fm-introduction.txt:804,搜「configures itself」)。原文:「An adaptable resource configures itself. It varies its format based on context, and flexes to meet future use cases that no one has yet envisioned.」 ↩ -
出处:「CHAPTER 4 Resource Representation」结论第 820 段(
text/11-fm-introduction.txt:820,搜「Forget an all-or-nothing goal」)。原文:「Improve resources step-by-step. Add a readable key. Nest related data. Insert a link. Make small, incremental, stepwise changes to refine the API.」 ↩