十、Mach
一个系统省略某些异常的特征和改进,但反映一套设计思想,比一个包含许多好的但独立且不协调的思想的系统要好。—佛瑞德·P·布鲁克斯
Node.js web 服务器并不缺乏。快速/连接、Kraken 和风帆都是受欢迎的选择。Mach 在这个领域是一个相对年轻的项目,尽管它的前身 Strata.js 多年来一直拥有强大的追随者。Mach 是由前 Twitter 开发者迈克尔·杰克逊创建的,他有几个明确的原则:
- HTTP 请求被无缝地传递给 JavaScript 函数。
- 面向承诺的接口允许 HTTP 响应被异步延迟。HTTP 错误也可能通过承诺链传播。(参见第 14 章关于承诺如何运作的详细描述。)
- 请求和响应都可以利用 Node.js 流,因此大量数据可以以块的形式发送和接收。
- 可组合中间件很容易扩展 Mach 的核心能力。
选择使用哪种 web 服务器——实际上,任何库、框架或一般编程语言的选择——应该由项目的特定用例决定。尽管 Mach 可以为任何基于 web 的应用提供很多功能,但它也可以是 HTTP 客户端和代理,将请求路由到虚拟主机(如 Apache 和 nginx),并重写 URL(如 Apache 的 mode_rewrite 模块)。Mach 的功能相当于 Node.js 模块,但是它的一些特性也可以在浏览器中使用,这使得它的用例表面积更大。
章节示例
本章包含了一些可运行的例子,包括在本章的示例代码中。在适用的地方,代码清单引用它们相应的文件,在清单的顶部有一个注释,如清单 10-1 所示。
Listing 10-1. Not a Real Example
// example-000/no-such-file.js
console.log('this is not a real example');
本章中的大多数示例启动 Node.js web 服务器。除非另有说明,否则假设可以通过运行每个清单中提到的 JavaScript 文件来启动服务器。例如,清单 10-2 中的命令将运行index.js
文件,启动example-001
目录中的 Mach web 服务器。
Listing 10-2. Launching an Example Web Server
example-001$ node index.js
>> mach web server started on node 0.10.33
>> Listening on 0.0.0.0:8080, use CTRL+C to stop
装置
Mach 是一个 Node.js 模块,可以和 Node.js 包管理器npm
一起安装。本章中的例子也使用了 Q promise 库和一些其他的 npm 模块。所有这些都被列为示例代码的package.json
文件中的依赖项,因此只需在示例代码目录中运行npm install
就可以下载并安装每个模块:
code/mach$ npm install
Mach,网络服务器
Mach web 服务器从表面上看与许多其他 web 服务器相似,从成熟的模式和设计中获取灵感,只有当它能提供重要的东西时才重新发明轮子。在 Mach web 服务器中创建路由来处理 HTTP 请求是一个相当简单的过程,对于使用过其他面向 REST 的 web 服务器(如 Express (JavaScript)、Sinatra (Ruby)、Nancy(。NET),以此类推。
将 Mach 作为应用依赖项导入后,通过调用mach.stack()
创建 Mach 应用栈。这是处理 HTTP 请求的第一步。清单 10-3 将堆栈分配给app
变量。
Listing 10-3. Creating the Application Stack
// example-001/index.js
'use strict';
var mach = require('mach');
// ... load other modules ...
// create a stack
var app = mach.stack();
// ...
每个 Mach 应用被称为“堆栈”,因为每个 HTTP 连接都将穿过中间件层——可组合功能的一小部分——这些层可能会在请求被传递到路由之前处理请求,在响应被传递到调用客户端之前处理响应。中间件还可能产生对 web 服务器环境很重要的其他副作用。
Mach 本身带有通用 web 服务器中间件,这将在下一节中介绍。清单 10-4 中的 web 服务器使用一个中间件mach.logger
,在 web 服务器接收请求时将 HTTP 诊断信息打印到终端。
Listing 10-4. Adding Middleware to the Application Stack
// example-001/index.js
'use strict';
var mach = require('mach');
// ... load other modules ...
// create a stack
var app = mach.stack();
// add some middleware
app.use(mach.logger);
// ...
路由只是一个与特定 HTTP 方法和 URL 模式配对的函数,当服务器收到 HTTP 请求时,它将处理这些请求。路由通常是在中间件之后最后添加到应用堆栈中的,因此中间件有机会在路由有机会与每个路由交互之前解析请求信息,并在路由有机会与每个路由交互之后操纵响应信息。
应用堆栈公开映射到标准 HTTP 请求方法的函数方法,如清单 10-5 所示。通过调用适当的方法,后跟 URL 模式和路由回调,将路由附加到堆栈。当一个路由与一个传入的请求匹配时,它将接收一个连接(通常缩写为conn
),通过该连接它可以响应请求。
Listing 10-5. Adding HTTP Routes to the Application Stack
// example-001/index.js
// add some routes
app.get('/book', function (conn) {/*...*/});
app.get('/book/:id', function (conn) {/*...*/});
app.delete('/book/:id', function (conn) {/*...*/});
app.post('/book', function (conn) {/*...*/});
app.put('/book/:id', function (conn) {/*...*/});
app.get('/author', function (conn) {/*...*/});
app.get('/library', function (conn) {/*...*/});
app.get('/', function (conn) {/*...*/});
// ...
当所有的中间件和路由都连接到应用栈时,web 服务器就可以监听请求了。将应用堆栈传递给Mach.serve()
会创建一个 HTTP 侦听器,以默认的 HTTP 方案、主机和端口:http://localhost:5000
为请求提供服务。可以添加其他选项来更改此默认行为。在清单 10-6 中,一个新的端口号(8080)作为第二个参数传递给Mach.serve()
,以强制 HTTP 侦听器服务该端口。
Listing 10-6. Serving Up the Application Stack on Port 8080
// example-001/index.js
// serve the stack on a port
mach.serve(app, 8080);
// or mach.serve(app, {port: 8080});
如果需要更多选项,它们可以作为选项散列传递给Mach.serve()
。表 10-1 中描述了键和值。为方便起见,本章中的示例将使用端口号的简写形式。
表 10-1。
Mach Server Options
| 选项属性 | 描述 |
| --- | --- |
| host
| 仅监听到该主机名的连接。默认情况下没有限制。 |
| port
| 侦听此端口上的连接。默认为 5000。 |
| socket
| 通过 Unix 套接字监听连接。主机和端口被忽略。 |
| quiet
| true
抑制启动和关闭消息。默认为false
。 |
| timeout
| 接收 SIGINT 或 SIGTERM 信号后,强制关闭连接并终止之前等待的时间。 |
| key
| SSL 连接的私钥(HTTPS)。 |
| cert
| SSL 连接的公共 X509 证书(HTTPS)。 |
HTTP 路由
Mach routes 可以处理来自最常见的 HTTP 方法的请求,甚至一些不常见的方法:
- 得到
- 邮政
- 放
- 删除
- 头
- 选择
- 微量
清单 10-7 中的 HTTP GET 路由在一个假数据库中查找所有书籍,并将记录作为一个 JSON 对象数组发送给客户机。
Listing 10-7. Anatomy of a Route
// example-001/index.js
app.get('/book', function (conn) {
/*
* 1\. Routes return promises. Q can adapt the callback-
* driven database module so that its result (or error)
* is passed through a promise chain. The makeNodeResolver()
* method will provide a callback to feed the deferred.
*/
var deferred = Q.defer();
db.books.all(deferred.makeNodeResolver());
/*
* 2\. Adding handlers to the promise chain by calling
* promise.then()
*/
return deferred.promise.then(function (books) {
/*
* 3\. The Connection.json() method returns a promise.
* The HTTP status code will be sent as an HTTP header
* in the response, and the array of books will be
* serialized as JSON.
*/
return conn.json(200, books);
}, function (err) {
/*
* 4\. An HTTP 500 will be delivered to the client on
* error. The error’s message will be used in the
* serialized JSON response.
*/
return conn.json(500, {error: err.message});
});
});
在这条路线中发生的几件事对几乎所有创建的路线都是共同的。
首先,创建一个 deferred,它将最终生成一个从路由返回的承诺。(参见第 14 章关于承诺如何运作的详细解释,特别是价值和错误如何沿着承诺链传递。)这里 Q promise 库创建一个 deferred,然后用makeNodeResolver()
创建一个特殊的回调。这个回调直接传递给database.books.all()
方法,并将生成的任何值或错误提供给承诺链。
其次,两个处理程序被附加到 deferred 的承诺上:一个处理程序接收需要返回给客户机的图书数据,另一个处理程序在记录获取失败时接收来自数据库的任何错误。
第三,每个处理程序通过调用带有 HTTP 状态和有效负载的conn.json()
,将各自的数据转换成 HTTP 响应。这个方法是语法糖,封装了conn.send()
方法(稍后将详细介绍),设置适当的Content-Type
头,序列化 JSON 对象,并返回一个要沿承诺链传递的承诺。当这个承诺被解析时,实际的 HTTP 响应将被发送。
在终端会话中,curl
HTTP 工具可以向/book
路由发出 HTTP GET 请求。响应正文包含 JSON 格式的序列化图书数据:
example-001$ curl -X GET
http://localhost:8080/book
[{"id":1,"title":"God Emperor of Dune","author":"Frank Herbert"... }]
在运行 Mach 服务器的终端会话中,mach.logger
中间件将GET /book
的请求细节写入标准输出:
example-001$ node index.js
>> mach web server started on node 0.12.0
>> Listening on :::8080, use CTRL+C to stop
::1 - - [17/Mar/2015 19:58:07] "GET /book HTTP/1.1" 200 - 0.002
URL 参数
URL 参数是表示应用数据(如唯一标识符)的 URL 路径段。常见的是用类似于/<entity-type>/<entity-id>/<entity-particular>
的模式编写 REST URLs。清单 10-8 中的代码定义了通过标识符获取特定书籍的路径。实际参数:id
由冒号前缀标识。一个路由可以有任意数量的参数,但是每个参数必须有一个唯一的名称,并且必须是一个完整的 URL 段。
参数将作为属性在conn.params
对象上对路线可用。每个属性名将是不带冒号前缀的 URL 参数名。Mach 也将所有属性值解析为字符串。因为 id 在数据库中是数字,所以这个参数在被数据库查询使用之前使用Number
函数进行转换。
Listing 10-8. REST Route with a Single URL Parameter
// example-001/index.js
app.get('/book/:id', function (conn) {
var id = Number(conn.params.id);
var deferred = Q.defer();
db.book.findByID(id, deferred.makeNodeResolver());
return deferred.promise.then(function (book) {
if (!book) {
return conn.json(404);
}
return conn.json(200, book);
}, function (err) {
return conn.json(500, {error: err.message});
});
});
与清单 10-8 中的一般/book
路径不同,该路径搜索数据库中可能存在也可能不存在的特定实体。如果数据库操作成功,但是获取的book
对象是null
,则不存在该 ID 的记录,并且路由解析为一个空的 HTTP 404 响应。
查询字符串和请求正文
当 Mach 自动解析 URL 参数并使它们在conn.params
对象上可用时,必须调用getParams()
方法来解析查询字符串和请求体。因为请求体是流式的,所以默认情况下不会自动执行解析。由开发人员决定是否以及何时进行解析。(如果这听起来很乏味,不要担心:稍后介绍的params
中间件可以自动化这个过程。)
在清单 10-9 中,/author
路径接受一个查询参数genre
,然后传递一个 JSON 数组,该数组中包含了该流派书籍的作者。连接的getParams()
方法返回一个承诺,将经过解析的params
对象传递给解析回调。params
对象的每个属性都将是来自 URL、查询字符串或请求体的命名参数。
Listing 10-9. Extracting Values from a Query String
// example-001/index.js
app.get('/author', function (conn) {
return conn.getParams().then(function (params) {
var deferred = Q.defer();
db.author.findByGenre(params.genre, deferred.makeNodeResolver());
return deferred.promise.then(function (authors) {
return conn.json(200, authors);
}, function (err) {
return conn.json(500, {error: err.message});
});
});
});
清单 10-10 中的curl
命令向服务器发送恐怖流派参数,响应包含一个作者记录,在genres
数组中有一个匹配条目。
Listing 10-10. Using cURL to Send a Request with a Query String
example-001$ curl -X GET
http://localhost:8080/author?genre=Horror
[{"id":6,"name":"Dan Simmons","website":"
http://www.dansimmons.com/
T2】
getParams()
方法还有另外两个有用的特性。它接受单个对象参数,其中键表示要解析的白名单参数,值表示每个参数的解析函数。当在清单 10-11 中解析请求主体时,白名单中没有指定的任何主体参数都将被忽略。原始 JavaScript 函数String
、Number
和Date
都解析字符串并返回反序列化的对象。当params
对象被传递给 promise 的解析回调时,每个属性都将被正确地输入。自定义函数也可以用来反序列化带有专有数据格式的请求体参数。
一旦解析了参数,就会在数据库中创建一个新的 book 记录,然后序列化并在响应体中返回给客户机。
Listing 10-11. Extracting Values from a Request Body
// example-001/index.js
app.post('/book', function (conn) {
return conn.getParams({
title: String,
author: String,
seriesTitle: String,
seriesPosition: Number,
publisher: String,
publicationDate: Date
}).then(function (params) {
var book = Book.fromParams(params);
var deferred = Q.defer();
db.book.save(book, deferred.makeNodeResolver());
return deferred.promise.then(function (result) {
return conn.json(result.isNew ? 201 : 200, book);
}, function (err) {
return conn.json(500, {error: err.message});
});
});
});
Mach 可以反序列化 URL 编码、多部分和 JSON 格式的请求体。对于其他格式,可以添加定制中间件,在请求体到达路由处理器之前对其进行反序列化,或者可以在conn.request.content
访问原始请求体流。
清单 10-12 显示了两个以 URL 编码和 JSON 格式发布新书数据的curl
命令,以及每个 HTTP 响应生成的输出。
Listing 10-12. Sending a POST Request Body with cURL
example-001$ curl -X POST``http://localhost:8080/book
T2】
-H "Content-Type: application/x-www-form-urlencoded" \
-d "title=Leviathan%20Wakes&author=James%20S.A.%20Corey&publisher=Orbit&publicationDate=2011-06-15T05%3A00%3A00.000Z"
{"id":10,"title":"Leviathan Wakes","author":"James S.A. Corey","publisher":"Orbit"...}
example-001$ curl -X POST``http://localhost:8080/book
T2】
-H "Content-Type: application/json" \
-d @new-book.json
{"id":11,"title":"Ready Player One","author":"Ernest Cline","publisher":"Random House NY"...}
当来自不同来源的参数(即 URL 参数、查询字符串参数、主体参数)具有相同的名称时,将应用以下解决方案:
URL parameters always take precedence over query string and request body parameters. Query string parameters take precedence over request body parameters. Nonconflicting request body parameters are included.
发送响应
到目前为止,这些路由只提供了 JSON 响应,但是 Mach 可以将任何有效的 HTTP 响应内容传输到客户机。
在Connection
对象上最低级的响应方法是Connection.send()
。该方法接受 HTTP 状态代码和流、缓冲区或字符串,以便在响应正文中传递。在Connection
对象上的许多其他响应方法(如json()
或html()
)仅仅是通过在调用send()
之前添加适当的头来操纵响应的门面。
表 10-2 显示了每个 Mach 响应方法,通常传递给每个方法的内容类型,以及每个方法用于各种 HTTP 响应头的默认值(如果有的话)。除了back()
之外,HTTP 状态代码可以被指定为每个方法的第一个参数,后面是响应主体内容。虽然状态代码是一个可选参数,但本章中的示例总是显式设置它。
表 10-2。
Mach Response Methods
| 方法 | 有效载荷 | 回应标题默认值 |
| --- | --- | --- |
| Connection.send()
| 流、缓冲区或字符串 | (无) |
| Connection.redirect()
| 位置 | 302 Redirect Location:
|
| Connection.back()
| 位置 | 302 Redirect Location:
|
| Connection.text()
| 文本字符串 | Content-Type: text/plain
|
| Connection.html()
| HTML 字符串 | Content-Type: text/html
|
| Connection.json()
| JSON 对象或字符串 | Content-Type: application/json
|
| Connection.file()
| 文件内容(流、缓冲区、字符串或路径) | 如果文件扩展名可以确定适当的 MIME 类型,则设置Content-Type
。如果一个指定的大小在一个选项散列中被传递给file()
,或者如果有效负载是一个 can Node.js 可以解析并统计以确定文件大小的文件路径,则Content-Length
被设置。 |
redirect()
和back()
方法不传递响应体,而是操纵响应中的Location
头,将客户端定向到另一个页面。file()
方法接受文件内容(以流、缓冲区或字符串的形式)或文件路径,然后读入流中,并将文件内容传递给客户机。
也许 web 服务器对 web 浏览器最常见的响应是 HTML 响应。然而,HTML 页面很少再作为完整的文件存储;开发人员将 HTML 分解成可重用的组件,将标记与模板语言混合,并将模板绑定到动态数据以创建有效的 HTML。
在清单 10-13 中,swig 模板库将两个 swig 模板编译成函数library()
(显示用户的图书库)和err500()
(显示任何服务器错误)。当 route 处理一个传入的请求时,它从数据库加载图书数据,并使用library()
函数将该数据绑定到library.swig
模板。这会产生一个有效的 HTML 字符串,然后作为响应体传递给conn.html()
。如果在此过程中出现错误,err500()
功能会对错误模板和错误消息执行相同的操作。
Listing 10-13. Sending an HTML Response
// example-001/index.js
var swig = require('swig');
// ...
var library = swig.compileFile('./library.swig');
var err500 = swig.compileFile('./err500.swig');
app.get('/library', function (conn) {
var deferred = Q.defer();
db.book.all(deferred.makeNodeResolver());
return deferred.promise.then(function (books) {
return conn.html(200, library({books: books}));
}, function (err) {
return conn.html(500, err500({err: err.message}));
});
});
在清单 10-13 中使用conn.html()
而不是conn.send()
的好处纯粹是为了方便,因为html()
会自动设置适当的Content-Type: text/html
标题。conn.text()
方法同样适用于text/plain
内容类型。
对于 Mach 不换行的内容类型,可以在调用conn.send()
之前手动设置头。例如,清单 10-14 中的路由通过在路由返回承诺之前在连接的响应上显式设置一个Content-Type: application/xml
头,将库数据作为 XML 而不是 HTML 来传递。然后,图书数据在发送到客户机之前被序列化为 XML 字符串。
Listing 10-14. Setting the Content-Type
Header Manually
// example-001/index.js
var xmlify = require('./xmlify');
// ...
app.get('/library.xml', function (conn) {
var deferred = Q.defer();
db.book.all(deferred.makeNodeResolver());
conn.response.setHeader('Content-Type', 'application/xml');
return deferred.promise.then(function (books) {
return conn.send(200, xmlify('books', data));
}, function (err) {
return conn.send(500, xmlify('err', err.message));
});
});
并非所有响应方法都发送内容。conn.redirect()
方法将向 HTTP 客户端发送一个Location
头,并附带一个它应该跟随的 URL,大概是因为请求的内容在给定的路由上不再可用。相比之下,conn.back()
方法只是将客户端引导回它的引用者。如果请求的Referer
头为空(例如,用户直接在浏览器的 URL 栏中键入地址),可选的 URL 参数作为后备。
清单 10-15 显示了从 web 应用的根到/library
路由的简单重定向。
Listing 10-15. Sending a Redirect Response
// example-001/index.js
// ...
app.get('/', function (conn) {
return conn.redirect('/library');
});
建立联系
到目前为止,显然Connection
对象是所有与客户端通信的中心。它保存了关于每个 HTTP 请求和响应的技术细节,并为中间件和路由提供了与 HTTP 响应进行交互和操作的方法。
一个Connection
对象有几个对中间件和路由都重要的属性:
location
request
response
位置
属性包含关于连接请求的 URL 目标的信息。表 10-3 显示了它包含的属性和数据。
表 10-3。
Connection Location Data
| 位置属性 | 描述 | 例子 |
| --- | --- | --- |
| href
| 完整的 URL。 | http://user:pass@webapp.com:8080/admin/dashboard.html#news?showWelcome=1
|
| protocol
| 带有尾随冒号的 URL 方案。 | http:
,https:
|
| auth
| URL 身份验证凭据(如果提供)。 | user:pass
|
| host
| 完整的 URL 主机,包括任何非标准端口号(例如,不是 80 或 443)。 | webapp.com:8080
|
| hostname
| 主机名称 URL。 | webapp.com
|
| port
| URL 主机端口。 | 8080
|
| pathname
| 没有查询字符串的 URL 路径。 | /admin/dashboard.html#news
|
| search
| 带问号前缀的 URL 查询字符串。 | ?showWelcome=1
|
| queryString
| 不带问号前缀的 URL 查询字符串。 | showWelcome=1
|
| query
| 解析为对象哈希的 URL 查询字符串。 | {showWelcome: 1}
|
如果 location 对象的 API 看起来很熟悉,那是因为 Mach 从现代 web 浏览器中的window.location
对象获得了一些灵感。
一些Connection
属性作为组合Location
和标题数据的有用外观,如表 10-4 所示。
表 10-4。
Location Property Facades on the Connection
Object
| 连接属性 | 描述 | 例子 |
| --- | --- | --- |
| path
| Location.pathname
+ Location.search
。 | /admin/dashboard.html#news?showWelcome=1
|
| auth
| 授权头的值,或Location.auth
。 | user:pass
|
| isSSL
| true
如果Location.protocol
是“https:”,否则false
。 | true
|
请求和响应消息
连接公开了request
和response
属性,它们都是Message
的实例,后者是封装 HTTP 消息管道的内部 Mach 类型。
邮件标题
清单 10-14 中的例子说明了如何使用conn.response.setHeader()
在响应消息上设置一个单独的头。响应消息还公开了一个执行与Message.setHeader()
相同功能的addHeader()
方法,但是有一个警告。如果设置了头,它将覆盖以前任何同名的头键/值对。如果添加了一个头,Mach 认为应该将它附加到任何预先存在的同名头上,从而有效地创建一个多值头。
要获取一个特定的标题,用所需的标题名调用Message.getHeader()
。如果消息中存在标头,将返回该值。
可以通过Message.headers
属性对标题进行整体操作,该属性获取并设置内部标题散列,其关键字是标题名称(如Content-Type
)以及相关的标题值。
消息 Cookies
HTTP 请求和响应在 HTTP 服务器之间传递 cookie 值。这些 cookies 是分别存储在Cookie
和Set-Cookie
头中的键/值对。Mach 消息解析这些 cookie 值,并通过Message.cookies
属性将它们公开为一个对象散列,而Message.getCookie()
和Message.setCookie()
方法的行为类似于它们的面向头的对应物。
消息内容
请求和响应主体作为流存在于每个对象的Message.content
属性中。这些流可以通过其他转换流传输,或者在每个Message
对象上被完全替换。如果在设置content
属性值时使用字符串而不是流,它将自动转换为流。
几个Message
方法提供了对其内容流的可选访问。Message.bufferContent()
方法将把流读入内存缓冲区,并返回一个结果承诺。当承诺解析时,缓冲区将可供调用代码使用。可以传递一个可选的length
参数来限制读入缓冲区的数据量。如果超过实际缓冲区长度,承诺将失败。当消费代码需要将请求或响应主体作为一个整体来处理时,这种方法很有用。如果一个Message
已经被缓冲,它的isBuffered
属性将返回true
。
Message.stringifyContent()
方法返回内容字符串值的承诺。可以提供可选的长度和编码参数来限制转换的数据量,并对其进行适当的编码。像Message.bufferContent()
一样,如果提供了一个最大长度,字符串超过了那个长度,承诺就失效了。
Connection.getParams()
方法在幕后调用Message.parseContent()
方法,但是这个方法也可能被直接调用,如果需要的话,可能在中间件中调用。它根据媒体类型(例如,URL 编码的)对消息内容应用适当的解析器,并返回解析结果字符串的承诺。它还接受一个最大的length
参数。
通用中间件
Mach 与许多通用中间件模块捆绑在一起,这些模块封装了相当标准的 web 服务器功能,尽管 web 服务器可以不使用任何一个中间件而运行。它们都是可选的,可以根据需要添加。
本章中的每个例子都使用了mach.logger
中间件在 Mach web 服务器运行时向终端写入 HTTP 请求/响应输出。清单 10-16 展示了通过将中间件传递给app.use()
方法,将它附加到应用堆栈上。
Listing 10-16. mach.logger Middleware
// example-002/index.js
// add some middleware
app.use(mach.logger);
// add some routes...
在引擎盖下,中间件只是带有特定签名的功能。稍后将深入研究这个概念,但是一般来说,app.use()
方法将首先接受中间件函数,然后是可选的配置参数。
中间件添加到 Mach 应用的顺序很重要,因为每个中间件都可能修改请求和响应。有些中间件,比如Mach.file
,可能会阻止连接到达其他中间件或路由处理器。
当 web 服务器收到请求时,它会以上游方式通过中间件传递请求。每个中间件依次处理请求,传递请求,直到链被一个中间件终止,被一个路由处理,或者如果不能被正确处理就产生一个错误。然而,一旦处理了请求,连接就通过中间件链传递回下游,给每个中间件一个评估响应的机会。图 10-1 中的图表粗略地说明了中间件是如何相对于请求和响应流进行评估的。
图 10-1。
Order in which Mach middleware evaluates requests and responses
随着更多中间件添加到示例中,中间件顺序对应用的影响将变得更加明显。
这是什么样的内容?
Mach.contentType
和Mach.charset
中间件是两个非常简单的函数,如果Content-Type
响应头完全丢失,或者没有指定charset
值,它们会自动调整响应头。如果路由使用Message.send()
提供同质内容(比如 XML 数据),这些就很有用。可以在中间件中指定一个全局覆盖,而不是操纵每个路由中的Content-Type
头。这两个中间件都被添加到清单 10-17 中的应用堆栈中。
Listing 10-17. Setting Default Header Values with Mach.contentType
and Mach.charset
// example-002/index.js
// ...
app.use(mach.charset);
app.use(mach.contentType, 'application/xml');
// ...
默认情况下,Mach.charset
使用utf-8
编码,这足以满足大多数目的。可以用传递给app.use()
的第二个字符串参数来指定另一种编码。默认情况下,Mach.contentType
将使用text/html
,但在这种情况下,替代值application/xml
被指定。
如上所述,中间件添加到应用堆栈的顺序很重要。在这种情况下,Mach.charset
被添加在Mach.contentType
之前,考虑到一个charset
值是Content-Type
头值的一部分(这意味着需要首先设置头值),这看起来似乎是违反直觉的。不过,回想一下,响应是以“下游”方向通过中间件的。由于在路由向响应中添加内容之前,无法确定响应的内容类型和字符集,所以这些中间件将以相反的顺序执行它们的工作。
清单 10-18 中的curl
命令显示了一个简单的路由,它从磁盘流式传输一个 XML 文件,而没有在路由中指定一个Content-Type
头。冗长的curl
请求输出显示Content-Type
头已经用 Mach 中间件指定的默认内容类型和字符集进行了设置。
Listing 10-18. Automatically Setting XML Content Headers
// example-002/index.js
// ...
app.use(mach.charset);
app.use(mach.contentType, 'application/xml');
var insultsFilePath = path.join(__dirname, 'insults.xml');
app.get('/insults', function (conn) {
conn.send(200, fs.createReadStream(insultsFilePath));
});
example-002$ curl -v -X GET
http://localhost:8080/insults
* Hostname was NOT found in DNS cache
* Trying ::1...
* Connected to localhost (::1) port 8080 (#0)
> GET /insults HTTP/1.1
> User-Agent: curl/7.37.1
> Host: localhost:8080
> Accept: */*
>
< HTTP/1.1 200 OK
<
Content-Type: application/xml;charset=utf-8
< Date: Sat, 28 Mar 2015 18:05:13 GMT
< Connection: keep-alive
< Transfer-Encoding: chunked
<
<?xml version="1.0" encoding="UTF-8"?>
<insults source="The Curse of Monkey Island">
<insult value="Throughout the Caribbean my great deeds are celebrated!">
<reply>Too bad they’re all fabricated.</reply>
</insult>
<insult value="Coming face to face with me must leave you petrified.">
<reply>Is that your face? I thought it was your backside!</reply>
</insult>
<insult value="I can’t tell which of my traits has you the most intimidated.">
<reply>Your odor alone makes me aggravated, agitated, and infuriated!</reply>
</insult>
</insults>
我的王国换一个文件
Mach.file
中间件从磁盘上的物理目录中提供静态文件(如.html
、.css
、.js
)。当请求进入应用堆栈管道时,Mach.file
试图将请求 URL pathname
匹配到磁盘上的路径,如果匹配成功,Mach.file
将静态文件内容传输到连接响应。如果没有与请求路径匹配的静态文件,连接将被传递到下一个中间件(或路由)。
使用Mach.file
中间件只是将中间件功能附加到应用堆栈上,并指定静态文件内容将被提供的目录。在清单 10-19 中,一个选项散列作为第二个参数传递给app.use()
。该对象包含用于配置Mach.file
的几个选项,包括必需的root
目录选项。在这个例子中指定了example-003/public
目录。
Listing 10-19. Mach.file Middleware
// example-003/index.js
var path = require('path');
// ...
var publicDir = path.join(__dirname, 'public');
app.use(mach.file, {
root: publicDir
// ...other options...
});
// routes
Tip
因为root
是Mach.file
唯一需要的选项,所以目录路径字符串可以作为app.use()
的第二个参数,代替选项 hash。
清单 10-20 中的目录树显示了将从example-003/public
目录提供的静态内容。Mach 将该目录视为 web 服务器根目录的一部分,因此静态文件和目录都将拥有相对于/
(例如http://localhost:8080/styles/index.css
)的 URL。
Listing 10-20. Content of the Public Directory
■t0]
◆θ★★★★★★★★★★★★★★★★★★★★★★
◆θ★★★★★★★★★★★★★★★★★★★★★★
◆θ★★★★★★★★★★★★★★★★★★★★★★
◆θ★★★★★★★★★★★★★★★★★★★★★★
★★★★★★★★★★★★★★
■t0]
■t0]
★★★★★★★★★★★★★★
ε──t0″
ε──t0″
由于静态文件内容是只读的,Mach.file
将只服务于带有 HTTP 方法 GET 和 HEAD 的请求。它还会拒绝试图访问指定静态文件目录之外的文件路径的请求,向客户端发回一个403 Forbidden
响应。
启动 web 服务器后,浏览器可能会指向http://localhost:8080/index.html
。Mach 将服务于静态的index.html
页面及其素材,所有这些都在图 10-2 中进行了精彩的描述。
图 10-2。
Serving a static HTML page with Mach.file
您可能已经注意到,index.html
文件被显式包含在 URL 中。通常的做法是将一个index.html
(或者一些等价的“默认”.html
文件)映射到 web 服务器根目录,或者一些其他嵌套的目录路径。然而,如果从 URL 中删除了文件名,Mach.file
中间件将生成一个404 Not Found
响应。为了改变这种行为并自动提供索引文件,可以将一个index
属性添加到选项散列中。如果该属性为“truthy”,那么Mach.file
将自动在 URL 路径的任何终止段搜索index.html
文件,包括应用根。如果需要更细粒度的控制,该属性还可能包含要搜索的文件名数组,并按数组顺序排列优先级。清单 10-21 显示了该属性及其可能的值。
Listing 10-21. The Mach.file index
Option Searches for Index Files at Directory Paths
// example-003/index.js
// ...
app.use(mach.file, {
root: publicDir,
index: true
//or, index: ['index.html', 'index.htm', ...]
});
添加了index
选项并重启 web 服务器后,访问http://localhost:8080
将自动向浏览器提供index.html
文件。
Mach.file
中间件还可以为没有索引文件的目录生成目录列表。清单 10-22 中的autoIndex
选项激活该功能。
Listing 10-22. The Mach.file autoIndex
Option Creates a Directory Listing for Directories Without Index Files
// example-003/index.js
// ...
app.use(mach.file, {
root: publicDir,
autoIndex: true
});
浏览到http://localhost:8080/images
显示所有图像的目录列表、它们的大小、MIME 类型和最后修改的时间戳,如图 10-3 所示。
图 10-3。
Auto-indexing the images
directory
每个图像文件名都是指向图像本身的超链接,而Parent Directory
超链接将浏览器定向到父 URL 段,在本例中是网站根目录。如果同时使用了index
和autoIndex
选项,任何带有index
选项的索引页优先显示,而不是目录列表。
闭嘴
现代浏览器通过在每个请求中发布一个Accept-Encoding: gzip
头,自动向 web 服务器请求压缩资源。压缩可以显著减小响应大小,提高满足每个 HTTP 请求所需的速度和带宽。作为交换,服务器支付少量压缩成本,浏览器支付解压缩成本。
Mach 的gzip
中间件自动压缩任何带有以下Content-Type
头的响应:
text/*
application/javascript
application/json
响应体通过 Node.js zlib
模块压缩,压缩后的响应设置了以下头:
Content-Encoding: gzip
Content-Length: [compressed content length]
Vary: Accept-Encoding
Tip
Vary
头告诉任何中间 HTTP 缓存,这个响应的变化应该根据特定的头进行缓存,在本例中是Accept-Encoding
。如果浏览器 A 请求未压缩的响应,而浏览器 B 请求同一 URL 的压缩响应,HTTP 缓存将存储这两个响应,而不是只存储第一个响应。
清单 10-23 中的代码在设置静态文件服务器之前介绍了Mach.gzip
中间件。当一个响应向上游传播时,Mach.gzip
将评估请求头以查看Accept-Encoding: gzip
是否存在,然后评估响应头以确定Content-Type
是否是压缩的候选者。如果两个条件都为真,响应体将被压缩,如清单 10-23 中的curl
请求所示。
Listing 10-23. Mach.gzip
Compresses Response Bodies
// example-004/index.js
// ...
app.use(mach.gzip);
var publicDir = path.join(__dirname, 'public');
app.use(mach.file, {
root: publicDir,
index: true
});
example-004$ curl -X GET -H "Accept-Encoding: gzip" -v
http://localhost:8080/index.html
* Hostname was NOT found in DNS cache
* Trying ::1...
* Connected to localhost (::1) port 8080 (#0)
> GET /index.html HTTP/1.1
> User-Agent: curl/7.37.1
> Host: localhost:8080
> Accept: */*
> Accept-Encoding: gzip
>
< HTTP/1.1 200 OK
< Content-Type: text/html
< Last-Modified: Tue, 31 Mar 2015 13:52:50 GMT
< Content-Encoding: gzip
< Vary: Accept-Encoding
< Date: Tue, 31 Mar 2015 14:14:09 GMT
< Connection: keep-alive
< Transfer-Encoding: chunked
用于对压缩算法(压缩级别、内存消耗、压缩策略等)进行精细控制。),当附加了Mach.gzip
中间件时,可以将zlib
选项对象传递给应用堆栈。每个选项的技术细节超出了本章的范围。更多细节请参考 Node.js zlib
文档。
看看那身体
早些时候,在清单 10-11 中,Connection.getParams()
方法用于从查询字符串和 POST 请求体中解析和提取数据。然而,在单独的路线中执行这个步骤会很快变得乏味。Mach.params
中间件解除了开发人员的这一责任,自动解析查询字符串和请求主体数据,在连接被传递到路由之前将数据附加到Connection.params
(URL 参数数据所在的位置)。
在清单 10-24 中,当数据被发送到路由时,发送主体参数被附加到conn.params
对象上。然后,该对象被添加为数据库记录。来自curl
命令的输出显示Mach.params
中间件按照预期执行。
Listing 10-24. Parsing a Request Body Automatically with Mach.params
// example-005/index.js
// ...
// Mach.params
app.use(mach.params);
app.post('/hero', function (conn) {
var deferred = Q.defer();
db.hero.save(conn.params, deferred.makeNodeResolver());
return deferred.promise.then(function (result) {
return conn.json(201, result);
}, function (err) {
return conn.json(500, {err: err.message});
});
});
example-005$ curl -X POST``http://localhost:8080/hero
T2】
-H "Content-Type: application/x-www-form-urlencoded" \
-d "name=Minsc&race=Human&class=Ranger&subclass=Berserker&alignment=Neutral%20Good&companion=Boo"
{"id":6,"isNew":true}
Tip
请记住,URL 参数将始终优先于查询字符串和请求正文参数。如果任何参数源之间存在命名冲突,Mach 首先支持 URL 参数,然后查询字符串参数,最后请求主体参数。
为了验证发布的数据确实被添加到数据库中,可以向清单 10-25 中的路由发送一个请求,该请求带有两个查询字符串参数skip
和take
。这些参数允许客户通过定义一个偏移量(skip
)和从该偏移量加载的英雄数量(take
)来浏览可能是大量英雄的集合。因为Mach.params
处理请求体和查询字符串,所以不需要手动解析它们。
以下两个curl
请求可分别用于查询记录 1–3 和 4–6。张贴的英雄,明斯克,是最后一页数据中的最后一个英雄。
Listing 10-25. Parsing a Query String Automatically with Mach.params
params
// example-005/index.js
// ...
// Mach.params
app.use(mach.params);
// ...
app.get('/hero'/*?skip=#&take=#*/, function (conn) {
var skip = Number(conn.params.skip || 0),
take = Number(conn.params.take || 0);
var deferred = Q.defer();
db.hero.page(skip, take, deferred.makeNodeResolver());
return deferred.promise.then(function (heroes) {
return conn.json(200, heroes);
}, function (err) {
return conn.json(500, {err: err.message});
})
});
example-005$ curl -X GET
http://localhost:8080/hero?skip=0\&take=3
[{"id":1,"name":"Dynaheir"...},{"id":2,"name":"Imoen"...},{"id":3,"name":"Khalid"...}]
example-005$ curl -X GET
http://localhost:8080/hero?skip=3\&take=3
[{"id":4,"name":"Xan"...},{"id":5,"name":"Edwin"...},{"id":6,"name":"Minsc"...}]
谁
在 web 应用中识别和跟踪用户本身就是一个话题。Mach 为简单的安全用例提供了基本的身份验证支持,并提供了可以容纳更多安全用例的持久会话支持。
与所有其他中间件一样,Mach.basicAuth
中间件被添加到应用堆栈中,并且需要一个简单的验证函数作为其唯一的参数。这个函数有两个参数,用户名和密码,这两个参数都是从随请求一起发送的身份验证凭证中解析出来的。验证函数可能返回三个值之一:
- 经过验证的用户的用户名
- 如果验证失败,则为“falsy”值
- 将通过有效用户的用户名解析的承诺,或通过虚假值拒绝的承诺
清单 10-26 中的 web 服务器将为任何经过验证的用户提供一个index.html
文件。Mach.basicAuth
中间件将拦截每个请求,并在数据库中查询任何提供的凭证。db.user.byCredential()
方法返回一个承诺,该承诺将由经过身份验证的用户解决,或者因出错而被拒绝。如果被解析,用户名被返回并通过承诺链传播,最终被设置为Connection.remoteUser
的值。如果出现错误,将返回一个布尔值false
,向客户端发送一个带有适当的WWW-Authenticate
头值的401 Unauthorized
响应。
Listing 10-26. Securing Routes with Basic Authentication
// example-006/index.js
// ...
// Mach.basicAuth
app.use(mach.basicAuth, function (username, password) {
return db.user.byCredential(username, password).then(function (user) {
return user.username;
}, function (/*err*/) {
return false;
});
});
var indexPath = path.join(__dirname, 'index.html');
app.get('/', function (conn) {
return conn.html(200, fs.createReadStream(indexPath));
});
当服务器正在运行并且用户试图访问http://localhost:8080
时,他将被提示输入凭证以响应基本认证挑战。图 10-4 显示了 Chrome 显示的模态窗口。
图 10-4。
A browser prompts the user for credentials when Basic Authentication fails
空载时段的速度
用户通过身份验证后,通常会在服务器会话中跟踪特定于用户的数据。将Mach.session
中间件添加到应用堆栈中可以自动支持会话 cookie。在Mach.session
options 对象上唯一需要的配置属性是一个用于加密会话数据的秘密会话密钥。清单 10-27 显示了在定义任何路由之前添加到堆栈中的会话中间件。
Listing 10-27. Adding Session Middleware to the Application Stack
// example-007/index.js
// ...
var sessionSecret = 'c94ac0cf8f3b89bf9987d1901863f562592b477b450c26751a5d6964cbdce9eb085c013d5bd48c7b4ea64a6300c2df97825b9c8b677c352a46d12b8cc5879554';
// Mach.session
app.use(mach.session, {
secret: sessionSecret
});
var quizView = swig.compileFile('./quiz.swig');
app.get('/', function (conn) {
return conn.html(200, quizView(conn.session));
});
// ...
清单 10-27 中的路由向浏览器发送一个 HTML 测验,如清单 10-28 所示。这个测验是一个 swig 模板,它将会话对象的name
、quest
和colour
属性作为每个输入的值进行插值。第一次访问该路由时,会话对象将为空,因此这些输入将没有值。
Listing 10-28. A Perplexing Quiz (What Will Be Your Answers?)
<h1>Questions, three.</h1>
<form method="post" action="/questions/three">
<fieldset>
<h2>What... is your name?</h2>
<div>
<input name="name" type="text" value="{{name}}" />
</div>
<h2>What... is your quest?</h2>
<div>
<input name="quest" type="text" value="{{quest}}" />
</div>
<h2>What... is your favourite colour?</h2>
<div>
<input name="colour" type="text" value="{{colour}}" />
</div>
<div>
<button>Cross the Bridge of Death</button>
</div>
</fieldset>
</form>
当表单被发送到/questions/three
路由时,如清单 10-29 所示,表单值从请求和会话对象中提取,并用于填充会话对象。然后,用户被重定向到成功页面,在该页面中,他或她可以选择再次参加测验。
Listing 10-29. Setting Session Properties in a Route
// example-007/index.js
// ...
var successView = swig.compileFile('./success.swig');
var errView = swig.compileFile('./err.swig');
app.post('/questions/three', function (conn) {
return conn.getParams().then(function (params) {
conn.session.name = params.name;
conn.session.quest = params.quest;
conn.session.colour = params.colour;
return conn.html(201, successView());
}, function (err) {
return conn.html(500, errView(err));
});
});
当用户返回测验页面时,字段会自动填充上一次给出的每个问题的答案。回想一下,在清单 10-28 中,会话被绑定到测验模板,由于值先前存储在会话对象中,它们现在也可用于模板。图 10-5 显示了预先填充的表单值以及用于将浏览器连接到服务器端会话的会话 cookie。
图 10-5。
Mach session cookie
由于默认情况下Mach.session
使用 cookie 存储,当中间件被添加到堆栈中时,有许多额外的特定于 cookie 的选项属性可以被设置,如表 10-5 中所述。
表 10-5。
Mach.session cookie options
| 财产 | 描述 |
| --- | --- |
| name
| cookie 的名称。默认为_session
。 |
| path
| Cookie 路径。默认为/
。 |
| domain
| Cookie 域。默认为null
。 |
| secure
| 只把饼干送到 HTTPS。默认为false
。 |
| expireAfter
| cookie 过期的秒数。默认为0
(永不过期)。 |
| httpOnly
| true
将此 cookie 限制为 HTTP/S API。默认为true
。 |
然而,Mach 会话存储并不局限于 cookies。它本身支持内存和 Redis 会话。要更改中间件的会话存储机制,请从mach/middleware/session/*
中选择require()
适当的模块。通过设置选项对象上的store
属性,将该模块的新实例添加到会话配置中。清单 10-30 展示了如何用 Redis 会话存储替换默认的 cookie 会话存储。
Listing 10-30. Using Redis As a Session Store
// example-008/index.js
// ...
var RedisStore = require('mach/lib/middleware/session/RedisStore');
// Mach.session
app.use(mach.session, {
store: new RedisStore({url: 'redis://127.0.0.1:6379'})
});
// ...
改造后的班底
Mach 的modified
中间件可以简单地通过使用标准的 HTTP 头,通知 HTTP 客户端自从上一次请求资源以来,所请求的资源没有被修改。在传递响应之前,Mach.modified
可以处理两种资源修改场景。
ETag 和 If-无-匹配
Web 服务器可以通过在响应的ETag
头中包含某种版本标识符(通常是消息摘要)来识别所请求资源的特定版本。在对同一资源的后续请求中,可以在If-None-Match
请求头中将该标识符发送回服务器。如果资源没有改变——也就是说,如果它的版本标识符没有改变 web 服务器可能用一个304 Not Modified
响应来响应,在响应体中省略实际的资源。当这种情况发生时,客户端知道资源没有改变,并且它必须继续使用它从上一个请求中接收到的数据。清单 10-31 中的代码展示了如何将每个图书对象的摘要作为ETag
响应头添加到每个图书路径中。
Listing 10-31. Adding the ETag
Header to Each Book Response
// example-009/index.js
var jsonHash = require('./json-hash');
// ...
app.use(mach.modified);
app.get('/book/:id', function (conn) {
var id = Number(conn.params.id);
var deferred = Q.defer();
db.book.findByID(id, deferred.makeNodeResolver());
return deferred.promise.then(function (book) {
if (!book) {
return conn.json(404);
}
conn.response.setHeader('ETag', jsonHash(book));
return conn.json(200, book);
}, function (err) {
return conn.json(500, {error: err.message});
});
});
app.put('/book/:id', function (conn) {
var book = Book.fromParams(conn.params);
var deferred = Q.defer();
db.book.save(book, deferred.makeNodeResolver());
return deferred.promise.then(function (result) {
conn.response.setHeader('ETag', jsonHash(book));
return conn.json(result.isNew ? 201 : 200, book);
}, function (err) {
return conn.json(500, {error: err.message});
});
});
清单 10-32 中的第一个curl
请求获取一本书,弗兰克·赫伯特的《沙丘》。响应中的ETag
头显示消息摘要cf0fdc372106caa588f794467a17e893
,响应体包含序列化的 JSON book 数据。(ETag
消息摘要可能因您的操作系统而异。对于每个curl
命令,使用您在 HTTP 响应头中收到的ETag
进行进一步的比较。)
第二个curl
请求使用了相同的 URL,但是也包含了一个If-None-Match
头,带有在之前的响应中发送的ETag
值。因为 book 实体在服务器上没有改变(因此它的消息摘要保持不变),Mach 发送一个没有响应体的304 Not Modified
响应。
Listing 10-32. Using ETag
and If-None-Match
Headers to Test for Content Modification
example-009$ curl -v -X GET
http://localhost:8080/book/1
...
< HTTP/1.1 200 OK
< ETag: cf0fdc372106caa588f794467a17e893
< Content-Type: application/json
< Date: Mon, 06 Apr 2015 01:39:11 GMT
< Connection: keep-alive
< Transfer-Encoding: chunked
<
{"id":1,"title":"God Emperor of Dune","author":"Frank Herbert"...}
example-009$ curl -v -H "If-None-Match: cf0fdc372106caa588f794467a17e893" -X GET
http://localhost:8080/book/1
...
< HTTP/1.1 304 Not Modified
< ETag: cf0fdc372106caa588f794467a17e893
< Content-Type: application/json
< Content-Length: 0
< Date: Mon, 06 Apr 2015 01:39:31 GMT
< Connection: keep-alive
<
在清单 10-33 中,第一个curl
请求执行一个 HTTP PUT,将弗兰克·赫伯特的全名分配给《沙丘》这本书。第二个curl
请求与清单 10-32 中的第二个请求相同,但是这次服务器用 HTTP 200 OK
响应,因为消息摘要不同,反映了更新的图书资源。随后的获取将在响应的ETag
头中使用较新的消息摘要。
Listing 10-33. Updated ETag
Header Passes the If-None-Match Check
example-009$ curl -X PUT``http://localhost:8080/book/1
T2】
-H "Content-Type: application/x-www-form-urlencoded" \
-d "title=God%20Emperor%20of%20Dune&author=Franklin%20Patrick%20Herbert&publisher=Victor%20Gollancz&publicationDate=2003-03-13T06:00:00.000Z&seriesTitle=Dune%20Chronicles&seriesPosition=4"
{"id":1,"title":"God Emperor of Dune","author":"Franklin Patrick Herbert"...}
example-009$ curl -v -H "If-None-Match: cf0fdc372106caa588f794467a17e893" -X GET
http://localhost:8080/book/1
...
< HTTP/1.1 200 OK
< ETag: 2595cd82c364b04473358bb2d0153774
< Content-Type: application/json
< Date: Mon, 06 Apr 2015 01:54:33 GMT
< Connection: keep-alive
< Transfer-Encoding: chunked
<
{"id":1,"title":"God Emperor of Dune","author":"Franklin Patrick Herbert"...}
上次修改和如果修改自
Last-Modified
响应头类似于前一节中提到的ETag
头,但是它包含一个时间戳,而不是版本标识符,该时间戳指示资源最后一次更改的时间。当一个 HTTP 客户端发出请求时,它可以在一个If-Modified-Since
头中提供时间戳,然后在服务器上与资源的时间戳进行比较。web 服务器将只提供资源的较新版本;否则,它将发出304 Not Modified
响应,指示客户端应该依赖于之前的资源,因为未修改的资源不会包含在响应体中。
清单 10-34 中的代码使用每个作者记录上的lastModified
时间戳来设置每个响应中的Last-Modified
头值。当作者记录被更新时,这个时间戳由数据库自动改变。
Listing 10-34. Adding the Last-Modified
Header to Each Author Response
// example-009/index.js
app.get('/author/:id', function (conn) {
var id = Number(conn.params.id);
var deferred = Q.defer();
db.author.findByID(id, deferred.makeNodeResolver());
return deferred.promise.then(function (author) {
if (!author) {
return conn.json(404);
}
conn.response.setHeader('Last-Modified', author.lastModified);
return conn.json(200, author);
}, function (err) {
return conn.json(500, {error: err.message});
});
});
app.put('/author/:id', function (conn) {
var author = Author.fromParams(conn.params);
var deferred = Q.defer();
db.author.save(author, deferred.makeNodeResolver());
return deferred.promise.then(function (result) {
conn.response.setHeader('Last-Modified', author.lastModified);
return conn.json(result.isNew ? 201 : 200, author);
}, function (err) {
return conn.json(500, {error: err.message});
});
});
在清单 10-35 中,第一个curl
请求获取作者休豪伊,响应通知客户端最后一次修改休的记录是在2015-04-06T00:26:30.744Z
上。在第二个请求中,这个 ISO 日期字符串被用作If-Modified-Since
头的值,作为响应,Mach 发送一个304 Not Modified Response
。
Listing 10-35. Using Last-Modified
and If-Modified-Since
Headers to Test for Content Modification
example-009$ curl -v -X GET
http://localhost:8080/author/1
...
< HTTP/1.1 200 OK
< Last-Modified: 2015-04-06T00:26:30.744Z
< Content-Type: application/json
< Date: Mon, 06 Apr 2015 01:41:31 GMT
< Connection: keep-alive
< Transfer-Encoding: chunked
<
{"id":1,"name":"Hugh Howey","website":"
http://www.hughhowey.com
T2】
example-009$ curl -v -H "If-Modified-Since: 2015-04-06T00:26:30.744Z" -X GET
http://localhost:8080/author/1
...
< HTTP/1.1 304 Not Modified
< Last-Modified: 2015-04-06T00:26:30.744Z
< Content-Type: application/json
< Content-Length: 0
< Date: Mon, 06 Apr 2015 01:42:27 GMT
< Connection: keep-alive
<
可以预见,一旦记录被更新(因此,它的lastModified
日期被更改),Mach 的响应将在响应体中包含更新的 JSON 数据,以及一个新的Last-Modified
响应头。清单 10-36 显示了这个带有两个curl
请求的交换。
Listing 10-36. Updated Last-Modified
Header Passes the If-Modified-Since
Check
example-009$ curl -X PUT``http://localhost:8080/author/1
T2】
-H "Content-Type: application/x-www-form-urlencoded" \
-d "name=Hugh%20C.%20Howey&website=http%3A%2F%2F``www.hughhowey.com&genres=Science%20Fiction%2CFantasy%2CShort%20Stories
T2】
{"id":1,"name":"Hugh C. Howey","website":"
http://www.hughhowey.com
T2】
example-009$ curl -v -H "If-Modified-Since: 2015-04-06T00:26:30.744Z" -X GET
http://localhost:8080/author/1
...
< HTTP/1.1 200 OK
< Last-Modified: 2015-04-06T02:09:01.783Z
< Content-Type: application/json
< Date: Mon, 06 Apr 2015 02:09:09 GMT
< Connection: keep-alive
< Transfer-Encoding: chunked
<
{"id":1,"name":"Hugh C. Howey","website":"
http://www.hughhowey.com
T2】
这些不是你要找的路线...
Mach 可以用Mach.rewrite
中间件重写请求 URL。虽然不像 Apache 的mod_rewrite
模块这样复杂,但是Mach.rewrite
既简单又灵活,足以处理常见的重写用例。
将Mach.rewrite
添加到应用堆栈时,必须提供两个必需的参数:
- 匹配传入请求 URL 的正则表达式对象(或将被转换为正则表达式对象的字符串)
- 请求将被静默转发到的路由路径
考虑一个用例,作者将他的博客从基于 PHP 的系统迁移到运行 Mach 的 Node.js 系统。搜索引擎已经将他对这个世界的有价值的贡献编入索引,所以他的网址是永久固定的。通过用Mach.rewrite
设置重写规则,他可以确保他的旧 URL 仍然对外界可用,同时将它们映射到他的新路由方案。
清单 10-37 中的Mach.rewrite
中间件使用一个复杂的正则表达式对象来建立参数的捕获组,这些参数将作为新博客文章路径的 URL 参数输入:year
、month
、day
和slug
。在正则表达式之后,按照位置顺序,用每个提取的捕获组的占位符来定义表示重写的 URL 路由的字符串。在引擎盖下Mach.rewrite
使用String.prototype.replace()
方法对提取的值进行插值。
Listing 10-37. Rewriting a URL with Parameters
// example-010/index.js
var blogView = swig.compileFile(path.join(__dirname, 'blog.swig'));
var errView = swig.compileFile(path.join(__dirname, 'err.swig'));
app.use(
mach.rewrite,
// converts: /index.php/blog/2015-04-02/bacon-ipsum-dolor-amet
new RegExp('\/index\.php\/blog\/([\\d]{4})-([\\d]{2})-([\\d]{2})\/([^\/]+)'),
// into: /blog/2015/04/02/bacon-ipsum-dolor-amet
'/blog/$1/$2/$3/$4'
);
// :year=$1, :month=$2, :day=$3, :slug=$4
app.get('/blog/:year/:month/:day/:slug', function (conn) {
var year = Number(conn.params.year || 0),
month = Number(conn.params.month || 0),
day = Number(conn.params.day || 0),
slug = conn.params.slug || '';
var deferred = Q.defer();
db.posts.find(year, month, day, slug, deferred.makeNodeResolver());
return deferred.promise.then(function (post) {
if (post) {
return conn.html(200, blogView({posts: [post]}));
}
return conn.html(404, errView({message: 'I haven\t written that yet.'}))
}, function (err) {
return conn.html(500, errView(err));
});
});
对于一个 HTTP 客户端,比如图 10-6 所示的网络浏览器(或者一个搜索引擎机器人),重写的 URL 仍然是完全有效的,尽管在内部它们已经变得不同了。这不同于 HTTP 重定向或转发,在 HTTP 重定向或转发中,客户端负责解释响应头,然后加载另一个页面。在这种情况下,客户一无所知。
图 10-6。
Rewritten URLs appear unmodified to the HTTP client
清单 10-38 中的重写规则执行完全相同的工作,但是使用一个简单的字符串而不是正则表达式进行请求 URL 匹配,因为它不捕获任何参数。请注意,Mach.rewrite
会在将任何字符串转换成正则表达式之前自动转义它。如果您自己对这些字符串进行转义,它们将变成双重转义,您的匹配规则将失败。
Listing 10-38. Rewriting a URL with No Parameters
// example-010/index.js
app.use(
mach.rewrite,
'/index.php/blog',
'/blog'
);
app.get('/blog', function (conn) {
var deferred = Q.defer();
db.posts.all(deferred.makeNodeResolver());
return deferred.promise.then(function (posts) {
return conn.html(200, blogView({posts: posts}));
}, function (err) {
return conn.html(500, errView(err));
});
});
拥有最多的主机
Mach.mapper
的独特之处在于,它在 Mach 的正常路由机制之上执行自己的路由方式。到目前为止,一直假设路由路径存在于单个主机(本地主机),并且都与该主机的名称相关。Mach.mapper
中间件通过引入中间件过滤器改变了这种模式,该过滤器可以通过主机名和 URL 路径名来路由请求,这与 Apache 的虚拟主机的精神非常相似,但占用的内存要少得多。
为了演示 Mach 的映射特性是如何工作的,执行清单 10-39 中的echo
命令,将两个别名添加到您计算机上的/etc/hosts
文件中。因为/etc/hosts
在类 Unix 系统上受到保护,所以sudo
命令用于提升权限。如果这个命令失败,你也可以用 vim 或 nano 这样的文本编辑器手动添加别名到/etc/hosts
。cat
命令将把/etc/hosts
的内容输出到终端,这样您就可以验证条目是否已经被添加。
Listing 10-39. Adding Aliases to /etc/hosts
example-011$ sudo echo "127.0.0.1 house-atreides.org" >> /etc/hosts
example-011$ sudo echo "127.0.0.1 house-harkonnen.org" >> /etc/hosts
example-011$ cat /etc/hosts
...
127.0.0.1 house-atreides.org
127.0.0.1 house-harkonnen.org
Tip
如果您的计算机运行 Microsoft Windows 操作系统,您将需要修改文件C:\Windows\System32\drivers\etc\hosts
。该文件通常受 Windows 保护,因此您需要使用以管理员权限运行的文本编辑器来修改它。
一旦修改了/etc/hosts
文件,使用清单 10-40 中所示的ping
命令来验证每个别名都解析为127.0.0.1
(本地主机)。
Listing 10-40. Using ping
to Test Aliases in /etc/``hosts
example-011$ ping -t 3 house-atreides.org
PING house-atreides.org (127.0.0.1): 56 data bytes
64 bytes from 127.0.0.1: icmp_seq=0 ttl=64 time=0.044 ms
64 bytes from 127.0.0.1: icmp_seq=1 ttl=64 time=0.118 ms
64 bytes from 127.0.0.1: icmp_seq=2 ttl=64 time=0.074 ms
--- house-atreides.org ping statistics ---
3 packets transmitted, 3 packets received, 0.0% packet loss
round-trip min/avg/max/stddev = 0.044/0.079/0.118/0.030 ms
清单 10-41 中的 web 服务器演示了Mach.mapper
是如何工作的。它像任何正常的 Mach web 服务器一样开始:创建一个应用栈,添加一些中间件,然后事情有点不同。还创建了两个额外的独立应用堆栈— atreidesApp
和harkonnenApp
—每个堆栈都被分配了一个路由。事实上,所有的应用栈都有相同的路径,GET /about
。
Listing 10-41. Mach.mapper
Middleware Maps Apps to Hostnames
// example-011/index.js
// ...
var app = mach.stack();
app.use(mach.logger);
app.use(mach.params);
app.use(mach.file, path.join(__dirname, 'public'));
var atreidesApp = mach.stack();
atreidesApp.get('/about', function (conn) {
var pagePath = path.join(__dirname, 'atreides.html');
return conn.html(200, fs.createReadStream(pagePath));
});
var harkonnenApp = mach.stack();
harkonnenApp.get('/about', function (conn) {
var pagePath = path.join(__dirname, 'harkonnen.html');
return conn.html(200, fs.createReadStream(pagePath));
});
app.use(mach.mapper, {
'
http://house-atreides.org/
T2】
'
http://house-harkonnen.org/
T2】
});
app.get('/about', function (conn) {
var pagePath = path.join(__dirname, 'about.html');
return conn.html(200, fs.createReadStream(pagePath));
});
通过检查每个 route 函数的主体可以清楚地看到,这些应用在被调用时都会呈现不同的 HTML 页面。这些路由可以共存,因为Mach.mapper
中间件在其选项散列中将atreidesApp
应用栈映射到hose-atreides.org
主机名,将harkonnenApp
映射到house-harkonnen.org
主机名。当 web 服务器接收到请求时,它们会通过Mach.mapper
中间件,在那里对Connection.hostname
属性进行评估。如果它与 mapping options 对象上的任何键匹配,则连接将被传递给与该键相关联的应用堆栈,以便进一步处理。这有几个有趣的结果:
- 因主机名而异的应用栈可能有相同的路由,比如
GET /about
。 - 由于中间件直接连接到应用堆栈,每个堆栈可能有不同的中间件。
- 在
Mach.mapper
之前添加到托管应用堆栈的任何中间件将被应用到Mach.mapper
管理的所有应用堆栈。 - 在
Mach.mapper
之前添加到托管应用堆栈的任何路由将在Mach.mapper
有机会进行基于主机名的路由之前被评估。因为没有Mach.mapper
主机名不会被评估,所以主机应用堆栈上具有相同 URL 路径名值的路由将被解析,而不管主机名如何。 - 在
Mach.mapper
之后添加到托管应用堆栈的任何路由将充当“失效”路由。如果没有映射的应用堆栈可以处理对连接主机名的请求,那么将评估这些路由。
Tip
向Mach.mapper
添加主机时,协议很重要,但端口号不重要,因此可以安全地省略端口号。Mach 只监听一个端口。主机名密钥应该总是以斜杠结尾。
运行 web 服务器,然后启动 web 浏览器并导航到 URL http://localhost:8080/about
。这将打开如图 10-7 所示的页面,该页面来自托管应用堆栈上定义的/about
路由。该路由处理了该请求,因为主机名localhost
与Mach.mapper
配置中的任何主机名都不匹配。
图 10-7。
The /about
route from localhost
清单 10-42 中的页面源代码显示,两个超链接锚,一个是阿崔迪斯家族的,一个是哈肯南家族的,都链接到不同的主机。点击任一链接,将呈现由Mach.mapper
定义的映射路线页面。请注意,尽管在声明映射的应用堆栈时端口号并不重要,但它们必须包含在页面超链接中,否则浏览器将尝试自动使用端口 80。
Listing 10-42. Anchors on the Default /about
Page Link to Different Hosts
<h1>Great Houses of Arrakis</h1>
<h2>
<a href="
http://house-atreides.org:8080/about
T2】
</h2>
<h2>
<a href="
http://house-harkonnen.org:8080/about
T2】
</h2>
图 10-8 显示了完全渲染后的阿崔迪斯“关于”页面上的房子。图 10-9 显示了众议院哈肯南“关于”页面。
图 10-9。
The /about
route from house-harkonnen.``org
图 10-8。
The /about
route from house-atreides.``org
查看两个“关于”页面的源代码会发现一些有趣的事情。两个页面上引用的图像,例如清单 10-43 中的src
属性,没有指定主机名前缀。
Listing 10-43. Images Do Not Have Hostname Prefixes
<img class="flag" srcimg/Atreides_guidon_pennant.svg" />
这是可能的,因为将example-011/public
目录公开为静态资源目录的Mach.file
中间件是在Mach.mapper
之前添加到托管应用堆栈的,因此会影响上游的所有应用堆栈。所有静态资源(图像、字体、脚本等)都可以存储在同一个位置,无论主机名如何,所有应用堆栈都可以使用这些资源。当然,如果需要的话,每个应用栈可以使用另一个Mach.file
中间件来公开不同的静态素材目录。
定制中间件
创建定制的 Mach 中间件相对简单。创建定制中间件时,通常涉及三个“层”:
A top-level function that is responsible for capturing an “app” and any options that are passed to the middleware via app.use()
. This layer returns... a function that will receive an incoming request connection. This function may do one of two things. It may manipulate the connection directly and send a response without passing the connection through the remainder of the application stack (an authentication failure, for example), or... it may send the request downstream and then handle the response when the application stack’s promise chain has resolved.
清单 10-44 中的中间件展示了工作中的所有三个阶段。
Listing 10-44. Custom Middleware Module That Adds an API Version Header to the Response
// example-012/api-version.js
'use strict';
// layer 1
function apiVersion(app, options) {
// layer 2
return function (conn) {
// layer 3
return conn.call(app).then(function () {
conn.response.headers['X-API-Version'] = options.version;
});
};
}
module.exports = apiVersion;
顶层函数apiVersion()
通过module.exports
公开。当中间件附加到应用栈时,它将被传递给app.use()
。它捕获应用实例和选项对象(第 1 层),将两者保存在一个闭包中以供进一步处理。当接收到请求时,返回的函数(第 2 层)接收连接对象并做出决定。这个特定的中间件只关心将“API 版本”头添加到响应中,所以此时它调用Connection.call()
方法,将应用本身作为唯一的参数传递。
在这一点上,一些歧义是必要的。在 Mach 中,通过调用Mach.stack()
创建的“应用堆栈”是一个接受连接并返回Connection.call()
值的函数。这个过程与 Mach 中间件功能所做的是一样的。事实上,这几乎与路由的功能相同:无论是Connection.call()
还是所有路由都返回作为单个承诺链存在的承诺对象!
这种相似性的实际含义是,Mach 中间件功能接收的“应用”可能是下游中间件的另一部分,也可能是路由,这取决于中间件/路由添加到应用堆栈的顺序。然后,通过将app
对象传递给conn.call()
,定制中间件将连接传播到下游的所有东西,不管是什么。当conn.call()
返回的承诺解决时(第 3 层),所有下游中间件和/或路由已经处理了连接对象,定制中间件可以决定它必须对响应做什么(如果有的话)。
在清单 10-44 中,一旦响应再次向上游移动,API 版本号就被分配给响应对象上的自定义X-API-Version
头。如果这个中间件被设计成在将请求传递到下游之前修改请求,那么它应该在调用conn.call()
之前就这样做了。
定制中间件以与 Mach 的原生中间件相同的方式附加到应用栈,如清单 10-45 所示。在这个例子中,apiVersion
中间件将接收一个版本号为 1.2 的 options 对象,它将作为一个定制的头值添加到每个响应中。请注意,Mach.gzip
被添加到堆栈中的apiVersion
之后,这意味着apiVersion
中间件接收的“app”参数将是Mach.gzip
的中间件函数,因为它存在于堆栈的下游。
Listing 10-45. Adding Custom Middleware to the Application Stack
// example-012/index.js
var apiVersion = require('./api-version');
// create a stack
var app = mach.stack();
// custom middleware
app.use(apiVersion, {version: '1.2'});
// out-of-the-box middleware
app.use(mach.gzip);
app.get('/numbers', function (conn) {
return conn.json(200, [4, 8, 15, 16, 23, 42]);
});
当在清单 10-46 中查询 web 服务器时,可以在详细的响应中看到X-API-Version
头。
Listing 10-46. API Version Middleware Response Header
example-012$ curl -v -X GET
http://localhost:8080/numbers
* Hostname was NOT found in DNS cache
* Trying ::1...
* Connected to localhost (::1) port 8080 (#0)
> GET /numbers HTTP/1.1
> User-Agent: curl/7.37.1
> Host: localhost:8080
> Accept: */*
>
< HTTP/1.1 200 OK
< Content-Type: application/json
<
X-API-Version: 1.2
< Date: Fri, 10 Apr 2015 01:41:42 GMT
< Connection: keep-alive
< Transfer-Encoding: chunked
<
[4,8,15,16,23,42]
Mach,HTTP 客户端
Mach 不仅仅是一个 HTTP 服务器。其内部架构允许它在多种环境中扮演多种角色。事实上,对 Mach 源代码的研究表明,Mach 中特定于服务器的部分是作为扩展实现的。这意味着 Mach 的核心对象,比如Connection
、Location
和Message
,可以跨越多个用例。
清单 10-47 中的代码类似于目前给出的 web 服务器示例。创建了一个 Mach 应用栈来服务 HTTP 请求,添加了文件中间件来服务来自example-013/public
的静态内容,并且向栈注册了一个单独的路由GET /mach/tags
。然而,这个路径中的代码利用 Mach 的 HTTP 客户端特性向 Github API 发送一个对 Mach 的所有存储库标签的 GET 请求。
Listing 10-47. Mach As Both Server and Client
// example-013/index.js
var app = mach.stack();
app.use(mach.logger);
app.use(mach.file, {
root: path.join(__dirname, 'public'),
index: true
});
app.get('/releases', function (conn) {
function addUserAgent(conn) {
conn.request.setHeader('User-Agent', 'nicholascloud/mach');
}
return``mach.get('
https://api.github.com/repos/mjackson/mach/tags
', addUserAgent)
T4】
var tags = [];
JSON.parse(conn.responseText).forEach(function (tagData) {
tags.push(tagData.name);
});
return tags.sort(semver.rcompare);
}).then(function (tags) {
return conn.json(200, tags);
}, function (err) {
return conn.json(500, {err: err.message});
});
});
Mach 的 HTTP 客户端方法看起来很像 Mach 的路由方法,但是它们存在于 Mach 模块本身,而不是应用堆栈上。Mach 可以对任何标准的 HTTP 方法发出请求。
在清单 10-47 中,Mach.get()
方法接收请求 URL 作为它的第一个参数,并接收一个可选函数,在它作为第二个参数发送之前修改连接的请求。这个请求连接到 Github API,并获取mjackson/mach
存储库的标记信息。因为 Github API 在所有传入请求中都需要一个User-Agent
头,所以addUserAgent()
函数通过添加我自己的源代码分支作为代理来修改传出的请求(根据 Github 的指南)。
像 Mach API 的其他部分一样,Mach.get()
方法返回一个承诺。如果承诺被解析,它的值将是带有响应消息属性的连接对象。如果被拒绝,将向失败回调传递一个错误。
Github JSON 数据作为字符串存在于Connection.responseText
属性中(或者作为流存在于Connection.response.content
)。一旦这些数据被反序列化,就提取标记名,按降序排序,然后沿着 promise 链传递。
当用清单 10-48 中的curl
查询 web 服务器时,所有 Mach 的发布标签都以 JSON 数组的形式提交。
Listing 10-48. Fetching Mach Releases with cURL
example-013$ curl
http://localhost:8080/releases
["v1.3.4","v1.3.3","v1.3.2","v1.3.1","v1.3.0"...]
清单 10-49 中的 HTML 页面使用这些 JSON 数据。注意,它也使用Mach.get()
连接到本地 web 服务器。因为 Mach 的环境相关特性是作为扩展实现的,所以 Mach 在服务器和浏览器代码中都很有用。
Note
因为 Mach 是 Node.js 模块,所以它可以被任何 CommonJS 模块加载器使用,比如 Browserify 或 WebPack。所有其他的使用,比如清单 10-49 中显示的普通脚本包含,应该使用 Mach Github 库中的全局 Mach 构建。
浏览到http://localhost:8080
查看所有 Mach 版本的链接列表。
Listing 10-49. Mach.get()
in the Browser
<!-- example-013/public/index.html -->
<h1>Mach Releases</h1>
<h2>Git you one!</h2>
<ul id="tags"></ul>
<script src="/vendor/mach.min.js"></script>
<script>
(function (mach, document) {
var href = '
https://github.com/mjackson/mach/releases/tag/:tag
T2】
var ul = document.querySelector('#tags');
mach.get('/releases').then(function (conn) {
var tags = JSON.parse(conn.responseText);
tags.forEach(function (tag) {
var li = document.createElement('li');
var a = document.createElement('a');
a.innerHTML = tag;
a.setAttribute('href', href.replace(':tag', tag));
a.setAttribute('target', '_blank');
li.appendChild(a);
ul.appendChild(li);
});
});
}(window.mach, window.document))
</script>
Mach,HTTP 代理
虽然在技术上是中间件,但是 Mach 的 HTTP 代理功能可以单独使用来创建完整的 HTTP 代理服务器,或者与现有的应用堆栈集成来代理某些路由。对于逐段迁移 web 应用,同时仍然将调用代理到遗留 web 应用,或者对于通过 web 应用本身将调用代理到外部或第三方服务来避免浏览器中的同源问题,这可能是一个有用的工具。
清单 10-50 中的代码创建了一个简单的 Mach 应用,它同时服务于一个根应用路径和来自public
目录的静态文件。在路由声明之后,通过使用另一个服务器的 HTTP 方案、主机名和端口调用Mach.proxy()
来创建代理应用。对于这个例子,当 web 应用运行时,它将在端口 8080 上监听一些请求,同时将其他请求代理到另一个在端口 8090 上运行的 web 服务器。当两者都被传递给app.use()
时,这个代理应用栈成为Mach.proxy
的中间件选项参数。
Listing 10-50. Proxying Requests to Another Web Server
// example-014/web.js
var app = mach.stack();
app.use(mach.logger);
app.use(mach.file, path.join(__dirname, 'public'));
app.get('/', function (conn) {
var pagePath = path.join(__dirname, 'index.html');
return conn.html(200, fs.createReadStream(pagePath));
});
var apiProxy = mach.createProxy('``http://localhost:8090
T2】
app.use(mach.proxy, apiProxy);
mach.serve(app, 8080);
通常,中间件在路由之前被添加到应用堆栈中,以便它们有机会检查请求,如果不满足某些条件,就中断中间件承诺链,或者修改请求并传递它以供进一步处理。不幸的是,Mach.proxy
相当愚蠢,这意味着它不区分请求;任何通过Mach.proxy
的请求都将被发送到代理服务器。如果应用混合使用本地路由和代理路由,有两种方法可以处理这种“限制”:
- 添加应用路由后,添加代理中间件。这确保了如果一个应用路由可以处理一个请求,它将处理它,在它到达
Mach.proxy
之前停止连接的传播。这就是清单 10-50 中采用的方法。 - 将代理中间件封装在一个轻量级的定制中间件函数中,该函数区分并只将某些请求转发给代理。因为它过滤请求,所以定制中间件可以在任何路由之前添加到堆栈中。清单 10-51 中说明了这种替代方法。
Listing 10-51. Wrapping a Proxy Application in Custom Middleware
// example-014/web2.js
var apiProxy = mach.createProxy('``http://localhost:8090
T2】
app.use(function (app) {
return function (conn) {
if (conn.location.pathname.indexOf('/api') !== 0) {
// not an API method, call the app stack normally
return conn.call(app);
}
// API method, call the proxy app stack
return conn.call(apiProxy);
};
});
app.get('/', function (conn) { /*...*/ });
毫不奇怪,接收代理请求的模拟 API 服务器是另一个 Mach 服务器。它公开了两个常规的 JSON 路由,如清单 10-52 所示:一个用于获取投票的统计列表,另一个用于提交单个投票。
Listing 10-52. API Server’s Routes
// example-014/api.js
var votes = require('./votes');
// ...
app.get('/api/vote', function (conn) {
var tallies = {};
var voteCount = votes.length;
votes.forEach(function (vote) {
var tally = tallies[vote] || {
count: 0,
percent: 0.0
};
tally.count += 1;
tally.percent = Number((tally.count / voteCount * 100).toFixed(2));
tallies[vote] = tally;
return tallies;
});
return conn.json(200, tallies);
});
app.post('/api/vote', function (conn) {
console.log(conn.params);
var vote = conn.params.vote || '';
if (!vote) {
return conn.json(400, {err: 'Empty vote submitted.'});
}
votes.push(vote);
return conn.json(201, {count: 1});
});
mach.serve(app, 8090);
Note
要运行example-014
,必须用 Node.js 启动web.js
(或web2.js
)和api.js
,web 服务器将在端口 8080 监听 HTTP 请求,API 服务器将在端口 8090 监听。
web 服务器呈现一个 HTML 页面作为小型投票应用的用户界面。尽管众所周知“你不投票给国王”,农民仍然喜欢受欢迎程度的竞赛,这个网络应用纵容了他们。图 10-10 显示了http://localhost:8080
的渲染页面。
图 10-10。
Voting for a new monarch
提交表单时,事件处理程序会找到选中的选项值,并向 web 服务器发送包含投票数据的请求。清单 10-53 中的sendVote()
方法向 web 服务器上的POST /api/data
发出一个 AJAX 请求,然后这个请求被代理到记录投票的 API 服务器。
一旦提交完成,清单 10-53 中的getTallies()
函数在GET /api/vote
查询 web 服务器以获取投票结果。同样,这个请求被代理,JSON 数据被返回给客户机。
Listing 10-53. Submitting a Vote
// example-014/index.html
var formPoll = document.querySelector('#poll');
// ...
function sendVote(vote) {
function serializeVote(conn) {
conn.request.setHeader('Content-Type', 'application/json');
conn.request.content = JSON.stringify({
vote: vote
});
}
return mach.post('/api/vote', serializeVote);
}
function getTallies() {
return mach.get('/api/vote').then(function (conn) {
return JSON.parse(conn.responseText);
});
}
formPoll.addEventListener('submit', function (e) {
// ...
var vote = checkbox.value;
sendVote(vote).then(function () {
// ...
return getTallies().then(function (tallies) {
// show tally data...
});
}).catch(function (error) {
showError(error.err || error.message || 'The night is dark and full of errors.');
});
});
一旦响应被接收和解析,网页显示代理的计数数据,如图 10-11 所示。
图 10-11。
Tallies are displayed when a vote is submitted
如果在代理请求期间出现错误(例如,如果 API 服务器离线),它们将作为 HTTP 请求错误返回给客户端。因为这些错误与基础设施相关,而与应用无关,所以在定制的中间件包装器中处理它们并提供更有意义的错误可能是明智的。
摘要
虽然 Mach 肯定不是唯一可用的 Node.js web 服务器,甚至也不是最受欢迎的,但它具有很强的简单性和简洁的 API,这使它非常灵活。它的核心架构确保它的通用组件随处可用,而它的特定于环境的组件作为扩展加载。
一套插入到基于承诺的 API 中的通用中间件使得请求和响应链易于利用和操作。当需要更多功能时,定制中间件很容易编写。
通过按需解析请求查询和主体,请求和响应消息建立在节点的本地流上,并将响应内容以块的形式传递给客户端。这确保了在 HTTP 操作期间使用尽可能低的内存和处理开销。请求和响应内容也可以通过管道传输,转换到缓冲区进行内存操作,由各种格式处理程序解析,并转换为具有不同编码的字符串。
除了作为 HTTP 服务器的角色之外,Mach 还可以完成其他几个重要的 HTTP 相关角色:
- 重写请求 URL
- 将请求映射到虚拟主机
- 充当 HTTP 代理
- 发送 HTTP 客户端请求
Mach 的新想法是对 Node.js web 服务器的有益补充。
版权属于:月萌API www.moonapi.com,转载请注明出处