Niranjan Sathindran 2019-11-08
本文简要介绍 JSON API 规范。你可以在这里阅读完整的规范说明。
注意:本文假设读者已具备 REST API 的基础知识。
JSON API 与传统 REST API 的根本区别,首先体现在它使用了自己特定的 MIME 类型 —— application/vnd.api+json。我想在此重点探讨另一个方面。当我刚开始了解 JSON API 时,看到很多讨论标题为“REST API 与 JSON API 的对比”。我觉得这种说法有些奇怪。REST 的定义其实相当宽泛,其核心在于对用户自定义资源使用标准 HTTP 方法(而非像 SOAP 那样使用任意方法)。而 JSON API 并未偏离这一标准;实际上,它只是额外增加了一个约束:必须使用上述特定的 MIME 类型。这一约束使得信息交换必须遵循一套统一的标准。
明确了这一点后,我想谈谈 JSON API 的典型应用场景。JSON API 主要针对那些使用常规 MIME 类型(如 application/json)进行数据交换时变得过于“啰嗦”(chatty)的情况。下面我通过一个例子来说明这种“啰嗦”问题——为了简化,我会沿用 jsonapi.org 官网所用的示例。
设想一个博客网站,每次只显示一篇文章,并附带指向同一位作者或同一系列中上一篇/下一篇文章的链接。该页面还会展示文章的用户评论以及作者信息。如果你为此需求设计一个 API,可能会实现一个 GET /articles 接口返回所有文章列表,再实现一个 GET /articles/{id} 接口用于获取具体文章,其响应体可能如下所示:
{
"article": "<< All the details of the article in a nested key value pair structure >>",
"nextArticle": { "link to next article goes here" },
"previousArticle": { "link to previous article goes here" },
"author": {
"firstName": "Dan",
"lastName": "Gebhardt",
"twitter": "dgeb"
},
"comments": [{ "comment1": "<< All the details of a comment in a nested key value pair structure >>" }]
}
虽然我在这里做了简化,但实际 payload 很快就会变得非常冗长——比如一篇很长的文章加上数百条评论。而这还只是一个简单的博客示例,仅涉及文章、作者和评论三种实体。因此,你几乎不会真的去实现上面那种“一次性返回所有关联数据”的接口。更常见的做法是分别暴露多个资源端点,例如 /articles/{id}、/author、/comments 等。于是,前端需要多次来回调用这些接口,才能收集齐渲染单个博客页面所需的所有数据。换句话说,信息交换很快变得“啰嗦”——这在多数情况下完全可以接受,也符合 REST 的设计初衷。
但 JSON API 采取了不同的思路。
现在,我们来看看 jsonapi.org 对同一场景给出的 JSON API 示例:
{
"links": {
"self": "http://example.com/articles",
"next": "http://example.com/articles?page[offset]=2",
"last": "http://example.com/articles?page[offset]=10"
},
"data": [{
"type": "articles",
"id": "1",
"attributes": {
"title": "JSON:API paints my bikeshed!"
},
"relationships": {
"author": {
"links": {
"self": "http://example.com/articles/1/relationships/author",
"related": "http://example.com/articles/1/author"
},
"data": { "type": "people", "id": "9" }
},
"comments": {
"links": {
"self": "http://example.com/articles/1/relationships/comments",
"related": "http://example.com/articles/1/comments"
},
"data": [
{ "type": "comments", "id": "5" },
{ "type": "comments", "id": "12" }
]
}
},
"links": {
"self": "http://example.com/articles/1"
}
}],
"included": [{
"type": "people",
"id": "9",
"attributes": {
"firstName": "Dan",
"lastName": "Gebhardt",
"twitter": "dgeb"
},
"links": {
"self": "http://example.com/people/9"
}
}, {
"type": "comments",
"id": "5",
"attributes": {
"body": "First!"
},
"relationships": {
"author": {
"data": { "type": "people", "id": "2" }
}
},
"links": {
"self": "http://example.com/comments/5"
}
}, {
"type": "comments",
"id": "12",
"attributes": {
"body": "I like XML better"
},
"relationships": {
"author": {
"data": { "type": "people", "id": "9" }
}
},
"links": {
"self": "http://example.com/comments/12"
}
}]
}
application/vnd.api+json 这一 MIME 类型具有非常明确的结构。仔细观察你会发现,它就像是把来自多个关系表的数据全部打包进一个单一的响应体中。可以把上面这个 payload 看作是对主资源 /articles 发起 GET 请求后的结果——仅需这一次请求,你就能获取博客中所有文章及其关联信息(听起来有点疯狂,别急,我马上解释)。
下面我简要说明该 payload 的几个关键点(不深入细节):
links.self字段表示当前资源(/articles)的自引用链接。data数组包含每篇文章的详细信息。上面的示例只包含一篇文章,但实际可包含任意数量。data[0].relationships描述了data[0]中主实体与其他实体之间的关系。included数组则包含了data中所引用的所有关联实体的具体内容。
当然,你也可以用普通的 application/json MIME 类型返回类似的结构。那为什么还要用 JSON API 特定的 MIME 呢?答案在于:该 MIME 类型允许你通过带参数的 HTTP 请求,对上述庞大的 payload(想象一下包含 1000 篇文章的数据)进行灵活裁剪。你甚至可以在单个资源端点上完成排序、分页、创建、更新和删除等操作。
举个“按需获取字段”(sparse fieldsets)的例子:
GET /articles?include=author&fields[articles]=title,body&fields[people]=name
HTTP/1.1 Accept: application/vnd.api+json
在这个请求中:
- 获取所有文章;
- 显式包含
author关联; - 只返回文章的
title和body字段; - 只返回人员(
people)的name字段。
你还可以通过 HTTP 查询参数进行过滤、排序等操作。若要获取单篇文章,只需调用 GET /articles/{id},无需单独为该功能实现新的端点。当然,你依然可以结合稀疏字段集、排序等功能使用。
希望本文能帮助你理解 JSON API 在特定场景下所带来的额外优势!