1
0
mirror of https://github.com/apachecn/eloquent-js-3e-zh.git synced 2025-05-29 08:12:22 +00:00
This commit is contained in:
wizardforcel 2018-06-01 15:55:12 +08:00
parent 34abc5f388
commit 3bccf7ed17

268
20.md
View File

@ -362,117 +362,133 @@ async function notAllowed(request) {
这样启动服务器之后服务器永远只会产生405错误响应该代码表示服务器拒绝处理特定的方法。 这样启动服务器之后服务器永远只会产生405错误响应该代码表示服务器拒绝处理特定的方法。
函数respond被传递给处理不同方法的函数作为请求结束时的回调。该方法接受三个参数第一个是状态码第二个是响应体第三个是可选的内容类型。若传递的响应体是一个可读流该对象有一个pipe方法用于将可读流转发到可写流中。若不是可读流我们假定它是null没有响应体或一个字符串我们将字符串直接传递给响应的end方法 当请求处理程序的`Promise`受到拒绝时,`catch`调用会将错误转换为响应对象(如果它还不是),以便服务器可以发回错误响应,来通知客户端它未能处理请求
为了从请求的URL中获取路径urlToPath函数使用了Node内建的url模块来解析URL。我们取出parse函数返回值中的pathname属性结果类似于/file.txt并进行解码去除%20这类转义代码并加上一个句号作为前缀以产生相对于当前目录的路径。 响应描述的`status`字段可以省略,这种情况下,默认为 200OK`type`属性中的内容类型也可以被省略,这种情况下,假定响应为纯文本。
`body`的值是可读流时,它将有`pipe`方法,用于将所有内容从可读流转发到可写流。 如果不是,则假定它是`null`(无正文),字符串或缓冲区,并直接传递给响应的`end`方法。
为了弄清哪个文件路径对应于请求URL`urlPath`函数使用 Node 的`url`内置模块来解析 URL。 它接受路径名,类似`"/file.txt"`,将其解码来去掉`%20`风格的转义代码,并相对于程序的工作目录来解析它。
```js ```js
function urlToPath(url) { const {parse} = require("url");
var path = require("url").parse(url).pathname; const {resolve} = require("path");
return "." + decodeURIComponent(path);
const baseDirectory = process.cwd();
function urlPath(url) {
let {pathname} = parse(url);
let path = resolve(decodeURIComponent(pathname).slice(1));
if (path != baseDirectory &&
!path.startsWith(baseDirectory + "/")) {
throw {status: 403, body: "Forbidden"};
}
return path;
} }
``` ```
若你担心urlToPath函数的安全性那么你是正确的。我们将会在习题中回过头来看这个问题。 只要您建立了一个接受网络请求的程序,就必须开始关注安全问题。 在这种情况下,如果我们不小心,很可能会意外地将整个文件系统暴露给网络
我们来创建GET方法在读取目录时返回文件列表在读取普通文件时返回文件内容。 文件路径在 Node 中是字符串。 为了将这样的字符串映射为实际的文件,需要大量有意义的解释。 例如,路径可能包含`"../"`来引用父目录。 因此,一个显而易见的问题来源是像`/../ secret_file`这样的路径请求
一个棘手的问题是我们返回文件内容时添加的Content-Type头应该是什么类型。因为这些文件可以是任何内容我们的服务器无法简单地对所有文件返回相同的类型。但NPM可以帮助我们完成该任务。mime包以text/plain这种方式表示的内容类型名为MIME类型可以获取大量文件扩展名的正确类型。 为了避免这种问题,`urlPath`使用`path`模块中的`resolve`函数来解析相对路径。 然后验证结果位于工作目录下面。 `process.cwd`函数(其中`cwd`代表“当前工作目录”)可用于查找此工作目录。 当路径不起始于基本目录时,该函数将使用 HTTP 状态码来抛出错误响应对象,该状态码表明禁止访问资源
若你在服务器脚本所在目录中执行以下npm命令你可以使用require“mime”来获取库。 我们需要创建GET方法在读取目录时返回文件列表在读取普通文件时返回文件内容。
一个棘手的问题是我们返回文件内容时添加的Content-Type头应该是什么类型。因为这些文件可以是任何内容我们的服务器无法简单地对所有文件返回相同的内容类型。但NPM可以帮助我们完成该任务。mime包以text/plain这种方式表示的内容类型名为MIME类型可以获取大量文件扩展名的正确类型。
以下`npm`命令在服务器脚本所在的目录中,安装`mime`的特定版本。
``` ```
$ npm install mime@1.4.0 $ npm install mime@2.2.0
npm http GET https://registry.npmjs.org/mime
npm http 304 https://registry.npmjs.org/mime
mime@1.4.0 node_modules/mime
``` ```
当请求文件不存在时应该返回的正确HTTP错误代码是404。我们使用fs.stat查询文件信息来找出特定文件是否存在以及是否是一个目录。 当请求文件不存在时应该返回的正确HTTP状态码是404。我们使用stat函数来找出特定文件是否存在以及是否是一个目录。
```js ```js
methods.GET = function(path, respond) { const {createReadStream} = require("fs");
fs.stat(path, function(error, stats) { const {stat, readdir} = require("fs/promises");
if (error && error.code == "ENOENT") const mime = require("mime");
respond(404, "File not found");
else if (error) methods.GET = async function(request) {
respond(500, error.toString()); let path = urlPath(request.url);
else if (stats.isDirectory()) let stats;
fs.readdir(path, function(error, files) { try {
if (error) stats = await stat(path);
respond(500, error.toString()); } catch (error) {
else if (error.code != "ENOENT") throw error;
respond(200, files.join("\n")); else return {status: 404, body: "File not found"};
}); }
else if (stats.isDirectory()) {
respond(200, fs.createReadStream(path), return {body: (await readdir(path)).join("\n")};
require("mime").lookup(path)); } else {
}); return {body: createReadStream(path),
type: mime.getType(path)};
}
}; };
``` ```
因为fs.stat访问磁盘需要耗费一些时间因此该函数是异步的。当文件不存在时fs.stat会传递一个错误对象包含code属性值为“ENOENT”给回调。若Node为不同类型的错误定义了不同的Error子类型那是非常好的但现在不存在这种类型 因为stat访问磁盘需要耗费一些时间因此该函数是异步的。由于我们使用`Promise`而不是回调风格,因此必须从`fs/promises`而不是`fs`导入
我们只能在这里使用晦涩难懂的源于Unix的错误代码 当文件不存在时,`stat`会抛出一个错误对象,`code`属性为`'ENOENT'`。 这些有些模糊的,受 Unix 启发的代码,是你识别 Node 中的错误类型的方式
发生任何无法预料的错误时我们统一返回状态代码500表示服务器中存在问题与以4开头的错误代码比如404一般指的是错误请求相对应。在某些情况下这个错误代码可能不完全准确但对于小型示例程序而言这已经足够好了 由stat返回的stats对象告知我们关于文件的一系列信息比如文件大小size属性和修改日期mtime属性。这里我们想知道的是该文件是一个目录还是普通文件isDirectory方法可以告诉我们答案
由fs.stat返回的stats对象告知我们关于文件的一系列信息比如文件大小size属性和修改日期mtime属性。这里我们想知道的是该文件是一个目录还是普通文件isDirectory方法可以告诉我们答案。 我们使用readdir来读取目录中的文件列表并将其返回给客户端。对于普通文件我们使用createReadStream创建一个可读流并将其传递给respond对象同时使用mime模块根据文件名获取内容类型并传递给respond。
我们使用fs.readdir来读取目录中的文件列表并将其返回给用户。对于普通文件我们使用fs.createReadStream创建一个可读流并将其传递给respond对象同时使用mime模块根据文件名获取内容类型并传递给respond。
处理DELETE请求的代码就稍显简单了。 处理DELETE请求的代码就稍显简单了。
```js ```js
methods.DELETE = function(path, respond) { const {rmdir, unlink} = require("fs/promises");
fs.stat(path, function(error, stats) {
if (error && error.code == "ENOENT") methods.DELETE = async function(request) {
respond(204); let path = urlPath(request.url);
else if (error) let stats;
respond(500, error.toString()); try {
else if (stats.isDirectory()) stats = await stat(path);
fs.rmdir(path, respondErrorOrNothing(respond)); } catch (error) {
else if (error.code != "ENOENT") throw error;
fs.unlink(path, respondErrorOrNothing(respond)); else return {status: 204};
}); }
if (stats.isDirectory()) await rmdir(path);
else await unlink(path);
return {status: 204};
}; };
``` ```
这里删除不存在的文件会返回204状态码而不是错误对此读者可能感到疑惑。当删除不存在的文件时你可以认为我们已经完成了请求的任务。HTTP标准鼓励人们采用幂等的请求这意味着无论多少次应用请求都不会产生不同结果 当 HTTP 响应不包含任何数据时,状态码 204“no content”无内容可用于表明这一点。 由于删除的响应不需要传输任何信息,除了操作是否成功之外,在这里返回是明智的
```js 您可能想知道,为什么试图删除不存在的文件会返回成功状态代码,而不是错误。 当被删除的文件不存在时,可以说该请求的目标已经完成。 HTTP 标准鼓励我们使请求是幂等idempotent这意味着多次发送相同请求的结果会与一次相同。 从某种意义上说,如果你试图删除已经消失的东西,那么你试图去做的效果已经实现 - 东西已经不存在了。
function respondErrorOrNothing(respond) {
return function(error) {
if (error)
respond(500, error.toString());
else
respond(204);
};
}
```
若HTTP响应不包含任何数据我们可以用状态码204无内容来表示这种情况。因为我们需要提供回调来报告错误或在某些不同情况下返回204响应因此我编写了函数respondErrorOrNothing来创建一个回调。
下面是PUT请求的处理函数。 下面是PUT请求的处理函数。
```js ```js
methods.PUT = function(path, respond, request) { const {createWriteStream} = require("fs");
var outStream = fs.createWriteStream(path);
outStream.on("error", function(error) { function pipeStream(from, to) {
respond(500, error.toString()); return new Promise((resolve, reject) => {
from.on("error", reject);
to.on("error", reject);
to.on("finish", resolve);
from.pipe(to);
}); });
outStream.on("finish", function() { }
respond(204);
}); methods.PUT = async function(request) {
request.pipe(outStream); let path = urlPath(request.url);
await pipeStream(request, createWriteStream(path));
return {status: 204};
}; };
``` ```
这里我们不需要检查文件是否存在,若存在只需覆盖即可。我们再次使用pipe来将可读流中的数据移动到可写流中在本例中是将请求的数据移动到文件中。若流创建失败则触发error事件并在我们的响应中报告错误。当数据成功传输后pipe会关闭两个流并触发可写流的finish事件。当该事件发生后我们采用204响应向客户端报告任务成功完成 我们不需要检查文件是否存在,如果存在,只需覆盖即可。我们再次使用pipe来将可读流中的数据移动到可写流中在本例中是将请求的数据移动到文件中。但是由于`pipe`没有为返回`Promise`而编写,所以我们必须编写包装器`pipeStream`,它从调用`pipe`的结果中创建一个`Promise`
完整的服务器脚本可以从[http://eloquentjavascript.net/code/file_server.js](http://eloquentjavascript.net/code/file_server.js)获取。读者可以下载该脚本并使用Node启动你自己的文件服务器。当然你可以修改并扩展该脚本以完成本章的习题或进行实验 当打开文件`createWriteStream`时出现问题时仍然会返回一个流,但是这个流会触发`'error'`事件。 例如,如果网络出现故障,请求的输出流也可能失败。 所以我们连接两个流的`'error'`事件来拒绝`Promise`。 当`pipe`完成时,它会关闭输出流,从而导致触发`'finish'`事件。 这是我们可以成功解析`Promise`的地方(不返回任何内容)
命令行工具curl在类Unix系统中得到广泛使用可用于产生HTTP请求。接下来的会话用于简单测试我们的服务器。这里需要注意-x用于设置请求方法-d用于包含请求正文。 完整的服务器脚本请见[`eloquentjavascript.net/code/file_server.js`](http://eloquentjavascript.net/code/file_server.js)。读者可以下载该脚本,并且在安装依赖项之后,使用 Node 启动你自己的文件服务器。当然你可以修改并扩展该脚本,来完成本章的习题或进行实验。
命令行工具`curl`在类 Unix 系统(比如 Mac 或者 Linux中得到广泛使用可用于产生 HTTP 请求。接下来的会话用于简单测试我们的服务器。这里需要注意,`-x`用于设置请求方法,`-d`用于包含请求正文。
``` ```
$ curl http://localhost:8000/file.txt $ curl http://localhost:8000/file.txt
@ -487,125 +503,45 @@ File not found
由于file.txt一开始不存在因此第一请求失败。而PUT请求则创建文件因此我们看到下一个请求可以成功获取该文件。在使用DELETE请求删除该文件后第三次GET请求再次找不到该文件。 由于file.txt一开始不存在因此第一请求失败。而PUT请求则创建文件因此我们看到下一个请求可以成功获取该文件。在使用DELETE请求删除该文件后第三次GET请求再次找不到该文件。
### 20.10 错误处理
在文件服务器代码中有6个位置我们显式传递异常我们不知道如何处理异常。因为异常不会直接传播到回调中而是使用参数传递给回调因此我们不得不每次都显式处理这些异常。这完全抵消了异常处理的优势换言之我们无法将错误处理集中到一起。
当一些代码在系统中抛出一个异常会发生什么由于我们并未使用任何的try块因此exception会直接传播到调用栈顶部。在Node中这会导致程序停止运行并输出异常信息包括堆栈轨迹到程序的标准错误流中。
这就意味着我们的服务器在服务器代码内遇到任何问题都会崩溃而相对的异步问题会通过参数传递给回调。若我们想要处理所有在请求处理中引发的异常为了确保我们可以发送响应我们需要在每个回调中添加try/catch块。
这是无法工作的。许多Node程序都尽可能少使用异常如果没有这种假设当引发异常时由于程序无法处理这些异常正常的结果就是程序崩溃。
另一种方法是使用Promise第17章中已经有过介绍。Promise可以捕捉由回调引发的异常并将其作为错误向外层传播。我们可以在Node中加载promise库并使用其管理异步控制。几乎没有Node库集成了promise但包裹这些异常对象太烦琐了。而NPM提供了优秀的promise模块包含一个名为denodeify的函数用于将诸如fs.readFile之类的异步函数转换成返回promise的函数。
```js
var Promise = require("promise");
var fs = require("fs");
var readFile = Promise.denodeify(fs.readFile);
readFile("file.txt", "utf8").then(function(content) {
console.log("The file contained: " + content);
}, function(error) {
console.log("Failed to read file: " + error);
});
```
为了进行比较我基于promise编写了另一个版本的文件服务器该文件服务器可以从[http://eloquentjavascript.net/code/file_server_promises.js](http://eloquentjavascript.net/code/file_server_promises.js)获取。由于现在函数可以直接返回其结果,而不必要调用回调,因此代码变得稍微优雅一点,而异常处理路径也变成隐式处理,而非显式处理。
这里列举出几行基于promise的文件服务器代码展示两种程序设计风格的差别。
代码中使用的fsp对象使用Promise.denodeify包裹fs中的一系列函数提供了promise风格的变体。方法处理器返回的对象包含code和body属性是promise链中的最终结果我们用其判断应该向客户端发送何种响应。
```js
methods.GET = function(path) {
return inspectPath(path).then(function(stats) {
if (!stats) // Does not exist
return {code: 404, body: "File not found"};
else if (stats.isDirectory())
return fsp.readdir(path).then(function(files) {
return {code: 200, body: files.join("\n")};
});
else
return {code: 200,
type: require("mime").lookup(path),
body: fs.createReadStream(path)};
});
};
function inspectPath(path) {
return fsp.stat(path).then(null, function(error) {
if (error.code == "ENOENT") return null;
else throw error;
});
}
```
inspectPath函数是对fs.stat函数的简单包装用于处理找不到文件的情况。在这种情况下我们将错误信息替换为成功null其他的错误依然继续传播。当这些处理器中返回的promise失败HTTP服务器会响应500错误状态码。
### 20.11 本章小结 ### 20.11 本章小结
Node是一种优雅直接的系统可以让我们在非浏览器环境中执行JavaScript。Node最初的设计意图是完成网络任务扮演网络中的节点。但同时也能用来执行任何脚本任务如果你觉得编写JavaScript代码是一件惬意的事情那么使用Node来自动完成每天的任务是非常不错的。 Node 是一个不错的小型系统,可让我们在非浏览器环境下运行 JavaScript。Node 最初的设计意图是完成网络任务,扮演网络中的节点。但同时也能用来执行任何脚本任务,如果你觉得编写 JavaScript 代码是一件惬意的事情,那么使用 Node 来自动完成每天的任务是非常不错的。
NPM为你所能想到的功能当然还有相当多你想不到的提供了库,你可以通过执行简单的命令,获取并安装这些库。Node也附带了许多内建模块包括fs模块处理文件系统、http模块执行HTTP服务器并生成HTTP请求 NPM为你所能想到的功能当然还有相当多你想不到的提供了包你可以通过使用`npm`程序获取并安装这些包。Node也附带了许多内建模块包括fs模块处理文件系统、http模块执行HTTP服务器并生成HTTP请求
Node中的所有输入输出都是异步的除非你明确使用函数的同步变体比如fs.readFileSync。使用者提供回调Node会在适当的时候调用回调比如I/O请求结束时 Node中的所有输入输出都是异步的除非你明确使用函数的同步变体比如readFileSync。当调用异步函数时使用者提供回调并且Node会在准备好的时候使用错误值和结果如果有的话调用它们。
### 20.12 习题 ### 20.12 习题
#### 20.12.1 再次设定内容属性 ### 搜索工具
第17章中第一个习题是向[http://eloquentjavascript.net/author/](http://eloquentjavascript.net/author/)发送一些请求通过传递不同的Accept头来请求不同类型的内容 在 Unix 系统上,有一个名为`grep`的命令行工具,可以用来在文件中快速搜索正则表达式。
请使用Node的http.request函数再次完成该习题。至少需要请求以下几类媒体类型text/plain、text/html和application/json。你可以直接给定一个对象表示请求头部该对象通过http.request的第一个参数的headers属性传递 编写一个可以从命令行运行的 Node 脚本,其行为类似`grep`。 它将其第一个命令行参数视为正则表达式,并将任何其他参数视为要搜索的文件。 它应该输出内容与正则表达式匹配的,任何文件的名称。
输出每个请求的响应内容 当它有效时,将其扩展,以便当其中一个参数是目录时,它将搜索该目录及其子目录中的所有文件。
#### 20.12.2 修复漏洞 按照您认为合适的方式,使用异步或同步文件系统函数。 配置一些东西,以便同时请求多个异步操作可能会加快速度,但不是很大,因为大多数文件系统一次只能读取一个东西。
我非常喜欢在自己的机器上运行本章定义的文件服务器,这样我可以很容易地远程访问某些在/home/marijn/public中的文件。随后的一天我发现有人获取了我存储在浏览器中的所有密码。 ### 目录创建
发生了什么? 尽管我们的文件服务器中的`DELETE`方法可以删除目录(使用`rmdir`),但服务器目前不提供任何方法来创建目录。
你可能还不太清楚让我们回想一下urlToPath函数该函数定义如下 添加对`MKCOL`方法“make column”的支持它应该通过调用`fs`模块的`mkdir`创建一个目录。 `MKCOL`并不是广泛使用的 HTTP 方法,但是它在 WebDAV 标准中有相同的用途,这个标准在 HTTP 之上规定了一组适用于创建文档的约定。
```js 你可以使用实现`DELETE`方法的函数,作为`MKCOL`方法的蓝图。 当找不到文件时,尝试用`mkdir`创建一个目录。 当路径中存在目录时,可以返回 204 响应,以便目录创建请求是幂等的。 如果这里存在非目录文件,则返回错误代码。 代码 400“bad request”是适当的。
function urlToPath(url) {
var path = require("url").parse(url).pathname;
return "." + decodeURIComponent(path);
}
```
现在思考一下传递给fs函数的路径可以是个相对路径包含“../”来访问上级目录。当客户端使用如下所示的URL向服务器发送请求会发生什么
```
http://myhostname:8000/../.config/config/google-chrome/Default/Web%20Data
http://myhostname:8000/../.ssh/id_dsa
http://myhostname:8000/../../../etc/passwd
```
修改urlToPath来解决该问题。请考虑Node在Windows上的运行情况而Windows同时允许以斜杠和反斜杠作为目录分隔符。
同时也考虑一下,如果你在互联网上执行了没有充分考虑安全问题的系统而别人可以利用系统漏洞在你的机器上做一些危险的事情。
#### 20.12.3 创建目录
尽管DELETE方法可以删除目录使用了fs.rmdir但文件服务器现在没有提供创建目录的方式。
为了添加对MKCOL方法的支持读者可以调用fs.mkdir来创建目录。MKCOL并不是基本的HTTP方法但其确实存在且与我们的意图相同。该方法定义在WebDAV标准中WebDAV制定了一系列的HTTP扩展以适用于修改资源而不仅仅是读取资源。
#### 20.12.4 网络上的公共空间 #### 20.12.4 网络上的公共空间
由于文件服务器提供了任何类型的文件服务甚至只要包含正确的Content-Type头部你可以使用其提供网站服务。由于该服务允许每个人删除或替换文件因此这是一类非常有趣的网站任何人只要使用正确的HTTP请求都可以修改、截取并破坏文件。但这仍然是一个网站。 由于文件服务器提供了任何类型的文件服务甚至只要包含正确的Content-Type头部你可以使用其提供网站服务。由于该服务允许每个人删除或替换文件因此这是一类非常有趣的网站任何人只要使用正确的HTTP请求都可以修改、改进并破坏文件。但这仍然是一个网站。
请编写一个基础的HTML页面包含一个简单的JavaScript文件。将该文件放在文件服务器的数据目录下并在你的浏览器中打开这些文件。 请编写一个基础的HTML页面包含一个简单的JavaScript文件。将该文件放在文件服务器的数据目录下并在你的浏览器中打开这些文件。
接下来,作为进阶练习或是周末作业,将你迄今为止在本书中学习到的内容整合起来,构建一个对用户友好的界面,在网站内部修改网站。 接下来,作为进阶练习或是周末作业,将你迄今为止在本书中学习到的内容整合起来,构建一个对用户友好的界面,在网站内部修改网站。
使用一个HTML表单第18章编辑网站内部资源运行用户通过第17章中描述的HTTP请求更新服务器软件 使用 HTML 表单编辑组成网站的文件内容,允许用户使用 HTTP 请求在服务器上更新它们,如第十八章所述。
刚开始的时候该页面仅允许用户编辑单个文件然后进行修改允许选择想要编辑的文件。向文件服务器发送请求时若URL是一个目录服务器会返回该目录下的文件列表你可以利用该特性实现你的网页。 刚开始的时候该页面仅允许用户编辑单个文件然后进行修改允许选择想要编辑的文件。向文件服务器发送请求时若URL是一个目录服务器会返回该目录下的文件列表你可以利用该特性实现你的网页。
不要直接编辑文件服务器的代码,如果你犯了什么错误,很有可能就破坏了你的代码。相反,将你的代码保存在公共访问目录之外,测试时再将其拷贝到公共目录中。 不要直接编辑文件服务器开放的代码,如果你犯了什么错误,很有可能就破坏了你的代码。相反,将你的代码保存在公共访问目录之外,测试时再将其拷贝到公共目录中。
若你可以使用因特网直接连接你的计算机,而且没有防火墙、路由器或其他任何会干涉两台机器之间连接的设备,你可以邀请你的朋友使用你的网站。为了检查一下,你可以访问[http://whatismyip.com/](http://whatismyip.com/)复制网站显示的IP地址到你浏览器的地址栏中并在其后添加8000以选择正确端口。若这样可以访问你的网站说明网络上的任何人都可以访问你的网站。