通常我们使用标准的数据交换格式,如 JSON 或 XML 与 REST web 服务。然而,许多 REST 服务至少有一些操作很难仅用 JSON 或 XML 来完成。例如上传产品图片、使用上传的 CSV 文件导入数据或生成可下载的 PDF 报告。
在这篇文章中,我们关注那些通常被归类为文件下载和上传的操作。这有点不稳定,因为发送简单的 JSON 文档也可以看作是 (JSON) 文件上传操作。
想想你要表达的操作
一个常见的错误是关注操作所需的特定文件格式。相反,我们应该考虑我们想要表达的操作。文件格式仅决定用于操作的媒体类型。
例如,假设我们要设计一个 API,让用户将头像图片上传到他们的用户帐户。
在这里,出于各种原因,将头像图像与用户帐户资源分开通常是一个好主意:
- 头像图像不太可能改变,因此它可能是缓存的一个很好的候选者。另一方面,用户帐户资源可能包含诸如上次登录日期之类的经常更改的内容。
- 并非所有访问用户帐户的客户端都可能对头像图像感兴趣。因此,可以节省带宽。
- 对于客户端来说,通常最好单独加载图像(想想使用 <img> 标签的 Web 应用程序)
可以通过以下方式访问用户帐户资源:
/users/<user-id>
我们可以想出一个代表头像图像的简单子资源:
/users/<user-id>/avatar
上传头像是一个简单的替换操作,可以通过 PUT 表示:
PUT /users/<user-id>/avatar
Content-Type: image/jpeg
<image data>
如果用户想要删除他的头像,我们可以使用简单的 DELETE 操作:
DELETE /users/<user-id>/avatar
当然,客户需要一种显示头像的方法。因此,我们可以使用 GET 提供下载操作:
GET /users/<user-id>/avatar
返回
HTTP/1.1 200 Ok
Content-Type: image/jpeg
<image data>
在这个简单的例子中,我们使用了一个带有常见更新、删除、获取操作的新子资源。唯一的区别是我们使用图像媒体类型而不是 JSON 或 XML。
让我们看一个不同的例子。
假设我们提供了一个 API 来管理产品数据。我们希望通过一个选项来扩展此 API,从上传的 CSV 文件中导入产品。我们应该考虑一种表达产品导入操作的方法,而不是考虑文件上传。
可能最简单的方法是将 POST 请求发送到单独的资源:
POST /product-import
Content-Type: text/csv
<csv data>
或者,我们也可以将其视为产品的批量操作。PATCH
方法是一种表达对集合的批量操作的可能方式。在这种情况下,CSV 文档描述了对产品集合的期望更改。
例如:
PATCH /products
Content-Type: text/csv
action,id,name,price
create,,Cool Gadget,3.99
create,,Nice cap,9.50
delete,42,,
此示例创建两个新产品并删除 id 为42的产品。
处理文件上传可能需要相当长的时间。所以考虑将其设计为异步 REST 操作。
混合文件和元数据
在某些情况下,我们可能需要将额外的元数据附加到文件中。例如,假设我们有一个 API,用户可以在其中上传假日照片。除了实际的图像数据,照片还可能包含描述、拍摄地点等。
在这里,我会推荐使用两个单独的操作,原因与上一节中关于头像图像的原因类似。即使这里的情况有点不同(数据直接链接到图像),它通常也是更简单的方法。
在这种情况下,我们可以首先通过发送实际图像来创建照片资源:
POST /photos
Content-Type: image/jpeg
<image data>
作为回应,我们得到:
HTTP/1.1 201 Created
Location: /photos/123
之后,我们可以将额外的元数据附加到照片中:
当然,我们也可以反过来设计,在图像之前发送元数据。
在 JSON 或 XML 中嵌入 Base64 编码的文件
如果无法在单独的请求中拆分文件内容和元数据,我们可以使用Base64 编码将文件嵌入到 JSON/XML 文档中。使用 Base64 编码,我们可以将二进制格式转换为文本表示,该文本表示可以集成到其他基于文本的格式中,例如 JSON 或 XML。
示例请求可能如下所示:
POST /photos
Content-Type: application/json
{
"width": "1280",
"height": "920",
"filename": "funny-cat.jpg",
"image": "TmljZSBleGFt...cGxlIHRleHQ="
}
将媒体类型与多部分请求混合
在单个请求/响应中传输图像数据和元数据的另一种可能方法是多部分媒体类型。
多部分媒体类型需要一个边界参数,用作不同正文部分之间的分隔符。以下请求由两个正文部分组成。第一个包含图像,而第二个部分包含元数据。
例如:
POST /photos
Content-Type: multipart/mixed; boundary=foobar
--foobar
Content-Type: image/jpeg
<image data>
--foobar
Content-Type: application/json
{
"width": "1280",
"height": "920",
"filename": "funny-cat.jpg"
}
--foobar--
不幸的是,多部分请求/响应通常很难处理。例如,并非每个 REST 客户端都能够构建这些请求,并且很难在单元测试中验证响应。