APIs

作者: Reuven Lerner

如果您正在创建 Web 应用程序,您就是在设计 API。以下是您在开始之前需要记住的一些事项。

Web 是为人类设计的。当蒂姆·伯纳斯-李创建构成 Web 的三个标准——HTTP、HTML 和 URL——时,其目的是让人们浏览网站、向其提交信息并成为体验的核心。但长期以来,Web 作为一组人们浏览的站点的概念在某种程度上是不真实的。诚然,每天有数亿人访问同样大量的站点;然而,越来越多的站点访问者不是人,而是程序。

其中一些程序的存在是为了代表其他系统收集信息。例如,当您使用 Google 等站点搜索 Web 时,您显然不是在实时搜索所有这些站点。相反,您是在要求 Google 搜索其庞大的索引——该索引是通过其“机器人”创建和更新的,这些程序会访问网站,假装像人一样浏览,然后跟踪他们找到的任何内容。

但是,越来越多地,访问站点的程序不是代表搜索索引执行此操作。相反,他们这样做是为了……好吧,为了他们自己。计算机通过 Web 交换信息,使用各种协议和数据格式。移动设备上的原生应用程序正在幕后使用 Web 来查询 Web 应用程序。而且,即使是那些使用 Ajax 的 Web 应用程序也在与网站交互,而没有被直接要求这样做。

这代表了 Web 应用程序正在做的事情的巨大转变。我们不再仅仅为用户(和搜索机器人)生成 HTML。现在,我们正在生成旨在用于程序化消费的输出——并且在许多情况下,同一个人正在编写客户端和服务器端。当然,我们可以使用“抓取”技术来检索 HTML 并搜索它,但为什么要这样做呢?如果我们已经知道我们将向程序发送数据,则没有理由发送 HTML。相反,我们可以以更程序友好的数据格式发送它,而无需人们需要的所有花里胡哨的东西。

当这种使用开始时,人们对此大惊小怪。这种趋势被称为“Web 服务”,许多公司——最突出的是亚马逊——纷纷加入,描述了从 XML-RPC 到 SOAP 到 WSDL 的各种标准。这些协议仍然被使用,特别是被大型企业应用程序使用,以便彼此通信。

但在过去几年中,出现了一种更非正式的 API。有时它基于 XML,但更常见的是基于 JSON,“JavaScript 对象表示法”格式,它不仅适用于 JavaScript,而且适用于各种其他语言。

(通过“更非正式”,我并不是说它没有用,或者需要更多的正式性。我仅仅是指它需要客户端和服务器软件作者之间的协调,而不是遵守规范文档或已建立的协议。)

本月,我将研究这些类型的 API——您为什么要使用它们,您可以在设计它们时使用的不同风格,以及如何访问和使用它们。

为什么需要 API?

如果您正在运行 Web 应用程序,您可能在某个时候会想要提供 API。为什么?嗯,有很多原因

  • 允许您的用户通过第三方应用程序访问他们的数据。考虑一下存在多少第三方 Twitter 客户端,所有这些客户端都使用 Twitter 的 API,而不是网站。亚马逊和 eBay 等也是如此,它们允许用户通过 API 访问其目录数据甚至执行销售。

  • 允许移动应用程序开发人员访问您的网站。移动应用程序——即那些在 Android 和 iOS 等操作系统上运行的应用程序——通常使用 HTTP 发送和检索数据,提供他们自己的前端,我们可以将其视为“特定领域的浏览器”,尽管具有非 Web 界面。

  • 允许您自己的应用程序通过 Ajax 调用访问其自身的数据。当您使用 Ajax 在后台进行 JavaScript 调用时,您很可能希望使用 API 调用,接收 XML 或 JSON,而不是需要进一步解析的 HTML。

我确信还有其他提供 API 的原因,但即使其中一个也可能是您案例中的一个令人信服的理由——其中两到三个也可能适用。API 还带有一种客户开放性和信任感。我更倾向于使用提供 API 以实现其部分或全部功能的 Web 应用程序,尤其是当我是付费用户时。即使我最终从未使用过它,我也知道我可以潜在地这样做,这让我感觉自己是应用程序作者的潜在合作伙伴,而不是简单的用户。

以上内容也表明,即使您从不打算向外部世界开放您的 API,设计一个 API 可能仍然符合您的利益。事实上,我最近听到几位经验丰富的 Web 开发人员争辩说,现代网站不应设计为一组页面,而 API 作为事后才添加的东西,而应设计为一组 API,可以通过移动应用程序、远程客户端应用程序、Ajax 调用或客户端框架(如 Backbone)访问。换句话说,首先您建立您的 API,然后您开始编写使用这些 API 的应用程序。

在许多方面,这是一个有吸引力的想法,它有可能使应用程序更简洁、更易于编写和测试。毕竟,MVC(模型-视图-控制器)范例背后的思想是将不同组件分离,使得业务逻辑与呈现给用户的界面没有交互。MVC 风格的 Web 框架(如 Rails 和 Django)鼓励这种分离,但创建 API 使这种区别更加鲜明。

API 风格

如果您已决定创建 API,则需要询问几个问题。其中之一是您将使用哪种 API 风格。Ruby on Rails 社区围绕 REST 的概念团结起来——“具象状态传输”,Roy Fielding 提出的一个术语——它基本上假定每个 URL 唯一标识 Internet 上的资源。对该资源执行的不同操作不使用不同的 URL,而是使用不同的 HTTP 请求方法。

例如,如果您有一个地址簿,其中包含许多人的信息,则第一个条目可能具有以下 URL:


/people/1

在这种情况下,您可以使用以下命令检索信息:


GET /people/1

并使用以下命令创建一个新条目:


POST /people

请记住,POST 请求将其名称-值对与 URL 分开发送。由于参数不是 URL 的一部分,因此有时很难准确知道发送到服务器的内容。我通常使用工具组合检查正在发送的参数,包括服务器的日志文件、Firefox 的 Firebug 插件、Firefox 的 Web 开发人员插件或用于查找和显示选定网络数据包的 ngrep 命令行工具。

在 Ruby on Rails 世界中,您可以使用鲜为人知(并且至少目前支持力度不大)的 PUT 请求方法更新现有条目:


PUT /people/1

与 POST 一样,PUT 请求中的参数与 URL 分开发送。更棘手的是,许多浏览器无法直接处理 PUT 请求。Rails 目前采用的解决方案是使用 POST,但添加一个“_method”参数作为请求的一部分。当服务器看到这一点时,它会使用应与 PUT 关联的操作,而不是与 POST 关联的操作。该系统运行良好,尽管如果浏览器能够支持所有标准 HTTP 请求,显然会更好。

使用 REST 时要记住的关键事项之一是 URL 应引用名词,而不是动词。因此,在 REST 世界中,拥有引用系统上任何对象的 URL 是完全可以接受的,从用户到书籍到飞机到信用卡账单。但是,在 URL 中命名您希望执行的操作是不可接受的。所以


/airplanes/523

将是完全可以接受的,但是


/airplanes/get_passenger_list/523

将是不可接受的。当然,区别在于 get_passenger_list 可能是您希望对飞机资源执行的操作的名称。这意味着您不再使用 URL 来引用特定资源,而是引用操作。

RESTless 开发

当宣布 Rails 正在转向 REST 时,我必须承认我有点抵触。我更喜欢使用在那之前 Rails 世界中传统的 URL,在 URL 中命名控制器和操作以及对象 ID。因此,如果我想检索一个人的地址,我将使用如下 URL:


/people/get_address/2341

其中 2341 将是该人的唯一 ID。而且,在大多数情况下,这种范例运作良好。

但是,后来我发现许多 Rails 开发人员发现的:Rails 正如其声称的那样,是“有主见的”软件,这意味着如果您以其设计的方式做事,事情会非常容易,如果您以任何其他方式尝试,事情会非常困难。随着时间的推移,越来越明显的是,我的非 REST URL 给我带来了问题。Rails 的许多元素不再为我工作,因为它们依赖于 REST。当我在 2011 年开始使用 Backbone.js 时,我发现 Backbone 可以与各种后端(包括 Rails)一起使用,但是使用非 REST 接口很笨拙,即使有可能使用也是如此。

因此,我开始信奉 REST 宗教,并试图使我编写的每个应用程序都符合这种 API,并且它在许多方面都奏效了。API 是一致的,我的代码也是一致的。因为我使用了 Rails 提供的脚手架——即使只是在一个项目的开始——代码比其他情况更小、更一致。在 Rails 的情况下,它动态创建表示特定 URL(例如,地址)的标识符,坚持使用 RESTful 路由确实简化了事情。

也就是说,直到它没有这样做为止。REST,至少在 Rails 实现中,为您提供了每个您想对资源执行的操作的单个 HTTP 请求方法。根据我的经验,这效果很好,直到您想基于参数检索资源时,事情可能会变得有点复杂。当然,您可以在 HTTP 请求中传递参数,但在某个时候,我个人宁愿有几个小方法,也不愿有一个包含大量 if-then 语句的方法来反映参数的不同组合。

因此,我已经从我的 REST 绝对主义中稍微退后了一步,并且我觉得我达到了某种平衡。对于创建、更新和删除,我完全可以使用 RESTful 范例。但是,当涉及到从 Web 应用程序检索资源时,我放宽了我的要求,试图使我的 API 尽可能具有表现力和灵活性,而不会在我的控制器中创建大量路由或操作。

例如,我完全可以使用 /users URL 检索有关单个用户的信息,附加一个 ID 号以获取有关特定用户的信息。但是,我经常想实现一个搜索系统,以查找系统中姓名与特定模式匹配的人。在我 REST 前时代,我将使用搜索控制器,并有一个或多个执行搜索的方法。在我的新 REST 世界中,我只是在我的“users”资源中添加一个“search”方法,这样 URL /users/search 就代表搜索。这是否违反了 REST?绝对是。但是,我发现它使我的 API 对我的用户以及我自己来说都更易于理解和维护。

Rails 本身,尽管它可能是有主见的和 RESTful 的,但它在路由文件(config/routes.rb)中为您提供了一个出路。我最近工作的一个项目有以下路由:


resources :articles

这转化为 RESTful 路由和 HTTP 请求方法的常用组合。但是,当我决定添加三个额外的 API 调用来抓取特定类型的文章时,我可以通过在 resources 声明中添加一个块来做到这一点:


resources :articles do
    get :latest, :on => :collection
    get :article_links, :on => :member
    get :stories, :on => :collection
end

Rails 不仅提供了这种可能性,而且还区分了“成员”路由(需要资源 ID)和“集合”路由(不需要资源 ID,因为它们对整个集合进行操作)。

通用实践

设置好 API 命名约定后,您需要考虑要传递的数据。这意味着决定数据格式、您将以该格式接收的参数以及您将以该格式发回的响应。

在格式方面,现在主要有两个参与者:XML 和 JSON。正如我之前提到的,XML 在企业用户中非常流行,但 JSON 因其可以轻松地将 Python 和 Ruby 等语言的对象转换为 JSON(和转换回来)而变得非常流行。此外,JSON 几乎与 XML 一样具有自文档性,而没有巨大的文本开销或 XML 解析器的复杂性。像许多其他人一样,我已经为我所有的 API 需求切换到 JSON,而且我一点也不后悔。

也就是说,Rails 提供了使用 respond_to 块以几种不同格式响应的选项。它基本上让您说,“如果用户想要 JSON,则执行 A,但如果他们想要 XML,则执行 B。”

至于请求和响应参数,我尽量保持非常简单。但是,您绝对应该在您创建的任何 API 中包含一个参数,那就是版本号。您的 API 可能会随着时间的推移而发展和改进,添加和删除方法名称、参数和参数类型。通过传递版本号以及您的参数,您可以确保您获得所需的参数,并且客户端和服务器的期望都同步。

此外,我发现您可以使用该版本号作为参数列表哈希中的键。也就是说,与其在服务器端程序中拥有单独的变量来跟踪每个版本期望的参数,不如拥有一个哈希,其键是版本号,其值是预期参数的数组。您甚至可以比这更进一步,拥有一个多级哈希,用于跟踪各种 API 调用的不同参数。例如,考虑以下内容:


EXPECTED_PARAMS = { 'location' => {
    1 => ['longitude', 'latitude', 'altitude'],
    2 => [longitude', 'latitude', 'altitude', 'speed', 'timestamp'],
  },

  'reading' => {
    1 => ['time', 'area', 'mean', 'good', 'number'],
    2 => ['time', 'area', 'mean', 'good', 'number', 'seen', 'span',
    ↪'stdDev']
  }
}

然后,您可以执行以下操作:


version_number = params['version_number'].to_i
method_name = params['action']
required_fields = EXPECTED_PARAMS[method_name][version_number]

这比大量的 if-then 语句容易得多,并且它允许我集中定义我期望从我的 API 客户端接收的内容。如果我想进一步验证数据,我可以使每个数组元素成为哈希而不是字符串,列出一个或多个数据需要通过的标准。

最后,您给 API 用户的响应是您在公众面前的面孔。如果您提供无用的错误消息,例如“出了点问题”,您可能会发现开发人员不太乐意使用您的系统或在其上进行开发。但是,如果您在事情进展顺利和不顺利时都提供详细的响应,不仅开发人员会喜欢您的系统,而且他们的最终用户也会喜欢。

笔记本电脑图形 来自 Shutterstock.com。

Reuven M. Lerner 是一位长期从事 Web 开发的开发者,提供 Python、Git、PostgreSQL 和数据科学方面的培训和咨询服务。他撰写了两本编程电子书(Practice Makes Python 和 Practice Makes Regexp),并在 http://lerner.co.il/newsletter 上为程序员发布免费的每周新闻通讯。Reuven 在 Twitter 上的账号是 @reuvenmlerner,与妻子和三个孩子住在以色列的莫迪因。

加载 Disqus 评论