Skip to content

Web API

认识 Web API

API 通常表示低级的编程代码接口,程序提供一些外部接口来访问程序的功能,而用户并不需要知道内部的具体实现细节。
比如 Python 或 Flask 提供的类、函数、方法等就是 API。
近年来,API 越来越多地用来表示 Web API,即基于 HTTP 协议用来提供数据的接口。也就是我们经常听到的 API 接口和数据接口。

也许你并不熟悉 API,但它其实与我们日常生活紧密相连。
当我们在手机上使用某个社交软件,软件中的数据就是通过服务器端提供的 API 获取的。
近年来逐渐流行的 Mashup 应用也离不开 API。比如,快递查询网站借助多家快递公司提供的 API,可以实现在当个网站上查询多家快递公司的快递信息。

附注:
Mashup 常被翻译为糅合、混搭或是聚合。我们经常看到的社交聚合或是新闻聚合就是指这类程序。
借助其他公司/网站提供的 API,我们可以组合这些数据来创建一个新的程序,这类程序就被称为 Mashup。

Web API vs Web 程序

Web 程序提供了完整的交互流程,访问某个 URL,服务器返回指定的资源(以 HTML 的格式),浏览器接收响应并显示设计好的 HTML 页面,页面上的按钮和链接又指向其他资源。
而另外一种形式是,当我们访问某个资源,服务器返回的不是 HTML,而是使用特定格式表示的纯数据。没有按钮,没有表单,只有数据。
与 Web 程序相对,这种形式被称为 Web API 或是 Web 服务。
与 Web 程序不同,所以一般使用 JSON、XML 等格式以提高重用性。
这类 API 也因此被称为 JSON over HTTP 或 XML over HTTP

附注:
在 Web 中,资源(Resource)就是 URL 指向的目标,可以在 Web 中定位的对象,比如一个文件、一张图片等。
在 Web API 语境中,我们用它来表示可以通过 URL 获取的数据信息

Web API 的现状

近年来,越来越多的公司和网站都通过提供 Web API 将资源和服务开放出来(以收费或免费的形式)。
ProgrammableWeb(https://www.programmableweb.com/)是一个提供 API 目录和信息检索的网站,截至 2018 年 5 越,它已经收录了近 2 万个 API。
这些丰富的 API 又产出不计其数的 Mashup 应用,Web API 逐渐催生出“API 经济”

随着 Web API 的发展,Web 世界也变得更加丰富和繁荣。
借助 Web API,不同的程序可以通过其他在线服务提供的 Web API 来集成功能。
比如在阅读和资讯程序中集成第三方分享,使用社交网站的 Web API 来集成第三方登录功能,使用 PayPal、支付宝、Stripe 等服务的 Web API 提供支付功能。

为什么要编写 Web API

对于我们的程序拉说,为什么要提供 Web API 呢?
假设我们做了一个优秀的 Web 程序,用户疯狂增长,编写 Android 和 iOS 客户端的计划很快就要被排上日程了。
那么,我们如何让这些客户端都能和数据库进行数据交换操作呢?
这是什么我们需要有一个中间人专门处理数据的传递工作,这个中间人就是 Web API

同时,随着各种优秀 JavaScript 框架的流行,比如 Angular、React、Ember、Backbone、Vue.js 等,借助这些框架,我们可以直接在客户端实现路由处理(routing)、渲染模板(templating)、表单验证等功能,从而编写出交互性良好的现代 Web 应用,这时服务器仅需要提供数据操作功能。
如果你想使用这些框架编写程序客户端,那么我们就要先编写 Web API。

现在,几乎所有成功的在线服务和网站,都将自己的服务以 Web API 的形式开放出来。
开放 Web API 可以带来潜在的价值和影响力。其他用户使用你的 Web API 开发的其他应用,也会间接为你的查您做广告。

附注:
在某些公司中,开发大型程序往往由两个团队负责,分别为前端和后端。
这时后端开发者负责开发程序基础功能并以 Web API 的形式开发这些功能;
前端开发者(广义的前端也包括 Android、iOS 等客户端)负责编写页面逻辑,处理用户交互(HTML/CSS/JavaScript)。
如果后端能提供 Web API,那么前后端就可以完全做到并行开发,后端不用考虑页面交互,而前端可以通过 Mock 测试来(使用虚拟数据)模拟后端。这样可以在一定程度上提高开发效率

REST 与 Web API

既然要编写 API,我们就要考虑使用何种架构风格来实现。
在以前,服务器端和客户端的 API 通信主要通过 RPC(Remote Procedure Call,远程过程调用)和 SOAP(Simple Object Access Protocol,简单对象访问协议)实现。
但是由于这些协议的规范过于严格,实现起来不够灵活,已经被逐渐抛弃。
近年来,REST(Representational State Transfer,表现层状态转移)架构逐渐流行开来。
它结构清晰、易于理解,并且建立在 Web 的基础 -- HTTP 之上,所以得到越来越多网站和公司的采用

REST 起源于 Roy Thomas Fielding 的博士论文。
它是一种以网络为基础的程序架构风格,目标是构建可扩展的 Web Service。符合 REST 架构约束的 API 被称为 RESTful Web API

为了方便理解,我们可以补全 REST 前的主语 Resource,现在完整的词组就变成了 Resource Representational State Transfer。
这可以理解为"资源(Resource)在网络中以某种表现形式(Re-presentational)进行状态转移(State Transfer)"

虽然我们在设计 API 时主要参考了 REST 架构,但 REST 并不是规范,其只是一个架构风格,包含了设计 API 时的多种约束和建议。
需要注意的是,仅仅通过 HTTP 协议返回 JSON 或 XML 数据的 Web API 并不能算是严格意义上的 REST API。
REST 的提出者也在博文中指出,不是使用了 HTTP 的 API 都叫 REST API。
事实上,我们不必完全按照 REST 的架构要求来设计 API。 要尽量从 API 的自身特点和普适的规范来设计,而不是拘泥于 REST 一词

设计优美实用的 Web API

优美的 Web API 更利于使用,而且健壮性好。在设计 Web API 时有一个重要的考量,那就是主要面向的目标用户群。
Netflix 负责 API 设计的工程总监 Daniel Jacobson 在 《The future of API design: The orchestration layer》(http://tnw.to/c4aDZ)一文中提到了两个概念--LSUD(Large Set of Unknown Developers,大量未知的开发者) 和 SSKD(Small Set of Known Developers,少量已知的开发者)。这两个概念用来表示 API 所面向的主要开发人员分类。
显而易见,这两类 API 在设计时需要有不同的考虑。

我们要设计的 API 面向的对象更符合 SSKD,设计时不必话费太多精力处理大批量访问问题,而是专注于提供易于使用的 API,同时客户端认证的处理也相对简单。

使用 URL 定义资源

Web API 的根 URL 应该尽量简洁明了。一般情况下,设计者都会把关键字 "api" 加入到 URL 中。
根 URL 模式主要有两种: 一种是通过 URL 前缀指定,即 http://example.com/api;
另一种方法是直接把 api 加入主机名中,作为子域名,即 http://api.example.com
在实际应用中,后一种方法更为简洁,也是采用较为普遍的方法。

资源是 Web API 的核心,这里共有两种资源: 单个资源,比如一篇文章,一条评论;
集合资源,比如某用户的所有文章,或是某篇文章下的所有评论。每一个资源都使用一个独一无二的 URL 表示,URL 的设计应该遵循下列要求:

  • 尽量保持简短易懂
  • 避免暴露服务器端架构
  • 使用类似文件系统的层级结构

在 Web API 的语境中,表示资源的 URL 也被称为端点或 API 端点。
假设我们在 api.example.com 上为一个博客程序编写了 Web API,那么博客中的各类资源与其端点将会是这样:

  • api.example.com/users: 所有用户
  • api.example.com/users/123/: id 为 123 的用户
  • api.example.com/users/123/posts: id 为 123 的用户的所有文章
  • api.example.com/posts: 所有文章
  • api.example.com/posts/23: id 为 23 的文章
  • api.example.com/posts/23/comments: id 为 23 的文章的所有评论

使用 HTTP 方法描述操作

既然有资源,我们就需要对资源进行常见的擦坐,比如创建、读取、更新、删除(CRUD)。
对同一个资源的不同操作可以使用不同的 HTTP 方法来表示。
比如,向 api.example.com/posts/23 发送 GET 请求就代表要获取这篇文章的数据,而向这个 URL 发送 DELETE 请求则表示要删除这个资源。
API 中常用的 HTTP 方法与对应 URL 的关系如表所示:

URL GET PUT PATCH POST DELETE
资源集合,比如 https://api.example.com/posts 列出集合成员的所有信息 替换整个集合的资源 一般不使用 在集合中创建一个新条目,新条目的 URL 自动生成并包含在响应中返回 删除整个资源
单个元素,比如 https://api.example.com/posts/123 获取指定资源的详细信息,采用 XML 或 JSON 等表现形式 替换指定的集合成员,如果不存在则创建 更新集合成员仅提供更新的内容 一般不使用 删除指定的集合成员

提示:
我们不需要为每类资源实现所有的 HTTP 方法。
如果客户端使用了不受支持的方法,Flask 会自动处理并返回 405(Method Not Allow)错误响应,表示不允许使用的方法。

每种方法应该返回的响应内容如表所示:

HTTP 方法 返回的响应
GET 返回主体为目标资源的表现层,200(OK)响应
POST 返回指向数据新地址的表现层,首部 Location 字段为指向资源的 URL, 201(Created)响应
PUT 包括请求处理状态的表现层,返回 200 响应; 空数据,返回 204(No Content) 响应
PATCH 包含请求处理状态的表现层,返回 200 响应; 空数据,返回 204 响应
DELETE 如果请求被接收,但删除操作还未执行,返回 202(Accepted)响应; 如果删除操作已经执行,返回 204 响应;如果删除操作已经执行,且返回包含状态信息的表现层,返回 200 响应

附注:
(1) 详细的定义和规则可以在 RFC7231(https://tools.ietf.org/html/rfc7231)中看到
(2) PATCH 方法的标准化经历了一些曲折,起初在 RFC 2068 中定义,后来又在 2616 中删除。在 2010 年 3 越发布的 RFC5789(https://tools.ietf.org/html/rfc5789)中,它又被重新确立为 HTTP 的标准方法。
和 PUT 相比,当更新某个资源时,PUT 方法提供完整的资源数据,而 PATCH 方法仅提供被更新的数据。
(3) 这里的表现层(representataion)即资源的某种表现形式,比如 JSON 格式的数据

使用 JSON 交换数据

JSON 已经取代 XML 成为了 API 的标准数据格式。大多数在线服务都使用 JSON 作为数据格式

在设计良好的 Web API 中,一篇文章可能会用下面的 JSON 数据表示:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
{
    "id": 123,
    "url": "http://api.helloflask.com/items/1",
    "html_url": "http://todoism.helloflask.com/item/1",
    "title": "Hello, Flask!",
    "body": "Something...",
    "created_at": "2017-01-26T13:01:12Z",
    "comments_url": "http://api.helloflask.com/post/123/comments",
    "author": {
        "id": 1,
        "url": "http://api.helloflask.com/users/1",
        "html_url": "http://todoism.helloflask.com/user/greyli",
        "username": "greyli",
        "website": "http://greyli.com",
        "posts_url": "http://api.helloflask.com/users/1/posts",
        "type": "User",
        "is_admin": false
    },
}

数据中除了包含文章的基本内容(标题、正文)外,还应该添加指向其他相关资源的 URL(比如作者、评论等),这样 Web API 的使用者就可以自己探索其他资源了。

设置 API 版本

Web API 和程序一样,都需要在完成后进行维护和更新。
当程序的 Web 版本需要更新时,因为客户端是浏览器,每次请求都会虫灾页面,所以更新一般都可以立即生效

而如果是其他安装在用户设备商的专用客户端,比如桌面软件或是易懂软件,更新就不会那么简单了。
虽然你可以通过添加没有取消按钮的弹窗来强迫用户更新,但这并不是个友好的做法。
当打算对 API 进行更新时,我们就不得不考虑还有大量的用户使用的客户端依赖于旧版本的 API。
如果我们贸然更新,那么这些用户的客户端很快可能会无法正常工作。为了解决这个问题,我们需要保留旧版本的 API,创建一个新版本。

为了同时提供多个版本的 API,较为常见的做法是在 API 的 URL 中指定版本:

  • version1: http://api.example.com/v1
  • version2: http://api.example.com/v2

还有一个更简洁的方法,就是直接在子域中指定:

  • version1: http://api.example.com
  • version2: http://api2.example.com