一、REST 101
Electronic supplementary material The online version of this chapter (doi: 10.1007/978-1-4842-0917-2_1) contains supplementary material, which is available to authorized users.
如今,缩写 REST 已经成为一个流行词,因此,它被许多技术人员非常不小心地扔进了数字风中,而没有完全理解它的真正含义。仅仅因为您可以使用 HTTP 与系统交互,并来回发送 JSON,并不意味着它是 RESTful 系统。REST 远不止这些——这也是我们将在本章讨论的内容。
让我们从罗伊·菲尔丁的论文开始,回顾一下他的观点的主要特征。我将试着解释它的主要方面,他添加的约束和原因。我将回顾一些例子,然后跳回到过去,因为即使 REST 已经被证明是分布式系统互连方面的一个巨大进步,在 Fielding 的论文流行之前,开发人员仍然在寻找问题的解决方案:如何轻松地互连一组非同构系统。
我将快速回顾一下当时开发人员用来互连系统的选项,主要是 SOAP 和 XML-RPC(REST 之前的两个主要参与者)。
最后,我将跳回到我们当前的时代,比较 REST 带给我们的优势,从而展示为什么它在今天如此受欢迎。
但是首先,需要做一个小小的澄清:正如您将在几分钟后读到的,REST 是独立于协议的(只要该协议支持 URI 方案),但是出于本书的考虑,并且因为我们将重点放在 API 设计上,所以让我们假设我们使用的协议是 HTTP,这将简化解释和示例。只要你记住其他协议(如 FTP)也是如此,你就不会有问题。
这一切是从哪里开始的?
这一切都始于 1965 年出生的美国计算机科学家罗伊·菲尔丁。他是 HTTP 协议 1 (整个 Web 基础设施所基于的协议)的主要作者之一。他也是 Apache Web 服务器 2 的合著者之一,并且是 Apache 软件基金会 3 成立前三年的主席。
如你所见,Fielding 为 IT 界做出了很多巨大的贡献,尤其是在互联网方面,但我认为他的博士论文最受关注,让他的名字在很多人当中广为人知,否则他们不会听说过他。
在 2000 年,Fielding 提交了他的博士论文,架构风格和基于网络的软件架构的设计。在这本书中,他创造了术语 REST,一种分布式超媒体系统的架构风格。
简而言之,REST(表述性状态转移的缩写)是一种架构风格,旨在帮助创建和组织分布式系统。这个定义的关键词应该是风格,因为 REST 的一个重要方面(这也是像这本书存在的主要原因之一)是它是一种架构风格——不是指南,不是标准,或者任何暗示有一组硬规则要遵循以获得 RESTful 架构的东西。
因为它是一种风格,而且没有征求意见稿(RFC)来定义它,所以它容易被阅读它的人误解。不仅如此,有些人甚至省略了某些部分,并实现了其功能的一个子集,这反过来又导致了一种普遍的、不完整的 REST 理想,省略了那些原本有用的、有助于系统用户的功能。
REST 背后的主要思想是,RESTfully 组织的分布式系统将在以下方面得到改进:
- 性能:REST 提出的通信风格是高效和简单的,允许采用它的系统的性能提升。
- 组件交互的可伸缩性:任何分布式系统都应该能够足够好地处理这一方面,REST 提出的简单交互极大地考虑到了这一点。
- 界面的简单性:一个简单的界面允许系统间更简单的交互,这反过来可以带来前面提到的好处。
- 组件的可修改性:系统的分布式本质,以及 REST 提出的关注点分离(稍后将详细介绍),允许以最小的成本和风险独立地修改组件。
- 可移植性:REST 是技术和语言不可知的,这意味着它可以被任何类型的技术实现和使用(有一些限制,我稍后会详细介绍,但是没有强制使用特定的技术)。
- 可靠性:REST 提出的无状态约束(稍后将详细介绍)允许系统在故障后更容易地恢复。
- 可见性:同样,所提出的无状态约束具有提高可见性的额外好处,因为任何监控系统都不需要看得比单个请求消息更远,就可以确定所述请求的完整状态(一旦我谈到约束,这一点就变得很清楚了)。
从这个列表中,可以推断出一些直接的好处:
- 以组件为中心的设计允许您创建容错能力很强的系统。一个组件的故障不会影响系统的整体稳定性,这对任何系统都是一大好处。
- 互连组件非常容易,从而在添加新功能或扩大或缩小规模时将风险降至最低。
- 考虑到 REST 而设计的系统,由于其可移植性(如前所述),将会被更广泛的受众所使用。通过通用接口,该系统可以被更广泛的开发者使用。
为了实现这些属性和好处,REST 中添加了一组约束来帮助定义统一的连接器接口。
REST 约束
根据菲尔丁的说法,有两种方法来定义一个系统。一种是从空白的白板开始,在需求得到满足之前,对正在构建的系统或熟悉组件的使用没有任何初步的了解。第二种方法是从系统的全套需求开始,将约束添加到各个组件,直到影响系统的各种力量能够和谐地相互作用。
REST 遵循第二种方法。为了定义一个 REST 架构,首先定义一个空状态——一个没有任何约束的系统,其中组件差异只是一个神话——然后一个接一个地添加约束。
客户端-服务器
要添加的第一个约束是基于网络的体系结构中最常见的约束之一:客户机-服务器。服务器负责处理一组服务,并监听关于所述服务的请求。反过来,请求由需要这些服务之一的客户端系统通过连接器发出(参见图 1-1 )。
图 1-1。
Client-Server architecture diagram
这个约束背后的主要原则是关注点的分离。它允许将前端代码(信息的表示和可能的 UI 相关处理)从服务器端代码中分离出来,服务器端代码应该负责数据的存储和服务器端处理。
这种约束允许两个组件独立发展,通过让客户端应用在不影响服务器代码的情况下进行改进,提供了很大的灵活性,反之亦然。
无国籍的
在前一个约束之上添加的约束是无状态约束(见图 1-2 )。客户端和服务器之间的通信必须是无状态的,这意味着客户端发出的每个请求都必须包含服务器理解它所需的所有信息,而不利用任何存储的数据。
图 1-2。
Representation of the stateless constraint
这个约束代表了对底层架构的几个改进:
- 可见性:当所有需要的信息都包含在请求中时,监控系统就变得容易了。
- 可伸缩性:由于不必在请求之间存储数据,服务器可以更快地释放资源。
- 可靠性:如前所述,无状态系统比无状态系统更容易从故障中恢复,因为唯一需要恢复的是应用本身。
- 更容易实现:编写不必跨多个服务器管理存储的状态数据的代码要容易得多,因此整个服务器端系统变得更简单。
虽然乍一看这种约束似乎很好,但正如通常发生的那样,这是一种权衡。一方面,系统获得了好处,但另一方面,由于发送重复的状态信息而对每个请求增加了少量的开销,网络流量可能会受到损害。根据所实现的系统类型和重复信息的数量,这可能不是一个可接受的折衷方案。
可缓冲的
可缓存约束被添加到当前约束组中(见图 1-3 )。它建议对请求的每个响应都必须显式或隐式地设置为可缓存的(如果适用)。
图 1-3。
Representation of a client-stateless-cache-server architecture
通过缓存响应,有一些明显的好处被添加到架构中:在服务器端,当内容被缓存时,一些交互(例如,数据库请求)被完全绕过。在客户端,性能有了明显的提高。
这种约束的代价是,由于糟糕的缓存规则,缓存的数据可能会过时。同样,这种限制取决于所实现的系统的类型。
Note
图 1-3 显示了缓存作为客户端和服务器之间的外层。这只是它的一种可能的实现。缓存层可以位于客户端(即浏览器缓存)或服务器本身内部。
统一界面
与其他替代方案相比,REST 的主要特点和优势之一是统一接口约束。通过在组件之间保持统一的接口,当客户端与您的系统交互时,您简化了客户端的工作(参见图 1-4 )。这里的另一个主要优点是,客户端的实现独立于您的实现,因此,通过为您的所有服务定义一个标准和统一的接口,您可以通过为独立客户端提供一组清晰的规则来有效地简化它们的实现。
图 1-4。
Different client types can interact seamlessly with servers thanks to the uniform interface
所说的规则不是 REST 风格的一部分,但是有一些约束可以用来为每个单独的情况创建这样的规则。
然而,这种好处不是没有代价的;与许多其他约束一样,这里有一个权衡:当存在更优化的通信形式时,为所有与系统的交互提供标准化和统一的接口可能会损害性能。特别是,REST 风格是为 Web 优化而设计的,所以你离它越远,界面就越低效。
Note
为了实现统一接口,必须向接口添加一组新的约束:资源的标识、通过表示对资源的操作、自描述消息以及作为应用状态引擎的超媒体(也称为 HATEOAS)。我将很快讨论其中的一些约束。
分层系统
REST 在设计时就考虑到了互联网,这意味着遵循 REST 的架构有望与 web 中存在的大量流量一起正常工作。
为了实现这一点,引入了层的概念(见图 1-5 )。通过将组件分成不同的层,并允许每一层只使用下面的一层,并将其输出传递给上面的一层,可以简化系统的整体复杂性,并保持组件的耦合性。这在所有类型的系统中都是很大的好处,尤其是当这种系统的复杂性不断增长时(例如,具有大量客户端的系统、当前正在发展的系统等)。).
图 1-5。
Example of a multilayered architecture
这种限制的主要缺点是,对于小型系统,由于各层之间的交互不同,它可能会给整个数据流增加不必要的延迟。
按需编码
按需编码是 REST 强加的唯一可选约束,这意味着使用 REST 的架构师可以选择是否使用该约束,要么获得其优点,要么遭受其缺点。
有了这个约束,客户端就可以下载并执行服务器提供的代码(如 Java 小程序、JavaScript 脚本等。).在 REST APIs 的情况下(这也是本书关注的重点),这种约束似乎是不必要的,因为 API 客户端要做的正常事情只是从端点获取信息,然后根据需要进行处理;但是对于 REST 的其他用途,比如 web 服务器,客户端(比如浏览器)可能会从这个约束中受益(见图 1-6 )。
图 1-6。
How some clients might execute the code-on-demand, whereas others might not
所有这些约束提供了一组虚拟的墙,架构可以在其中移动,并且仍然可以获得 REST 设计风格的好处。
但是让我们后退一步。我最初将 REST 定义为表述性状态转移的设计风格;换句话说,你通过使用某种表示来转移事物的状态。但是这些“东西”是什么呢?REST 架构的主要焦点是资源,即您正在转移的状态的所有者。就像在现实状态下(差不多),都是资源,资源,资源。
资源,资源,资源
REST 架构的主要构件是资源。任何可以命名的东西都可以是资源(网页、图像、人、气象服务报告等。).资源定义了服务的内容、要传输的信息类型以及它们的相关动作。资源是万物诞生的主要实体。
资源是任何可以概念化的东西的抽象(从图像文件到纯文本文档)。资源的结构如表 1-1 所示。
表 1-1。
Resource Structure Description
| 财产 | 描述 | | --- | --- | | 陈述 | 它可以是任何表示数据的方式(二进制、JSON、XML 等。).一个资源可以有多个表示。 | | 标识符 | 在任何给定时间只检索一个特定资源的 URL。 | | [计]元数据 | 内容类型、最后修改时间等等。 | | 控制数据 | 是-可修改的-自,缓存-控制。 |陈述
其核心是一组字节和一些描述这些字节的元数据。一个资源可以有多个表示形式;想象一下气象服务报告(它可以作为一种可能的资源)。
某一天的天气预报可能会返回以下信息:
- 报告引用的日期
- 一天的最高温度
- 一天的最低温度
- 要使用的温度单位
- 湿度百分比
- 表示天气多云程度的代码(例如,高、中、低)
既然已经定义了资源的结构,下面是同一资源的几种可能的表示形式:
JSON
{
"date": "2014-10-25",
"max_temp": 25.5,
"min_temp": 10.0,
"temp_unit": "C",
"humidity_percentage": 75.0,
"cloud_coverage": "low"
}
XML
<?xml version='1.0' encoding='UTF-8' ?>
<root>
<temp_unit value="C" />
<humidity_percentage value="75.0" />
<cloud_coverage value="low" />
<date value="2014-10-25" />
<min_temp value="10.0" />
<max_temp value="25.5" />
</root>
自定义管道分隔值:
2014-10-25|25.5|10.0|C|75.0|low
可能还有更多。它们都成功地正确表示了资源;由客户机来读取和解析信息。即使当资源有多个表示时,客户机(由于开发的简单性)通常只请求其中的一个。除非您正在对 API 进行某种一致性检查,否则请求同一资源的多个表示是没有意义的,不是吗?
有两种非常流行的方法让客户机请求一个资源上的特定表示,这个资源有不止一个。第一种直接遵循 REST 描述的原则(当使用 HTTP 作为基础时),称为内容协商,是 HTTP 标准的一部分。第二个是这个的简化版,好处有限。为了完整起见,我将快速浏览一下它们。
内容协商
如前所述,这种方法是 HTTP 标准的一部分, 5 所以它是 REST 的首选方式(至少当专注于 HTTP 之上的 API 开发时)。它也比其他方法更灵活,并提供更多优势。
它包括客户端发送一个特定的报头,该报头带有所支持的不同内容类型(或表示类型)的信息,还带有一个可选的指示符,指示该格式受支持/首选的程度。让我们来看一个来自维基百科“内容协商”页面的例子:
Accept: text/html; q=1.0, text/*; q=0.8, image/gif; q=0.6, image/jpeg; q=0.6, image/*; q=0.5, */*; q=0.1
该示例来自一个浏览器,该浏览器配置为接受各种类型的资源,但更喜欢 HTML 而不是纯文本,更喜欢 GIF 或 JPEG 图像而不是其他类型,但最终还是接受任何其他内容类型作为最后手段。
在服务器端,API 负责读取这个头,并根据客户机的偏好为每个资源找到最佳表示。
使用文件扩展名
尽管这种方法不是 REST 建议的风格的一部分,但它被广泛使用,并且是相对于稍微复杂一些的其他选项的一种相当简单的替代方法,所以我还是会介绍它。
在过去的几年中,使用文件扩展名已经成为一种比使用内容协商更受欢迎的选择;这是一个更简单的版本,它不依赖于发送的标题,而是使用文件扩展名的概念。
文件名的扩展名部分向操作系统和试图使用它的任何其他软件指示内容类型;因此,在下面的例子中,添加到资源的 URL(唯一标识符)的扩展向服务器指示所需的表示类型。
GET /api/v1/books
.json
GET /api/v1/books
.xml
两个标识符引用相同的资源——图书列表,但是它们请求不同的表示。
Note
这种方法可能看起来更容易实现,甚至更容易被人理解,但是它缺乏内容协商所带来的灵活性,只有在不需要复杂的情况下才应该使用这种方法,在这种情况下,可以用相关的首选项指定多种内容类型。
资源标识符
资源标识符应该在任何给定时刻提供唯一的标识方式,并且应该提供资源的完整路径。一个典型的错误是假设它是所使用的存储介质上的资源 ID(即数据库上的 ID)。这意味着您不能将简单的数字 ID 视为资源标识符;您必须提供完整的路径,因为我们是基于 HTTP 的 REST,所以访问资源的方式需要提供完整的 URI(唯一资源标识符)。
还有一个方面需要考虑:每个资源的标识符必须能够在任何给定的时刻明确地引用它。这是一个重要的区别,因为像下面这样的 URI 可能会在某段时间引用《哈利·波特与混血王子》,然后在一年后引用《哈利·波特与死亡之谷》。:
GET /api/v1/books/last
这使得 URI 成为无效的资源 id。相反,每本书都需要一个独特的 URI,它肯定不会随着时间的推移而改变;例如:
GET /api/v1/books
/j-k-rowling/harry-potter-and-the-deathly-hollows
GET /api/v1/books
/j-k-rowling/harry-potter-and-the-half-blood-prince
这里的标识符是唯一的,因为您可以放心地假设作者不会出版更多同名书籍。
为了提供获取最后一本书的有效示例,您可以考虑这样做:
GET /api/v1/books?limit=1&sort=created_at
前面的 URI 引用图书列表,它只要求一本书,按出版日期排序,从而呈现添加的最后一本书。
行动
识别资源很容易:您知道如何访问它,甚至知道如何请求特定的格式(如果有多个格式的话);但这并不是 REST 提出的全部。由于 REST 使用 HTTP 协议作为立足点,后者提供了一组动词,可用于引用在资源上执行的操作类型。
除了访问,客户端应用还可以对 API 提供的资源进行其他操作;这些依赖于 API 提供的服务。这些动作可能是任何东西,就像系统处理的资源类型一样。尽管如此,任何面向资源的系统都应该能够提供一组通用操作:CRUD(创建、检索、更新和删除)操作。
这些所谓的动作可以直接映射到 HTTP 动词,但是 REST 并没有强制采用一种标准化的方式来实现。然而,有一些动作是由动词自然派生出来的,还有一些动作是 API 开发社区多年来已经标准化的,如表 1-2 所示。
表 1-2。
HTTP Verbs and Their Proposed Actions
| HTTP 动词 | 提议的行动 | | --- | --- | | 得到 | 以只读模式访问资源。 | | 邮政 | 通常用于向服务器发送新资源(创建动作)。 | | 放 | 通常用于更新给定的资源(更新操作)。 | | 删除 | 用于删除资源。 | | 头 | 不是 CRUD 操作的一部分,但是动词用于询问给定的资源是否存在,而不返回它的任何表示。 | | 选择 | 不是 CRUD 动作的一部分,但是用于检索给定资源上的可用动词列表(例如,客户端可以用特定资源做什么?). |也就是说,客户端可能支持也可能不支持所有这些操作;这取决于需要实现什么目标。例如,web 浏览器——REST 客户端的一个明显而常见的例子——只支持页面 HTML 代码中的 GET 和 POST 动词,比如链接和表单(尽管使用 JavaScript 中的XMLHTTPRequest
对象可以为前面提到的主要动词提供支持)。
Note
动词及其相应动作的列表是建议。例如,有些开发人员喜欢切换 PUT 和 POST,让 PUT 添加新元素,POST 更新它们。
复杂动作
CRUD 操作通常是必需的,但它们只是客户端可以对特定资源或资源集执行的全部操作中非常小的一部分。
例如,采取常见的操作,如搜索、过滤、处理子资源(例如,某个作者的书、某本书的评论等。)、分享博客等等。所有这些动作都无法直接匹配我提到的一个动词。
许多开发人员屈服的第一个解决方案是将所采取的动作指定为 URL 的一部分;因此,您可能会得到如下结果:
GET /api/v1/blogpost/12342
/like
GET /api/v1/books
/search
GET /api/v1/authors
/filtering
这些 URL 违反了 URI 原则,因为它们在任何给定的时间都没有引用唯一的资源;相反,它们引用的是对一个资源(或一组资源)的操作。起初,它们似乎是一个好主意,但从长远来看,如果系统继续增长,将会有太多的 URL,这将增加使用 API 的客户端的复杂性。
所以为了简单起见,使用下面的经验法则:把你行为的复杂性隐藏在?签名。
这条规则可以应用于所有动词,而不仅仅是 GET,并且可以帮助实现复杂的操作,而不会影响 API 的 URL 复杂性。对于前面的示例,URIs 可能会变成这样:
PUT /api/v1/blogposts/12342
?action=like
GET /api/v1/books
?q=[SEARCH-TERM]
GET /api/v1/authors
?filters=[COMMA SEPARATED LIST OF FILTERS]
注意第一个动作是如何从 GET 变成 PUT 的,因为这个动作是通过 like 来更新资源的。
超媒体的回应和主要切入点
为了使 REST 的接口统一,必须应用几个约束。其中之一是作为应用状态引擎的超媒体,也称为 HATEOAS。我将介绍这个概念的含义,RESTful 系统如何应用它,最后,您将看到一个伟大的新特性,它允许任何 RESTful 系统客户端在只知道整个系统的一个端点(根端点)的情况下开始交互。
同样,资源的结构包含一个称为元数据的部分;在这个部分中,每个资源的表示应该包含一组超媒体链接,让客户知道如何处理每个资源。通过在响应本身中提供这些信息,任何客户端都可以采取下一步措施,从而在客户端和服务器之间提供更高级别的解耦。
通过这种方法可以提供对资源标识符的改变,或者添加和删除功能,而根本不影响客户端,或者在最坏的情况下,影响最小。
想象一个网络浏览器:它只需要帮助用户浏览一个喜欢的网站的主页 URL 之后,以下动作在表示(HTML 代码)中以链接的形式出现。这些是用户可以采取的唯一合乎逻辑的后续步骤,从那里,新的链接将被呈现,等等。
在 RESTful 服务的情况下,同样的事情也可以说:通过调用主端点(也称为书签或根端点),客户端将发现所有可能的第一步(通常是资源列表和其他相关端点)。
让我们看看清单 1-1 中的一个例子。
根端点:GET /api/v1/
Listing 1-1. Example of a JSON Response from the Root Endpoint
{
“元数据”:{
“链接”:{
“书籍”:{
" uri": "/books ",
“内容类型”:"应用/json "
},
“作者”:{
" uri": "/authors ",
“内容类型”:"应用/json "
}
}
}
}
书籍列表端点:GET /api/v1/books
Listing 1-2. Example of Another JSON Response with Hyperlinks to Other Resources
{
"resources": [
{
"title": "Harry Potter and the Half Blood prince",
"description": "......",
"author": {
"name": "J.K.Rowling",
"metadata": {
"links": {
"data": {
"uri": "/authors/j-k-rowling",
"content-type": "application/json"
},
"books": {
"uri": "/authors/j-k-rowling/books",
"content-type": "application/json"
}
}
}
},
"copies": 10
},
{
"title": "Dune",
"description": "......",
"author": {
"name": "Frank Herbert",
"metadata": {
"links": {
"data": {
"uri": "/authors/frank-herbert",
"content-type": "application/json"
},
"books": {
"uri": "/authors/frank-herbert/books",
"content-type": "application/json"
}
}
}
},
"copies": 5
}
],
"total": 100,
"metadata": {
"links": {
"next": {
"uri": "/books?page=1",
"content-type": "application/json"
}
}
}
}
清单 1-2 中突出显示了三个部分;这些是响应中返回的链接。有了这些信息,客户端应用就知道了以下逻辑步骤:
How to get the information from the books authors How to get the list of books by the authors How to get the next page of results
请注意,无法通过此端点访问作者的完整列表;这是因为在这个特定的用例中不需要它,所以 API 不返回它。但是它出现在根端点上;因此,如果客户端在向最终用户显示信息时需要它,它应该仍然可用。
前面示例中的每个链接都包含一个指定该资源表示的内容类型的属性。如果资源有不止一种可能的表示,不同的格式可以作为不同的链接添加到每个资源的元数据元素中,让客户端选择最适合当前用例的,或者类型可以基于客户端的偏好而改变(内容协商)。
注意,早期的 JSON 结构(更具体地说,元数据元素的结构)并不重要。示例的相关部分是响应中提供的信息。每台服务器都可以根据需要自由设计结构。
没有标准的结构可能会损害开发人员在与系统交互时的体验,所以采用一个标准的结构可能是个好主意。REST 当然不会强制执行这一点,但这将是支持您的系统的一个要点。在这种情况下,采用超文本应用语言(Hypertext Application Language,简称 HAL, 6 是一个很好的标准,它试图在用 XML 和 JSON 表示资源时,为这两种语言创建一个标准。
关于哈尔的几点注记
HAL 试图将一个表示定义为具有两个主要元素:资源和链接。
根据 HAL 的说法,资源有链接、嵌入资源(其他与其父资源相关的资源)和状态(描述资源的实际属性)。另一方面,链接有一个目标(URI)、一个关系和一些其他可选属性来处理弃用、内容协商等等。
清单 1-3 显示了前面使用 HAL 格式表示的例子。
Listing 1-3. JSON Response Following the HAL Standard
{
"_embedded": [
{
"title": "Harry Potter and the Half Blood prince",
"description": "......",
"copies": 10,
"_embedded": {
"author": {
"name": "J.K.Rowling",
"_links": {
"self": {
"href": "/authors/j-k-rowling",
"type": "application/json+hal"
},
"books": {
"href": "/authors/j-k-rowling/books",
"type": "application/json+hal"
}
}
}
}
},
{
"title": "Dune",
"description": "......",
"copies": 5,
"_embedded": {
"author": {
"name": "Frank Herbert",
"_links": {
"self": {
"href": "/authors/frank-herbert",
"type": "application/json+hal"
},
"books": {
"href": "/authors/frank-herbert/books",
"type": "application/json+hal"
}
}
}
}
}
],
"total": 100,
"_links": {
"self": {
"href": "/books",
"type": "application/json+hal"
},
"next": {
"href": "/books?page=1",
"type": "application/json+hal"
}
}
}
清单 1-3 中的主要变化是,实际的书籍被移到了一个名为"_embedded"
的元素中,正如标准所规定的,因为它们实际上是所表示的资源(即书籍列表)中的嵌入文档(属于资源的唯一属性是"total"
,表示结果总数)。对于作者来说也是一样,现在在每本书的"_embedded"
元素里面。
状态代码
基于 HTTP 的 REST 可以受益的另一个有趣的标准是 HTTP 状态代码的使用。 7
状态代码是一个总结与其相关的响应的数字。有一些常见的错误,比如 404 表示“找不到页面”,200 表示“正常”,500 表示“内部服务器错误”(这是一个讽刺,以防不够清楚)。
状态代码有助于客户端开始解释响应,但在大多数情况下,它不应该是它的替代品。作为 API 所有者,您不能仅仅通过回复数字 500 来真正地在响应中传递到底是什么导致了您这边的崩溃。不过,在某些情况下,一个数字就足够了,比如 404;虽然一个好的响应总是会返回应该帮助客户解决问题的信息(对于 404,到主页的链接或根 URL 是很好的起点)。
这些代码根据其含义分为五组:
- 1xx:信息性的,仅在 HTTP 1.1 下定义。
- 2xx:请求通过,这是您的内容。
- 3xx:资源不知何故被移动到了某个地方。
- 4xx:请求的来源做了一些错误的事情。
- 5xx:服务器由于代码错误而崩溃。
考虑到这一点,表 1-3 列出了一些 API 可能使用的经典状态代码。
表 1-3。
HTTP Status Codes and Their Related Interpretation
| 状态代码 | 意义 | | --- | --- | | Two hundred | 好的。请求很顺利,请求的内容被返回。这通常用于 GET 请求。 | | Two hundred and one | 已创建。资源已创建,服务器已确认。这对于响应 POST 或 PUT 请求可能很有用。此外,新资源可以作为响应体的一部分返回。 | | Two hundred and four | 没有内容。操作成功,但没有返回任何内容。适用于不需要响应正文的操作,如删除操作。 | | Three hundred and one | 永久移动。该资源被移动到另一个位置,并且该位置被返回。当 URL 随时间变化时(可能是由于版本变化、迁移或一些其他破坏性变化),这个头特别有用,保留旧的 URL 并返回到新位置的重定向允许旧客户端在自己的时间内更新它们的引用。 | | four hundred | 错误的请求。发出的请求有问题(例如,可能缺少一些必需的参数)。对 400 响应的一个很好的补充可能是一个错误消息,开发人员可以用它来修复请求。 | | Four hundred and one | 未经授权。当拥有请求的用户无法访问所请求的资源时,这对于身份验证尤其有用。 | | Four hundred and three | 禁止。资源不可访问,但与 401 不同,身份验证不会影响响应。 | | Four hundred and four | 没有找到。提供的 URL 未标识任何资源。对这个响应的一个很好的补充是一组有效的 URL,客户端可以使用它们回到正轨(根 URL、以前使用的 URL 等。). | | Four hundred and five | 不允许使用方法。不允许在资源上使用 HTTP 谓词。例如,对只读资源执行 PUT 操作。 | | Five hundred | 内部服务器错误。遇到意外情况且服务器崩溃时的一般错误代码。通常,此响应会伴随一条错误消息,解释发生了什么问题。 |Note
要查看 HTTP 状态代码的完整列表及其含义,请参考 HTTP 1.1 的 RFC。 8
安息与过去
在 REST 流行之前,每个企业都希望在服务中为客户提供 RESTful API,对于希望实现系统互联的开发人员来说,还有其他选择。这些仍然被用在旧的服务上,或者被需要它们特定特性的服务使用,但是每年越来越少。
早在 20 世纪 90 年代,软件行业就开始考虑系统互操作性以及两台(或更多)计算机如何实现它。一些解决方案诞生了,比如微软创造的 COM、 9 ,对象管理组创造的 CORBA、 10 。这是当时最早的两个实现,但是它们有一个主要问题:它们彼此不兼容。
其他的解决方案也出现了,比如 RMI,但是它是专门针对 Java 的,这意味着它依赖于技术,并没有真正赶上开发社区。
到 1997 年,微软决定研究将 XML 作为主要传输语言的解决方案,并允许系统通过 HTTP 使用 RPC(远程过程调用)进行互连,从而实现某种程度上与技术无关的解决方案,这将大大简化系统互连。这项研究在 1998 年左右催生了 XML-RPC。
清单 1-4 是一个经典的 XML-RPC 请求,摘自维基百科 11 :
Listing 1-4. Example of an XML-RPC Request
<?xml version="1.0"?>
<methodCall>
<methodName>examples.getStateName</methodName>
<params>
<param>
<value><i4>40</i4></value>
</param>
</params>
</methodCall>
清单 1-5 显示了一个可能的响应。
Listing 1-5. Example of an XML-RPC Response
<?xml version="1.0"?>
<methodResponse>
<params>
<param>
<value><string>South Dakota</string></value>
</param>
</params>
</methodResponse>
从清单 1-4 和清单 1-5 所示的例子中,很明显消息(请求和响应)过于冗长,这与 XML 的使用直接相关。XML-RPC 的实现目前已经存在于几种操作系统和编程语言中,如 Apache XML-RPC 12 (用 Java 编写)、XMLRPC-EPI 13 (用 C 编写),以及用于 C 和 C++的 XML-RPC-C 14 (参见图 1-7 )。
图 1-7。
Diagram showing the basic architecture of an XML-RPC interaction
在 XML-RPC 变得更加流行之后,它变异成了 SOAP, 15 同一原则的一个更加标准化和形式化的版本。SOAP 仍然使用 XML 作为传输语言,但是消息格式现在更加丰富(因此也更加复杂)。清单 1-6 是 W3C 关于 SOAP 的规范页面中的一个例子:
Listing 1-6. Example of a SOAP Request
<?xml version='1.0' ?>
<env:Envelope xmlns:env="
http://www.w3.org/2003/05/soap-envelope
<env:Header>
<m:reservation xmlns:m="
http://travelcompany.example.org/reservation
env:role="
http://www.w3.org/2003/05/soap-envelope/role/next
env:mustUnderstand="true">
<m:reference>uuid:093a2da1-q345-739r-ba5d-pqff98fe8j7d</m:reference>
<m:dateAndTime>2001-11-29T13:20:00.000-05:00</m:dateAndTime>
</m:reservation>
<n:passenger xmlns:n="
http://mycompany.example.com/employees
env:role="
http://www.w3.org/2003/05/soap-envelope/role/next
env:mustUnderstand="true">
<n:name>Åke Jógvan Øyvind</n:name>
</n:passenger>
</env:Header>
<env:Body>
<p:itinerary
xmlns:p="
http://travelcompany.example.org/reservation/travel
<p:departure>
<p:departing>New York</p:departing>
<p:arriving>Los Angeles</p:arriving>
<p:departureDate>2001-12-14</p:departureDate>
<p:departureTime>late afternoon</p:departureTime>
<p:seatPreference>aisle</p:seatPreference>
</p:departure>
<p:return>
<p:departing>Los Angeles</p:departing>
<p:arriving>New York</p:arriving>
<p:departureDate>2001-12-20</p:departureDate>
<p:departureTime>mid-morning</p:departureTime>
<p:seatPreference/>
</p:return>
</p:itinerary>
<q:lodging
xmlns:q="
http://travelcompany.example.org/reservation/hotels
<q:preference>none</q:preference>
</q:lodging>
</env:Body>
</env:Envelope>
图 1-8 显示了清单 1-6 中示例的基本结构。
图 1-8。
Image from the W3C SOAP spec page
SOAP 服务实际上依赖于另一种叫做 Web 服务描述语言(WSDL)的技术。作为一种基于 XML 的语言,它描述了提供给想要使用它们的客户的服务。
清单 1-7 是摘自 W3C 网站的一个带注释的 WSDL 示例。 16
Listing 1-7. WSDL Example
<?xml version="1.0"?>
<!-- root element wsdl:definitions defines set of related services -->
<wsdl:definitions name="EndorsementSearch"
targetNamespace="
http://namespaces.snowboard-info.com
xmlns:es="
http://www.snowboard-info.com/EndorsementSearch.wsdl
xmlns:esxsd="
http://schemas.snowboard-info.com/EndorsementSearch.xsd
xmlns:soap="
http://schemas.xmlsoap.org/wsdl/soap/
xmlns:wsdl="
http://schemas.xmlsoap.org/wsdl/
<!-- wsdl:types encapsulates schema definitions of communication types; here using xsd -->
<wsdl:types>
<!-- all type declarations are in a chunk of xsd -->
<xsd:schema targetNamespace="
http://namespaces.snowboard-info.com
xmlns:xsd="
http://www.w3.org/1999/XMLSchema
<!-- xsd definition: GetEndorsingBoarder [manufacturer string, model string] -->
<xsd:element name="GetEndorsingBoarder">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="manufacturer" type="string"/>
<xsd:element name="model" type="string"/>
</xsd:sequence>
</xsd:complexType>
</xsd:element>
<!-- xsd definition: GetEndorsingBoarderResponse [. . . endorsingBoarder string . . .] -->
<xsd:element name="GetEndorsingBoarderResponse">
<xsd:complexType>
<xsd:all>
<xsd:element name="endorsingBoarder" type="string"/>
</xsd:all>
</xsd:complexType>
</xsd:element>
<!-- xsd definition: GetEndorsingBoarderFault [. . . errorMessage string . . .] -->
<xsd:element name="GetEndorsingBoarderFault">
<xsd:complexType>
<xsd:all>
<xsd:element name="errorMessage" type="string"/>
</xsd:all>
</xsd:complexType>
</xsd:element>
</xsd:schema>
</wsdl:types>
<!-- wsdl:message elements describe potential transactions -->
<!-- request GetEndorsingBoarderRequest is of type GetEndorsingBoarder -->
<wsdl:message name="GetEndorsingBoarderRequest">
<wsdl:part name="body" element="esxsd:GetEndorsingBoarder"/>
</wsdl:message>
<!-- response GetEndorsingBoarderResponse is of type GetEndorsingBoarderResponse -->
<wsdl:message name="GetEndorsingBoarderResponse">
<wsdl:part name="body" element="esxsd:GetEndorsingBoarderResponse"/>
</wsdl:message>
<!-- wsdl:portType describes messages in an operation -->
<wsdl:portType name="GetEndorsingBoarderPortType">
<!-- the value of wsdl:operation eludes me -->
<wsdl:operation name="GetEndorsingBoarder">
<wsdl:input message="es:GetEndorsingBoarderRequest"/>
<wsdl:output message="es:GetEndorsingBoarderResponse"/>
<wsdl:fault message="es:GetEndorsingBoarderFault"/>
</wsdl:operation>
</wsdl:portType>
<!-- wsdl:binding states a serialization protocol for this service -->
<wsdl:binding name="EndorsementSearchSoapBinding"
type="es:GetEndorsingBoarderPortType">
<!-- leverage off soap:binding document style @@@(no wsdl:foo pointing at the soap binding) -->
<soap:binding style="document"
transport="
http://schemas.xmlsoap.org/soap/http
<!-- semi-opaque container of network transport details classed by soap:binding above @@@ -->
<wsdl:operation name="GetEndorsingBoarder">
<!-- again bind to SOAP? @@@ -->
<soap:operation soapAction="
http://www.snowboard-info.com/EndorsementSearch
<!-- furthur specify that the messages in the wsdl:operation "GetEndorsingBoarder" use SOAP? @@@ -->
<wsdl:input>
<soap:body use="literal"
namespace="
http://schemas.snowboard-info.com/EndorsementSearch.xsd
</wsdl:input>
<wsdl:output>
<soap:body use="literal"
namespace="
http://schemas.snowboard-info.com/EndorsementSearch.xsd
</wsdl:output>
<wsdl:fault>
<soap:body use="literal"
namespace="
http://schemas.snowboard-info.com/EndorsementSearch.xsd
</wsdl:fault>
</wsdl:operation>
</wsdl:binding>
<!-- wsdl:service names a new service "EndorsementSearchService" -->
<wsdl:service name="EndorsementSearchService">
<wsdl:documentation>snowboarding-info.com Endorsement Service </wsdl:documentation>
<!-- connect it to the binding "EndorsementSearchSoapBinding" above -->
<wsdl:port name="GetEndorsingBoarderPort"
binding="es:EndorsementSearchSoapBinding">
<!-- give the binding an network address -->
<soap:address location="
http://www.snowboard-info.com/EndorsementSearch
</wsdl:port>
</wsdl:service>
</wsdl:definitions>
这些类型的服务的主要缺点是所使用的信息量,包括描述它们和使用它们。尽管 XML 提供了对在两个系统之间传输的数据进行编码的技术不可知的方法,但它也明显地掩盖了所发送的消息。
这两种技术(XML-RPC 和 SOAP + WSDL)在需要的时候为系统互连性提供了解决方案。他们提供了一种在所有系统之间使用“通用”语言传输消息的方法,但与当今的领先标准相比,他们也有几个主要问题(见表 1-4 )。这可以清楚地看到,例如,开发人员对使用 XML 而不是 JSON 的感觉。
表 1-4。
Comparison of XML-RPC/SOAP and REST Services
| XML-RCP / SOAP | REST | | --- | --- | | 必须为每种编程语言创建特定的 SOAP 客户端。即使 XML 是通用的,新的客户端也必须编写代码来解析 WSDL,以理解服务是如何工作的。 | REST 完全与技术无关,不需要特殊的客户端,只需要一种能够通过所选协议(例如 HTTP、FTP 等)进行连接的编程语言。). | | 客户端需要在开始交互之前了解关于服务的一切(因此前面提到了 WSDL)。 | 客户机只需要知道主根端点,有了响应上提供的超媒体,自我发现就成为可能。 | | 因为服务是从客户机源代码中使用的,并从服务器代码中调用特定的函数或方法,所以这两个系统之间的耦合太大了。服务器代码的重写可能会导致客户端代码的重写。 | 该接口与实现无关;完整的服务器端代码可以重写,API 的接口也不必改变。 |Note
将 XML-RPC/SOAP 与 REST 进行比较可能不完全公平(或可能),因为前两者是协议,而后者是架构风格;但是如果你记住这个区别,有些点还是可以比较的。
摘要
本章简要概述了 REST 的含义以及遵循 REST 风格会给系统带来什么样的好处。这一章还介绍了一些额外的原则,比如 HTTP 动词和状态代码,它们虽然不是 REST 风格的一部分,但确实是 HTTP 标准的一部分,这是本书所基于的协议。
最后,我讨论了 REST 之前使用的主要技术,您看到了它们与当前领先的行业标准的比较。
在下一章,我将回顾一些好的 API 设计实践,你将看到如何使用 REST 来实现它们。
Footnotes 1
https://www.ietf.org/rfc/rfc2616.txt
见。
2
3
4
http://www.ics.uci.edu/∼fielding/pubs/dissertation/rest_arch_style.htm
见。
5
http://tools.ietf.org/html/rfc7231#section-5.3
见。
6
http://stateless.co/hal_specification.html
见。
7
http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
见。
8
http://tools.ietf.org/html/rfc7231#section-6
见。
9
http://www.microsoft.com/com/default.mspx
见。
10
11
http://en.wikipedia.org/wiki/XML-RPC
见。
12
http://ws.apache.org/xmlrpc/
见。
13
http://xmlrpc-epi.sourceforge.net
见。
14
http://xmlrpc-c.sourceforge.net/
见。
15
16
http://www.w3.org/2001/03/14-annotated-WSDL-examples
见。
二、API 设计最佳实践
API 设计的实践是一件棘手的事情。即使有如此多的选择——使用的工具、应用的标准、遵循的风格——在开始任何类型的设计和开发之前,有一个基本问题需要回答,并且需要在开发人员的头脑中弄清楚……
什么定义了一个好的 API?
众所周知,“好”和“坏”的概念是非常主观的(一个人可能会读几本书来讨论这个问题),因此,不同的人有不同的观点。也就是说,多年来处理不同种类 API 的经验让开发人员社区(以及本文作者)对任何好的 API 都必须具备的特性有了很好的认识。(免责声明:像干净的代码、良好的开发实践和其他内部考虑的事情在这里不会被提及,但是会被假定,因为它们应该是每个软件任务的一部分。)
所以让我们检查一下这个列表。
- 开发者友好:使用你的 API 的开发者在处理你的系统时不应该受到影响。
- 可扩展性:您的系统应该能够在不破坏客户端的情况下处理新特性的添加。
- 最新的文档:好的文档是新开发人员获得 API 的关键。
- 适当的错误处理:因为事情会出错,你需要做好准备。
- 提供多个 SDK/库:您为开发人员简化的工作越多,他们就会越喜欢您的系统。
- 安全:任何全球系统的一个关键方面。
- 可伸缩性:伸缩能力是任何好的 API 都应该具备的正确提供服务的能力。
我将逐一讨论这些要点,并展示它们如何影响 API,以及遵循 REST 风格如何有所帮助。
开发者友好
根据定义,API 是一个应用编程接口,关键词是接口。当考虑设计一个除了你自己以外的开发人员使用的 API 时,有一个关键的方面需要考虑:开发人员体验(或 DX)。
即使当 API 将被另一个系统使用时,集成到该系统中也是首先由一个或多个开发人员完成的——将人的因素带入该集成中的人。这意味着您希望 API 尽可能易于使用,这有助于实现出色的 DX,并且应该转化为更多的开发人员和客户端应用使用 API。
然而,这是一种权衡,因为为人类简化事物可能导致界面的过度简化,这反过来可能导致处理复杂功能时的设计问题。
将 DX 作为 API 的主要方面之一来考虑是很重要的(老实说,没有开发人员使用它,API 就没有任何意义),但是在设计决策中还必须考虑其他一些方面。简单一点,但不要简单到假装。
接下来的部分提供了一些好的诊断指南。
通信协议
这是界面最基本的方面之一。在选择通信协议时,使用 API 开发人员熟悉的协议总是一个好主意。有几个标准已经在许多编程语言(例如 HTTP、FTP、SSH 等)中提供了库和模块。).
定制的协议并不总是一个好主意,因为在如此多的现有技术中,你会失去即时的可移植性。也就是说,如果您准备为最常用的语言创建支持库,并且您的定制协议对您的用例更有效,那么它可能是正确的选择。
最后,由 API 设计人员根据他工作的环境来评估最佳解决方案。
在本书中,您假设为 REST 选择的协议是 HTTP。 1 这是一个非常知名的协议;任何现代编程语言都支持它,它是整个互联网的基础。你可以放心,大多数开发者对如何使用它有基本的了解。如果没有,有大量的信息可以让你更好地了解它。
总之,没有完美的银弹协议适用于所有场景。考虑你的 API 需求,确保你选择的任何东西都与 REST 兼容,你就没事了。
易于记忆的接入点
所有客户端应用和 API 之间的接触点称为端点。API 需要提供它们来允许客户端访问它的功能。这可以通过选择任何通信协议来实现。这些访问点应该有一个简单的名字来帮助开发者理解它们的用途。
当然,名称本身不应该替代详细的文档,但是引用正在使用的资源,并在调用该访问点时提供某种操作指示,通常被认为是一个好主意。
下面是一个命名不当的访问点的好例子(用于列出书店中的书籍):
GET /books/action1
这个例子使用 HTTP 协议来指定访问点,即使使用的实体(books)被引用,动作名也不清楚;可能意味着任何事情,甚至更糟的是,这个意思将来可能会改变,但这个名字仍然合适,所以任何现有的客户无疑都会破产。
一个更好的例子——遵循 REST 和第一章中讨论的标准——是这样的:
GET /books
这将为开发人员提供足够多的信息,让他们理解对资源根(/books
)的 GET 请求将总是产生这种类型的项目列表;然后,开发人员可以将这种模式复制到其他资源中,只要接口在所有其他端点上保持一致。
统一界面
易于记忆的接入点很重要,但是在定义接入点时保持一致也很重要。同样,在使用 API 时,您必须回到人的因素:您拥有它。因此,让他们的生活更容易是必须的,如果你想让任何人使用它,你不能忘记的 DX。这意味着在定义端点名称、请求格式和响应格式时,您需要保持一致。后两者可以有多个(更具体地说,响应格式与资源可以拥有的各种表示直接相关),但是只要缺省值始终相同,就不会有问题。
不一致接口的一个很好的例子,即使不在 API 上,也可以在编程语言 PHP 中看到。它在大多数函数名上都有下划线符号,但是在一些函数名中没有使用下划线,所以开发人员总是被迫回到文档中检查如何编写这些函数(或者更糟,依靠他/她的记忆)。
例如,str_replace
是一个使用下划线分隔两个单词(str
和replace
)的函数,而htmlentities
根本没有单词分隔。
API 中糟糕的设计实践的另一个例子是基于所采取的动作而不是所处理的资源来命名端点;例如:
/getAllBooks
/submitNewBook
/updateAuthor
/getBooksAuthors
/getNumberOfBooksOnStock
这些例子清楚地显示了这个 API 遵循的模式。乍看起来,它们可能没有那么糟糕,但是考虑一下,随着新的特性和资源被添加到系统中,界面会变得多么糟糕(更不用说如果动作被修改)。系统的每一个新增加都会导致 API 接口的额外端点。客户端应用的开发者对于这些新的端点是如何命名的毫无头绪。例如,如果扩展 API 以支持书籍的封面图像,使用当前的命名方案,这些都是可能的新端点:
/addNewImageToBook
/getBooksImages
/addCoverImage
/listBooksCovers
这样的例子不胜枚举。因此,对于任何现实世界的应用,您可以放心地假设,遵循这种类型的模式将产生一个非常大的端点列表,增加服务器端代码和客户端代码的复杂性。这也将损害系统捕获新开发者的能力,因为多年来它将具有继承的复杂性。
为了解决这个问题,并在整个 API 中生成一个易于使用的统一接口,您可以将 REST 风格应用于端点。如果你还记得 REST 在第一章提出的约束,你会得到一个以资源为中心的接口。多亏了 HTTP,你还可以用动词来表示动作。
表 2-1 展示了使用 REST 之前的界面是如何变化的。
表 2-1。
List of Endpoints and How They Change When the REST Style Is Applied
| 旧历法 | REST 风格 | | --- | --- | | /getAllBooks | 获取/书籍 | | /submitNewBook | 邮件/书籍 | | /updateAuthor | 上传/作者/:id | | /getBooksAuthors | GET /books/:id/authors | | /getNumberOfBooksOnStock | GET /books(这个数字很容易作为这个端点的一部分返回。) | | /addNewImageToBook | PUT /books/:id | | /getbookimages | 获取/书籍/:id/图像 | | /addCoverImage | POST/books/:id/封面 _ 图像 | | /list books 封面 | GET /books(此信息可以使用子资源在此端点中返回。) |您不必记住九个不同的端点,只需要记住两个,额外的好处是,一旦定义了标准,所有 HTTP 动词在所有情况下都是相同的;现在没有必要记住每种情况下的特定角色(它们总是意味着相同的事情)。
运输语言
接口要考虑的另一个方面是使用的传输语言。多年来,事实上的标准是 XML 它提供了一种与技术无关的方式来表达可以在客户机和服务器之间轻松发送的数据。如今,有一种新的标准正在 XML 之上流行起来——JSON。
为什么是 JSON?
在过去的几年中,JSON 作为标准数据传输格式越来越受欢迎(见图 2-1 )。这主要是由于它提供的优势。下面列出了几个例子:
图 2-1。
Trend of Google searches for “JSON” vs. “XML” over the last few years
- 它很轻。JSON 文件中很少有数据与正在传输的信息没有直接关系。这是超越 XML 等更冗长格式的一个主要优势。 2
- 它是人类可读的。这种格式本身非常简单,人们可以很容易地阅读和书写。考虑到任何好的 API 界面的焦点都是人的因素(也称为 DX),这一点尤其重要。
- 它支持不同的数据类型。因为不是所有被传输的都是字符串,所以这个特性允许开发人员为被传输的信息提供额外的含义。
这个列表还可以继续下去,但这是帮助 JSON 在开发人员社区中赢得如此多追随者的三个主要方面。
尽管 JSON 是一种很好的格式,并且越来越受欢迎,但它并不是总能解决所有问题的灵丹妙药;所以给客户提供选择也很重要。这就是 REST 发挥作用的地方。
由于 REST 所基于的协议是 HTTP,开发人员可以使用一种叫做内容协商的机制来允许客户端指定他们想要接收的支持格式(如第一章所讨论的)。这为 API 提供了更多的灵活性,并且仍然保持了界面的统一性。
回到端点列表,最后一个谈到使用子资源作为解决方案。这可以有多种解释,因为不仅用于传输数据的语言很重要,而且您提供给被传输数据的结构也很重要。我对统一接口的最后建议是标准化所用的格式,或者更好的是,遵循现有的格式,比如 HAL。
这在前一章中已经介绍过了,所以请回头参考它以获取更多信息。
展开性
一个好的 API 永远不会完全完成。这可能是一个大胆的主张,但它来自于社区的经验。让我们来看看一些大的。 3
- 谷歌 APIs 一天 50 亿次调用4;于 2005 年推出
- 脸书 APIs 一天 50 亿个呼叫 5 个;于 2007 年推出
- Twitter APIs 一天 130 亿次调用6;于 2006 年推出
这些例子表明,即使当 API 背后有一个伟大的团队时,API 也会不断增长和变化,因为客户端应用开发人员找到了使用它的新方法,API 所有者的商业模式会随着时间的推移而变化,或者只是因为功能的添加和删除。
当发生这种情况时,可能需要扩展或更改 API,添加新的访问点或更改旧的访问点。如果最初的设计是正确的,那么从 v1 到 v2 应该没有问题,但如果不是,那么这种迁移可能会给每个人带来灾难。
扩展性是如何管理的?
当扩展 API 时,你基本上是在发布软件的一个新版本,所以你需要做的第一件事就是让你的用户(开发者)知道一旦新版本出来会发生什么。他们的应用还能用吗?这些变化是向后兼容的吗?你会在线维护你的 API 的几个版本,还是只维护最新的一个?
一个好的 API 应该考虑以下几点:
- 添加新端点有多容易?
- 新版本向后兼容吗?
- 在更新代码的同时,客户端可以继续使用旧版本的 API 吗?
- 以新 API 为目标的现有客户端会发生什么?
- 对于客户来说,瞄准 API 的新版本有多容易?
一旦所有这些问题都解决了,那么您就可以安全地发展和扩展 API 了。
通常,通过立即放弃版本 A 并使其脱机以支持版本 B 来从版本 A 升级到版本 B 被认为是一个糟糕的举动,当然,除非只有极少数客户端应用使用该版本。
对于这种情况,更好的方法是允许开发人员选择他们想要使用的 API 版本,将旧版本保留足够长的时间,以便让每个人都迁移到新版本。为了做到这一点,API 会在资源标识符(即每个资源的 URL)中包含其版本号。这种方法使版本号成为 URL 的强制部分,以清楚地显示正在使用的版本。
另一种方法可能不太清楚,就是提供一个指向 API 最新版本的无版本 URL,以及一个可选的 URL 参数来覆盖该版本。这两种方法各有利弊,需要由创建 API 的开发人员来权衡。
表 2-3。
Pros and Cons of Having the API Version Hidden from the User
| 赞成的意见 | 骗局 | | --- | --- | | 更简单的网址。 | 隐藏的版本号可能会导致混淆正在使用的版本。 | | 即时迁移到 API 的最新工作代码。 | 非向后兼容的更改将破坏没有引用特定 API 版本的客户端。 | | 从客户端的角度来看,从一个版本到下一个版本的简单迁移(只改变属性的值)。 | 使版本选择可用需要复杂的架构。 | | 根据最新版本轻松测试客户端代码(只是不要发送特定于版本的参数)。 |表 2-2。
Pros and Cons of Having the Version of the API As Part of the URL
| 赞成的意见 | 骗局 | | --- | --- | | 版本号清晰可见,有助于避免混淆正在使用的版本。 | URL 更加冗长。 | | 从客户端的角度来看,很容易从一个版本迁移到另一个版本(所有的 URL 都改变相同的部分——版本号) | 当从一个版本迁移到另一个版本时,API 代码上的错误实现可能会导致大量的工作(即,如果版本是在端点的 URL 模板上硬编码的,每个端点都有单独的版本)。 | | 当不止一个版本的 API 需要保持工作时,允许更清晰的架构。 | | | 从 API 的角度来看,从一个版本到下一个版本的迁移清晰而简单,因为两个版本可以并行工作一段时间,从而允许较慢的客户端在不中断的情况下进行迁移。 | | 正确的版本控制方案可以使补丁和向后兼容的新特性立即可用,而不需要在客户端进行更新。 |请记住,在设置软件产品的版本时,有几种版本化方案可供使用:
Ubuntu 的 7 版本号代表发布的年份和月份;所以 14.04 版本意味着是 2014 年 4 月发布的。
在 Chromium 项目中,版本号有四个部分8:MAJOR . MINOR . build . patch .以下来自 Chromium 项目关于版本的页面:MAJOR 和 MINOR 可能会随着任何重要的 Google Chrome 版本而更新(Beta 或稳定更新)。对于任何向后不兼容的用户数据更改,必须更新 MAJOR(因为该数据在更新后仍然存在)。每当从当前主干构建一个发布候选时,构建必须得到更新(对于开发通道发布候选,至少每周一次)。构建号是一个不断增加的数字,代表 Chromium 主干的一个时间点。每当从构建分支构建一个发布候选时,补丁必须得到更新。
另一种中间方法,被称为语义版本化或 SemVer, 9 被社区广泛接受。它提供了适量的信息。每个版本都有三个数字:MAJOR.MINOR.PATCH
- MAJOR 表示不向后兼容的更改。
- MINOR 表示使 API 向后兼容的新特性。
- PATCH 代表小的变化,如错误修复和代码优化。
在这种模式下,第一个数字是唯一真正与客户相关的数字,因为这是表示与客户当前版本兼容的数字。
通过始终在 API 上部署最新版本的 MINOR 和 PATCH,您可以为客户提供最新的兼容特性和错误修复,而无需客户更新他们的代码。
因此,使用这个简单的版本控制方案,端点看起来像这样:
GET /1/books?limit=10&size=10
POST /v2/photos
GET /books?v=1
选择版本化方案时,请考虑以下因素:
- 通过使用错误版本的 API,使用错误的版本化方案可能会在实现客户端应用时导致混乱或问题。例如,在你的 API 中使用 Ubuntu 的版本控制方案可能不是交流每个新版本中发生的事情的最佳方式。
- 错误的版本控制方案可能会迫使客户端进行大量更新,比如在部署一个小的补丁或添加一个新的向后兼容特性时。这些变化不需要更新客户端。因此,除非您的方案需要,否则不要强迫客户端指定版本的这些部分。
最新文档
不管你的端点有多复杂,你仍然需要文档来解释你的 API 所做的一切。无论是可选参数还是接入点的机制,文档都是获得良好 DX 的基础,这将转化为更多用户。
一个好的 API 需要的不仅仅是解释如何使用一个接入点的几行代码(没有什么比发现你需要一个接入点,但是它根本没有文档更糟糕的了),而是需要一个完整的参数列表和解释性的例子。
一些提供商给开发人员一个简单的 web 界面,让他们不用写任何代码就可以尝试他们的 API。这对新人特别有用。
有一些在线服务允许 API 开发者上传他们的文档,以及那些提供 web UI 来测试 API 的服务;比如 Mashape 免费提供这个服务(见图 2-2 )。
图 2-2。
The service provided by Mashape
详细文档的另一个好例子是脸书的开发者网站。 10 提供了脸书支持的所有平台的实现和使用示例(见图 2-3 )。
图 2-3。
Facebook’s API documentation site
图 2-4 显示了一个糟糕的文档示例。是 4chan 的 API 文档。 11
图 2-4。
Introduction to 4chan’s API documentation
是的,这个 API 看起来不够复杂,不值得写一整本书来介绍它,但是这里没有提供任何例子,只有一个关于如何找到端点和使用什么参数的一般性解释。
新手可能会发现很难理解如何实现一个使用这个 API 的简单客户端。
Note
比较 4chan 和脸书的文件是不公平的,因为团队和公司的规模完全不同。但是你应该注意到 4chan 的文档缺乏质量。
虽然在开发 API 时,这看起来不是最有成效的想法,但是团队需要考虑处理大量的文档。这是确保 API 成功或失败的主要因素之一,原因有两个:
- 它应该可以帮助新手和高级开发人员毫无问题地使用您的 API。
- 如果保持更新,它应该作为开发团队的蓝图。如果有一个关于 API 如何工作的写得很好、解释得很清楚的蓝图,那么进入项目中期开发会更容易。
Note
当 API 发生变化时,这也适用于更新文档。你需要保持更新;否则效果和根本没有文档一样。
适当的错误处理
API 上的错误处理非常重要,因为如果处理得当,它可以帮助客户端应用理解如何处理错误;从人的角度来看(DX),它可以帮助开发人员了解他们做错了什么以及如何修复它。
在 API 客户端的生命周期中,有两个非常明显的时刻需要考虑错误处理:
- 阶段 1:客户端的开发。
- 阶段 2:最终用户实现并使用客户端。
阶段 1:客户端的开发
在第一阶段,开发人员实现所需的代码来使用 API。开发人员很可能会在请求中出错(比如缺少参数、错误的端点名称等等。)在这个阶段。
这些错误需要得到适当的处理,这意味着返回足够的信息,让开发人员知道他们做错了什么,以及如何修复它。
一些系统的常见问题是它们的创建者忽略了这个阶段,当请求出现问题时,API 就会崩溃,返回的信息只是一个错误消息,带有堆栈跟踪和状态代码 500。
图 2-5 中的响应显示了当您忘记在客户端开发阶段添加错误处理时会发生什么。返回的堆栈跟踪可能会给开发人员一些线索(最好的情况是)关于到底哪里出错了,但是它也显示了许多不必要的信息,所以它最终会令人困惑。这肯定会影响开发时间,毫无疑问,这也是反对 API DX 的主要原因。
图 2-5。
A classic example of a crash on the API returning the stack trace
另一方面,让我们看看图 2-6 中相同错误的正确错误响应。
图 2-6。
A proper error response would look like this
图 2-6 清楚显示出现了错误,错误是什么,以及错误代码。响应只有三个属性,但它们都很有用:
- 错误指示器为开发人员提供了一种明确的方法来检查响应是否是错误消息(您也可以根据响应的状态代码进行检查)。
- 该错误消息显然是为开发人员准备的,它不仅指出了缺少什么,还解释了如何修复它。
- 如果在文档中解释了自定义错误代码,当这种类型的响应再次发生时,它可以帮助开发人员自动执行操作。
阶段 2:最终用户实现并使用客户端
在客户端生命周期的这一阶段,您不会再遇到任何开发人员错误,比如使用错误的端点、缺少参数等等,但是仍然有可能出现由用户生成的数据引起的问题。
向用户请求某种输入的客户端应用总是会出现用户方面的错误,尽管在输入到达 API 层之前总是有方法验证输入,但假设所有客户端都会这样做也是不安全的。因此,对于任何 API 设计者和开发者来说,最安全的做法是假设客户端没有进行任何验证,任何可能出错的地方都是数据出错。从安全性的角度来看,这也是一个安全的假设,因此它提供了一个微小的安全性改进作为副作用。
有了这种心态,实现的 API 应该坚如磐石,能够处理输入数据中的任何类型的错误。
响应应该模仿第 1 阶段的响应:应该有一个错误指示器、一条说明错误的错误消息(如果可能,还有如何修复它)和一个定制的错误代码。自定义错误代码在这一阶段特别有用,因为它将为客户端提供自定义向最终用户显示的错误的能力(甚至显示不同但仍然相关的错误消息)。
多个 SDK/库
如果您希望您的 API 在不同的技术和平台上被广泛使用,那么开发并提供对可用于您的系统的库和 SDK 的支持可能是个好主意。
通过这样做,您为开发人员提供了消费您的服务的方法,因此他们所要做的就是使用它们来创建他们的客户端应用。本质上,您正在削减潜在的几周或几个月(取决于系统的大小)的开发时间。
另一个好处是,大多数开发人员会更信任你的库,因为你是这些库所使用的服务的所有者。
最后,考虑开源你的库的代码。如今,开源社区正在蓬勃发展。如果对他们有用,开发人员无疑会帮助维护和改进你的库。
让我们再来看看一些最大的 API:
- 脸书 API 为 iOS、Android、JavaScript、PHP 和 Unity 提供 SDK。 12
- Google Maps API 提供了多种技术的 SDK,包括 iOS、Web 和 Android。 十三
- Twitter API 为他们的几个 API 提供了 SDK,包括 Java、ASP、C++、Clojure、。NET,Go,JavaScript,还有很多其他语言。 14
- 亚马逊为他们的 AWS 服务提供 SDK,包括 PHP,Ruby,。NET,还有 iOS。他们甚至在 GitHub 上发布了 SDK,任何人都可以看到。 十五
安全
保护您的 API 是开发过程中非常重要的一步,它不应该被忽略,除非您构建的足够小,并且没有敏感数据值得花费精力。
在设计 API 时,有两个大的安全问题需要处理:
- 认证:谁将访问 API?
- 授权:一旦登录,他们能够访问什么?
认证处理让合法用户访问 API 提供的特性。授权处理那些经过身份验证的用户在系统内部实际可以做什么。
在详细讨论每个具体问题之前,在处理 RESTful 系统(至少是基于 HTTP 的系统)的安全性时,需要记住一些常见的方面:
- RESTful 系统应该是无状态的。请记住,REST 将服务器定义为无状态的,这意味着在首次登录后将用户数据存储在会话中并不是一个好主意(如果您想遵守 REST 提供的准则)。
- 记得用 HTTPS。在基于 HTTP 的 RESTful 系统上,应该使用 HTTPS 来确保通道的加密,这使得捕获和读取数据流量更加困难(中间人攻击)。
进入系统
有一些广泛使用的身份验证方案可以在用户登录系统时提供不同的安全级别。最常见的有 TSL 基本认证、摘要认证、OAuth 1.0a 和 OAuth 2.0。
我将回顾一下这些,并谈谈它们各自的优缺点。我还将介绍另一种方法,它应该被证明是最 RESTful 的,也就是说它是 100%无状态的。
几乎无状态的方法
OAuth 1.0a、OAuth 2.0、摘要认证和基本认证+ TSL 是目前流行的认证方法。它们工作得非常好,它们已经在所有现代编程语言中实现,并且它们已经被证明是这项工作的正确选择(当用于正确的用例时)。也就是说,正如你将要看到的,他们没有一个是 100%无国籍的。
它们都依赖于让用户将信息存储在服务器端的某种缓存层上。这个小细节,特别是对于纯粹主义者来说,意味着在设计 RESTful 系统时不要去做,因为它违背了 REST 强加的最基本的约束之一:客户机和服务器之间的通信必须是无状态的。
这意味着用户的状态不应该存储在任何地方。
然而,在这种特殊的情况下,你会以另一种方式看待。无论如何,我将涵盖每种方法的基础,因为在现实生活中,你必须妥协,你必须在纯粹性和实用性之间找到平衡。但别担心。我将介绍一种替代设计,它将解决身份验证并保持 REST 的真实性。
与 TSL 的基本授权
由于本书的目的是将 REST 基于 HTTP,所以后者提供了大多数语言都支持的基本认证方法。
但是请记住,这个方法的名字很恰当,因为它非常基本,并且通过 HTTP 发送未加密的用户名和密码。因此,使它安全的唯一方法是通过 HTTPS (HTTP + TSL)的安全连接来使用它。
该认证方法的工作原理如下(参见图 2-7 ):
图 2-7。
The steps between client and server on Basic Auth First, a client makes a request for a resource without any special header. The server responds with a 401 unauthorized response, and within it, a WWW-Authenticate header, specifying the method to use (Basic or Digest) and the realm name. The client then sends the same request, but adds the Authorization header, with the string USERNAME:PASSWORD encoded in base 64.
在服务器端,需要一些代码来解码身份验证字符串,并从所使用的会话存储(通常是数据库)中加载用户数据。
除了这种方法是众多打破非静态约束的方法之一这一事实之外,它实现起来既简单又快速。
Note
使用此方法时,如果已登录用户的密码被重置,则根据请求发送的登录数据会变旧,并且当前会话会终止。
摘要授权
这种方法是对前一种方法的改进,因为它通过加密登录信息增加了额外的安全层。与服务器的通信以同样的方式工作,来回发送相同的头。
使用这种方法,在接收到对受保护资源的请求时,服务器将用 WWW-Authenticate 头和一些特定的参数进行响应。以下是一些最有趣的例子:
- Nounce:唯一生成的字符串。这个字符串需要在每个 401 响应中是唯一的。
- Opaque:由服务器返回的字符串,必须由客户端不加修改地发送回来。
- Qop:即使是可选的,也应该发送该参数来指定所需的保护质量(在该值中可以发送多个令牌)。发送回
auth
意味着简单的认证,而发送auth-int
意味着带有完整性检查的认证。 - 算法:该字符串指定用于计算客户端校验和响应的算法。如果不存在,则应假设 MD5。
有关参数和实现细节的完整列表,请参考 RFC。 16 以下是一些最有趣的例子:
- 用户名:未加密的用户名。
- URI:你试图进入的 URI。
- 响应:响应的加密部分。这证明了你就是你所说的那个人。
- Qop:如果存在,它应该是服务器发送的支持值之一。
为了计算响应,需要应用以下逻辑:
MD5(HA1:STRING:HA2)
HA1 的这些值计算如下:
- 如果响应中没有指定算法,那么应该使用
MD5(username:realm:password)
。 - 如果算法是 MD5-less,那么应该是
MD5(MD5(username:realm:password):nonce:cnonce)
HA2 的这些值计算如下:
- 如果
qop
是auth
,那么应该使用MD5(method:digestURI)
。 - 如果
qop
是auth-int
,那么MD5(method:digestURI:MD5(entityBody))
最后,回应如下:
MD5(HA1:nonce:nonceCount:clientNonce:HA2) //for the case when "qop" is "auth" or "auth-int"
MD5(HA1:nonce:HA2) //when "qop" is unspecified.
这种方法的主要问题是所使用的加密是基于 MD5 的,并且在 2004 年已经证明这种算法是不抗冲突的,这基本上意味着中间人攻击将使攻击者有可能获得必要的信息并生成一组有效的凭证。
对这种方法的一个可能的改进,就像它的“基本”兄弟一样,是增加 TSL;这肯定有助于使它更加安全。
oath 1.0a
OAuth 1.0a 是本节描述的四种非静态方法中最安全的。这个过程比之前描述的要繁琐一些(见图 2-8 ),但是这里的代价是安全级别的显著提高。
图 2-8。
The interaction between client and server
在这种情况下,服务提供商必须允许客户端应用的开发者在提供商的网站上注册应用。通过这样做,开发人员获得了消费者密钥(他的应用的唯一标识密钥)和消费者秘密。完成该过程后,需要执行以下步骤:
- 客户端应用需要请求令牌。目的是接收用户的批准,然后请求访问令牌。要获得请求令牌,服务器必须提供一个特定的 URL 在这个步骤中,使用消费者密钥和消费者秘密。
- 获得请求令牌后,客户端必须在特定的服务器 URL(即
http://provider.com/oauth/authorize
)上使用令牌发出请求,以获得最终用户的授权。 - 在用户授权后,客户端应用向提供商请求访问令牌和令牌密钥。
- 一旦获得访问令牌和秘密令牌,客户端应用就能够通过签署每个请求来代表用户为提供者请求受保护的资源。
有关此方法如何工作的更多详细信息,请参考完整的文档。 17
OAuth 2.0
OAuth 2.0 是 OAuth 1.0a 的发展;它关注客户端开发人员的简单性。使用 OAuth 1.0 的系统实现的主要问题是最后一步中隐含的复杂性:对每个请求进行签名。
由于其复杂性,最后一步是该算法的关键弱点:如果客户端或服务器犯了一个小错误,那么请求将不会被验证。即使同一个方面使它成为唯一不需要在 SSL(或 TSL)之上工作的方法,这种好处也是不够的。
OAuth 2.0 试图通过一些关键的改变来简化最后一步,主要是:
- 它依靠 SSL(或 TSL)来确保来回发送的信息是加密的。
- 生成令牌后,请求不需要签名。
总而言之,这个版本的 OAuth 试图简化 OAuth 1.0 引入的复杂性,同时牺牲安全性(通过依靠 TSL 来确保数据加密)。如果您处理的设备支持 TSL(计算机、移动设备等),这是优于 OAuth 1.0 的首选方法。);否则,您可能需要考虑使用其他选项。
一个无状态的选择
正如您所看到的,在实现允许用户登录 RESTful API 的安全协议时,您所拥有的选择并不是无状态的,尽管您应该准备好做出这样的承诺,以便获得保护您的应用的可靠方法的好处,但也有一种完全兼容 REST 的方法可以做到这一点。
如果你回到第一章,无状态约束基本上意味着客户端和服务器之间的任何和所有通信状态都应该包含在客户端发出的每个请求中。这当然包括用户信息,所以如果您想要无状态认证,您也需要在您的请求中包括这些信息。
如果想确保每个请求的真实性,可以借用 OAuth 1.0a 的签名步骤,通过使用客户端和服务器之间预先建立的密钥,以及 MAC(消息认证码)算法来进行签名,将其应用于每个请求(见图 2-9 )。
图 2-9。
How the MAC signing process works
由于保持无状态,生成 MAC 所需的信息也需要作为请求的一部分发送,这样服务器就可以重新创建结果并证实其有效性。
在我们的案例中,这种方法有一些明显的优势,主要是:
- 它比 OAuth 1.0a 和 OAuth 2.0 都简单。
- 需要零存储,因为验证加密所需的任何和所有信息都需要在每次请求时发送。
可量测性
最后但同样重要的是可伸缩性。
可伸缩性通常是 API 设计中被低估的一个方面,主要是因为在一个 API 发布之前,很难完全理解和预测它将达到的范围。如果团队以前有过类似项目的经验(例如,Google 可能已经很擅长在发布日之前计算新 API 的可伸缩性),估计这一点可能更容易,但是如果这是他们的第一个项目,那么可能就不那么容易了。
一个好的 API 应该能够伸缩,也就是说,它应该能够处理尽可能多的流量,而不影响其性能。但这也意味着,如果不需要资源,就不应该花费。这不仅反映了 API 所在的硬件(尽管这是一个重要的方面),也反映了 API 的底层架构。
多年来,软件体系结构中的经典单体设计已经迁移到完全分布式设计中,因此将 API 拆分成彼此交互的不同模块是有意义的。
这提供了所需的灵活性,不仅可以扩大或缩小受影响的资源,还可以提供容错能力,帮助开发人员维护更干净的代码库以及其他优势。
下图(图 2-10 )展示了一个标准的整体设计,将你的应用放在一个服务器中,就像一个单独的实体。
图 2-10。
Simple diagram of a monolithic architecture
在图 2-11 中,您可以看到一个分布式设计,如果与之前的设计相比,您可以看到优势来自哪里(更好的资源利用、容错、更容易扩展或缩小,等等)。
图 2-11。
A diagram showing an example of a distributed architecture.
使用 REST 实现分布式架构以确保可伸缩性非常简单。Fielding 的论文提出了一种基于客户机-服务器模式的分布式系统。
因此,将整个系统分割成一组更小的 API,让它们在需要时相互通信将确保前面提到的优势。
例如,让我们看一个书店的内部系统(表 2-4 ),主要实体是:
表 2-4。
List of Entities and Their Role Inside the System
| 实体 | 描述 | | --- | --- | | 书 | 表示商店的库存。它将控制一切,从书籍数据,到副本数量,等等。 | | 客户 | 客户的联系方式。 | | 用户 | 内部书店用户,他们将有权访问系统。 | | 购买 | 记录图书销售的信息。 |现在,考虑一个小书店的系统,一个刚刚起步并且只有几个员工的书店。采用单片设计是非常有诱惑力的,不会花费太多的资源,而且设计非常简单。
现在,考虑一下,如果这家小书店突然增长如此之快,以至于它扩展到其他几家书店,它们从只有一家书店发展到 100 家,员工人数增加,书籍需要更好的跟踪,购买量上升,会发生什么。
以前的简单系统不足以应对这种增长。它需要改变以支持网络、集中数据存储、分布式访问、更好的存储容量等等。换句话说,扩大规模成本太高,而且可能需要完全重写。
最后,考虑另一个开始,如果您花时间使用基于 REST 的分布式架构创建第一个系统,会怎么样?让每个子系统都成为不同的 API,并让它们相互通信。
然后,您将能够更容易地扩展整个系统,在每个子系统上独立工作,不需要完全重写,系统可能会不断增长以满足新的需求。
摘要
本章涵盖了开发人员社区所认为的“好的 API”,其含义如下:
- 记住开发者体验(DX)。
- 能够在不破坏现有客户的情况下成长和提高。
- 拥有最新的文档。
- 提供正确的错误处理。
- 提供多个 SDK 和库。
- 考虑安全问题。
- 能够根据需要向上和向下扩展。
在下一章中,您将了解为什么 Node.js 是实现您在本章中学到的所有内容的完美匹配。
Footnotes 1
http://www.w3.org/Protocols/rfc2616/rfc2616.html
见。
2
严格来说,XML 不是一种数据传输格式,但它正被用作一种数据传输格式。
3
来源: http://www.slideshare.net/3scale/apis-for-biz-dev-20-which-business-model-15473323
。
4
2010 年 4 月。
5
2009 年十月。
6
2011 年 5 月。
7
https://help.ubuntu.com/community/CommonQuestions#Ubuntu_Releases_and_Version_Numbers
见。
8
http://www.chromium.org/developers/version-numbers
见。
9
参见 semver.org。
10
https://developers.facebook.com/docs/graph-api/using-graph-api/v2.1
见。
11
https://github.com/4chan/4chan-API
见。
12
参见https://developers.facebook.com
(SDK 列表见页面底部)。
13
https://developers.google.com/maps/
见。
14
https://dev.twitter.com/overview/api/twitter-libraries
见。
15
16
https://www.ietf.org/rfc/rfc2617.txt
见。
17
三、Node.js 和 REST
目前有太多的技术存在——无论是编程语言、平台还是框架。那么,为什么 node . js——一个在撰写本文时还没有达到 1.0 版本的项目——现在如此受欢迎呢?
硬件的进步使得开发人员有可能减少对代码的过度优化以提高速度,从而让他们更多地关注开发速度;因此,一套新的工具出现了。这些工具使开发新手更容易开发新项目,同时为高级开发人员提供了使用旧工具时获得的相同类型的功能。这些工具是当今新的编程语言和框架(Ruby on Rails、Laravel、Symfony、Express.js、Node.js、Django 等等)。
在这一章中,我将介绍其中最新的一个:Node.js。它是由 Ryan Dahl 在 2009 年创建的,并由 Dahl 工作过的 Joyent 公司赞助。在其核心,Node.js 1 利用 Google V8 2 引擎在服务器端执行 JavaScript 代码。我将介绍它的主要特性,以帮助您理解为什么它是 API 开发的一个非常好的工具。
以下是本章涉及的 Node.js 的一些方面:
- 异步编程:这是 Node.js 的一个很棒的特性。我将讨论如何利用它来获得比使用其他技术更好的结果。
- 异步 I/O:虽然与异步编程有关,但这值得单独提及,因为在输入/输出繁重的应用中,这一特性是选择 Node.js 而不是其他技术的制胜法宝。
- 简单性:Node.js 使入门和编写第一个 web 服务器变得非常容易。你会看到一些例子。
- 与基于 JSON 的服务(如其他 API、MongoDB 等)惊人的集成。).
- 社区和 Node 包管理器(npm):我将回顾拥有一个使用该技术的大型开发人员社区的好处,以及 npm 是如何提供帮助的。
- 谁在使用它?最后,我将快速浏览一些在其生产平台中使用 Node.js 的大公司。
异步编程
异步(async)编程可能同时是 Node.js 最好和最令人困惑的特性之一。
异步编程意味着,对于您执行的每个异步函数,您不能期望它在继续程序流程之前返回结果。相反,您需要提供一个回调块/函数,一旦异步代码完成,就会执行这个回调块/函数。
图 3-1 显示了一个规则的非异步流程。
图 3-1。
A synchronous execution flow
图 3-1 表示一组以同步方式运行的指令。为了执行指令#4,您需要等待“长时间运行指令”花费的时间,然后等待指令#3 完成。但是如果指令#4 和指令#3 没有真正的联系呢?如果你真的不介意指令#3 和指令#4 的执行顺序,那会怎么样呢?
然后,您可以让“长时间运行指令”以异步方式执行,并提供指令#3 作为对它的回调,从而允许您更快地执行指令#4。图 3-2 显示了它的样子。
图 3-2。
An asynchronous execution flow
指令#4 不是等待它完成,而是在指令#2 启动异步“长时间运行指令”后立即执行。
这是异步编程潜在好处的一个非常简单的例子。可悲的是,就像这个数字世界中的大多数情况一样,没有什么是没有代价的,额外的好处也伴随着一个令人讨厌的交易:调试异步代码可能是一件真正令人头疼的事情。
开发人员被训练以他们编写代码的顺序方式来思考他们的代码,所以调试一个不是顺序的代码对新手来说可能是困难的。
例如,清单 3-1 和 3-2 分别显示了以同步和异步方式编写的同一段代码。
Listing 3-1. Synchronous Version of a Simple Read File Operation
console.log("About to read the file...")
var content = Fs.readFileSync("/path/to/file")
console.log("File content: ", content)
Listing 3-2. Asynchronous Version of a Simple File Read Operation with a Common Mistake
console.log("About to read the file...")
var content = ""
fs.readFile("/path/to/file", function(err, data) {
content = data
})
console.log("File content: ", content)
如果您还没有猜到,清单 3-2 将打印如下内容:
File content:
其原因与图 3-3 所示的图表直接相关。让我们用它来看看有问题的异步版本是怎么回事。
图 3-3。
The error from Listing 3-2
文件内容没有被写入的原因很清楚:回调是在最后一行console.log
之后执行的。这是新开发人员非常常见的错误,不仅是 Node.js,更具体地说是前端的 AJAX 调用。他们设置自己的代码,以便在异步调用实际结束之前使用异步调用返回的内容。
为了结束这个例子,清单 3-3 展示了需要如何编写代码才能正常工作。
Listing 3-3. Correct Version of the Asynchronous File Read Operation
console.log("About to read the file...")
var content = ""
fs.readFile("/path/to/file", function(err, data) {
content = data
console.log("File content: ", content)
})
很简单。您刚刚将最后一行console.log
移动到回调函数中,所以您确定content
变量设置正确。
异步高级
异步编程不仅仅是确保正确设置回调函数,它还允许一些有趣的流控制模式,可以用来提高应用的效率。
让我们来看看异步编程的两种截然不同且非常有用的控制流模式:并行流和串行流。
平行流
并行流背后的思想是,程序可以并行运行一组不相关的任务,但只在所有任务执行完毕后调用所提供的回调函数(以收集它们的集体输出)。
基本上,清单 3-4 显示了你想要的。
Listing 3-4. Signature of the Parallel Function
//functionX symbols are references to individual functions
parallel([function1, function2, function3, function4], function(data) {
///do something with the combined output once they all finished
})
为了知道数组中传递的每个函数何时完成执行,它们必须执行一个回调函数,并给出它们的操作结果。回调将是他们收到的唯一属性。清单 3-5 显示了并行函数。
Listing 3-5. Implementation of the Parallel Function
function parallel(funcs, callback) {
var results = [],
callsToCallback = 0
funcs.forEach(function(fn) { // iterate over all functions
setTimeout(fn(done), 200) // and call them with a 200 ms delay
})
function done(data) { // the functions will call this one when they finish and they’ll pass the results here
results.push(data)
if(++callsToCallback == funcs.length) {
callback(results)
}
}
}
清单 3-5 中的实现非常简单,但它完成了它的任务:它以并行的方式运行一组函数(您将看到,由于 Node.js 在单线程中运行,真正的并行是不可能的,所以这是您能得到的最接近的结果)。这种类型的控制流在处理对外部服务的调用时特别有用。
让我们看一个实际的例子。假设您的 API 需要执行几个操作,尽管这些操作彼此不相关,但都需要在用户看到结果之前发生。例如,从数据库加载图书列表,查询外部服务以获取本周新书的新闻,并将请求记录到文件中。如果您要执行一系列任务中的所有任务(参见清单 3-6 ),在下一个任务运行之前等待一个任务完成,那么用户很可能会遇到响应延迟,因为执行所需的总时间是所有单个时间的总和。
但是,如果您可以并行执行所有这些任务(参见清单 3-7 ,那么总时间实际上等于执行最慢的任务所花费的时间。??
让我们看看清单 3-6 和 3-7 中的两种情况。
Listing 3-6. Example of a Serial Flow (takes longer)
//request handling code...
//assume "db" is already initialized and provides an interface to the data base
db.query("books", {limit:1000, page: 1}, function(books) {
services.bookNews.getThisWeeksNews(function(news) {
services.logging.logRequest(request, function() { //nothing returned, but you need to call it so you know the logging finished
response.render({listOfBooks: books, bookNews: news})
})
})
})
Listing 3-7. Example of a Parallel Execution Flow
//request handling code...
parallel([
function(callback) { db.query("books", {limit: 1000, page: 1}, callback) }),
function(callback) { services.bookNews.getThisWeeksNews(callback) }),
function(callback) { services.logRequest(request, callback) })
], function(data) {
var books = findData(‘books’, data)
var news = findData(‘news’, data)
response.render({listOfBooks: books, bookNews: news})
})
清单 3-6 和 3-7 展示了每种方法的样子。findData
函数只是查看data
数组,并根据条目的结构返回所需的条目(第一个参数)。在parallel
的实现中,它是必需的,因为你不能确定函数以什么顺序完成,然后返回结果。
除了代码获得明显的速度提升之外,它还更容易阅读,更容易向并行流程添加新任务——只需向数组添加一个新项目。
串行流
串行流提供了方便地指定需要以特定顺序执行的功能列表的方法。这种解决方案不像并行流那样提供速度提升,但是它提供了编写这样的代码并保持其整洁的能力,远离了通常被称为意大利面条代码的东西。
清单 3-8 显示了你应该努力完成的任务。
Listing 3-8. Signature of the Serial Function
serial([
function1, function2, function3
], function(data) {
//do something with the combined results
})
清单 3-9 显示了你不应该做的事情。
Listing 3-9. Example of a Common Case of Nested Callbacks
function1(function(data1) {
function2(function(data2) {
function3(function(data3) {
//do something with all the output
}
}
}
您可以看到,如果函数的数量持续增长,清单 3-9 中的代码可能会失控。所以串行方法有助于保持代码的组织性和可读性。
让我们看看清单 3-10 中串行函数的可能实现。
Listing 3-10. Implementation of the Serial Function
function serial(functions, done) {
var fn = functions.shift() //get the first function off the list
var results = []
fn(next)
function next(result) {
results.push(result) //save the results to be passed into the final callback once you don’t have any more functions to execute.
var nextFn = functions.shift()
if (nextFn) nextFn(next)
else done(results)
}
}
这些函数还有更多的变化,比如使用一个错误参数来自动处理错误,或者限制并行流中同时执行的函数的数量。
总而言之,异步编程给实现 API 带来了很多好处。当处理外部服务时,并行工作流非常方便,这通常是任何 API 都会处理的;例如,数据库访问、其他 API、磁盘 I/O 等等。同时,串行工作流在实现像 Express.js 中间件这样的东西时也很有用。 4
对于一个在异步编程上蓬勃发展的全功能且经过测试的库,请查看 async.js. 5
异步输入输出
异步编程的一个具体案例与 Node.js 提供的一个非常有趣的特性有关:异步 I/O,这个特性与 Node.js 的内部架构高度相关(见图 3-4 )。我说过,Node.js 不提供多线程;它实际上与运行事件循环的单个线程一起工作。
图 3-4。
How the EventLoop orchestrates the execution of the code
简而言之,Node.js 的设计思想是 I/O 操作是每个操作的实际瓶颈,而不是处理能力;因此,Node 进程收到的每个请求都将在事件循环中工作,直到找到 I/O 操作。当这种情况发生时,回调被注册在一个单独的队列中,主程序的流程继续。一旦 I/O 操作完成,回调就会被触发,回调中的代码就会运行。
异步 I/O 与同步 I/O
最后,为了证明到目前为止我所说的一切都是正确的,并且 Node.js 使用异步 I/O 工作得最好,我做了一些非常简单的基准测试。我创建了一个简单的 API,它有两个端点:
/async
:在返回一个简单的 JSON 响应之前,异步读取一个 1.6MB 的文件。/sync
:在返回一个简单的 JSON 响应之前,同步读取一个 1.6MB 的文件。
两个端点做的完全一样;只是方式不同(见清单 3-11 )。这个想法是为了证明,即使在这样简单的代码中,当底层代码利用平台提供的异步 I/O 时,事件循环也可以更好地处理多个请求。
清单 3-11 是两个端点的代码;API 是用梵蒂冈 6编写的
Listing 3-11. Example of Two Endpoints Coded Using the Vatican.js Framework
//Async handler
var fs = require("fs")
module.exports = AsyncHdlr;
function AsyncHdlr(_model) { this.model = _model }
//@endpoint (url: /async method: get)
AsyncHdlr.prototype.index = function(req, res, next) {
fs.readFile(__dirname + "/../file.txt", function (err, content) {
res.send({
success: true
})
})
}
//Sync handler
var fs = require("fs")
module.exports = SyncHdlr;
function SyncHdlr(_model) { this.model = _model }
//@endpoint (url: /sync method:get)
SyncHdlr.prototype.index = function(req, res, next) {
var content = fs.readFileSync(__dirname + "/../file.txt")
res.send({
success: true
})
}
基准测试是使用 Apache 基准测试工具 7 完成的,使用了以下参数:
- 请求数:10 000
- 并发请求:100
结果如表 3-1 所示。
表 3-1。
Results from the Benchmark of the Two Endpoints Shown in Listing 3-11
| 同步端点 | 异步端点 | | --- | --- | | 每秒请求数:2411.28#/秒。每请求时间 41.472 毫秒每请求时间:0.415 毫秒传输速率:214.28 [KBps]已接收 | 每秒请求数:2960.79#/秒。每个请求的时间:33.775 毫秒每个请求的时间:0.338 毫秒传输速率:263.12 [KBps]已接收 |正如你在表 3-1 中看到的,即使是最简单的例子,在相同的时间内,异步代码处理的请求也比同步代码多 549 个。另一个有趣的项目是,每个请求在异步端点上几乎快了 8 毫秒;这可能不是一个很大的数字,但是考虑到您正在使用的代码的不存在的复杂性,这是非常相关的。
简单
Node.js(更确切地说是 JavaScript)并不是一种复杂的语言。它遵循了类似脚本语言(如 Ruby、Python 和 PHP)遵循的基本原则,但有所改变(就像所有其他语言一样)。Node.js 足够简单,任何开发人员都可以很快学会并开始编码,但它足够强大,几乎可以实现开发人员想做的任何事情。
尽管 JavaScript 是一种令人惊叹的语言,也是本书的重点,就像我已经说过并将继续说的那样:在编程方面没有灵丹妙药。多年来,JavaScript 获得了很大的吸引力,但它也吸引了很多讨厌它的人,他们有非常合理的理由:非标准的面向对象模型、this
关键字的奇怪用法、缺乏语言内置的功能(它有很多专用于实现其他语言内置的基本特性的库),等等。最后,每种工具都需要根据其优势来选择。正如您将要看到的,Node.js 是开发 API 的一个非常好的选择。
Node.js 为该语言增添了某种有用的味道,简化了开发人员开发后端代码的工作。它不仅添加了处理 I/O 所需的实用程序(出于明显的安全原因,前端 JavaScript 没有这些功能),而且还为每个 web 浏览器支持的所有不同风格的 JavaScript 提供了稳定性。这方面的一个例子是,只需几行代码就可以轻松地建立一个 web 服务器。让我们看看清单 3-12 中的内容。
Listing 3-12. Simple Example of a Web Server Written in Node.js
var http = require("http")
http.createServer(function(req, res) { //create the server
//request handler code here
});
http.listen(3000) //start it up on port 3000
JavaScript 还有一个优势,它是所有商业 web 浏览器的标准前端语言,这意味着如果你是一个有前端经验的 web 开发人员,你肯定会遇到 JavaScript。
这对从前端迁移到后端的开发人员来说更简单;既然语言基础没变,你只需要学习新的东西,换一个后端的心态。同时,这有助于公司更快地找到 Node.js 开发者。
记住所有这些,让我们看看 JavaScript 的一些主要特征,这些特征使它成为如此简单(但功能强大)的选项。
动态打字
动态类型是一个基本特性,在当今大多数通用语言中都存在,但是它的功能并没有因此而减弱。这个小特性允许开发者在声明变量时不必想太多;给它一个名字,然后继续前进。
清单 3-13 展示了一些你不能用静态类型语言做的事情。
Listing 3-13. Example of Code Taking Advantage Of Dynamic Typing
var a, b, tmp //declare the variables (just give them names)
//initialize them with different types
a = 10
b = "hello world"
//now swap the values
tmp = a
a = b //even with automatic casting, a language like C won’t be able to cast "hello world" into an integer value
b = tmp
console.log(a) //prints "hello world"
console.log(b) //prints 10
简化的面向对象编程
JavaScript 不是一种面向对象的语言,但它确实支持其中一些特性(参见清单 3-14 和清单 3-16 )。您将有足够多的概念来使用对象概念化问题和解决方案,这总是一种非常直观的思维方式,但同时,您没有处理像多态、接口或其他概念,这些概念尽管有助于构建代码,但已被证明在设计应用时是可有可无的。
Listing 3-14. Simplified Object Orientation Example
var myObject = { //JS object notation helps simplify definitions
myAttribute: "some value",
myMethod: function(param1, param2) {
//does something here
}
}
//And the just...
myObject.myMethod(...)
而对于其他语言,比如 Java(一种非常面向对象的语言),您必须做清单 3-15 中所示的事情。
Listing 3-15. Example of a Class Definition in Java
class myClass {
public string myAttribute;
public void myClass() {
}
public void myMethod(int param1, int param2) {
//does something here
}
}
//And then
myClass myObj = new myClass();
myObj.myMethod(...);
少了很多罗嗦,不是吗?
在清单 3-16 中,让我们看看另一个强大的面向对象的例子。
Listing 3-16. Another Example of the Features Provided by Object Orientation in JavaScript
var aDog = { //behave like a dog
makeNoise: function() {
console.log("woof!");
}
}
var aCat = { //behave like a cat
makeNoise: function() {
console.log("Meewww!");
}
}
var myAnimal = { //our main object
makeNoise: function() {
console.log("cri... cri....")
},
speak: function() {
this.makeNoise()
}
}
myAnimal.speak() //no change, so.. crickets!
myAnimal.speak.apply(aDog) //this will print "woof!"
//switch behavior
myAnimal.speak.apply(aCat) //this will now print "Meewww!"
您能够将一个简单的行为封装到一个对象中,并将其传递到另一个对象中,以自动覆盖其默认行为。这是语言中固有的东西。您不必编写任何特定的代码来实现这个特性。
原型遗传
与上一个相联系,原型继承特性允许在对象生命周期的任何时刻对其进行难以置信的简单扩展;强大而简单。
让我们看看清单 3-17 来更好地理解这一点。
Listing 3-17. Example of Prototypal Inheritance in JavaScript
var Parent = function() {
this.parentName = "Parent"
}
var Child = function() {
}
Child.prototype = new Parent()
var childObj = new Child();
console.log(childObj.parentName)
console.log(childObj.sayThanks) //that's undefined so far
Parent.prototype.sayThanks = function() { //you "teach" the method to the parent
console.log("Thanks!")
}
console.log(childObj.sayThanks()) //and booom! the child suddenly can say thanks now
你是否动态影响了父对象——然后子对象突然更新了?是的,刚刚发生了!强大?我会这么说!
函数式编程支持
JavaScript 不是函数式编程语言;不过话说回来,它确实支持它的一些特性(参见清单 3-18 、 3-19 和 3-20 ),比如拥有一级公民函数,允许你像传递参数一样传递它们,并且很容易返回闭包。这个特性使得使用回调成为可能,正如您已经看到的,回调是异步编程的基础。
让我们看看清单 3-18 中一个快速简单的函数式编程例子(记住,JavaScript 只提供了一些函数式编程的好东西,而不是全部)。创建一个加法函数。
Listing 3-18. Simple Example of an Adder Function Defined Using Functional Programming
function adder(x) {
return function(y) {
return x+y
}
}
var add10 = adder(10) //you create a new function that adds 10 to whatever you pass to it.
console.log(add10(100)) //will output 110
让我们看一个更复杂的例子,一个map
函数的实现,它允许你通过传递数组和转换函数来转换数组的值。让我们首先看看如何使用地图功能。
Listing 3-19. Example of a Map Function Being Used
map([1,2,3,4], function(x) { return x * 2 }) //will return [2,4,6, 8]
map(["h","e","l","l","o"], String.prototype.toUpperCase) //will return ["H","E","L","L","O"]
现在让我们来看一个使用函数方法的可能实现。
Listing 3-20. Implementation of a Map Function, Like the One Used in Listing 3-19
function reduce(list, fn, init) {
if(list.length == 0) return init
var value = list[0]
init.push(fn.apply(value, [value])) //this will allow us to get both the functions that receive the value as parameters and the methods that use it from it’s context (like toUpperCase)
return reduce(list.slice(1), fn, init) //iterate over the list using it’s tail (everything but the first element)
}
function map(list, fn) {
return reduce(list, fn, [])
}
鸭子打字
你听过这句话吗“如果它看起来像鸭子,游泳像鸭子,叫声像鸭子,那它很可能就是鸭子。”?那么,用 JavaScript 输入也是一样的。变量的类型由其内容和属性决定,而不是由固定值决定。所以同一个变量可以在脚本的生命周期中改变它的类型。鸭式打字既是一个非常强大的功能,同时也是一个危险的功能。
清单 3-21 提供了一个简单的演示。
Listing 3-21. Quick Example of Duck Typing in JavaScript
var foo = "bar"
console.log(typeof foo) //will output "string"
foo = 10
console.log(typeof foo) //this will now output "number"
对 JSON 的本机支持
这是一个棘手的问题,因为 JSON 实际上是从 JavaScript 衍生而来的,但是我们不要在这里纠缠于先有鸡还是先有蛋的问题。对现在使用的主要传输语言的本地支持是一大优势。
清单 3-22 是遵循 JSON 语法的一个简单例子。
Listing 3-22. Example of How JSON Is Natively Supported by JavaScript
var myJSONProfile = {
"first_name": "Fernando",
"last_name": "Doglio",
"current_age": 30,
"married": true,
"phone_numbers": [
{
"home_phone": "59881000293",
"cell_phone": "59823142242"
}
]
}
//And you can interact with that JSON without having to parse it or anything
console.log(myJSONProfile.first_name, myJSONProfile.last_name)
这一特殊功能在几种情况下特别有用;例如,当使用基于文档的存储解决方案(如 MongoDB)时,因为数据建模最终在两个地方(您的应用和数据库)都是本地的。此外,在开发 API 时,您已经看到目前选择的传输语言是 JSON,所以直接用原生符号格式化您的响应的能力(就此而言,您甚至可以只输出您的实体)对于易用性来说是一个非常大的优势。
这个列表还可以扩展,但是这些是 JavaScript 和 Node.js 带来的非常强大的特性,对开发人员没有太多要求。它们很容易理解和使用。
Note
提到的特性并不是 JavaScript 独有的;其他脚本语言也有一些。
npm:Node 包管理器
Node.js 的另一个优点是它惊人的包管理器。正如您现在可能知道的(或者即将发现的),Node 中的开发是非常依赖于模块的,这意味着您不会开发整个系统;很可能你会以模块的形式重用别人的代码。
这是 Node.js 的一个非常重要的方面,因为这种方法允许您专注于使您的应用独一无二的东西,并让通用代码无缝集成。您不必为 HTTP 连接重新编码库,也不必为每个项目重新编码路由处理程序(换句话说,您不必重新发明轮子);只需将项目的依赖项设置到package.json
文件中,使用最合适的模块名,然后 npm 会检查整个依赖树并安装所有需要的东西(想想 APT for Ubuntu 或 Homebrew for Mac)。
可用的活跃用户和模块数量(每月超过 100,000 个包和超过 6 亿次下载)确保您可以找到您需要的内容;在极少数情况下,如果您不知道,您可以通过将特定的模块上传到注册表来帮助下一个寻找它的开发人员。
这种模块数量也可能是一件坏事,因为如此大的数量意味着将有几个模块试图做同样的事情。(例如,email-validation、sane-email-validation、mailcover 和 mailgun-email-validation 都试图做同样的事情——使用不同的技术验证电子邮件地址;根据你的需要,你必须选择一个。)你必须浏览它们,找到最合适的候选人。
这一切都要归功于自 Node.js 于 2009 年上市以来形成的令人惊叹的开发人员社区。
要开始使用 npm,只需访问他们的网站 www.npmjs.org
。在那里,你会看到一个最近更新的软件包列表,让你开始,以及一些最受欢迎的。
图 3-5。
The npm site
如果想直接安装它,只需在您的 Linux 控制台中写入下面一行:
$ curl
https://www.npmjs.org/install.sh
您需要安装 Node.js 版才能正确使用它。完成后,您只需输入以下命令即可开始安装模块:
$ npm install [MODULE_NAME]
该命令将指定的模块下载到名为node_modules
的本地文件夹中;所以试着在你的项目文件夹中运行它。
您还可以使用 npm 开发自己的模块,并通过以下方式将它们发布到站点中:
$ npm publish #run this command from within your project’s folder
前面的命令从package.json
文件中获取属性,打包模块,并将所有内容上传到 npm 的注册表中。之后,您可以进入站点并检查您的包裹;它会列在那里。
Note
除了查看 www.npmjs.org
之外,你还可以查看 Google Groups nodejs8和 nodejs-dev 9 来直接联系 Node.js 社区中的人。
谁在用 Node.js?
这整本书旨在验证 Node.js 在开发 RESTful 系统方面有多好,并提供相关示例,同时验证让 Node.js 驱动的系统在生产中运行的想法有多有效(这是最难克服的障碍,尤其是在试图说服您的老板将堆栈转换为基于 Node.js 的堆栈时)。
但是有什么更好的验证呢,看看生产中 Node.js 的一些最重要的用户?
- PayPal:使用 Node.js 支持其网络应用。
- 易贝:使用 Node.js 主要是因为异步 I/O 带来的好处。
- LinkedIn:整个后端移动堆栈都是在 Node.js 中完成的。使用它的两个原因是与以前的堆栈相比获得的规模和性能。
- 网飞:在几个服务上使用 Node.js。经常在位于
http://techblog.netflix.com
的科技博客上写一些使用 Node.js 的经历。 - 雅虎!:在几个产品上使用 Node.js,比如 Flickr,My Yahoo!和主页)。
这个列表还可以继续下去,包括其他公司的一个非常大的列表,有些公司比其他公司更知名,但要点仍然是:Node.js 用于整个互联网的生产服务,它处理各种流量。
摘要
本章介绍了 Node.js 对于普通开发人员的优势,尤其是它的特性如何提高 I/O 密集型系统(如 API)的性能。
在下一章中,您将获得更多的实践机会,并了解在最后一章中用于开发 API 的基本架构和工具。
Footnotes 1
http://en.wikipedia.org/wiki/Node.js
见。
2
见 [http://en.wikipedia.org/wiki/V8_(JavaScript_engine
](http://en.wikipedia.org/wiki/V8_(JavaScript_engine) )
。
3
这是一个粗略的近似值,因为需要将并行功能增加的时间考虑在内,才能得到准确的数字。
4
http://expressjs.com/guide/using-middleware.html
见。
5
https://github.com/caolan/async
见。
6
7
http://httpd.apache.org/docs/2.2/programs/ab.html
见。
8
https://groups.google.com/forum/#!forum/nodejs
见。
9