NodeJS 入门指南(三)
六、HTTP 入门
Node.js 是专门为可伸缩的服务器端和网络应用而创建的。它具有久经考验的功能,可以有效地处理网络连接。这为社区构建成熟的应用服务器提供了基础。
在这一章中,我们将看看 Node.js 为创建 web 应用提供的核心功能。然后我们将回顾 connect 中间件框架,它允许您创建可重用的 web 服务器组件。最后,我们将看看用 HTTPS 保护你的 Web 服务器。
Node.js HTTP 的基础知识
以下是在 Node.js 中创建 web 应用的主要核心网络模块:
- net /
require('net'):为创建 TCP 服务器和客户端提供基础 - dgram /
require('dgram'):提供创建 UDP /数据报套接字的功能 - http /
require('http'):为 http 栈提供高性能的基础 - https /
require('https'):为创建 TLS / SSL 客户端和服务器提供 API
我们将从使用http模块创建我们的简单服务器来服务静态文件开始。从头开始创建我们的 web 服务器将使我们对社区 NPM 模块提供的功能有更深的理解,我们将在后面探讨这些功能。
注意我们将使用
curl来测试我们的 web 应用。默认情况下,它在 Mac OS X / Linux 上可用。您可以将curl for windows 作为 Cygwin ( www.cygwin.com/)的一部分。
http模块有一个可爱的小函数createServer,它接受一个回调并返回一个 HTTP 服务器。对于每个客户端请求,回调通过两个参数传递——传入的请求流和传出的服务器响应流。要启动返回的 HTTP 服务器,只需调用它的listen函数,传递您想要监听的端口号。
清单 6-1 提供了一个简单的服务器)监听端口 3000,并简单地对每个 HTTP 请求返回“hello client!”。
清单 6-1 。1create/1raw.js
var http = require('http');
var server = http.createServer(function (request, response) {
console.log('request starting...');
// respond
response.write('hello client!');
response.end();
});
server.listen(3000);
console.log('Server running at http://127.0.0.1:3000/');
要测试服务器,只需使用 Node.js 启动服务器,如清单 6-2 所示。
清单 6-2 。启动服务器
$ node 1raw.js
Server running at http://127.0.0.1:3000/
然后在一个新窗口中使用curl测试一个 HTTP 连接,如清单 6-3 所示。服务器按照我们的预期发送数据。
清单 6-3 。使用 curl 发出客户端请求
$ curl http://127.0.0.1:3000
hello client!
要退出服务器,只需在服务器启动的窗口中按 Ctrl+C。
检查割台
即使在这一点上,很多 HTTP 逻辑已经被默默地处理了。由curl发送的实际请求包含一些重要的 HTTP 头。为了看到这些,让我们修改服务器来记录在客户端请求中收到的头(由curl发送),如清单 6-4 所示。
清单 6-4 。1create/2defaultheaders.js
var http = require('http');
var server = http.createServer(function (req, res) {
console.log('request headers...');
console.log(req.headers);
// respond
res.write('hello client!');
res.end();
}).listen(3000);
console.log('server running on port 3000');
现在启动服务器。我们还将要求curl使用-i(即在输出中包含协议头)选项注销服务器响应头,如清单 6-5 所示。
清单 6-5 。发出客户端请求并显示返回的响应头
$ curl http://127.0.0.1:3000 -i
HTTP/1.1 200 OK
Date: Thu, 22 May 2014 11:57:28 GMT
Connection: keep-alive
Transfer-Encoding: chunked
hello client!
从curl发送的 HTTP 请求头由 Node.js HTTP 服务器处理,记录在服务器控制台上,如清单 6-6 所示。如您所见,req.headers是一个简单的 JavaScript 对象文字。您可以使用req['header-name']访问任何标题。
清单 6-6 。根据客户请求打印输出请求标题
$ node 2defaultheaders.js
server running on port 3000
request headers...
{ 'user-agent': 'curl/7.30.0',
host: '127.0.0.1:3000',
accept: '*/*',
connection: 'Keep-Alive' }
注意维基百科在
http://en.wikipedia.org/wiki/List_of_HTTP_status_codes有很好的 HTTP 状态代码列表。这包括不属于 HTTP/1.1 规范的代码,这些代码将在http://tools.ietf.org/html/rfc2616中描述。
使用调试代理
帮助您探索和试验 HTTP 的一个很好的工具是 web 调试代理。调试代理是位于客户机和服务器之间的应用,它记录客户机和服务器之间交换的所有请求和响应。图 6-1 简要概述了这种交换将如何发生。
图 6-1 。web 调试代理位于客户端和服务器之间
一个非常受欢迎的免费调试代理是 fiddler ( www.telerik.com/fiddler),它有一个简单的一键安装程序,可用于 Windows 和 Mac OS X。(注意:在 Mac OS X 上,你需要安装 mono www.mono-project.com/download/才能安装 fiddler)。一旦启动 fiddler,默认情况下它会监听端口 8888。您需要告诉客户端应用使用代理来连接到服务器。对于curl,您可以使用-x(使用代理)选项来实现。启动我们刚刚创建的简单服务器并启动 fiddler。然后运行下面的命令(清单 6-7 )使用 fiddler 作为代理发出一个客户端请求。
清单 6-7 。发出指定代理服务器的 curl 请求
$ curl http://127.0.0.1:3000 -x 127.0.0.1:8888
hello client!
由于 fiddler 正在运行,它将捕获请求和响应。正如你从图 6-2 中看到的,在服务器响应中从服务器发送的实际数据被略微编码,如清单 6-8 所示。
图 6-2 。Fiddler 展示了 HTTP 客户端请求和服务器响应的完整细节
清单 6-8 。服务器响应消息正文
d
hello client!
0
这种编码是因为,默认情况下,Node.js 试图将响应流式传输到客户机。您会看到Transfer-Encoding服务器响应头被设置为chunked。分块传输编码是 HTTP 协议的一种数据传输机制,允许你使用块(也称为流)发送数据。在分块传输中,传输的大小(十六进制)在数据块本身之前发送,因此接收方可以知道它何时完成了该数据块的数据接收。Node.js 发送了 d(十进制 13),因为那是']'的长度。通过发送长度为 0 的块(因此结尾为 0)来终止传输。所有这些都由内置的 Node.js HTTP 服务器来完成。
响应流的关键成员
除了响应实现了一个可写的流之外,您还需要了解一些其他有用的方法。响应分为两部分:编写头部和编写主体。这是因为主体可能包含需要流式传输的大量数据。报头指定了该数据将如何呈现,并且需要由客户端在这样的流式传输可以开始之前进行解释。
一旦您调用了response.write或response.end,您设置的 HTTP 头就会被发送,随后是您想要编写的正文部分。此后,您不能再修改标题。在任何时候,您都可以使用只读的response.headersSen t 布尔值来检查报头是否被发送。
设置状态代码
默认情况下,状态代码将为 200 OK。只要没有发送消息头,就可以使用statusCode响应成员显式设置状态代码(例如,要发送 404 NOT FOUND,可以使用下面的代码:
response.statusCode = 404;
设置标题
您可以使用response.setHeader(name, value)成员函数将响应中的任何 HTTP 头显式排队。您需要设置的一个常见头是响应的Content-Type,以便客户端知道如何解释服务器在主体中发送的数据。例如,如果您正在向客户端发送一个 HTML 文件,您应该将Content-Type设置为text/html,您可以使用以下代码来实现:
response.setHeader("Content-Type", "text/html");
内容类型头的值的正式术语是 MIME 类型。一些关键内容类型的 MIME 类型如表 6-1 所示。
表 6-1 。流行的 MIME 类型
|
名字
|
MIME 类型
| | --- | --- | | 超文本标记语言 | 文本/html | | 层叠样式表 | 文本/css | | Java Script 语言 | 应用/javascript | | JavaScript 对象符号(JSON) | 应用/json | | JPEG 图像 | 图像/jpeg | | 便携式网络图形(PNG) | 影像/png |
对于这个以及更多的 mime 类型,有一个简单的 NPM 包叫做 mime ( npm install mime),你可以用它从一个文件扩展名中获取官方的 MIME 类型。清单 6-9 展示了如何使用它。
清单 6-9 。演示使用哑剧 NPM 包
var mime = require('mime');
mime.lookup('/path/to/file.txt'); // => 'text/plain'
mime.lookup('file.txt'); // => 'text/plain'
mime.lookup('.TXT'); // => 'text/plain'
mime.lookup('htm'); // => 'text/html'
回到我们的头讨论,您可以使用response.getHeader函数获得一个排队等待发送的头:
var contentType = response.getHeader('content-type');
您可以使用response.removeHeader功能从队列中删除标题:
response.removeHeader('Content-Encoding');
仅发送邮件头
当您想要显式地发送消息头(而不仅仅是将它们排队)并将响应移入仅主体模式时,您可以调用response.writeHead成员函数。该函数获取状态代码和可选的头,这些头将被添加到您可能已经使用response. setHeader 排队的任何头中。例如,下面的代码片段将状态代码设置为200,并为提供 HTML 设置了Content-Type标题:
response.writeHead(200, { 'Content-Type': 'text/html' });
请求流的主要成员
请求也是一个可读的流。这对于客户端希望将数据传输到服务器的情况非常有用,例如文件上传。客户端 HTTP 请求也被分成头部和主体部分。我们可以得到关于客户端请求 HTTP 头的有用信息。例如,我们已经看到了request.headers属性,它只是标题名和值的只读映射(JavaScript 对象文字)(如清单 6-10 所示)。
清单 6-10 。演示读取请求头的代码片段
// Prints something like:
//
// { 'user-agent': 'curl/7.30.0',
// host: '127.0.0.1:3000',
// accept: '*/*' }
console.log(request.headers);
要检查单个头,可以像索引任何其他 JavaScript 对象文字一样索引该对象:
console.log(request.headers['user-agent']); // 'curl/7.30.0'
响应请求时需要的一条关键信息是客户机发出请求时使用的 HTTP 方法和 URL。为了创建 RESTful web 应用,这些信息是必需的。您可以从request.method只读属性中获取使用的 HTTP 方法。您可以使用request.url属性获得客户端请求的 URL。例如,考虑以下客户端请求:
GET /resources HTTP/1.1
Accept: */*
在这种情况下,request.method将是GET,request.url将是/resources。
创建自己的文件 Web 服务器
提供基本 HTML
既然我们对响应流和 MIME 类型有了更深的理解,我们可以创建一个简单的 web 服务器,从文件夹中返回 HTML 文件。创建一个简单的 HTML 文件,名为index.html ,我们计划在每次请求获取服务器上的“/”时返回该文件,如清单 6-11 中的所示。
清单 6-11 。2server/public/index.html
<html>
<head>
<title>Hello there</title>
</head>
<body>
You are looking lovely!
</body>
</html>
首先,让我们创建几个实用函数。将功能分解成独立的函数总比分解成一整块代码要好。如果我们收到一个我们不接受的url请求,我们应该返回一个 404(未找到)HTTP 响应。清单 6-12 提供了一个功能来做这件事。
清单 6-12 。一个实用函数返回 404 未找到 HTTP 响应
function send404(response) {
response.writeHead(404, { 'Content-Type': 'text/plain' });
response.write('Error 404: Resource not found.');
response.end();
}
如果我们能够满足请求,我们应该返回 HTTP 200 以及内容的 MIME 类型。返回 HTML 文件非常简单,只需创建一个读取文件流,并通过管道将其发送到响应。清单 6-13 显示了完整的服务器代码。
清单 6-13 。来自 2server/server.js 的代码
var http = require('http');
var fs = require('fs');
function send404(response) {
response.writeHead(404, { 'Content-Type': 'text/plain' });
response.write('Error 404: Resource not found.');
response.end();
}
var server = http.createServer(function (req, res) {
if (req.method == 'GET' && req.url == '/') {
res.writeHead(200, { 'content-type': 'text/html' });
fs.createReadStream('./public/index.html').pipe(res);
}
else {
send404(res);
}
}).listen(3000);
console.log('server running on port 3000');
如果你启动服务器(从第六章/第二章服务器目录运行node server.js)并在http://localhost:3000打开浏览器,你会看到我们之前创建的 HTML 页面(图 6-3 )。
图 6-3 。浏览器显示 index.html 已成功返回
类似地,如果你访问localhost上的任何其他 URL,你会得到一个 404 错误信息(图 6-4 )。
图 6-4 。浏览器显示对不存在的资源请求返回的错误
服务目录
对于快速手写静态文件服务器来说,这是一个好的开始。但是,它只服务于一个文件。让我们把它打开一点,为一个目录的所有内容提供服务。首先,创建一个简单的客户端 JavaScript 文件,在 HTML 加载完成后附加到主体,如清单 6-14 所示。我们计划向服务器请求这个 JavaScript 文件。
清单 6-14 。3serverjs/public/main.js 中的代码
window.onload = function () {
document.body.innerHTML += '<strong>Talk JavaScript with me</strong>';
}
让我们通过在<head>中添加一个脚本标签来加载客户端 JavaScript 文件,从而修改我们的简单 HTML 文件
<script src="./main.js"></script>
现在,如果我们运行相同的旧服务器,当我们的浏览器解析index.html并试图从服务器加载main.js时,我们将得到 404。为了支持 JavaScript 加载,我们需要做以下工作:
- 使用 path 模块根据
request.url属性解析文件系统上文件的路径 - 查看我们是否为请求的文件类型注册了 MIME 类型
- 在我们尝试从文件系统中读取文件之前,请确保该文件存在
基于我们已经知道的,我们可以编写如清单 6-15 所示的服务器。
清单 6-15 。3serverjs/server.js 中的代码
var http = require('http');
var fs = require('fs');
var path = require('path');
function send404(response) {
response.writeHead(404, { 'Content-Type': 'text/plain' });
response.write('Error 404: Resource not found.');
response.end();
}
var mimeLookup = {
'.js': 'application/javascript',
'.html': 'text/html'
};
var server = http.createServer(function (req, res) {
if (req.method == 'GET') {
// resolve file path to filesystem path
var fileurl;
if (req.url == '/') fileurl = '/index.html';
else fileurl = req.url;
var filepath = path.resolve('./public' + fileurl);
// lookup mime type
var fileExt = path.extname(filepath);
var mimeType = mimeLookup[fileExt];
if (!mimeType) {
send404(res);
return;
}
// see if we have that file
fs.exists(filepath, function (exists) {
// if not
if (!exists) {
send404(res);
return;
};
// finally stream the file
res.writeHead(200, { 'content-type': mimeType });
fs.createReadStream(filepath).pipe(res);
});
}
else {
send404(res);
}
}).listen(3000);
console.log('server running on port 3000');
示例中的大部分代码都是不言自明的,并且突出显示了有趣的部分。如果你现在打开浏览器并访问localhost:3000,你会看到 HTML 被请求,JavaScript 被成功加载并运行(图 6-5 )。
图 6-5 。显示客户端 JavaScript 被成功请求和呈现的浏览器
我们当前的实现仍然缺少很多功能。首先,它无法抵御恶意 URL。例如,您可以利用我们的实现中有一个简单的path.resolve来从服务器文件系统请求任何文件,如清单 6-16 所示(这里我们从服务器请求服务器代码)。
清单 6-16 。演示我们的简单文件服务器中的文件系统列表漏洞
$ curl 127.0.0.1:3000/../server.js
var http = require('http');
var fs = require('fs');
var path = require('path');
...truncated... the rest of server.js
然后是错误处理和文件缓存,这两者在我们的实现中都是缺乏的。拥有从头构建自己的 Node.js web 服务器的知识是非常宝贵的,但是您不必从头构建 web 服务器。社区已经为您完成了,我们将在稍后探索这些选项。
介绍 Connect
正如我们所看到的,core Node.js 模块为构建您自己的 web 应用提供了基本但重要的特性。NPM 上有很多基于此的 web 框架。很流行的一个是 connect ( npm install connect),这是一个中间件框架。
中间件 基本上是位于你的应用代码和一些低级 API 之间的任何软件。Connect 扩展了内置的 HTTP 服务器功能,并添加了一个插件框架。插件充当中间件,因此 connect 是一个中间件框架。
Connect 最近进行了一次彻底检查(connect 3.0),现在核心 connect 只是中间件框架。每个中间件都是自己独立的 NPM 模块,是更大的 Connect/ExpressJS 生态系统的一部分。我们将在下一章探讨这些中间件。在这里,我们重点关注使用 connect 和创作我们自己的中间件。
创建一个基本的连接应用
connect 的核心是connect功能。调用此函数将创建连接调度程序。connect dispatcher 只是一个接受请求/响应参数的函数,这意味着 dispatcher 可以用作http.createServer(我们前面已经看到)的参数。清单 6-17 显示了基本的连接应用。
清单 6-17 。4connect/1basic.js
var connect = require('connect')
, http = require('http');
// Create a connect dispatcher
var app = connect();
// Register with http
http.createServer(app)
.listen(3000);
console.log('server running on port 3000');
如果您运行此服务器,它将为每个客户端请求返回 404(未找到)。这是 connect 提供的内置行为。如果没有一些中间件来处理客户端请求,connect 将返回 404。我们将在下一节创建中间件。
除了可以接受请求和响应对象的函数之外,connect dispatcher 还有一个成员函数use,用于向 connect 注册中间件。当我们创建自己的连接中间件时,我们将很快看到这个函数。
一个效用函数是listen 。我们将在内部调用http.createServer,用它注册 connect dispatcher,就像我们之前展示的那样,(即http.createServer(app)),最后调用创建的服务器的listen函数。所以你可以简单地做清单 6-18 所示的事情,但是知道它仍然是 Node 核心http之上的一个调度器是很有用的。
清单 6-18 。4 连接/2 简单者. js
var connect = require('connect');
// Create a connect dispatcher and register with http
var app = connect()
.listen(3000);
console.log('server running on port 3000');
创建连接中间件
要向 connect 注册中间件,请调用 connect dispatcher 上的'use'成员方法,传递一个函数,该函数有三个参数—请求、响应和下一个回调:
- 请求派生自我们前面看到的 Node.js HTTP 请求类
- response 来自我们前面看到的 Node.js HTTP 响应类
- next 允许您可选地将控制传递给注册了 connect 的下一个中间件,或者通知 connect 一个错误
最简单的无操作(no operation)中间件是不查看请求、不修改响应、不简单地将控制权移交给下一个中间件的中间件,如清单 6-19 所示。
清单 6-19 。5 中间件/1 OOP . js
var connect = require('connect');
// Create a connect dispatcher and register with http
var app = connect()
// register a middleware
.use(function (req, res, next) { next(); })
.listen(3000);
console.log('server running on port 3000');
现在我们已经熟悉了中间件的基础,让我们创建一个记录客户端请求的method和url的中间件,如清单 6-20 所示。
清单 6-20 。5 中间件/2logit.js
var util = require('util');
// a simple logging middleware
function logit(req, res, next) {
util.log(util.format('Request recieved: %s, %s', req.method, req.url));
next();
}
var connect = require('connect');
connect()
.use(logit)
.listen(3000);
让我们创建另一个中间件,它将客户机请求回显给客户机。由于客户端请求是一个读流,而响应是一个写流,我们可以简单地通过管道传输这两个流,如清单 6-21 所示。
清单 6-21 。5 中间件/3echo.js
function echo(req, res, next) {
req.pipe(res);
}
var connect = require('connect');
connect()
.use(echo)
.listen(3000);
现在,如果您运行这个应用并发出 curl 请求,请求体(-d,即curl的数据参数)将成为响应体:
$ curl http://127.0.0.1:3000/ -d "hello world!"
hello world!
通过路径前缀挂载中间件
use函数采用可选的第一个参数来指定将触发指定中间件的端点。这被称为挂载,因为它类似于操作系统磁盘挂载。例如,假设我们希望仅在请求“/echo”时回应。对于所有其他请求,我们将返回消息“Wassup”。这可以实现,如清单 6-22 所示。
清单 6-22 。5 中间件/4prefix.js
function echo(req, res, next) {
req.pipe(res);
}
var connect = require('connect');
connect()
.use('/echo', echo)
.use(function (req, res) { res.end('Wassup!'); })
.listen(3000);
所有以“/echo”开头的请求将由echo中间件处理,而其他请求将被传递给我们的Wassup!响应器。正如你在清单 6-23 中看到的,它的行为和预期的一样。
清单 6-23 。演示安装
$ curl http://127.0.0.1:3000/echo -d "hello world!"
hello world!
$ curl http://127.0.0.1:3000/ -d "hello world!"
Wassup!
需要路径前缀的一个简单例子是在特定的前缀(例如,'/public ')托管静态文件中间件。
挂载的另一个优点是,它允许您轻松地更改 URL,而不需要更新中间件。你的中间件应该而不是检查req.url。假设它被安装在需要进行处理的地方。
使用对象作为中间件
作为一个中间件作者,您可以选择使用一个对象(而不是一个简单的函数)来创建一个中间件,只要该对象有一个handle方法。例如,echo中间件作为一个对象将如清单 6-24 所示。
清单 6-24 。5 中间件/5object.js
var echo = {
handle: function (req, res, next) {
req.pipe(res);
}
};
var connect = require('connect');
connect()
.use(echo)
.listen(3000);
这允许您使用类实例作为中间件,只要它们有一个句柄成员函数。这只是为了方便起见,你可以放心地忽略这一点。
创建可配置的中间件
您可以使用 JavaScript 闭包的力量来创建可配置的中间件。例如,在清单 6-25 中,我们展示了一个中间件,它总是根据它的配置返回相同的消息。配置message由我们返回的函数在闭包中捕获。
清单 6-25 。5 中间件/6configurable.js
// Configurable middleware creator
function greeter(message) {
return function (req, res, next) {
res.end(message);
};
}
var helloWorldGreeter = greeter('Hello world!');
var heyThereGreeter = greeter('Hey there!');
var connect = require('connect');
connect()
.use('/hello', helloWorldGreeter)
.use('/hey', heyThereGreeter)
.listen(3000);
结果显示在清单 6-26 中。
清单 6-26 。演示如何使用已配置的中间件
$ curl http://127.0.0.1:3000/hello
Hello world!
$ curl http://127.0.0.1:3000/hey
Hey there!
连锁的力量
中间件的链接很棒有很多原因。例如,它允许中间件共享处理请求和响应的功能。您还可以使用它来提供授权和身份验证。让我们考虑几个实际的例子。
共享请求/响应信息
传递到每个中间件的请求和响应对象是可变的和共享的。您可以利用这一点让一个中间件为您部分处理请求,使它更容易被后来的中间件使用。作为一个例子,考虑一个简单的中间件,如果它检测到这是一个 JSON 请求,它试图将主体处理成一个 JavaScript 对象,如清单 6-27 所示。
清单 6-27 。来自 6chain/1parse.js 的片段
function parseJSON(req, res, next) {
if (req.headers['content-type'] == 'application/json') {
// Load all the data
var readData = '';
req.on('readable', function () {
readData += req.read();
});
// Try to parse
req.on('end', function () {
try {
req.body = JSON.parse(readData);
}
catch (e) { }
next();
})
}
else {
next();
}
}
下面是它的工作原理:
- 它只是检查客户机请求是否属于类型
application/json。如果没有,它将控制权传递给下一个中间件。 - 否则,它等待客户机请求完全传输到服务器,一旦完成,就尝试使用
JSON.parse解析数据。 - 如果成功,则设置
req.body。 - 无论 JSON 是否被解析和
req.body是否被设置,我们仍然将控制权传递给下一个中间件。
因为链接了在我们的parseJSON之后出现的任何中间件,如果请求包含有效的 JSON,中间件将访问在req.body中解析的 JSON 对象。在清单 6-28 中,我们有一个简单的连接服务器,它添加了一个中间件,如果找到有效的 JSON,这个中间件使用parseJSON的结果告诉客户端req.body.foo的值。
清单 6-28 。来自 6chain/1parse.js 的片段
var connect = require('connect');
connect()
.use(parseJSON)
.use(function (req, res) {
if (req.body) {
res.end('JSON parsed!, value of foo: '+ req.body.foo);
}
else {
res.end('no JSON detected!');
}
})
.listen(3000);
如果您使用curl测试它,您将看到传递的 JSON 对象的foo成员的值(如果存在的话)。否则,如果你传递一个无效的 JSON 或非 JSON 请求,你将得到“没有检测到 JSON”的消息,如清单 6-29 所示。
清单 6-29 。parseJSON 中间件的运行演示
$ curl http://127.0.0.1:3000/ -H "content-type: application/json" -d "{\"foo\":123}"
JSON parsed!, value of foo: 123
$ curl http://127.0.0.1:3000/ -H "content-type: application/json" -d "{\"foo\":123,}"
no JSON detected!
链接示例:验证请求/限制访问
因为我们需要通过调用 next 显式地将控制传递给下一个中间件,所以我们可以通过不调用 next 并自己终止响应(res.end)来随时选择性地停止执行。
让我们实现一个基本访问授权中间件,如果客户端请求没有正确的凭证,它将返回 401 未授权。基本授权是一个简单的标准化协议,其中每个客户端请求都需要包含一个Authorization头。标头需要按如下方式构建:
- 用户名和密码组合成一个字符串:“用户名:密码”。
- 然后使用 Base64 对结果字符串进行编码。
- 然后,授权方法和一个空格(即“Basic ”)被放在编码字符串的前面。
一个示例客户端报头是Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==。
此外,为了通知客户机它需要添加一个Authorization头,服务器应该在拒绝客户机请求的响应中发送一个WWW-Authenticate头。这很容易做到,我们可以创建一个如清单 6-30 所示的效用函数。
清单 6-30 。实用程序函数发送 401 未授权的 HTTP 响应,请求基本授权
function send401(){
res.writeHead(401 , {'WWW-Authenticate': 'Basic'});
res.end();
}
为了解码客户端的Authorization头,我们反向执行创建步骤。换句话说,我们通过在空间上拆分的方式去掉授权方法,也就是“”,加载第二段为 Base64,转换成一个简单的字符串。最后,我们使用“:”来获取用户名/密码。代码如清单 6-31 所示。
清单 6-31 。读取客户端发送的基本身份验证凭据的代码段
var auth = new Buffer(authHeader.split(' ')[1], 'base64').toString().split(':');
var user = auth[0];
var pass = auth[1];
我们现在有足够的信息来创建添加基本访问授权的中间件,如清单 6-32 所示。
清单 6-32 。7auth/1auth.js 中列出身份验证中间件的代码段
function auth(req, res, next) {
function send401(){
res.writeHead(401 , {'WWW-Authenticate': 'Basic'});
res.end();
}
var authHeader = req.headers.authorization;
if (!authHeader) {
send401();
return;
}
var auth = new Buffer(authHeader.split(' ')[1], 'base64').toString().split(':');
var user = auth[0];
var pass = auth[1];
if (user == 'foo' && pass == 'bar') {
next(); // all good
}
else {
send401();
}
}
作为一个演示,这个中间件目前只接受username = foo和password = bar,但是如果我们想的话,我们可以很容易地使它可配置。请注意,只有当访问被授权时,我们才调用next(),这样就可以使用它来提供针对不良凭证的保护。我们在清单 6-33 中演示了如何使用这个中间件。
清单 6-33 。来自 7auth/1auth.js 的代码段演示了如何使用身份验证中间件
var connect = require('connect');
connect()
.use(auth)
.use(function (req, res) { res.end('Authorized!'); })
.listen(3000);
我们来测试一下(从7auth/1auth.js开始启动服务器)。如果你在http://localhost:3000点打开浏览器,你会看到一个熟悉的用户名/密码提示。因为中间件响应了一个 401 未授权响应和一个WWW-Authenticate头,所以浏览器要求您提供凭证。(参见图 6-6 )。
图 6-6 。当服务器请求基本身份验证时,浏览器内置对话框
如果您键入了错误的用户名/密码,它将继续提示您输入凭据,因为我们将不断返回 401,直到成功进行身份验证尝试。在这种情况下,我们的中间件将控制权传递给下一个中间件,后者只是返回消息“Authorized!”如图图 6-7 所示。
图 6-7 。浏览器发送有效凭据时的服务器响应
我们可以重用这个认证中间件来限制特定的区域。(例如,在清单 6-34 中,只有'/admin '受到限制。)默认情况下,它会落到公共处理程序。请注意,我们根本不需要更改中间件代码来实现这一点。
清单 6-34 。来自 7auth/2authArea.js 的代码段演示了如何为管理区域安装
connect()
.use('/admin', auth)
.use('/admin', function (req, res) { res.end('Authorized!'); })
.use(function (req, res) { res.end('Public') })
.listen(3000);
这很好地总结了一个可链接的、可选的中间件框架的力量。
引发连接错误
最后值得一提的是,您可以选择向'next'传递一个参数,通知 connect 您的中间件发生了错误。链中没有其他中间件被调用,错误消息被发送到客户端请求,HTTP 状态代码为 500 内部服务器错误。清单 6-35 是演示这一点的一个简单例子。
清单 6-35 。8 错误/1 错误. js
var connect = require('connect');
connect()
.use(function (req, res, next) { next('An error has occurred!') })
.use(function (req, res, next) { res.end('I will never get called'); })
.listen(3000);
如果您运行此服务器并发出请求,您将收到错误消息“发生了错误!”:
$ curl http://127.0.0.1:3000
An error has occurred!
可以看到第二个中间件从未被调用过。此外,通常使用实际的Error对象,而不是我们在这里使用的字符串——换句话说,next(new Error('An error has occurred'))。
如果您想处理其他中间件错误,您可以注册一个带有四个参数的中间件(而不是我们已经看到的三个req、res、next)。这种情况下的第一个参数是错误,所以error,req,res,next是四个参数。这样的中间件只有出错时才会调用。清单 6-36 展示了它是如何工作的。
清单 6-36 。8 错误/2 错误处理程序. js
var connect = require('connect');
connect()
.use(function (req, res, next) { next(new Error('Big bad error details')); })
.use(function (req, res, next) { res.end('I will never get called'); })
.use(function (err, req, res, next) {
// Log the error on the server
console.log('Error handled:', err.message);
console.log('Stacktrace:', err.stack);
// inform the client
res.writeHead(500);
res.end('Unable to process the request');
})
.listen(3000);
如果您运行此服务器并发出请求,您将仅获得我们在响应中有意发送的信息:
$ curl http://127.0.0.1:3000
Unable to process the request
然而,服务器日志的信息更加丰富,如清单 6-37 所示(因为我们使用了new Error)。
清单 6-37 。展示来自中间件错误处理程序的服务器日志
node 2errorHandler.js
Error handled: Big bad error details
Stacktrace: Error: Big bad error details
at Object.handle (2errorHandler.js:4:43)
at next (/node_modules/connect/lib/proto.js:194:15)
... truncated ...
还要注意,这个错误处理程序是为在这个错误处理程序之前发生在中间件任何地方的所有错误调用的。比如在清单 6-38 中,我们故意抛出一个错误,而不是next(error),它仍然被正确处理。
清单 6-38 。8error/3throwErrorHandler.js
var connect = require('connect');
connect()
.use(function () { throw new Error('Big bad error details'); })
.use(function (req, res, next) { res.end('I will never get called'); })
.use(function (err, req, res, next) {
console.log('Error handled:', err.message);
res.writeHead(500);
res.end('Server error!');
})
.listen(3000);
这种错误处理方法实际上使 connect 比 raw http.createServer更安全,在 rawhttp.createServer中,未处理的错误会使服务器崩溃。
请注意,错误处理程序仅在出现错误时调用。例如,在清单 6-39 所示的服务器中,它永远不会被调用。所以你永远不需要检查错误处理程序中的错误——如果错误处理程序被调用,它应该一直在那里。
清单 6-39 。8error/4onlyCalledOnError.js
var connect = require('connect');
connect()
.use(function (req, res, next) { next(); })
.use(function (err, req, res, next) {
res.end('Error occured!');
})
.use(function (req, res, next) { res.end('No error'); })
.listen(3000);
最后,值得注意的是,您可以选择从错误处理程序调用 next,并将控制权传递给链中的任何其他中间件。
安全超文本传输协议
HTTPS 是在许多以前的 web 框架中难以实现的东西之一。Node.js 对 HTTPS 内置有一流的支持。在我们展示通过 Node.js 使用 HTTPS 有多简单之前,我们将为初学者提供一个快速概述。
不对称密码术
使 HTTPS 成为可能的基本概念是公钥加密(也称为非对称加密)。对于这种类型的加密,您需要两个加密密钥:
- 一个每个人都知道的公钥,甚至可能是恶意用户
- 只有你知道的私钥
而且,
- 公钥用于加密。这意味着每个人都可以和你交谈。
- 解密需要一个私钥。这意味着只有你能理解别人说的话!
你可以看到,它让每个人都可以安全地与你交谈,而没有偷听的机会。(参见图 6-8 )。
图 6-8 。使用公钥/私钥组合可以让别人安全地与你交谈
出于您的兴趣,值得一提的是,有大量的算法可以轻松计算这样的密钥对。优势在于,从相应的公钥中确定一个正确生成的私钥几乎是不可能的(在计算上是不可行的)。
通过两种方式确保通信安全
因此,共享公钥使得与服务器开始对话变得安全。你如何安全地回嘴?很简单。用户(基本上是浏览器)即时生成一个预主密钥,并以加密消息(用服务器公钥加密)的形式安全地发送给服务器。预主密钥用于生成会话密钥,该密钥仅对客户端和服务器之间的会话有效。现在,如果服务器和客户机用这个共享的会话密钥加密消息,它们就可以互相交谈了。
这是 SSL(或称为 TLS 的新版本)握手的简化描述。握手之后,实际的标准 HTTP 对话发生,其中整个 HTTP 消息(包括头)使用会话密钥加密。HTTPS 就是通过 SSL 安全通信通道进行的 HTTP 通信。
生成密钥
在本节中,我们将自己生成公钥/私钥对。为此,我们将使用 OpenSSL 命令行工具,它在 Mac OS X 和 Windows 上都可用(作为 Cygwin 的一部分,我们在curl中也需要它)。
为了生成私钥,我们指定一个加密算法及其复杂性。使用清单 6-40 中的命令生成一个 1024 位的 RSA 密钥。创建的key.pem文件的一个样本如清单 6-41 所示。
清单 6-40 。生成私钥
$ openssl genrsa 1024 > key.pem
清单 6-41 。生成的私钥示例
-----BEGIN RSA PRIVATE KEY-----
MIICXQIBAAKBgQDJW6ZZLTawfDyhR8v6/nQMX+PIGtPMO8n7OwRdv1AqqW7a+5Au
... truncated ...
0j/PimhgOvsD0TDxccytEsLgoldWcx4YLGjzDtoyyaVj
-----END RSA PRIVATE KEY-----
接下来,我们需要生成相应的公钥,您可以使用以下命令来完成:
$ openssl req -x509 -new -key key.pem > cert.pem
一旦你运行这个命令,你会被问一些问题,比如你的国家名称,所有这些都有一个简单的答案。这创建了一个我们可以与世界共享的公共证书,看起来像清单 6-42 中的代码。
清单 6-42 。公共证书样本
-----BEGIN CERTIFICATE-----
MIIDLjCCApegAwIBAgIJAMdFJbVshZIGMA0GCSqGSIb3DQEBBQUAMG4xCzAJBgNV
... truncated ...
9i+ULx/F6dKgwTLV5L5urT4kIOitM6+QyT+bd1uZ3MXeKaaaJ+dh93aFuFVvxZ3d
t2E=
-----END CERTIFICATE-----
现在让我们用这些钥匙。
创建一个 HTTPS 服务器
Node.js 有一个核心的https模块,您可以使用require('https')来加载它。它有一个createServer函数,其行为与http.createServer完全相同,除了它额外采用了第一个'options'参数,您可以用它来提供公钥/私钥。
清单 6-43 是我们在本章前面看到的简单 HTTP 服务器,更新后可以使用 HTTPS。
清单 6-43 。9ssl/1basic.js
var https = require('https');
var fs = require('fs');
var options = {
key: fs.readFileSync('./key.pem'),
cert: fs.readFileSync('./cert.pem')
};
https.createServer(options, function (req, res) {
res.end('hello client!');
}).listen(3000);
如您所见,侦听器功能完全相同—它传递一个我们已经熟悉的请求和响应对象。你可以使用curl来测试,如清单 6-44 中的所示。k 参数允许不安全的(insekure)/未验证的证书工作,因为我们自己创建了我们的证书。
清单 6-44 。从 curl 测试我们的 HTTPS 服务器
$ curl https://localhost:3000 -k
hello client!
因为http.createServer和https.createServer的监听器函数是相同的,所以在 HTTPS 中使用 connect 就像在 HTTP 中使用它一样简单。这在清单 6-45 中有演示。
清单 6-45 。9ssl/2connect.js
var https = require('https');
var fs = require('fs');
var options = {
key: fs.readFileSync('./key.pem'),
cert: fs.readFileSync('./cert.pem')
};
var connect = require('connect');
// Create a connect dispatcher
var app = connect();
// Register with https
https.createServer(options, app)
.listen(3000);
对于面向公众的网站,您需要从可信(用户信任的)认证机构(例如,VeriSign 和 Thawte)获得 SSL 证书。证书颁发机构可以保证这个公钥对于跟你说话来说是唯一安全的。这是因为在没有这种权威的情况下,可能有人坐在客户端和服务器之间(称为中间人 MitM 攻击)并声称自己是服务器,向你发送他们的公钥,而不是服务器公钥。这样,MitM 可以解密您的消息(因为他们有相应的私钥),监视它,然后通过用服务器公钥重新加密它,将它转发给服务器。
默认情况下使用 HTTPS
如果您只使用 HTTPS 并将所有 HTTP 流量重定向到使用 HTTPS,那么创建一个安全的网站会更容易确保。现在很多网站都这么做。例如,如果你访问http://www.facebook.com,它会向你发送一个 301(永久移动)HTTP 响应,其中Location头设置为https://www.facebook.com。你可以自己试试;如果在浏览器中打开http://www.facebook.com,地址将变为https://www.facebook.com。因此,只要有可能,就使用 HTTPS 并将所有 HTTP 流量重定向到 HTTPS。
虽然可能,但在同一个端口上运行 HTTP 和 HTTPS 服务器并不容易。然而,你不必。按照惯例,当您请求一个 HTTP 网站而没有指定端口(例如, http ://127.0.0.1)时,客户端会尝试连接到端口 80。然而,如果你请求一个 HTTPS 网站而没有指定端口(比如https??),客户端会尝试连接到服务器上的端口 443。这允许您在端口 443 上运行一个 HTTPS 服务器,在端口 80 上运行一个 HTTP 服务器,简单地重定向客户端请求以使用 HTTPS。完成这项工作的代码如清单 6-46 所示。
清单 6-46 。10 重定向/secure.js
var https = require('https');
var fs = require('fs');
var options = {
key: fs.readFileSync('./key.pem'),
cert: fs.readFileSync('./cert.pem')
};
https.createServer(options, function (req, res) {
res.end('secure!');
}).listen(443);
// Redirect from http port 80 to https
var http = require('http');
http.createServer(function (req, res) {
res.writeHead(301, { "Location": "https://" + req.headers['host'] + req.url });
res.end();
}).listen(80);
现在运行这个服务器。(在 Mac OS X 上,您需要使用sudo—也就是sudo node 10redirect/secure.js—来运行,因为只有超级用户可以监听端口 80 和端口 443)。如果你在浏览器中访问http://127.0.0.1,你会注意到网址变成了https://127.0.0.1,你会得到消息“安全!”。这是因为浏览器知道如何处理 HTTP 重定向响应,并为您无声地完成它。要查看内部工作原理,你可以使用curl。你可以分别在清单 6-47 的和清单 6-48 的中看到 HTTP 和 HTTPS 场景。
清单 6-47 。向我们的安全服务器发出 HTTPS 请求
$ curl https://127.0.0.1 -k
secure!
清单 6-48 。向我们的安全服务器发出 HTTP 请求会得到重定向响应
$ curl http://127.0.0.1 -i
HTTP/1.1 301 Moved Permanently
Location: https://127.0.0.1/
Date: Sun, 01 Jun 2014 06:15:16 GMT
Connection: keep-alive
Transfer-Encoding: chunked
如果您的所有通信都通过 HTTPS 进行,您可以使用基本的 HTML 输入表单接受客户端密码,而不用担心被拦截。
摘要
本章开始时,我们对 core Node.js 中的内置 HTTP 功能进行了彻底的检查。这种仔细的检查是必要的,因为社区框架依赖它来提供高级功能,并且很好地掌握基础知识将有助于使您成为一名成功的 Node.js 开发人员。
随后,我们深入研究了连接中间件框架。Connect 允许您共享和创建可管理的 HTTP 插件。这有助于您管理复杂性,从而提高您的生产力,因为您可以划分并征服软件需求。Connect 专注于 HTTP 应用,而不是非常专注于网站,这是 express js(connect 团队的另一个框架)非常适合的。我们将在本书的后面讨论 ExpressJS。但是要知道所有的 connect 中间件都可以与 ExpressJS 一起使用,因为它具有相同的中间件约定(即use成员函数)。
虽然我们在这一章中写了一些中间件,但是 Node.js 社区已经写了很多优秀的(经过测试的和安全的)中间件。我们将在下一章研究这些。
最后,我们展示了 Node.js 中的 HTTPS 有多简单。只要有可能,您应该对所有服务器使用 HTTPS。
Node.js 专注于应用服务器。这意味着你可以深深地嵌入 HTTP,并完全接受网络提供的所有创新。事实上,网络协议开发人员转向 Node.js 进行原型开发并不少见,这是因为 node . js 的访问级别较低,但内存管理非常好。
七、Express 简介
如果您今天要制作 Node.js 网站,您可能会使用 Express web 应用框架。
在前一章中,我们讨论了 Node.js 提供的 HTTP/HTTPS 功能的核心。我们还演示了 Connect 如何在原始的createServer调用之上提供一个中间件框架。ExpressJS 提供了 Connect 所提供的一切(与我们在上一章中看到的use函数相同,还有一个调度程序),并且走得更远。它构成了许多 web 应用的基础,我们将在本章中探讨它。
在这个过程中,我们将介绍一些与成为 HTTP/Node.js 专家相关的概念。
快速基础
快递在 NPM 有express ( npm install express)。让我们从 Connect 的共同点开始。Express 来自开发 Connect 的同一个开发团队。当你调用require('express')时,你会得到一个函数,你可以调用它来创建一个快速应用。这个应用具有我们在前一章中看到的连接调度程序的所有行为。例如,它可以接受使用'use'函数的中间件,并且可以向http.createServer注册,如清单 7-1 所示。
清单 7-1 。intro/1basic.js
var express = require('express'),
http = require('http');
// Create an express application
var app = express()
// register a middleware
.use(function (req, res, next) {
res.end('hello express!');
});
// Register with http
http.createServer(app)
.listen(3000);
能够注册为 HTTP 的侦听器允许您使用 HTTPS(与 Connect 相同)。类似于 Connect,Express 提供了一个实用程序listen函数向http注册自己。最简单的 Express 应用可以像清单 7-2 中的一样简单。
清单 7-2 。intro/2simpler.js
var express = require('express');
express()
.use(function (req, res, next) {
res.end('hello express!');
})
.listen(3000);
此外,错误处理与 Connect 的工作方式相同,错误处理中间件接受四个参数。正如你所看到的,我们从上一章学到的知识在这里都适用。
流行的 Connect/ExpressJS 中间件
所有连接中间件都是 Express 中间件。然而,并不是所有的 Express 中间件都是 Connect 中间件,因为为了方便起见,Express 对请求和响应做了更多的修改。对于大多数简单的中间件来说,这不是一个问题,但这是一个你需要知道的事实。
在这一节中,我们将展示核心团队中流行的连接/快捷中间件 。
提供静态页面
最常见的事情之一,你会想马上是服务静态网站内容。serve-static中间件(npm install serve-static)就是专门为此设计的。我们在前一章中介绍了一个类似的概念,但是我们没有完成这件事(例如,容易受到基于路径的攻击),因为虽然这些概念很有价值,但是您最好使用serve-static。(参见清单 7-3 。)
清单 7-3 。static/1basic.js
var express = require('express');
var serveStatic = require('serve-static');
var app = express()
.use(serveStatic(__dirname + '/public'))
.listen(3000);
使用 node 运行这段代码,您将得到一个简单的 web 服务器,它提供来自/public目录的 web 页面。这个小服务器做了很多好事,包括:
- 设置响应的正确 mime 类型
- 具有良好的 HTTP 响应代码(例如,如果您刷新页面,而 HTML 没有改变,您会注意到它发送的响应是 304 Not Modified,而不是 200 OK。如果你请求一个不存在的文件,你会得到一个 404。如果由于某种原因无法访问该文件,它会发送 500 内部服务器错误响应。)
- 默认情况下,不允许您获取想要提供服务的目录以上的文件(不容易受到前一章中我们的简单服务器中的
../path错误的影响) - 如果路径解析为目录,则为目录中的
index.html提供服务
注意通过使用“
__dirname”,我们确保路径总是相对于当前文件,而不是当前工作目录(CWD)。如果我们从另一个目录运行我们的应用,比如从上一级使用“node static/1basic.js”而不是同一个目录,即“”),CWD 可能与文件目录不同。相对路径名,如']'相对于 CWD 进行解析。利用‘??’,使其独立于 CWD。
您还可以将附加选项作为第二个参数传递给serve-static中间件。例如,要设置它应该查找的索引文件,请使用index选项:
app.use(serveStatic(__dirname + '/public', {'index': ['default.html', 'default.htm']}))
Express 将中间件作为其 NPM 包的一部分。所以如果你使用 Express,你可以使用express.static,它是require('serve-static')的别名。清单 7-4 展示了如何重写这个例子。
清单 7-4 。static/2static.js
var express = require('express');
var app = express()
.use(express.static(__dirname + '/public'))
.listen(3000);
列表目录内容
要列出一个目录的内容,有一个serve-index (npm install serve-index)中间件。因为它只列出了目录的内容,所以通常将它与serve-static中间件结合使用,以允许用户获取文件。清单 7-5 演示了它的用法。
清单 7-5 。serveindex/basic.js
var express = require('express');
var serveIndex = require('serve-index');
var app = express()
.use(express.static(__dirname + '/public'))
.use(serveIndex(__dirname + '/public'))
.listen(3000);
默认情况下,它会给出一个漂亮的带有搜索框的目录列表页面,如图 7-1 所示。
图 7-1 。服务索引中间件的默认目录列表
请注意,我们在serve-index之前注册了serve-static,因为它给了serve-static一个提供索引文件的机会(如果有的话),而不是serve-index用一个目录列表来响应。
接受 JSON 请求和 HTML 表单输入
主体解析是将基于字符串的客户端请求主体解析成 JavaScript 对象的行为,您可以在您的应用代码中轻松使用该对象。这是 web 开发中一个非常常见的任务,这使得中间件成为你工具箱中的必备工具。它只做以下两件事:
- 如果
content-type匹配 JSON (application/JSON)或用户提交的 HTML 表单(浏览器将其作为 MIME 类型application/x-www-form-urlencoded发送),则将请求体解析为 JavaScript 对象 - 将这个 JavaScript 对象(如果解析成功)放在
req.body中,以便在以后的中间件中访问
清单 7-6 提供了一个简单的例子,根据body-parser中间件解析的内容来响应客户端。
清单 7-6 。bodyparser/basic.js
var express = require('express');
var bodyParser = require('body-parser');
var app = express()
.use(bodyParser())
.use(function (req, res) {
if (req.body.foo) {
res.end('Body parsed! Value of foo: ' + req.body.foo);
}
else {
res.end('Body does not have foo!');
}
})
.use(function (err, req, res, next) {
res.end('Invalid body!');
})
.listen(3000);
如果请求主体不包含任何JSON或urlencoded有效载荷,那么主体解析器将req.body设置为空对象。然而,如果客户端发送了一个无效的 JSON 内容,它会引发一个明显的错误,您可以使用一个错误处理中间件来处理这个错误(如清单 7-7 所示)。
我们可以像在上一章测试我们自己的 JSON 中间件一样测试它。首先,我们发送一个有效的 JSON 负载,然后我们发送一些无效的 JSON,如清单 7-7 所示。
清单 7-7 。使用 JSON 内容测试 bodyparser/basic.js
$ curl http://127.0.0.1:3000/ -H "content-type: application/json" -d "{\"foo\":123}"
Body parsed! Value of foo: 123
$ curl http://127.0.0.1:3000/ -H "content-type: application/json" -d "{\"foo\":123,}"
Invalid body!
如果客户端发送一个 HTML 表单数据(而不是 JSON),中间件允许我们使用相同的代码,如清单 7-8 所示。
清单 7-8 。使用 HTML 表单内容测试 bodyparser/basic.js
$ curl http://127.0.0.1:3000/ --data-urlencode "foo=123"
Body parsed! Value of foo: 123
在前一章中,我们创建了自己的 JSON 解析器。我们的简单实现存在一些问题。首先,它很容易受到恶意客户端的攻击,导致服务器内存耗尽,因为我们需要在调用JSON.parse之前加载整个主体。默认情况下,body-parser将只解析最大 100KB 的有效负载。这是一个很好的默认设置。您可以在创建中间件时通过传递一个选项参数来指定不同的限制,比如use('/api/v1',bodyParser({limit:'1mb'}))。
注意
Body-parser内部使用字节(npm install bytes ) NPM 包解析极限值。这是一个简单的包,它导出了一个函数(var bytes = require('bytes')),允许您将常见的字节字符串解析为字节数,如bytes('1kb'), bytes('2mb')、bytes('3gb')。
记住,所有的中间件都可以安装在特定的路径上,body-parser也不例外。因此,如果您只想对某些 API 端点(如"/api/v1")进行主体解析,您可以使用use('/api/v1',bodyParser())。
处理 Cookies
一个 cookie 是从 Web 服务器发送并存储在用户网络浏览器中的一些数据。每次用户的浏览器向 web 服务器发出请求时,web 浏览器都会发回它从服务器收到的 cookie。cookie 为创建用户会话提供了一个很好的基础。
Express response 对象包含一些有用的成员函数来设置客户端 cookies。要设置 cookie,调用res. cookie (cookieName,value,[options])函数。例如,清单 7-9 中的代码会将一个名为'name'的 cookie 设置为'foo':
清单 7-9 。cookie/1basic.js
var express = require('express');
var app = express()
.use(function (req, res) {
res.cookie('name', 'foo');
res.end('Hello!');
})
.listen(3000);
如果您运行这个 web 服务器,您将在响应中看到'set-cookie'头,如清单 7-10 中的所示。
清单 7-10 。用 curl 测试 cookie/1 basic . js
$ curl http://127.0.0.1:3000 -i
HTTP/1.1 200 OK
X-Powered-By: Express
Set-Cookie: name=foo; Path=/
Date: Sun, 08 Jun 2014 01:02:23 GMT
Connection: keep-alive
Transfer-Encoding: chunked
Hello!
如果此响应由浏览器处理,那么如果服务器上的路径以“/”开头,浏览器将总是发送名为“name”且值为“foo”的 cookie。客户端在“cookie”报头中发送 cookie。在清单 7-11 中,修改我们的服务器来记录客户端请求中发送的任何 cookies。
清单 7-11 。cookie/2show.js
var express = require('express');
var app = express()
.use(function (req, res) {
console.log('---client request cookies header:\n', req.headers['cookie']);
res.cookie('name', 'foo');
res.end('Hello!');
})
.listen(3000);
如果您在浏览器中打开http://localhost:3000,您将会看到服务器控制台中记录的 cookie:
---client request cookies header:
name=foo
虽然头很有用,但是您需要将它解析成 JavaScript 对象。这就是 cookie 解析器 ( npm install cookie-parser)中间件的用武之地。将这个中间件放入你的队列中,它将解析后的 cookies 填充到'req.cookies'对象中,如清单 7-12 所示,以演示它的用法。
清单 7-12 。cookie/3parsed.js
var express = require('express');
var cookieParser = require('cookie-parser');
var app = express()
.use(cookieParser())
.use(function (req, res) {
if (req.cookies.name) {
console.log('User name:', req.cookies.name);
}
else {
res.cookie('name', 'foo');
}
res.end('Hello!');
})
.listen(3000);
如果您运行这个服务器,它将记录下在客户端请求中找到的name cookie 的值(例如,User name: foo)。否则,它将设置 cookie。该示例还展示了如何通过简单地检查是否在req.cookies对象中设置了特定的键来检查客户端请求中是否存在特定的 cookie。
您也可以使用 Express 提供的res. clearCookie (cookieName, [options])成员函数清除服务器响应中的客户端 cookies。例如,清单 7-13 中的服务器如果没有找到 cookie 就会设置它,如果找到了就会清除它。
清单 7-13 。cookie/4clear.js
var express = require('express');
var cookieParser = require('cookie-parser');
var app = express()
.use(cookieParser())
.use('/toggle', function (req, res) {
if (req.cookies.name) {
res.clearCookie('name');
res.end('name cookie cleared! Was:' + req.cookies.name);
}
else {
res.cookie('name', 'foo');
res.end('name cookie set!');
}
})
.listen(3000);
如果你在浏览器中访问http://localhost:3000/toggle,你会得到消息“名称 cookie 设置!”和“名称 cookie 已清除!”在交替尝试中。在我们清除它之前,我们还向您展示了浏览器发送的 cookie 值(应该是“foo”)。
注意如果你好奇的话,我们会使用之前看到的用于设置初始 cookie 的旧的
set-cookie头来清除 cookie。但是,对于 clearing,该值被设置为空,而 expiry 被设置为 UNIX epoch,即服务器响应中的'Set-Cookie: name=; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT'头。这告诉浏览器删除 cookie。
使用签名防止 Cookie 用户修改
由于 cookie 存储在客户端系统上(并在客户端请求中发送),用户有可能伪造 cookie。我们可以对 cookie 进行数字签名,以检测任何客户端 cookie 伪造。这个特性也是由同一个cookie-parser中间件提供的。
数字签名确保了数据的真实性。快速 cookie 签名是使用一个密钥哈希消息认证码(HMAC) **完成的。**HMAC 是通过获取一个密钥(只有服务器知道)并将其与哈希算法相结合来计算 cookie 内容的哈希。由于秘密只有服务器知道,HMAC 只能由服务器计算和验证。
如果我们使用 Express(通过提供一个密钥)创建一个签名的 cookie,HMAC 值将被附加到我们发送给客户端的 cookie 值中。因此,当客户端在请求中将 cookie 发回给我们时,我们可以查看 cookie 中的 HMAC 值,并将其与重新计算的 HMAC 值进行比较,以检查它是否与内容匹配。如果不匹配,我们知道饼干是坏的,我们可以丢弃它。所有这些都是由cookie-parser中间件为您完成的。
您可以通过将密钥传递给cookie-parser中间件创建函数来为 cookie 签名设置密钥——换句话说,use(cookieParser('optional secret string')).到设置一个签名的 cookie,您只需调用res.cookie(name,value,{ signed:true })(换句话说,名称和值为普通值,并传入一个选项signed=true)。为了读取客户端请求中发送的签名 cookie,使用req.signedCookies就像使用req.cookies一样。在读取时使用不同的属性使您很容易知道 cookie 签名已经过验证。作为一个例子,清单 7-14 展示了我们更新的切换 cookie 服务器,以使用签名 cookie。
清单 7-14 。cookie/5sign.js
var express = require('express');
var cookieParser = require('cookie-parser');
var app = express()
.use(cookieParser('my super secret sign key'))
.use('/toggle', function (req, res) {
if (req.signedCookies.name) {
res.clearCookie('name');
res.end('name cookie cleared! Was:' + req.signedCookies.name);
}
else {
res.cookie('name', 'foo', { signed: true });
res.end('name cookie set!');
}
})
.listen(3000);
httpOnly and Secure
默认情况下,用户的浏览器 JavaScript 可以读取为当前网页设置的 cookie(使用document.cookie)。这使得 cookie 容易受到跨端脚本(XSS)的攻击。也就是说,如果某个恶意用户设法将 JavaScript 注入到您的网站内容中,它将允许该 JavaScript 读取可能包含当前登录用户的敏感信息的 cookies,并将它发送到恶意网站。为了防止 JavaScript 访问 cookies,可以将httpOnly设置为true(res.cookie(name,value,{http only:true}))。这告诉浏览器不应该允许任何 JavaScript 访问这个 cookie,并且只应该在与服务器通信时使用它。
此外,正如我们在前一章中所展示的,你应该在你所有的公共服务器上使用 HTTPS。HTTPS 确保包括cookie报头在内的所有报头都被加密,并且不会受到中间人攻击。要让浏览器永远不要通过 HTTP 发送特定的 cookie,只对 HTTPS 使用,可以设置安全标志,即res.cookie(name,value,{ secure:true })。
基于你现在所知道的,对于敏感的 cookies 你应该总是使用设置为true的httpOnly和secure。当然,安全需要一个 HTTPS 服务器。
设置 Cookie 到期时间
Cookies 非常适合在浏览器中存储与特定用户相关的持久信息。但是,如果没有在Set-Cookie标题中提到的 cookie 到期时间,浏览器会在关闭后清除 cookie!这种 cookie 通常被称为浏览器会话 cookie(因为它只对当前浏览器会话有效)。如果您希望 cookies 持续一段时间,您应该总是设置expiry。您可以通过向setCookie传递一个maxAge选项来做到这一点,这需要这个 cookie 有效并在客户端请求中发送的毫秒数。例如:
res.cookie('foo', 'bar', { maxAge: 900000, httpOnly: true })
基于 Cookie 的会话
为了在同一用户的不同 HTTP 请求之间提供一致的用户体验,通常在客户端 HTTP 请求旁边提供用户会话信息。举个简单的例子,我们可能想知道用户是否已经登录。
Cookies 为我们希望与客户端请求相关联的少量特定于用户的信息提供了良好的基础。但是 API 太低级了。例如,cookie 值只能是字符串。如果想要 JavaScript 对象,需要做 JSON 解析。这就是cookie-session中间件(npm install cookie-session)的用武之地。它允许您使用单个 cookie 来存储您认为与该用户会话相关的信息。
当使用cookie-session中间件时,用户会话对象被公开为req.session。您可以使用对req.session成员的简单赋值来设置或更新一个值。您可以通过从req.session删除一个键来清除一个值。默认情况下,cookie-session中间件必须被传递至少一个密钥,正如我们前面看到的,它使用这个密钥通过签名来确保会话 cookie 的完整性。清单 7-15 给出了一个简单的例子来演示如何使用cookie-session中间件。
清单 7-15 。cookiesession/counter.js
var express = require('express');
var cookieSession = require('cookie-session');
var app = express()
.use(cookieSession({
keys: ['my super secret sign key']
}))
.use('/home', function (req, res) {
if (req.session.views) {
req.session.views++;
}
else{
req.session.views = 1;
}
res.end('Total views for you: ' + req.session.views);
})
.use('/reset',function(req,res){
delete req.session.views;
res.end('Cleared all your views');
})
.listen(3000);
启动该服务器,打开浏览器,访问http://localhost:3000/home查看计数器的增量,访问http://localhost:3000/reset重置计数器。除了像我们演示的那样从会话中清除单个值之外,您还可以通过将req.session设置为null,换句话说就是req.session=null,来删除整个用户会话。
您可以在cookieSession函数中传递附加选项(除了keys)。要指定存储会话的 cookie 名称,可以传入name选项。默认情况下,cookie 名称为express:sess。还支持其他一些 cookie 选项,例如maxage、path、httpOnly(默认为true)和signed(默认为true)。
谨慎使用 Cookie 会话
请注意,cookie 需要在 HTTP 报头中随每个客户端请求一起发送(如果路径匹配),因此大的 cookie 会影响性能。浏览器允许在 cookie 中存储多少信息也有限制(大多数浏览器的一般指导是 4093 字节,每个站点最多 20 个 cookie)。因此,让所有用户信息成为 cookie 的一部分是不可行的。
更强大的会话管理策略是使用数据库来存储用户会话信息,而不是使用 cookies。在这种情况下,您只需在用户 cookie 中存储一个令牌,该令牌将指向我们可以从服务器数据库中读取的会话信息。为此,您可以将express-session中间件(npm install express-session)与数据库(如 Mongo 或 Redis)一起使用。我们将在第八章中看到一个例子。
压缩
多亏了压缩(npm install compression)中间件,在基于 Express 和 Connect 的应用中,通过网络进行 Zip 压缩非常容易。清单 7-16 是一个简单的服务器,它在将大于1kb的页面发送给客户端之前对其进行压缩。
清单 7-16 。compression/compress.js
var express = require('express');
var compression = require('compression');
var app = express()
.use(compression())
.use(express.static(__dirname + '/public'))
.listen(3000);
您可以使用curl通过指定--compressed命令行标志来测试它,如清单 7-17 所示,告诉服务器您可以处理压缩信息。
清单 7-17 。使用 curl 测试 compression/compress.js
$ curl http://127.0.0.1:3000 -i --compressed
HTTP/1.1 200 OK
...truncated...
Content-Encoding: gzip
Connection: keep-alive
Transfer-Encoding: chunked
<div>Hello compression!</div>
<div>
lalalalalalalalalalalalalalalalalalalala
... truncated ...
请注意,即使启用了压缩,Node.js 仍然可以传输响应。要指定不同于默认1kb的阈值,您可以使用threshold选项来指定字节数。例如,compression({threshold: 512})将压缩长度超过 512 字节的响应。
超时挂起请求
可能会出现这样的情况:一些中间件无法结束请求*,而*无法调用next。例如,如果您的数据库服务器关闭,而您的中间件正在等待数据库服务器的响应,就会发生这种情况。在这种情况下,客户机 HTTP 请求将被挂起,占用服务器内存。在这些情况下,您应该让客户端请求超时,而不是让它挂起。
这正是连接超时 ( npm install connect-timeout)中间件的用途,如清单 7-18 所示。
清单 7-18 。timeout/basic.js
var express = require('express');
var timeout = require('connect-timeout');
var app = express()
.use('/api', timeout(5000),
function (req, res, next) {
// simulate a hanging request by doing nothing
})
.listen(3000);
如果您启动这个 web 服务器并访问http://localhost:3000/api,请求将挂起五秒钟,之后connect-timeout中间件启动并终止请求,向客户端发送 503 服务不可用 HTTP 响应。
您可以通过添加一个错误处理中间件来定制超时的响应,并通过检查req.timedout属性来检查是否发生了超时,如清单 7-19 所示。
清单 7-19 。timeout/error.js
var express = require('express');
var timeout = require('connect-timeout');
var app = express()
.use('/api', timeout(5000)
, function (req, res, next) {
// simulate a hanging request by doing nothing
}
, function (error, req, res, next) {
if (req.timedout) {
res.statusCode = 500;
res.end('Request timed out');
}
else {
next(error);
}
})
.listen(3000);
请注意,您不应该在顶层使用这个中间件('/'),因为您可能希望流式传输一些响应,这些响应可能比您之前所想的要长。
小心休眠的中间件
当您使用这个中间件时,您需要注意挂起的中间件突然醒来并调用next的情况(例如,一个数据库请求花费了比预期更长的时间,但最终成功了)。在这种情况下,您应该检查req.timedout并阻止中间件继续操作,因为错误处理响应已经被发送了。这在清单 7-20 中演示过,它会在第一次请求时崩溃。
清单 7-20 。超时/比例错误。射流研究…
var express = require('express');
var timeout = require('connect-timeout');
var app = express()
.use(timeout(5000))
.use(function (req, res, next) {
// simulate database action that takes 6s
setTimeout(function () {
next();
}, 6000)
})
.use(function (req, res, next) {
res.end('Done'); // ERROR request already terminated
})
.listen(3000);
为此,你应该在你的链中的每一个中间件之后使用一个实用程序 halt 函数,这很容易被挂起,如清单 7-21 所示。
清单 7-21 。time out/progogateerrorhandled . js
var express = require('express');
var timeout = require('connect-timeout');
var app = express()
.use(timeout(1000))
.use(function (req, res, next) {
// simulate database action that takes 2s
setTimeout(function () {
next();
}, 2000)
})
.use(haltOnTimedout)
.use(function (req, res, next) {
res.end('Done'); // Will never get called
})
.listen(3000);
function haltOnTimedout(req, res, next) {
if (!req.timedout) next();
}
快递响应对象
Express response 源自我们在前一章中看到的标准 Node.js 服务器响应对象。它还添加了许多有用的实用功能,让您的 web 开发体验更加有趣。其实我们已经看到了res.cookie / res.clearCookie函数,是 Express 提供的。
响应有一个函数res.status,除了它是可链接的之外,与设置res.statusCode的效果相同。例如:
res.status(200).end('Hello world!');
要一次设置单个或多个响应头,你可以使用res.set函数,如清单 7-22 所示。
清单 7-22 。使用 set 方法
res.set('Content-Type', 'text/plain');
res.set({
'Content-Type': 'text/plain',
'Content-Length': '123',
'ETag': '12345'
})
同样,要得到一个排队头,除了res.getHeader上的 good,还有一个res.get,就是case-insensitive:
res.get('content-Type'); // "text/plain"
如果您想做的只是设置content-type(一个常见任务),它提供了一个很好的utility res.type(type)函数,可以直接获取content-type,甚至可以根据文件扩展名或文件扩展名为您查找内容类型。例如,以下所有内容具有相同的效果:
res.type('.html');
res.type('html');
res.type('text/html');
发送重定向响应是一项非常常见的任务。Express 提供了res.redirect([status], url)功能,让您的工作变得非常简单。url参数可以是绝对的,相对于站点根目录,相对于当前 URL,甚至相对于中间件挂载点,如清单 7-23 所示。
清单 7-23 。使用重定向方法
res.redirect('http://example.com'); // absolute
res.redirect('/login'); // relative to site root
res.redirect('../bar'); // relative to current url
res.redirect('foo/bar'); // relative to middleware mount point
// Status code demo
res.redirect(301, 'http://example.com');
默认的状态代码 302 FOUND 是很好的,但是如果您愿意,可以通过传递第一个数字参数来覆盖它。
简化发送
有一个非常有用的功能,一旦你学会了,你就不能停止使用。其res.send([body|status], [body])。每当您想要发送非流响应时,都应该使用这个函数。这极大地简化了我们一直使用的声明状态和发送主体的常见模式,如清单 7-24 所示。
清单 7-24 。发送拯救线路,线路拯救小猫
// instead of
res.statusCode = 404;
res.end('These are not the droids you are looking for');
// you can do
res.send(404, 'These are not the droids you are looking for');
它还允许您一次性将 JavaScript 对象作为 JSON 发送。如果您传入一个 JavaScript 对象作为主体,它也会为您将content-type头设置为application/json:
res.send({ some: 'json' });
最后,您可以发送一个状态代码。如果是已知的状态代码,将自动为您填充正文。例如,在下面的示例中,它将显示 OK:
res.send(200); // OK
快速请求对象
与响应类似,Express request 对象源自我们在第六章中看到的 Node.js 请求对象。Express 增加了一些很好的特性,我们将在本节中探讨这些特性。
Express 用一个req.get函数简化了对请求头的访问(正如我们在上一章看到的req.headers),它允许不区分大小写的查找,如清单 7-25 所示。
清单 7-25 。演示 Get 方法
req.get('Content-Type'); // "text/plain"
req.get('content-type'); // "text/plain"
req.get('not-present'); // undefined
如果你想做的只是查找请求的content-type,你可以使用如清单 7-26 所示的实用程序req.is(type)函数,它甚至会为你做一个 mime 类型的检查。
清单 7-26 。使用 Is 方法
// When Content-Type is application/json
req.is('json'); // true
req.is('application/json'); // true
req.is('application/*'); // true
req.is('html'); // false
您可以使用req.ip属性获取客户端 IP 地址。
要检查请求是否来自 HTTPS,可以使用req.secure标志,如果请求来自 HTTPS,则为true,否则为false。
URL 处理
Express 将来自 URL 的查询参数解析到req.query JavaScript 对象中。当您想要返回搜索结果时,查询参数非常有用。清单 7-27 提供了一个如何将 URL 查询部分解析成 JavaScript 对象的例子。
清单 7-27 。展示内置查询解析的演示
// GET /shoes?order=desc&shoe[color]=blue&shoe[type]=converse
req.query.order // "desc"
req.query.shoe.color // "blue"
req.query.shoe.type // "converse"
如果只是想要 URL 的路径段(也就是查询前的段),可以在req.path中找到:
// GET /users?sort=desc
req.path // "/users"
当您的中间件被挂载时,Express 试图让您更容易地只访问req.url的相关部分。例如,如果您在''/api'挂载您的中间件,对于请求'/api/admin',req.url将只是'/admin'。清单 7-28 展示了这一点。
清单 7-28 。requestmount/mountUrl.js
var express = require('express');
express()
.use('/home', function (req, res, next) {
console.log('first:', req.url); // GET /home => "first: /"
next();
})
.use(function (req, res, next) {
console.log('second:', req.url); // GET /home => "second: /home"
next();
})
.listen(3000);
要获得完整的原始 URL,您可以使用req.originalUrl属性。
使请求和响应交叉可见
Express 还将响应对象分配给req.res,将请求对象分配给res.req。这使得您可以只传递其中的一个(请求或响应),并在调试时访问相应的请求或相应的响应对象。
理解休息
REST ( 表述性状态转移)是 Roy Fielding(HTTP 规范的主要作者之一)创造的一个术语,作为一种通用的架构风格,规定了分布式超媒体系统中连接的组件应该如何行为的约束。遵循这些约束的 Web APIs 被称为 RESTful。
在 REST 中,有两大类 URL。指向集合的 URL(如http://example.com/resources),以及指向集合中单个项目的 URL(如http://example.com/resources/item5identifier)。为了实现 RESTful,您需要根据 URL 的种类和客户端使用的 HTTP 方法,坚持如表 7-1 中针对集合和表 7-2 中针对项目 URL 所示的行为。还要注意集合 URL 和集合 URL 中的项之间的关系(集合 URL +集合中的项标识符)。
表 7-1 。用于集合URL 的 RESTful API HTTP 方法行为
|
HTTP 方法
|
行为
| | --- | --- | | 得到 | 获取集合成员的汇总详细信息,包括它们的唯一标识符。 | | 放 | 用新系列替换整个系列。 | | 邮政 | 在收藏中添加一个新的物品。通常为创建的资源返回一个唯一的标识符。 | | 删除 | 删除整个收藏 |
表 7-2 。用于项目URL 的 RESTful API HTTP 方法行为
|
HTTP 方法
|
行为
| | --- | --- | | 得到 | 获取物品的详细信息。 | | 放 | 替换该项目。 | | 邮政 | 会将该项目视为一个集合,并且在集合中添加一个新的子项。通常不使用它,因为您倾向于简单地替换整个项目的属性(换句话说,使用 PUT)。 | | 删除 | 删除项。 |
建议您将新项目的详细信息放在 put 和 POST 消息的正文中。同样值得一提的是,在 HTTP 中,GET 和 DELETE 方法中不能有请求体。
快速申请路线
由于 HTTP 动词在制作良好的 web APIs 时的重要性,Express 提供了一流的基于动词+ URL 的路由支持。
让我们从基础开始。您可以调用app.get / app.put / app.post /app.delete—换句话说,app.VERB(path, [callback...], callback)—来注册一个中间件链,只有当客户端请求中的 path + HTTP 动词匹配时,才会调用这个中间件链。您还可以调用app.all来注册一个只要路径匹配就被调用的中间件(不考虑 HTTP 动词)。清单 7-29 是一个简单的演示来说明这一点。
清单 7-29 。approute/1verbs.js
var express = require('express');
var app = express();
app.all('/', function (req, res, next) {
res.write('all\n');
next();
});
app.get('/', function (req, res, next) {
res.end('get');
});
app.put('/', function (req, res, next) {
res.end('put');
});
app.post('/', function (req, res, next) {
res.end('post');
});
app.delete('/', function (req, res, next) {
res.end('delete');
});
app.listen(3000);
所有这些方法形成了一个标准的中间件链,其中 order + calling next很重要。如果您运行这个服务器,您会注意到,.all中间件总是在相关动词中间件之后被调用。我们可以通过使用curl并指定要使用的请求(-X)动词来测试它,如清单 7-30 所示。
清单 7-30 。使用 curl 测试 approute/1 verbs . js
$ curl http://127.0.0.1:3000
all
get
$ curl -X PUT http://127.0.0.1:3000
all
put
$ curl -X POST http://127.0.0.1:3000
all
post
$ curl -X DELETE http://127.0.0.1:3000
all
delete
创建路线对象
现在,在这些路线中指定路径可能很麻烦(并且容易出现拼写错误)。因此,Express 有一个很好的小app.route成员函数,只指定一次前缀,它返回一个具有相同all/get/put/post/delete函数的 route 对象。所有这些都在清单 7-31 中的示例中进行了演示。输出与我们在前面的例子中看到的完全一样(清单 7-20 )。
清单 7-31 。approute/2route.js
var express = require('express');
var app = express();
app.route('/')
.all(function (req, res, next) {
res.write('all\n');
next();
})
.get(function (req, res, next) {
res.end('get');
})
.put(function (req, res, next) {
res.end('put');
})
.post(function (req, res, next) {
res.end('post');
})
.delete(function (req, res, next) {
res.end('delete');
});
app.listen(3000);
深入了解路径选项
与采用路径 前缀的app.use函数不同,ExpressJS 中基于动词的路由匹配精确路径(而不是精确 URL ,因为查询字符串部分被忽略)。如果要匹配路径前缀,可以使用*占位符来匹配前缀后面的任何内容。您还可以基于正则表达式设置路由。清单 7-32 展示了所有这些选项。
清单 7-32 。approute/3path.js
var express = require('express');
var app = express();
app.get('/', function (req, res) {
res.send('nothing passed in!');
});
app.get(/^\/[0-9]+$/, function (req, res) {
res.send('number!');
});
app.get('/*', function (req, res) {
res.send('not a number!');
});
app.listen(3000);
第一个中间件仅在路径恰好是'/'时被调用,在这种情况下,它发送响应,并且不将控制传递给任何其他中间件。只有当 number regex 匹配时,number 中间件才会被调用,同样,它会返回一个响应,并且不再传递控制。最后,我们有一个包罗万象的中间件。你可以使用curl来测试这个,如清单 7-33 所示。
清单 7-33 。使用 curl 测试 approute/3path.js
$ curl http://127.0.0.1:3000/
nothing passed in!
$ curl http://127.0.0.1:3000/123
number!
$ curl http://127.0.0.1:3000/foo
not a number!
基于参数的路由
一个更好的选择是使用路径参数,而不是在路径前缀匹配中加入太多的过滤逻辑。您可以使用:parameterName语法指定路径参数。例如,/user/:userId将匹配/user/123并为您填充userId请求参数。Express 将所有参数值放在req.params对象中。清单 7-34 展示了它的用法(以及清单 7-35 中显示的一个示例运行)。
清单 7-34 。逼近/4param.js
var express = require('express');
var app = express();
app.get('/user/:userId', function (req, res) {
res.send('userId is: ' + req.params['userId']);
});
app.listen(3000);
清单 7-35 。使用 curl 测试 approute/4param.js
$ curl http://127.0.0.1:3000/user/123
userId is: 123
实际上,通过使用app.param函数,你可以注册一个中间件来为你加载相关信息。每当路由中的参数名称匹配时,就会调用app.param中间件函数,并且还会将参数值作为第四个参数传入。这在清单 7-36 中得到了演示。
清单 7-36 。逼近/5paramload.js
var express = require('express');
var app = express();
app.param('userId', function (req, res, next, userId) {
res.write('Looking up user: ' + userId + '\n');
// simulate a user lookup and
// load it into the request object for later middleware
req.user = { userId: userId };
next();
});
app.get('/user/:userId', function (req, res) {
res.end('user is: ' + JSON.stringify(req.user));
});
app.listen(3000);
运行这个服务器并执行一个简单的curl请求。您可以看到,如果具有指定参数的路由在任何其他中间件之前匹配,那么就调用了param函数。这允许您创建一个可重用的参数加载中间件:
$ curl http://127.0.0.1:3000/user/123
Looking up user: 123
user is: {"userId":"123"}
快速路由对象
快速路由是中间件+路由的孤立实例。它可以被认为是一个“迷你”Express 应用。你可以使用express.Router()函数很容易地创建一个路由对象。
在根级别,它有use, all, get, post, put, delete, param和route函数,它们的行为与我们已经看到的 Express app 完全相同。
除此之外,路由对象的行为就像任何其他中间件一样。也就是说,一旦设置了路由对象,就可以使用app.use函数向 Express 注册它。显然,您可以通过向app.use函数传递第一个参数,在指定的挂载点挂载它,就像我们之前已经看到的那样。
为了展示这种模式的威力,让我们创建一个简单的Router,它遵循 REST 的原则,创建一个 web API 来管理内存中任意对象的集合,如清单 7-37 所示。
清单 7-37 。router/basic.js
var express = require('express');
var bodyParser = require('body-parser');
// An in memory collection of items
var items = [];
// Create a router
var router = express.Router();
router.use(bodyParser());
// Setup the collection routes
router.route('/')
.get(function (req, res, next) {
res.send({
status: 'Items found',
items: items
});
})
.post(function (req, res, next) {
items.push(req.body);
res.send({
status: 'Item added',
itemId: items.length - 1
});
})
.put(function (req, res, next) {
items = req.body;
res.send({ status: 'Items replaced' });
})
.delete(function (req, res, next) {
items = [];
res.send({ status: 'Items cleared' });
});
// Setup the item routes
router.route('/:id')
.get(function (req, res, next) {
var id = req.params['id'];
if (id && items[Number(id)]) {
res.send({
status: 'Item found',
item: items[Number(id)]
});
}
else {
res.send(404, { status: 'Not found' });
}
})
.all(function (req, res, next) {
res.send(501, { status: 'Not implemented' });
});
// Use the router
var app = express()
.use('/todo', router)
.listen(3000);
除了我们正在创建一个 Express Router之外,所有这些代码您都已经很熟悉了。我们创建一个内存中对象集合。然后我们创建一个路由,并要求它使用body-parser中间件(我们已经看到了)。我们建立了一个根级别的'/'路由。如果您进行 GET 调用,您将获得集合中的所有项目。如果你发表了一篇文章,我们在集合中创建一个新的条目并返回它的索引。如果您发出上传请求,我们会用您上传的内容替换集合。如果您发出删除调用,我们将清除集合。
我们还支持通过 id 获取项目的项目级途径。对于任何其他 HTTP 动词,我们返回 501 未实现。
请注意,由于路由的可安装性,如果我们愿意,我们可以在另一个点(而不是“/todo”)重用相同的路由。这使得功能高度可重用和可维护。
注意记得我们说过
req.originalUrl指向原始 URL,而req.url是基于挂载点的精简版本。这正是让在挂载点注册路由成为可能的原因,因为它只在内部查看req.url,并且只获取与其路由相关的部分。
现在让我们用清单 7-38 中的来测试一下。
清单 7-38 。使用 curl 测试 router/basic . js
$ curl http://127.0.0.1:3000/todo
{"status":"Items found","items":[]}
$ curl http://127.0.0.1:3000/todo -H "content-type: application/json" -d
"{\"description\":\"test\"}"
{"status":"Item added","itemId":0}
$ curl http://127.0.0.1:3000/todo/0
{"status":"Item found","item":{"description":"test"}}
$ curl http://127.0.0.1:3000/todo/
{"status":"Items found","items":[{"description":"test"}]}
$ curl http://127.0.0.1:3000/todo/ -X DELETE
{"status":"Items cleared"}
$ curl http://127.0.0.1:3000/todo/
{"status":"Items found","items":[]}
$ curl http://127.0.0.1:3000/todo -X PUT -H "content-type: application/json" -d
"[{\"description\":\"test\"}]"
{"status":"Items replaced"}
$ curl http://127.0.0.1:3000/todo/
{"status":"Items found","items":[{"description":"test"}]}
$ curl http://127.0.0.1:3000/todo/0 -X DELETE
{"status":"Not implemented"}
在这个演示中,)我们得到初始集合,它是空的。然后,我们添加一个条目(使用 POST)并查询该条目(GET /todo/0),返回该条目。然后我们查询所有的条目(GET /todo)。接下来,我们删除所有条目并进行查询验证。然后我们放一个新的集合,再次验证。最后,我们表明您不能删除单个项目,因为我们有意屏蔽了该功能。
当设计一个 API 时,你应该保持一致。最常见的情况是,您将使用 JSON,所以确保您接受有效的 JSON ( bodyParser会为您这样做),并在所有情况下返回一个 JavaScript 对象。这包括错误条件,所以客户端总是获得 JSON。还有,标准化的 HTTP 状态码是你的朋友。
在这一点上,我们似乎为一个简单的 API 做了很多工作,但是请记住,如果没有好的 API 设计,你的 UI + API 将会杂乱无章。我们将在本书的后面看到,我们如何在一个干净的 API 之上开发一个好的前端,它既容易维护,又比过去的页面重载 web 设计表现得更好。
额外资源
罗伊·菲尔丁最初在他的论文中提到休息,可以在www.ics.uci.edu/~fielding/pubs/dissertation/rest_arch_style.htm找到。
摘要
在本章中,我们深入探讨了各种 HTTP 概念。我们首先展示了用于各种 web 相关任务的流行中间件。这包括提供静态网页和使用 cookies。
我们展示了 Express 提供了与 Connect 相同的中间件框架。然后,我们深入研究了 Express 在标准请求和响应对象之上提供的一些不错的增值特性。
最后,我们讨论了休息的原则。我们展示了 Express 框架如何拥抱 web,并使创建可维护的 RESTful web APIs 变得轻而易举。
我们没有涉及 Express 的视图渲染方面,这将在第九章讨论前端设计时涉及。但是,首先,让我们坚持下去。