NodeJS-秘籍-全-
NodeJS 秘籍(全)
一、了解 Node.js
Node.js 是一个服务器端框架,用于构建高度可伸缩的快速应用。Node.js 是一个基于 v8 的平台,V8 是一个 JavaScript 运行时,支持谷歌设计的 Chrome 浏览器。Node.js 的设计非常适合使用非阻塞事件驱动架构的密集型 I/O 应用。虽然 Node.js 可以以同步方式提供函数,但它通常异步执行操作。这意味着,当您开发应用时,您使用注册的回调来调用事件,以便处理函数的返回。在等待返回时,应用中的下一个事件或函数可以排队等待执行。一旦第一个函数完成,它的回调事件就由调用回调的函数调用来执行和处理。这种事件驱动的处理在 Node.js 自己的定义中有描述:
Node.js 是一个基于 Chrome 的 JavaScript 运行时构建的平台,用于轻松构建快速、可扩展的网络应用。Node.js 使用事件驱动的非阻塞 I/O 模型,这使得它轻量级且高效,非常适合跨分布式设备运行的数据密集型实时应用。
用 Node.js 编写的应用是用 web 平台无处不在的语言 JavaScript 编写的。由于许多有经验的开发人员和新手都可以访问 JavaScript,Node.js 平台和社区已经起飞,并成为许多公司和开发人员开发环境的重要部分。
这本书是关于 Node.js 的。特别是,这本书是一本食谱书,旨在提供大量有用的高质量的例子,说明 Node.js 能够完成什么。这本书是为对 JavaScript 有一些经验并且至少对 Node.js 有所了解的开发人员准备的。通过阅读这本书,您将了解许多被高度利用的模块,包括 Node.js 的原生模块和由第三方贡献者编写的模块,这些模块是 Node.js 开发人员的主要目标。
这第一章与本书其余部分的食谱格式不同。它旨在帮助开发人员从头开始安装和运行,并概述了如何在 Node.js 平台中运行。您将了解如何安装 Node.js,并理解许多常见范例和运行 Node.js 应用的基本工作流。正如您将看到的,我们花了相当多的时间介绍 Node.js 是如何工作的。一旦你读完了这一章,你就应该做好准备去钻研接下来几章的食谱了。
1-1.在计算机上安装 Node.js
有几种方式可以安装 Node.js,并且它们在不同的操作系统之间略有不同。安装 Node.js 的三种主要方法是通过二进制安装程序,通过包管理器,或者通过编译源代码。
要通过二进制安装程序在您的计算机上安装 Node.js,您首先需要安装程序。目前 Node.js 唯一可用的安装程序是针对 Windows 和 Macintosh OS X 的,要找到这些安装程序,你需要前往http://nodejs.org/download/。在这里你可以找到你选择的安装程序来下载,如图 1-1 所示。

图 1-1 。可供下载的特定平台安装程序
窗户
在 Windows 上,首先下载。msi 安装程序包。当您打开该文件时,您将开始使用设置向导进行演练,如图 1-2 所示。

图 1-2 。开始安装
与大多数 Windows 应用一样,您将看到一个默认位置,您可以将应用文件安装到该位置。然而,该目的地可以被覆盖,并显示在图 1-3 中。

图 1-3 。您可以选择使用或覆盖默认文件位置
在 Windows 上完成安装之前的最后一步是设置 Node.js 安装所需的任何自定义配置。例如,您不能将 Node.js 添加到您的路径中;也许你想测试多个版本,并在测试阶段明确地调用可执行文件。该自定义步骤如图 1-4 所示。

图 1-4 。自定义设置
X
Macintosh 上的安装程序与 Windows 安装程序非常相似。首先,下载。pkg 文件。当你打开它时,它会引导你完成在 OS X 上运行的标准安装程序。这就像你在图 1-5 中看到的那样。

图 1-5 。在 OS X 上安装
有时在安装 Node.js 时,您希望只有一部分潜在用户能够访问它。该功能内置于 OS X 安装程序中,为您提供了如何安装 Node.js 的选项,如图 1-6 所示。

图 1-6 。为指定用户安装
就像在 Windows 上一样,您可以自定义安装。点击自定义安装按钮,然后根据图 1-7 中的所示设置您的配置。例如,您可能不希望安装 npm,而是希望进行更加定制的 npm 安装,我们将在下一节中对此进行概述。

图 1-7 。OS X 上的自定义 Node.js 安装
当然,有许多平台不是 Macintosh 或 Windows,但是您仍然希望不必从源代码中下载和编译 Node.js。这个问题的解决方案是找到一个会为您安装 Node.js 的包管理器。有几个不同平台的包管理系统,每一个都有自己获取新包的风格。
Ubuntu 和 Linux Mint
Ubuntu 和 Linux Mint 的软件包要求在安装 Node.js 之前在你的机器上安装一些组件,为了满足这些先决条件,你必须首先运行清单 1-1 中的代码。
清单 1-1 。确保安装了先决条件
sudo apt-get install python-software-properties python g++ make
然后,您可以通过添加托管 Node.js 的存储库、更新您的源代码并使用清单 1-2 中的命令进行安装。
清单 1-2 。在 Ubuntu 和 Linux Mint 上安装 Node.js
sudo add-apt-repository ppa:chris-lea/node.js
sudo apt-get update
sudo apt-get install nodejs
Fedora
Fedora 18 有一个简单的 Node.js 安装,它是一个单一的包管理器指令,如清单 1-3 所示。
清单 1-3 。在 Fedora 上安装 Node.js
sudo yum --enablerepo=updates-testing install nodejs npm
在 Fedora 的未来版本中,Node.js 应该默认集成到操作系统中。
Arch Linux
对于 Arch Linux ,通过定位“nodejs”包来利用 pacman 包管理器,如清单 1-4 所示。
清单 1-4 。在 Arch Linux 上通过 pacman 安装
pacman -S nodejs
FreeBSD 和 OpenBSD
Berkeley 软件分发(BSD)平台上的安装利用了端口安装程序,如清单 1-5 所示。
清单 1-5 。在 BSD 上安装
/usr/ports/www/node
open use〔??〕
当使用 openSUSE 时,可以使用 zypper 命令行包管理工具安装 Node.js,如清单 1-6 所示。
清单 1-6 。使用 zypper 在 openSUSE 上安装 Node.js
sudo zypper ar http://download.opensuse.org/repositories/devel:/languages:/nodejs/openSUSE_12.1/ NodeJSBuildService
sudo zypper in nodejs nodejs-devel
许多开发人员更喜欢在 OS X 甚至 Windows 上使用软件包管理器,而不是使用安装程序。Node.js 也可以通过这些包管理器来安装。
Windows 操作系统
使用 chocolatey 包管理器,简单地用 Chocolatey 命令安装,如清单 1-7 所示。
清单 1-7 。用 Chocolately 在 Windows 上安装 Node.js
cinst nodejs
x 是什么
无论你是在 Macintosh 上使用 MacPorts 还是 Homebrew 来安装你的软件包管理器,你都可以安装 Node.js,如清单 1-8 和清单 1-9 所示。
清单 1-8 。MacPorts
port install nodejs
清单 1-9 。自制
brew install node
此时,您应该已经在您的机器上安装了 Node.js,方法是在您的首选平台上使用您选择的方法。接下来,您需要确保有一种方法来发现和管理 Node.js 包。
1-2。安装 npm 软件包管理器
许多编程语言和平台依赖于使用第三方模块来扩展平台的实用性。Node.js 没有什么不同,因为它通过使用包管理器 : npm 得到了极大的扩展。 npm 是从 Node.js 本身独立出来的,现在仍然作为一个独立的项目维护着。但是,由于它越来越受欢迎和接受,从 Node.js 版本 0.6.3 开始,它就与 Node.js 二进制安装一起构建和部署。这意味着安装 npm 就像获取上一节中概述的 Node.js 的最新版本一样简单。因此,如果您已经使用了某个二进制安装程序,npm 已经可供您使用了。当然,如前一节所示,您可以选择在安装中省略 npm。如果 npm 不可用,您可以运行make install command,很快就可以使用了。
如您所料,安装 npm 的方法并不简单。如果您想要调试 npm 或测试在默认 npm 安装中不容易获得的特定功能,这些功能将非常有用。要进入“奇特的”安装,您必须首先找到 install.sh shell 脚本,它位于https://npmjs.org/install.sh。
这个安装 shell 脚本包含许多工具,用于以满足您特定需求的方式调用 npm。例如,如果您希望创建一个调试模式的 npm 实例,您可以调用 install.sh,如清单 1-10 所示。
清单 1-10 。npm 调试安装
npm_debug=1 sh install.sh
您还可以使用 npm 安装脚本来设置配置参数,如清单 1-11 中的所示。
清单 1-11 。npm 的附加配置参数
npm_config_prefix=/my/path sh install.sh
当然,您可以为 npm 构建一个补丁,在这种情况下,您最好从 GitHub 源代码下载并自己构建。这需要在您下载 npm 源代码的文件夹中运行 make 命令(见清单 1-12 )。
清单 1-12 。手动安装 npm
make install
安装了 npm 后,您的计算机现在就可以利用通过该软件包实用程序可以轻松访问的软件包和模块了。
1-3.了解常用模块
因为 Node.js 是一个用 JavaScript 编写程序的框架,所以它也有一些 JavaScript 的限制。其中缺少的一项是健壮的标准库的概念,就像人们在 C++这样的语言中可能会发现的那样。因此,在 JavaScript 应用中包含模块有很多变化和方法。例如,在浏览器世界中,这可以是从简单的<script>标签排序到脚本加载器到模块加载器的任何事情。对于 Node.js,一个简单且健壮的模块加载系统被大量使用,尽管这不是必需的。这种模块化系统被称为 CommonJS,代表用于共享的方法;它包括 Node.js 应用中的标准模块和第三方模块。
CommonJS 是一个社区驱动的项目,它将为整个 JavaScript 社区带来一个标准的库加载功能。CommonJS 实际上代表了一组规范建议,旨在创建一个标准化的模块加载器系统。CommonJS 模块的概念很简单,包括两个部分。首先,CommonJS 模块的创建者应该拿出一段可重用的 JavaScript,并从这段可重用的 JavaScript 中导出一个或多个特定的对象。第二,模块的消费者将需要从模块导出的对象,然后这些对象将被加载到应用中。规范(http://commonjs.org/specs/modules/1.0/ ))中概述的基本模块合同如下:
模块上下文
-
1.在一个模块中,有一个自由变量“require”,它是一个函数。
-
a.“require”函数接受一个模块标识符。
-
b.“require”返回外部模块的导出 API。
-
c.如果存在依赖关系循环,外部模块可能在它的一个可传递依赖关系需要它的时候还没有完成执行;在这种情况下,由“require”返回的对象必须至少包含外部模块在调用 require 之前已经准备好的、导致当前模块执行的导出。
-
d.如果请求的模块不能被返回,“require”必须抛出一个错误。
-
2 在一个模块中,有一个名为“exports”的自由变量,它是一个对象,模块可以在执行时向其添加 API。
-
3.模块必须使用“exports”对象作为唯一的导出方式。
模块标识符
- 模块标识符是由正斜杠分隔的“术语”字符串。
- 术语必须是 CamelCase 标识符:“”,或者”。. "。
- 模块标识符的文件扩展名不能像“.”这样。js”。
- 模块标识符可以是“相对的”或“顶级的”如果第一项是“.”,则模块标识符是“相对的”或者”。. "。
- 顶级标识符从概念模块命名空间根解析出来。
- 相对标识符是相对于编写和调用“require”的模块的标识符来解析的。
现在您可以检查 CommonJS 模块的简单实现是什么样子的。假设您创建了一个名为“describe.js ”的文件,该文件将导出一个文本字符串,该字符串以清单 1-13 中的模块描述作为响应。
清单 1-13 。describe.js 导出自身的描述
/**
* Describe module
*/
exports.describe = function() {
return 'I am a CommonJS Module';
};
该模块不需要任何其他模块来运行;它所做的只是导出 describe 函数,该函数返回一个字符串描述。但是这是非常无趣的,如果你想在你的应用中的其他地方包含这个模块,你需要在你的代码中安装这个模块。为此,使用 CommonJS require()函数,如清单 1-14 所示。
清单 1-14 。需要描述模块
var describeModule = require('./describe.js');
现在您有了对 describe 模块的引用,但是这意味着什么呢?调用 require()时会发生什么?当您调用 require()时,Node.js 将定位资源并读取和解析文件,授予您对模块的导出 API 的访问权限。当 Node.js 将文件加载到您的应用中时,它会自动将模块隔离到它自己的作用域名称空间中,以防止全局名称在灾难性的哭泣中发生冲突。因为 Node.js 已经为您加载了这个资源,所以您可以从这个资源中调用导出的功能(参见清单 1-15 )。
清单 1-15 。引用所需模块中的导出函数
var describeModule = require('./describe.js');
console.log(describeModule.describe());
CommonJS 模块也不完全是关于导出功能的。它们可用于创建一个 API,该 API 建立在模块文件的功能之上,但将该功能留给模块本身。假设您有一个更健壮的模块,其中只需要暴露模块的某一部分;您可以很容易地为这个“私有”功能创建一个方法,并且仍然可以在导出的解决方案中看到它,如清单 1-16 中的所示。
清单 1-16 。导出模块中的“私有”方法
/**
* Desc module with private method
*/
var _getType = function() {
return 'CommonJS Module';
};
exports.describe = function() {
return 'I am a ' + _getType();
};
稍后,您将看到更多关于创作和使用 CommonJS 模块的内容,但是,现在,重要的是了解这一切是如何工作的。CommonJS 模块导出功能,并且只导出显式导出的功能。其他函数或方法可能与 CommonJS 模块共存,但它们仅限于模块本身的私有范围,而不适用于访问该模块的应用。如果您仔细并适当地构建 CommonJS 模块,这可以为您的应用生成非常干净的 API。
了解 Node.js 如何实现 CommonJS 方法来加载模块,有助于您创造性地思考应用的结构和 API,允许代码共享和重用,并使您的应用代码更清晰、更易于理解。
1-4.为您的应用编写模块
现在您已经理解了什么是 CommonJS,以及它如何作为模块加载器与 Node.js 相关,您可以开始考虑如何为您的应用构建自己的模块了。如果您决定不为您的应用构建模块,您将很快看到您的应用代码变得难以处理,并导致您的维护噩梦,异构数据结构、对象和回调散布在将成为一个整体 Node.js 应用的各处。
当您开始考虑编写模块时,首先想到的应该是简单的任务划分。如果您的应用要求用户通过服务器验证才能访问内容,那么您可能需要为您的用户数据创建一个模块。这可能包含会话状态、用户信息、身份验证协议等等。如果在 Node.js 应用的核心包含了这部分特定于用户的数据,那么每次看到这段代码时,您都会后悔不得不绕过这段代码。清单 1-17 展示了当你的应用缺少模块时,这些数据会是什么样子。
清单 1-17 。带有可导出特性的杂乱代码
/**
* User authentication code not in its own module
*/
var db = require('./db.js');
app.on('/createUser', function(req, res) {
var user = req.username,
pwd = req.password,
email = req.email;
db.lookup('user', {username: user }, function(err, data) {
if (err) {
return;
}
// Didn't find a user by that name
if (data.userid === null) {
createSalt(10, function(err, salt) {
if (err) {
return;
}
createHash(pwd, salt, function(err, hash) {
db.create('user', {username: user, password: pwd, email: email }, function(err, user) {
if (err) {
return;
} else {
user.isauthenticated = true;
app.users.push[user];
res.send(user);
}
});
});
});
}
});
});
function createSalt(depth, callback) {
// do salting here
if (err) {
return callback(err);
};
callback();
};
function createHash(password, salt, callback) {
// hashify
if (err) {
return callback(err);
}
callback();
}
上面的代码是客户端请求创建新用户的示例。在成功创建用户之前,必须通过应用的各个部分。它必须首先检查数据库,以确保用户名没有被占用。然后它必须创建一个 salt 并用这个 salt 散列密码。然后,它必须将用户信息存储在数据库中,并将新的用户对象传输给应用。这本身看起来并不笨拙,但是您可以想象作为一个大型项目的一部分,您希望将它从主应用移到它自己的模块中。我们可以通过创建一个身份验证模块来保存 createSalt 和 createHash 方法 ,从而消除大部分代码。这些新方法如清单 1-18 所示。
清单 1-18 。导出 Salt 和 Hash 方法
/**
* Authentication module
*/
exports.createSalt = function(depth, callback) {
//do salty things
if (err) {
return callback(err);
}
callback();
}
exports.createHash = function(password, salt, callback) {
//hashification
if (err) {
return callback(err);
}
callback();
}
我们通过将两个大函数放在一个模块中,从主代码中删除了它们。接下来我们创建一个用户模块,它将处理清单 1-19 中所有与用户相关的事情。
清单 1-19 。用户模块
/**
* User module
*/
var db = require('./db.js');
var auth = require('./auth.js');
exports.create = function(req, res, callback) {
var user = req.username,
pwd = req.password,
email = req.email;
db.findOrCreate('user', {username: user});
db.lookup('user', {username: user }, function(err, data) {
if (err) {
return callback(err);
}
// Didn't find a user by that name
if (data.userid === null) {
auth.createSalt(depth, function(err, salt) {
if (err) {
return callback(err);
}
auth.createHash(pwd, salt, function(err, hash) {
db.create('user', {username: user, password: pwd, email: email }, function(err, user) {
if (err) {
return callback(err);
} else {
user.isauthenticated = true;
return callback(user);
}
});
});
});
}
});
};
这现在在应用之外,所以我们最初的 createUser 处理程序现在简化为清单 1-20 中显示的简明信息。
清单 1-20 。带有所需用户模块的主应用
/**
* User Authentication code within its own module
*/
var user = require('./user.js');
app.on('/createUser', user.create(function(err, user){
if (err) {
return;
}
app.users.push[user];
}));
这个例子概述了一种通用的方法,通过使用模块化来将您的代码减少到可管理的部分。编写 CommonJS 模块时,记住它们的一些基本规则是很重要的。您可以根据您认为合适的任何准则来创建您的模块,但是您必须使用 exports 变量,以便将您的模块的任何方法公开给您希望在其中看到它的任何代码部分。您还需要为您的模块设计一个逻辑位置以便加载它,因为 require 函数需要一个标识符来找到该模块。在许多情况下,这可以是 Node.js 应用结构中的本地相对路径,或者是一个更全球化的包(如果使用 npm 模块的话)。当然,用例会有所不同,但是根据经验,如果代码妨碍了您,您应该能够将它提取到它自己的模块中。
1-5.应用中需要模块
在构建 Node.js 应用时,几乎不可避免地需要在应用中使用一组模块,就像上一节中创建的那些模块一样。为此,您将使用 CommonJS 模块加载需求函数。这个函数将在文件系统中按名称查找模块,并加载其导出的 API。这听起来非常简单,但是要真正理解加载模块时会发生什么,您必须理解模块是如何被检索的。
Node.js 在尝试加载模块时使用了一种复杂的策略。当加载一个模块时,首先检查的地方是模块缓存,所以如果你已经加载了一个模块,你就可以访问它了。如果 Node.js 找不到缓存的模块,则优先考虑 Node.js 本地模块,如 crypto、http、fs 等。如果传递给require()的标识符没有找到本机模块,那么 Node.js 将在文件系统中搜索传递给它的标识符。
Node.js 模块的文件系统查找比按名称查找本机或缓存模块稍微复杂一些。当需要 Node.js 中的模块时,标识符可以有几种形式,执行的查找可以相应地改变。
当您试图加载一个非本机模块时,Node.js 遇到的第一种情况是,如果您提供一个带有文件扩展名的标识符,比如 require ('aModule.js');.,Node.js 将尝试只加载您所要求的基本路径中的那个文件,除非您已经在 require 前面加上了一个相对路径,如require('./modules/aModule.js');。在这种情况下,Node.js 将尝试从您指定的路径中加载您的模块。在 Node.js 中加载模块时,文件扩展名是可选的。这允许以更简洁的方式编写模块,但也给 Node.js 提供了更模糊的解析路径。要加载不提供扩展名的模块,Node.js 要做的第一件事就是尝试加载带有每个扩展名的文件:。js,。如果 Node.js 没有基于向模块标识符隐式附加扩展名来解析文件,则假定该标识符是相对于基的路径。一旦假设这是一个路径,Node.js 将解析该路径并首先搜索package.json,如果它存在就加载它。如果没有,Node.js 下一步假设路径中一定有一个“索引”文件,并再次尝试加载这个文件,并隐式添加扩展名。此时,Node.js 要么有一个可以加载的文件(在这种情况下,它会将该模块添加到模块缓存中),要么找不到该模块并将抛出错误。
为了形象化这些场景,让我们创建一个假设的应用,它的文件夹结构看起来像清单 1-21 中的轮廓。
清单 1-21 。概述嵌套的应用
myApp/
-main.js
-aModule.js
-subfolder/
bModule.js
index.js
您可以假设应用根在 JavaScript 文件“main.js”中,该文件加载了我们的应用所需的所有依赖项,如清单 1-22 所示。
清单 1-22 。加载依赖项
/**
* main.js - module loading
*/
// First we require 'http' which is a native Node.js Module var http = require('http'),
// load a module with an extension Node.js has not trouble with this
modA = require('./aModule.js'),
// Load ambiguous filename from subdirectory load bModule.js fine
modB = require('./subfolder/bModule'),
// Load index.js from subdirectory
sub = require('/subfolder/'),
// not a file or native module
// Error: Cannot find Module 'cheese'
missing = require('cheese');
当需要 Node.js 中的模块时,您可以自由决定如何构建应用和文件名。这些规则不仅适用于本地创建的文件和本机 Node.js 模块,还适用于通过 npm 加载到您的应用中的模块,您将在下一节中详细了解这些模块。
1-6.使用国家预防机制模块
您安装了 Node.js 和 npm。您还知道应该如何在 Node.js 应用中包含 CommonJS 模块,但是您不希望每次创建应用时都要从头开始。此外,您可能知道一个 Node.js 包,它用于您希望在代码中完成的任务。进入 npm 模块。
npm 是一个由社区驱动的 Node.js 包和实用程序的存储库,它们以允许任何人访问的方式发布。npm 系统发展非常迅速,在整个发展过程中与 Node.js 的发展保持同步。根据在https://npmjs.org的 npm 站点,目前有超过 25000 个可用的 npm 包。有了这么多的软件包,很容易就很难找到符合您需求的软件包。如图 1-8 所示,使用https://npmjs.org/上的搜索功能,即谷歌网站搜索。

图 1-8 。https://npmjs.org/上的 npm 搜索
或者,您可以利用 npm 本身内置的搜索机制。这是在你终端的命令行上运行的(见清单 1-23 )。
清单 1-23 。命令行 npm 搜索
npm search <term(s) or package name>
当您执行搜索命令时,npm 将首先缓存所有包的本地索引,然后搜索包名称、描述、作者以及包的关键字。结果以表格的形式返回,其中显示了包名、描述、作者、发布日期、版本和关键字。这对于确定您想要使用哪个包很有用。要查看关于某个包的详细信息,请运行 npm view 命令,如清单 1-24 所示。
清单 1-24 。包的详细视图
npm view <package name>
一旦你发现了你想要安装的包,并查看了包的详细信息,你就可以安装它了,也是通过命令行(见清单 1-25 )。
清单 1-25 。通过命令行安装
npm install <package name>
运行 npm install 命令将下载该软件包,并将其放在应用的一个目录中。该目录名为“node_modules ”,通常与名为 package.json 的包定义文件位于同一目录中。该文件夹有助于定义 npm 包。这意味着,如果您有一个需要此 npm 模块的文件,它将在此目录中查找。子目录中的文件也是如此,这意味着在一个包中,npm 安装的模块将从该目录安装和引用。这可以防止太多子目录包含引用单个模块的“node_modules”目录,从而使应用结构变得混乱。
与许多将包下载到中央共享目录的包管理器(例如 Python 的 easy_install)相反,npm 相对于包本身在本地安装模块。但是,当您从 npm 安装软件包时,也可以设置一个全局标志。通过将软件包设置为全局安装,该用户的任何应用都可以访问它,因为它安装在\(HOME 目录中(Windows 上为\)USERPROFILE)。要进行全局安装,只需在安装命令中添加标志,如清单 1-26 所示。
清单 1-26 。全球化软件包安装
npm install –g <package name>
# or
npm install –-global <package name>
node_modules 目录是 Node.js 的模块查找例程中的一个特例,这在上一节中已有概述。模块查找不会直接从查找本机 Node.js 模块跳到在目录结构中搜索文件名。如果 Node.js 没有将该模块识别为本机模块,那么它将检查 node_modules 目录,以查看该模块是否位于那里,然后继续加载瀑布。
因为这是模块加载路径的一部分,所以需要一个 npm 模块和您自己创建的模块没有任何区别,这允许您以相同的方式引用它。
1-7.npm 和依赖关系入门
在开发健壮的 Node.js 应用时,使用 npm 有很多好处。如您所见,在您的应用中发现、检索和包含任何已发布的 npm 包非常容易。有更简单的方法来确定如何构建您的应用,以及您的应用包含哪些 npm 模块。这是通过应用目录中的 npm 包管理文件 来完成的。这些文件被命名为 package.json,包含完整管理应用的远程依赖项所需的所有细节。
让我们详细研究一下 package.json 是什么,以及它在 Node.js 应用中是如何工作的。首先,package.json 包含 JavaScript Object Notation (JSON),,Node.js 和 npm 对其进行解析,以便读取应用的详细信息。当这个文件被解析时,它可以帮助加载依赖项、提供应用元数据、启动和停止应用、列出作者和贡献者、代码库、开发依赖项等等。让我们看看 package.json 的各个字段可能是什么,以及它们告诉 npm 和 Node.js 什么。其中一些字段应该包含在所有 package.json 文件中,而其他字段在您将软件包发布到 npm 注册表时更有用,这将在后面的章节中介绍。
名称
name 字段在 package.json 中是必需的。没有它,您的应用将无法安装。这个名字的一个规则是它必须是一个 URL 安全的名字。如果您发布包,该名称将成为用于定位包的 URL 的一部分,因此必须能够被解析为 URL。当其他模块需要您的模块时,它也将利用您的模块。这意味着不推荐使用一个长得离谱的名字,因为不太可能有人想要输入一个特别长的包名(就像清单 1-27 中的那个)。
清单 1-27 。一个长得离谱的包名
var poppins = require('supercalifragilisticexpialidocious');
但是,您可能需要创造一个特别的名称,因为 npm 模块名称在整个 npm 注册表中必须是唯一的。也不要在名称中包含“js”或“node”。假设文件是 JavaScript,您可以添加“Node”作为“引擎”指令的一部分,稍后您将看到这一点。
版本
版本字段也是必需的。它管理安装哪个版本,并与 package.json 文件中的“name”字段一起使用,以确定一个完全唯一的标识符。每次你对你的包进行修改的时候,你都应该修改版本号。版本号可以有多种形式:一些常见的形式是简单的数字(0.0.1 或 v0.0.1)。有时候开发人员喜欢在版本号上加一个限定符,比如 0.0.2-alpha,0.0.2beta,或者 0.0.2-42。这些都表示不同的版本,并且适合由 npm 语义版本解析器 node-semver ,解析的层次结构。
描述
描述字段仅仅是包的文本描述;描述中的术语可通过 npm 搜索进行搜索。
关键词
关键字也用于 npm 搜索;这些帮助其他开发者瞄准你的包。
主页〔??〕
在此字段中,您可以放置包或项目主页的 URL。
bug
这为开发人员指出了一个地方(问题跟踪器或电子邮件地址)来查找或提交 bug,这样他们可以帮助您使您的项目更加令人惊叹。
许可证
此字段描述您的代码将使用的许可证。这可以很简单,如 MIT、BSD 或 GPL,或者您可以使用带有许可文件类型和 URL 的定制许可。
作者、投稿人
这些部分包含负责该包的人员的姓名、电子邮件和 URL。
文件
这个字段对于 package.json 文件 中良好的依赖性管理至关重要。该字段列出了您希望包含在软件包中的文件。这也可以包含需要与包捆绑在一起的文件夹列表。您可以加载一个文件目录,然后用。npmignore 文件,它指定要忽略哪些文件。
主要的
该字段告诉模块加载器当 Node.js 应用中需要包时,加载哪个模块。这应该是该模块的模块标识符。
容器
该字段控制 npm 将任何可执行文件安装到什么位置,这些文件将位于 node_modules/中。bin/目录或者将被象征性地全局链接。这正是 npm 本身为 npm 安装命令行界面的方式。bin 字段 接受一个键字段(将是链接的命令)和一个值(将链接到该键的脚本)。例如,bin 字段类似于清单 1-28 中所示。
清单 1-28 。package.json 中的 bin 字段
{"bin": {"program": "./path/to/program"}}
储存库
该字段指示中央存储库的位置。该存储库包含 Node.js 包的代码。该字段接受一个类型(表示版本控制类型,如 git 或 svn)和一个 URL 参数,以便定位您的代码。
配置
该字段可用于您的包中持续存在的配置选项。您可以设置一个已配置的端口号,让应用在生产模式下运行,或者在 npm 注册表中设置要关联的用户名。
属国
在设计 package.json 文件时,这是一个非常重要的字段:它保存着对应用的成功至关重要的信息。因为有各种版本的其他可用包,所以这个字段将列出那些您可能已经用 Node.js 应用测试过并且知道可以正常工作的包。依赖关系不仅可以概括应用依赖于哪些 npm 库,还可以针对这些项目的特定版本。有多种方法可用于指定目标版本。这可以通过明确列出确切的版本号、明确定义范围、使用比较运算符隐式定义范围、列出 URL、波浪号版本系统和“X 版本范围”来实现(参见清单 1-29 )。
清单 1-29 。管理 package.json 文件中的依赖关系
明确列出依赖关系的版本号
"package": "0.0.1"
"package": "=0.0.1"
依赖关系也可以通过一个范围提供一组版本来管理
"package": "0.0.1 – 0.0.3"
"package": ">=0.0.1
包范围 也通过比较运算符如列在文件的依赖部分
"package": ">0.0.1"
"package": "<0.0.1"
"package": ">=0.0.1"
"package": "<=0.0.1"
范围也可以用“x”占位符表示,该占位符允许任何数字代替“x”
"package": "0.1.x"
"package": "0.0.x"
作为 URL 的包必须指向一个 tarball 或者一个可以被检出的 git 端点(
tarball
"package": "https://example.com/package.tar.gz"
Git
"package": "git://github.com/organization/source.git"
波浪号范围 表示范围的子集,该子集必须至少等于带有波浪号的版本,但不能大于下一个主要版本
"package": "∼0.8.4"
Is equivalent to:
"package": ">=0.8.4 <0.9.0"
有一些依赖项的变体也可以成为 package.json 文件的一部分。它们是 devDependencies、optionalDependencies 和 bundledDependencies。
devDependencies
如您所料,如果使用开发标志--dev调用 npm install,那么devDependencies是将被下载的依赖项。如果开发分支使用了一个在安装产品版本时不需要的框架,这将是有用的。
bundle dependencies
bundledDependencies 标志表示在发布包时要捆绑哪些项目。
可选依赖关系
无论是否存在,都是需要在应用中处理的依赖项。如果你有一段代码可能依赖于某个包,你必须考虑这个包是否在 optionalDependencies 散列中,如清单 1-30 所示。
清单 1-30 。实践中的可选依赖性
try {
var optional = require('optional');
} catch(err) {
optional = null;
}
if (optional) {
optional.doThing();
} else {
doThingWithoutOptionalPackage();
}
发动机
您也可以在 package.json 文件中指定引擎。这意味着,如果您知道您的应用只能在某个版本范围内的 Node.js 或 npm 上工作,请在这里设置它。引擎的值遵循与依赖项相同的值(见清单 1-31 )。
清单 1-31 。定义引擎
"engines": {
"node": "0.8.x",
"npm": "*"
}
操作系统和 CPU
如果您意识到应用的某些方面只能在给定的操作系统上执行,或者您只针对特定的操作系统,那么您也可以在 package.json 文件中添加特定的值。这也适用于你的目标 CPU,比如 64 位机器,这可以在清单 1-32 中看到。
清单 1-32 。为您的应用定义架构
"os" : ["linux", "darwin", "!win32"],
"cpu": ["!arm", "x64" ]
首选全局
如果您维护的软件包最好作为全局软件包运行,或者您希望它作为全局软件包安装,请将此标志设置为 true,如果用户选择在本地安装它,它将设置一条控制台警告消息。
将 package.json 文件中的所有字段放在一起有助于指定运行 Node.js 应用所需的配置和依赖关系。这使得您的包非常容易移植,并且在新机器上的设置通常很简单。当您组装一个简单的应用时,您将能够通过检查 package.json 文件获得关于您的应用的有价值的信息。在构建应用时,管理所有这些设置似乎很麻烦。幸运的是,如果您已经在 Node.js 中构建了一个应用,那么您可以通过运行命令npm init追溯创建一个有效的 package.json 文件,这将产生一个类似于清单 1-33 中的包的文件。
清单 1-33 。通过 npm init 初始化应用
{
"name": "squirrel",
"version": "0.0.1",
"private": true,
"scripts": {
"start": "node app"
},
"dependencies": {
"express": "3.0.0rc4",
"ejs": "*",
"feedparser": ""
},
"gitHead": "e122...",
"description": "A sample app to parse through your RSS feeds",
"main": "app.js",
"devDependencies": {},
"repository": {
"type": "git",
"url": "ssh://git@bitbucket.org/username/squirrel.git"
},
"keywords": [
"rss"
],
"author": "cory gackenheimer",
"license": "MIT"
}
当您检查上面由npm init生成的 package.json 框架时,您会注意到文件中并没有包含所有可用的字段。这没问题,但是这个文件表明的是应用的名称和版本。然而,更重要的是,它规定了依赖关系,因此当您从应用的根目录执行npm install时,npm 将安装 express、ejs 和 feedparser 的指定版本,以便解析 package.json 中列出的依赖关系。
1-8.安装 Node.js 版本管理器
如果您打算花时间开发 Node.js 应用,您将不可避免地遇到 Node.js 的新版本。这是因为 Node.js 的开发正以令人难以置信的速度前进。在撰写本文时,当前的稳定发布版本是 0.10.x。该版本包含适用于生产环境的稳定 API。还有第二个实验性版本,当前版本为 0.11.x。实验性版本不适合生产,因为它包含为即将到来的版本创建的新功能。可能您正在为当前的稳定版本开发一个包,但是您也想确保它将在即将到来的版本中继续与 Node.js API 变化一起工作。如果是这种情况,您将需要安装 Node.js 的多个版本。首先,您可以访问 Node.js 网站,下载 Node.js,并安装您的目标版本。第二,也是更优雅的,你可以使用一个版本管理工具。
一旦您决定使用 Node.js 版本管理工具,您就可以在不同的工具之间进行选择。目前有三种主要的 Node.js 版本管理工具:nvm、n 和 nave。这些工具略有不同,可能提供更适合您的特定用例的选项。
由 Tim Caswell 创建的 node Version Manager(nvm)是一个可安装的 shell 脚本,它将下载并安装您指定的 Node.js 的多个版本。要安装 nvm 实用程序,您需要获得安装脚本,它也是一个 shell 脚本。有三种方法可以获得和安装这个脚本:cURL、Wget 或手动安装。
要手动安装,首先需要获得位于 GitHub ( https://github.com/creationix/nvm ). It can be downloaded from that location directly, or fetched at git clone git://github.com/creationix/nvm.git ∼/nvm)上项目存储库中的nvm.sh文件。
`然后,您需要运行刚刚下载到 nvm 目录的 shell 脚本:
∼/nvm/nvm.sh
幸运的是,如果你使用 cURL(参见清单 1-34 )或 Wget(参见清单 1-35 ),有一种更简单的方法可以安装 nvm:你可以下载 shell 脚本并在你的∽/中添加一个 nvm 别名。bash_profile 或者∽/。配置文件。
清单 1-34 。使用卷曲
curl https://raw.github.com/creationix/nvm/master/install.sh | sh
清单 1-35 。使用 Wget
wget -qO- https://raw.github.com/creationix/nvm/master/install.sh | sh
安装其他 Node.js 版本管理工具同样简单。与 nvm 类似的脚本是一个名为 nave 的工具,由 Isaac Schlueter 开发,打包为 Node 的虚拟环境。Nave 运行 shell 脚本,就像 nvm 一样;但是,它可以通过 npm 安装。
npm install –g nave
将介绍的最后一个版本管理实用程序也是通过 npm 安装的,npm install –g n,并且在功能上与其他工具类似。
这些工具旨在使使用多个版本的 node 变得非常简单和直观。它们各有不同的特性,因此作为开发人员,您可以找到合适的工具。在下一节中,您将看到对每个版本管理工具的工作流和可用选项的深入研究。
1-9.在您的计算机上使用多个版本的 Node.js
可以想象,开发 Node.js 应用需要时间。您可能在 Node.js 处于给定版本时开始一个项目,并且所有的包都与它无缝地工作。但是,您可能会发现,由于这样或那样的原因,您需要升级(或降级)您的应用所使用的 Node.js 版本。要在您的应用中实现这一点,并在您的计算机上利用多个 Node.js 实例,您可以使用 Node.js 版本控制工具之一。虽然每个工具都提供相似的功能,但是它们的 API 略有不同,并且包含每个工具独有的某些功能。
非易失性存储器
nvm 的安装遵循 nvm 安装
清单 1-36 。nvm 安装
$ nvm install v0.8.23
$ nvm install 0.8.23
$ nvm install 0.8
安装脚本找到适当的版本号,转到https://nodejs.org/dist/目录中的适当位置,并在您的机器上安装 Node.js 的指定版本。该脚本不会为了在全球范围内使用而改变您的路径。为此,使用命令 use 指定您希望使用的版本(见清单 1-37 )。
清单 1-37 。nvm 使用
$ nvm use 0.10.1
如果不知道想要安装或使用的 Node.js 的具体版本,可以通过执行nvm ls-remote命令列出 Node.js 的远程版本。这将列出所有可供下载的版本。如果您的机器上已经安装了多个版本,那么您可以使用nvm ls命令来显示您的机器上当前可用的 Node.js 版本的列表。
nvm 允许您指定一个与您正在使用的已安装版本不同的特定版本,以运行您的应用。例如,您可以输入nvm run 0.6 server.js。这将使用最新安装的 Node.js 0.6.x 版本运行您的应用(server.js),即使使用 use 命令设置的版本完全不同。
使用 nvm,您还可以通过运行nvm alias <name> <version>为版本设置别名。这种情况的用例可能类似于 run 命令,但是如果您想要对您的应用多次测试一个构建,您可能会发现键入 run 命令很麻烦。为此,别名非常有用,例如nvm alias dev 0.11.0,它允许您使用更简单的命令(dev server.js,而不是nvm run 0.11.0 server.js),测试 node . js 0 . 11 . 0 版中的新特性。
当然,通过 nvm 安装多个版本,您可能会以内务管理噩梦告终。当试图维持某种秩序时,太多的版本可能会造成问题。这些问题通过nvm uninstall <version>命令和nvm unalias <alias>命令得到解决。这两个操作分别卸载指定版本的 Node.js 并从您的计算机中删除指定的别名。
中央广场
nave 和 nvm 没有太大的不同。事实上,nave 认为 nvm 是灵感的来源。实现和底层 shell 脚本之间存在一些差异,这些差异足以引起注意。重要的是要理解 nave 是作为 Node.js 虚拟环境提供者而不是版本管理器出现的。首先,nave install <version>期望 version 参数是 Node.js 的精确版本,除了获取最新的或者最新的稳定版本,它们看起来分别像nave install latest和nave install stable。
nave 的虚拟环境部分基于use命令。这个命令要么接受一个版本号本身,一个版本号后跟一个 Node.js 程序参数,要么接受一个名称后跟一个版本号,如清单 1-38 所示。
清单 1-38 。天真的使用
如果使用版本,这将使用指定的版本打开一个子外壳
$ nave use <version>
提供一个带有程序目标的版本,它将使用指定的版本在子 shell 中运行程序
$ nave use <version> <program>
这将提供一个基于指定版本的别名
$ nave use <name> <version>
要设置用于开发的主版本,命令是nave usemain <version>。您还可以通过运行nave uninstall <version>来删除一个已安装的实例。与此类似的是nave clean <version>命令。clean 命令不会卸载该版本,但会删除指定版本的源代码。nave还提供了一组列表命令,ls和ls-remote以与 nvm ls 和 ls-remote 命令相同的方式操作,通过提供可用的 Node.js 的本地或远程版本的列表。nave 脚本提供了一个额外的ls-all命令,它将列出开发人员可以使用的本地和远程 Node.js 版本。如果您想知道 Node.js 的最新版本,只需运行nvm latest命令。
n
Node.js 版本控制工具 n 在实现和 API 方面与 nvm 和 nave 有所不同,但它的主要目的仍然是允许您安装多个版本的 Node.js。使用 n,您可以通过使用命令n <version>指定 Node.js 的确切版本。这没有 nvm 宽松,它允许你选择一个主要的修订版,它将安装该版本的最新版本。不同之处在于你可以指定最新的(见清单 1-39 ),或者最新的稳定版本(见清单 1-40 ),n 会为你获取。
清单 1-39 。获取最新版本
$ n latest
清单 1-40 。获取最新的稳定版本
$ n stable
这些命令所做的是转到https://nodejs.org/dist/站点并搜索最新版本(可用于检索最新版本的最高编号或可用于最新稳定版本的最高偶数版本)。要移动到你已经安装的先前版本,只需使用n prev命令.
要查看本地安装的可用 Node.js 版本,并选择要使用的版本,只需键入命令 n。n 命令本身将列出测试版本,以及您指定要与该版本一起运行的任何标志。为了指定一个标志或配置选项,你只需在你的 n
清单 1-41 。将调试参数传递给此版本的 Node
n latest --debug
n bin <version>命令将输出您机器上的二进制安装的路径。如果您的机器上没有指定的版本,n 会让您知道它没有安装。如果你想不使用作为n use <version> [args...]运行的use命令而直接针对一个特定的版本,那么 bin 命令非常方便
要卸载,或者使用 n 删除 Node.js,命令是n rm <version list>。您会注意到这个命令接受一个版本列表,这意味着您可以在一次绑定中从系统中删除 Node.js 的多个版本。
摘要
在本章中,您回顾了在您的机器上安装 Node.js 的多种方法。您还了解了在 Node.js 应用中实现和使用 CommonJS 模块的基础知识,包括利用 npm Node.js 包管理工具。接下来的章节将开始有一个食谱为中心的格式。这种格式将许多有趣的 Node.js 主题分成一个问题-解决方案方法。这意味着您将从构建 Node.js 应用时可能遇到的问题的定义开始,然后阅读该问题的解决方案。您可以通读这些章节,了解 Node.js 的某些部分是如何工作的,并且能够在作为案头参考返回本书时轻松找到这些解决方案。`
二、使用 Node.js 访问网络
Node.js 被设计为在网络环境中运行良好。它的非阻塞、事件驱动架构允许使用高度可伸缩的网络应用。在这一章中,您将发现许多围绕 Node.js 及其网络功能的实现细节。特别是,您将看到的食谱将涵盖这些主题:
- 设置服务器
- 创建到服务器的连接
- 配置服务器默认值
- 创建客户端
- 使用套接字在服务器之间进行通信
- 正在检索有关已连接服务器的详细信息
- 控制套接字详细信息
一旦你阅读了这一章,你应该不仅有能力构建一个简单的网络应用,而且可能有一个健壮的解决方案来整合到你的工作流程中。
2-1.设置服务器
问题
您需要设置一个服务器来提供联网的 Node.js 应用。
解决办法
在 Node.js 中,构建在端点之间提供数据的网络应用的标准解决方案是利用一个名为net的内置 Node.js 模块。该模块提供了设置 Node.js TCP 服务器所需的全部内容。要设置一个 Web 服务器,你必须首先需要这个模块(见清单 2-1 )。
清单 2-1 。需要net模块
var net = require('net');
在需要这个模块之后,使用createServer()方法创建服务器。这个方法带有一个可选参数,它将在服务器上设置默认选项,还有一个connectionListener参数,它将监听到您的服务器的连接。要真正启用新创建的服务器,您需要告诉您的服务器监听哪个端口。这是通过调用由net模块提供的listen()方法来完成的。清单 2-2 中显示了一个完全运行的服务器。
清单 2-2 。一个简单的 TCP 服务器
var net = require('net');
var server = net.createServer(function(connectionListener) {
console.log('connected');
//Get the configured address for the server
console.log(this.address());
//get connections takes callback function
this.getConnections(function(err, count) {
if (err) {
console.log('Error getting connections');
} else {
console.log('Connections count: ' + count);
}
});
connectionListener.on('end', function() {
console.log('disconnected');
});
//Write to the connected socket
connectionListener.write('heyyo\r\n');
});
server.on('error', function(err) {
console.log('Server error: ' + err);
});
server.on('data', function(data) {
console.log(data.toString());
});
/**
* listen()
*/
server.listen(8181, function() {
console.log('server is listening');
});
现在您已经创建了一个简单的服务器。假设您已经将您的服务器文件命名为 server.js,您可以很容易地用 Nodeserver.js运行它。
它是如何工作的
让我们更详细地检查一下这个服务器。首先,回忆一下 Node.js 模块是如何加载的,如第一章中的所述。这就是 native Node.js 模块net的加载方式,require('net') ;.服务器是通过模块导出的createServer()方法创建的,该方法在net模块中实例化一个内部服务器对象,如清单 2-3 所示。
清单 2-3 。net 模块创建服务器方法
exports.createServer = function() {
return new Server(arguments[0], arguments[1]);
};
这个方法有两个参数,所以在服务器函数中,一定要确定哪个参数代表 options 对象,这个对象可以选择传递给createServer()方法,也就是连接监听器。如果您进一步研究这个函数,您会发现 Node.js 用来确定这些参数的是对它们的属性的简单检查。如果确定第一个参数的类型是函数,则第一个参数不可能是 options 对象,从而使第一个参数成为连接侦听器。或者,如果第一个参数不是函数,则假定它是 options 对象,如果第二个参数是函数—,则将其用作连接监听器。
连接监听器,像 Node.js 编程中的许多函数一样,是一个简单的回调函数。一旦net模块中的服务器对象将它标识为一个函数,它就会作为回调传递给服务器连接监听器,其形式类似于server.on('connection', connectionListener);。这会将任何新连接传递回应用中的侦听器。这个逻辑如清单 2-4 所示。
清单 2-4 。确定服务器选项和连接监听器
var self = this;
var options;
if (typeof arguments[0] == 'function') {
options = {};
self.on('connection', arguments[0]);
} else {
options = arguments[0] || {};
if (typeof arguments[1] == 'function') {
self.on('connection', arguments[1]);
}
}
在您创建的服务器开始监听端口后,会出现一个新的连接。端口由传递给服务器的listen()函数 的第一个参数决定。如果您的服务器要监听 UNIX 路径或任何可连接的句柄对象,那么listen()函数也可以接受一个路径。在清单 2-2 中的示例服务器中,端口被设置为 8181。第二个参数是回调,一旦服务器成功开始监听定义它的端口或路径,就会执行回调。listen()事件也假设了一个宿主。主机可以是任何 IPv4 地址,但是如果省略,Node.js 会认为您的目标是localhost。现在您有了一个简单的服务器,它将监听您选择的端口。
正如您在清单 2-2 中创建的服务器中所看到的,您还可以深入了解服务器的当前配置。首先,您可以检索关于服务器正在监听的地址的信息。这个信息是通过调用server.address()方法获取的。这将返回一个显示服务器地址、家族和端口的对象(见清单 2-5 )。
清单 2-5 。server.address( )
{
address: '127.0.0.1',
family: 'IPv4',
port: 8181
}
除了检索服务器地址,您还可以获得到您的服务器的连接数。这是通过在代码中调用getConnections() 方法来完成的。getConnections()函数接受一个回调函数,该函数应该接受两个参数:一个错误参数和一个计数参数。这将允许您在获取连接时检查错误,并获得到服务器的当前连接数。这显示在清单 2-2 中创建的服务器内的connectionListener回调中。
Node.js 的net模块中的服务器对象是一个event emitter,这是 Node.js 编程中常见的范式。event emitter提供了一种通用语言,对象可以用这种语言注册、删除和监听由系统生成或由开发人员定制的事件。服务器对象公开了几个事件,其中一些您已经见过了,比如连接和监听事件。connection 事件在每次新的套接字连接到服务器时发生,而 listening 事件,如您所见,是在服务器开始监听时发出的。作为net.Server对象基础的另外两个事件是 close 和 error。当服务器遇到错误时,将发出 error 事件。发出错误后,error 事件还会立即发出 close 事件。close 事件只是关闭服务器;但是,它会一直等到每个连接的套接字的连接结束。
2-2.创建到服务器的连接
问题
您需要创建一个到 Web 服务器的连接。
解决办法
为了建立到服务器的连接,您需要知道它监听的端口或 UNIX 路径。一旦了解了这一点,就可以通过 Node.js 创建一个连接。为此,您将再次使用 Node.js 本机net模块,该模块公开了一个createConnection方法 ,用于连接到一个远程(或本地)实例。
为了利用net模块通过 Node.js 连接到服务器,你必须再次通过一个 CommonJS require 设置到net模块的连接,如清单 2-6 所示。
清单 2-6 。导入网络模块进行连接
var net = require('net');
然后下一步是调用createConnection方法,传递要连接的端口或 UNIX 路径。或者,如果需要指定 IP 地址,也可以传递主机。现在我们可以创建一个记录控制台连接的 connectListener,如清单 2-7 所示。
清单 2-7 。创建到服务器的连接
var net = require('net');
// createConnection
var connection = net.createConnection({port: 8181, host:'127.0.0.1'},
// connectListener callback
function() {
console.log('connection successful');
});
它是如何工作的
在本节中,您创建了一个到 TCP 服务器的连接。这是用 Node.js 的net模块完成的。这包含了与connect()函数相同的createConnection函数。connect 方法首先检查您传递给它的参数。它将评估设置了哪些选项。
检查发送的参数是通过首先检查第一个参数是否是一个对象,然后如果它确实是一个对象就解析这个对象。如果第一个参数不是一个对象,它将被评估以查看是否是一个有效的管道名,在这种情况下,它将被设置为 UNIX path 选项。如果它不是管道的名称,它将默认为一个端口号。对参数的最后检查是对可选回调参数的检查,通过检查传递给connect()函数的最后一个参数是否是函数本身来评估。整个过程在一个名为normalizeConnectArgs的函数中运行,如清单 2-8 所示。
清单 2-8 。提取 createConnection 参数
function normalizeConnectArgs(args) {
var options = {};
if (typeof args[0] === 'object') {
// connect(options, [cb])
options = args[0];
} else if (isPipeName(args[0])) {
// connect(path, [cb]);
options.path = args[0];
} else {
// connect(port, [host], [cb])
options.port = args[0];
if (typeof args[1] === 'string') {
options.host = args[1];
}
}
var cb = args[args.length - 1];
return (typeof cb === 'function') ? [options, cb] : [options];
}
接下来,net模块创建一个新的 socket 对象,传递新规范化的连接参数。这个套接字在其原型上有一个名为connect的方法。
调用套接字上的这个 connect 方法,并向其传递规范化的参数。connect 方法将尝试创建一个新的套接字句柄,并连接到参数中指定的路径或端口和主机组合。如果没有为给定端口指定主机,则假定目标主机是localhost或127.0.0.1。有趣的是,如果参数中提供了主机名或 IP 地址,Node.js 将需要dns模块并执行 DNS 查找来定位主机。如果查找没有错误地返回 null,这将再次默认为localhost,如清单 2-9 所示。
清单 2-9 。Socket.prototype.connect 的方法 解析路径、端口和主机
/* ... */
if (pipe) {
connect(self, options.path);
} else if (!options.host) {
debug('connect: missing host');
connect(self, '127.0.0.1', options.port, 4);
} else {
var host = options.host;
debug('connect: find host ' + host);
require('dns').lookup(host, function(err, ip, addressType) {
// It's possible we were destroyed while looking this up.
// XXX it would be great if we could cancel the promise returned by
// the lookup.
if (!self._connecting) return;
if (err) {
// net.createConnection() creates a net.Socket object and
// immediately calls net.Socket.connect() on it (that's us).
// There are no event listeners registered yet so defer the
// error event to the next tick.
process.nextTick(function() {
self.emit('error', err);
self._destroy();
});
} else {
timers.active(self);
addressType = addressType || 4;
// node_net.cc handles null host names graciously but user land
// expects remoteAddress to have a meaningful value
ip = ip || (addressType === 4 ? '127.0.0.1' : '0:0:0:0:0:0:0:1');
connect(self, ip, options.port, addressType, options.localAddress);
}
});
}
/* ... */
从清单中可以看出,发现路径、端口或端口和主机的结果是调用函数connect()。这个函数只是将套接字句柄连接到路径或端口和主机。一旦连接请求被连接,就调用connectListener回调作为connect函数的代码,如清单 2-10 所示。
清单 2-10 。函数 connect()在 net 模块中实现
function connect(self, address, port, addressType, localAddress) {
assert.ok(self._connecting);
if (localAddress) {
var r;
if (addressType == 6) {
r = self._handle.bind6(localAddress);
} else {
r = self._handle.bind(localAddress);
}
if (r) {
self._destroy(errnoException(process._errno, 'bind'));
return;
}
}
var connectReq;
if (addressType == 6) {
connectReq = self._handle.connect6(address, port);
} else if (addressType == 4) {
connectReq = self._handle.connect(address, port);
} else {
connectReq = self._handle.connect(address, afterConnect);
}
if (connectReq !== null) {
connectReq.oncomplete = afterConnect;
} else {
self._destroy(errnoException(process._errno, 'connect'));
}
}
这是清单 2-8 中的函数,您在这里将“连接成功”记录到控制台。正如您将在 2-4 节中看到的,监听和连接客户端不仅仅是简单地将一个字符串记录到控制台,但是首先您将检查配置服务器的各种方式以及配置选项附带的默认设置。
2-3.配置服务器默认值
问题
您正在 Node.js 中创建一个服务器,并且需要控制该服务器的可访问缺省值。
解决办法
当您创建任何类型的 Web 服务器时,您经常会发现可能需要调整默认配置以满足您的特定需求。除了为 TCP 服务器设置主机和端口之外,您可能希望能够设置最大连接数,或者像在您的服务器中那样控制挂起连接的系统积压队列长度。许多这些设置在您的服务器上都有默认值。
很自然,服务器中您可以控制的最简单的部分之一就是服务器将要监听的端口和主机。这些是在服务器上调用listen()方法时设置的。listen 方法(如 2-1 节所见)也接受侦听器回调,但是第三个参数是 backlog 设置,可以选择放在这个回调之前,它限制服务器的连接队列长度。将这些缺省设置到位,您可以看到listen()函数在清单 2-11 中的样子。
清单 2-11 。设置 listen()默认值
server.listen(8181, '127.0.0.1', 12, function() {
// listen on 127.0.0.1:8181
// backlog queue capped at 12
console.log('server is listening');
});
另一个需要考虑的默认选项是调用createServer()方法时设置的选项,它允许半开连接,默认为 false,但在方法中设置,如清单 2-12 所示。
清单 2-12 。allowalfopen:true
var server = net.createServer({ allowHalfOpen: true }, function(connectionListener) {
/* connection Listener stuffs */
});
在 Node.js 应用中,设置到服务器的最大连接数也非常有用。如果您希望对此加以限制,您必须显式设置该数字,因为它默认为未定义。这最好在connectionListene r回调中设置,如清单 2-13 所示。
清单 2-13 。设置到服务器的最大连接数
var server = net.createServer({ allowHalfOpen: true }, function(connectionListener) {
console.log('connected');
//get maxConnections - default undefined
console.log(this.maxConnections);
// set maxConnections to 4
this.maxConnections = 4;
// check set maxConnections is 4
console.log(this.maxConnections);
});
它是如何工作的
通过对照默认设置检查服务器默认值,可以设置和覆盖服务器默认值;然后它们会被覆盖。向 Node.js 中的listen()方法传递 backlog 参数会发生什么?首先,传递给 backlog 参数的默认值是 511。传递值 511 是因为操作系统内核是如何确定积压工作大小的。
//使用 512 个条目的积压。我们将 511 传递给 listen()调用,因为
//内核确实:backlogsize = round up _ pow _ of _ two(backlogsize+1);
这将会给我们带来 512 个条目的积压。
知道这个很有趣。因为您在清单 2-11 中的server.listen() 示例中将 backlog 队列设置为上限为 12,所以您现在可以知道这将被计算为 16。这是因为您设置的值 12 递增 1,然后向上舍入到最接近的 2 的幂,即 16。需要注意的是,在清单 2-11 的示例server.listen中,您将主机地址的值设置为 127.0.0.1,也就是 IPv4。然而,Node.js 同样容易处理 IPv6 连接,因此您可以更改您的默认服务器监听以使用 IPv6,如清单 2-14 所示。
清单 2-14 。使用 IPv6 配置服务器
server.listen(8181, '::1', 12, function() {
console.log(server.address());
});
随后,server.address()函数将记录新主机,并且该系列现在将是 IPv6 而不是 IPv4。
{ address: '::1', family: 'IPv6', port: 8181 }
允许半开连接是你在清单 2-12 、{ allowHalfOpen: true }中设置的选项。这将连接设置为允许对服务器连接进行更细粒度的控制。这将允许连接发送 TCP FIN 数据包,该数据包请求终止连接,但不会自动向连接发送响应 FIN 数据包。
这意味着您将保留一半的 TCP 连接,允许套接字保持可写但不可读。要正式关闭连接,必须通过调用。end()方法。
您还看到了如何通过 Node.js 和net模块的maxConnections设置来限制到服务器的最大连接数。默认情况下,这是未定义的,但是在清单 2-13 中,它被设置为一个较小的数字 4。这意味着您的连接数限制为 4,但是当您连接或试图连接到一个设置了最大连接数的服务器时会发生什么呢?你可以在清单 2-15 中看到 Node.js 源码对这个设置做了什么。
清单 2-15 。Node.js 处理 maxConnections 设置
if (self.maxConnections && self._connections >= self.maxConnections) {
clientHandle.close();
return;
}
这让您对为什么 maxConnections 默认为 undefined 有了更多的了解。这是因为如果没有设置它,Node.js 就没有必要为这部分代码费心。但是,如果设置了它,一个简单的检查将查看服务器上的当前连接数是否大于或等于 maxConnections 设置,并且它将关闭连接。如果你有一个 Node.js 客户端连接想要连接(你将在 2-4 节中读到更多),但是连接数超过了这个限制,你将看到这个连接的关闭事件被发出,你可以适当地处理它,如清单 2-16 所示。
清单 2-16 。处理连接句柄上的关闭事件
connection.on('close', function() {
console.log('connection closed');
});
另一方面,如果你只是通过 Telnet (telnet ::1 8181)点击服务器端点,响应将是“连接被外来主机关闭”,如图 2-1 中的所示。

图 2-1 。Telnet 连接关闭
2-4.创建客户端
问题
您希望使用 Node.js 创建一个连接到 Web 服务器的客户机。
解决办法
创建一个功能性 Node.js 客户机扩展了您在第 2-2 节中学到的概念。也就是说,客户机只是一个到服务器端点的连接。前面您已经看到了如何启动连接;在本节中,您将学习如何获取那个连接的套接字,并理解与之相关联的事件。
让我们假设我们将把我们的客户机连接到一个简单的 Node.js 服务器,类似于您在第 2-1 节中创建的服务器。但是,该服务器将从客户端接收消息,并向客户端写入消息。该消息将是一个简单的文本消息,显示当前到服务器的连接数。该服务器如清单 2-17 中的所示。
清单 2-17 。简单的 Node.js 服务器回显到客户端
var net = require('net');
var server = net.createServer(function(connectionListener) {
//get connection count
this.getConnections(function(err, count) {
if (err) {
console.log('Error getting connections');
} else {
// send out info for this socket
connectionListener.write('connections to server: ' + count + '\r\n');
}
});
connectionListener.on('end', function() {
console.log('disconnected');
});
//Make sure there is something happening
connectionListener.write('heyo\r\n');
connectionListener.on('data', function(data) {
console.log('message for you sir: ' + data);
});
// Handle connection errors
connectionListener.on('error', function(err) {
console.log('server error: ' + err);
});
});
server.on('error', function(err) {
console.log('Server error: ' + err);
});
server.on('data', function(data) {
console.log(data.toString());
});
server.listen(8181, function() {
console.log('server is listening');
});
首先,您会看到,当使用 Node.js 中的net模块创建连接的客户端时,您需要注册可以通过 Node.js 事件发射器发出的底层事件。在您将创建的示例客户端中,这些事件被设置为监听data、end和error。这些事件接受回调,回调可用于处理通过这些事件传输的数据。这以 2-2 节中显示的服务器为例,并把它变成你在清单 2-18 中看到的样子。
清单 2-18 。带有套接字事件的客户端
var net = require('net');
// createConnection
var connection = net.createConnection({port: 8181, host:'127.0.0.1'},
// connectListener callback
function() {
console.log('connection successful');
this.write('hello');
});
connection.on( 'data' , function(data) {
console.log(data.toString());
});
connection.on('error', function(error) {
console.log(error);
});
connection.on('end', function() {
console.log('connection ended');
});
如您所见,在客户机上注册事件侦听器有许多选项。这些事件是确定服务器状态或处理来自服务器的响应缓冲区的网关。这些可以帮助您确定 Node.js 应用中联网客户端发送的状态和信息。
客户端(见清单 2-18 )中还有一种最简单的可以发送给服务器的通信形式:套接字上的write()方法。在这种情况下,套接字是在实例化连接时创建的。一旦连接建立,它只需向服务器发送一个字符串“hello”。这在客户端通过connectionListener's数据事件绑定来处理。
connectionListener.on('data', function(data) {
console.log('message for you sir: ' + data);
});
如果一切运行正常,您将在控制台输出中看到客户机与您的服务器交互,如清单 2-19 和清单 2-20 所示。
清单 2-19 。命令行上的服务器交互
$ node server.js
server is listening
message for you sir: hello
清单 2-20 。客户端与服务器通信
$ node client.js
Connection successful
Heyo
它是如何工作的
当您研究这个客户端如何与您的服务器连接和通信时,您会再次看到我们已经使用 Node.js 自带的net模块创建了一个到服务器的连接。这个模块具有在 TCP 服务器和客户端之间顺利通信的能力。在你在清单 2-17 中创建的例子中,你创建了一个监听端口和主机的连接,如 2-2 节所述。一旦创建了这个连接,并将其设置为变量“client ”,就需要三个参数。因为客户端实际上是一个 TCP 套接字的表示,所以它们是公开的。
无论如何,套接字是在实现net.createConnection()方法时创建的。这意味着您现在可以访问在套接字之间传递的选项和事件。这可以通过查看这些套接字的 Node.js 源代码来演示。在 Node.js 中,net套接字是一个流的表示。这意味着为了理解当connection.end发生时正在执行的代码,你可以看到它实际上是socket.end方法的一个表示,如清单 2-21 所示。
清单 2-21 。Socket.end 方法
Socket.prototype.end = function(data, encoding) {
stream.Duplex.prototype.end.call(this, data, encoding);
this.writable = false;
DTRACE_NET_STREAM_END(this);
// just in case we're waiting for an EOF.
if (this.readable && !this._readableState.endEmitted)
this.read(0);
return;
};
从清单 2-21 中可以看到,你可以访问实际上是一个流的套接字。“end”方法调用此流的 end,并立即将该流设置为不可写。当流的另一端发送 FIN 包时,会触发 end 事件,您可以在前面的小节中看到这一点。在那里,您检查了半开的套接字连接;然而,在这种情况下,套接字不再是可写的。然后是最后一轮检查,看看流中是否还有可读的实体,在它返回之前读取,最终确定套接字的“结束”。
在清单 2-19 和 2-20 中,您看到服务器是用命令node server.js启动的。这立即产生了.listen()回调,它将消息“服务器正在监听”打印到您的控制台。然后启动客户端(node client.js,并调用connectListener回调函数,在控制台中显示“连接成功”。这个连接还从服务器发起一个Socket.write(),从客户端发起一个Socket.write()。在下一节中,您将了解更多关于利用套接字进行通信的内容,但是现在您确实需要理解Socket.write的最终结果是每个套接字沿着套接字发送它的数据。这导致在服务器上产生来自客户机的“hello”消息,并通过服务器在客户机上产生“heyo”消息。
如果您检查数据事件(为客户端处理数据接收的事件),您会看到每次接收数据时都会发出该事件。当您监听这个事件时,您将能够看到从您的服务器传输的数据。Node.js 中的数据以缓冲区或字符串的形式传输。默认情况下它是作为缓冲区发出的,但是如果你设置了socket.setEncoding()函数,你会看到数据是作为一个字符串传输的。在这个解决方案中,您通过Socket.write()方法发送数据,该方法默认使用 UTF-8 编码发送数据。data 事件是在 Node.js 的 stream 模块中触发的。stream 模块是从 Node.js 的net模块中的socket.write()方法触发的,如清单 2-22 所示。
清单 2-22 。从 socket.write()触发 Streams 模块
if (typeof chunk !== 'string' && !Buffer.isBuffer(chunk))
throw new TypeError('invalid data');
return stream.Duplex.prototype.write.apply(this, arguments);
一旦您将数据处理到流接口中,您就可以在模块中蜿蜒前进,直到找到可读流所在的位置。小溪。Readable 是一个有函数的可读流的实例,emitDataEvents . This is the “data” that will be read into the server that you send from your client. This lets an event listener, which is registered on the data event, actually go through the readable event on the stream, emitting the stream.read() 作为“数据”返回.on('data')。这部分源代码可以在清单 2-23 中查看。
`清单 2-23 。从流模块发出数据事件
stream.readable = true;
stream.pipe = Stream.prototype.pipe;
stream.on = stream.addListener = Stream.prototype.on;
stream.on('readable', function() {
readable = true;
var c;
while (!paused && (null !== (c = stream.read())))
stream.emit('data', c);
if (c === null) {
readable = false;
stream._readableState.needReadable = true;
}
});
这部分代码强调了将数据作为流传输的要点。您可以看到在emitDataEvents()方法 中,流监听它自己的可读事件。一旦可读事件注册,则调用 stream.read()事件,将数据传递给变量“c”,然后流发出数据事件,同时传递参数“c”
本节中为 Node.js 客户机创建的另一个事件侦听器是在 error 事件上注册的。当套接字遇到错误时,将发出此事件。一个很好的例子是,如果您的客户机连接到服务器,当连接到服务器失败时,您将得到一个错误。如果您关闭服务器,您将收到的错误是连接重置。这将是一个类似于清单 2-24 中的的对象。
清单 2-24 。错误:连接重置
{ [Error: read ECONNRESET] code: 'ECONNRESET', errno: 'ECONNRESET', syscall: 'read' }
现在,您应该能够在 Node.js 环境中构建一个联网的客户端了。通过套接字进行通信的过程将在第 2-5 节中详细介绍。
2-5.使用套接字在服务器之间进行通信
问题
您希望在 Node.js 中构建一个网络应用,并利用套接字在实例之间进行通信。
解决办法
套接字对于 Node.js net模块来说是本地的。这意味着如果您希望利用套接字,您需要在脚本中使用net模块。然后,您将通过调用Socket()构造函数来创建一个新的套接字实例。然后要连接一个套接字,你只需用socket.connect()方法创建一个连接,将套接字指向你想要连接的端口和主机(见清单 2-25 )。
清单 2-25 。创建套接字连接
var net = require('net');
var socket = new net.Socket();
socket.connect(/* port */ 8181, /*host*/ '127.0.0.1' /, *callback*/ );
假设可以在localhost的端口 8181 上建立连接,那么您现在就有了一个连接到该服务器的套接字。此时,除了通过这个套接字连接的流之外,什么也没有。任何传输的数据都将丢失。现在让我们仔细看看一个简单服务器的套接字连接,以便在彼此之间共享消息。为此,您可以创建一个简单的服务器(清单 2-26 ),它将监听套接字及其数据,并向套接字发回响应。
清单 2-26 。将与套接字通信的服务器
var net = require('net');
var server = net.createServer(connectionListener);
server.listen(8181, '127.0.0.1');
function connectionListener(conn) {
console.log('new client connected');
//greet the client
conn.write('hello');
// read what the client has to say and respond
conn.on('readable', function() {
var data = JSON.parse(this.read());
if (data.name) {
this.write('hello ' + data.name);
}
});
//handle errors
conn.on('error', function(e) {
console.log('' + e);
});
}
这个服务器将监听一个连接,然后通过套接字流用“hello”问候这个新连接。它还将监听来自套接字的数据,在这种情况下,套接字应该是一个 JSON 对象。然后,您可以解析“可读”流中的数据,并返回包含解析数据的响应。
清单 2-27 中的套接字连接展示了如何创建这个套接字,它将从清单 2-26 中的连接到服务器,并在两者之间发送通信。
清单 2-27 。插座连接
var net = require('net');
var socket = new net.Socket(/* fd: null, type: null, allowHalfOpen: false */);
socket.connect(8181, '127.0.0.1' /*, connectListener replaces on('connect') */);
socket.on('connect', function() {
console.log('connected to: ' + this.remoteAddress);
var obj = { name: 'Frodo', occupation: 'adventurer' };
this.write(JSON.stringify(obj));
});
socket.on('error', function(error) {
console.log('' + error);
// Don't persist this socket if there is a connection error
socket.destroy();
});
socket.on('data', function(data) {
console.log('from server: ' + data);
});
socket.setEncoding('utf-8'); /* utf8, utf16le, ucs2, ascii, hex */
socket.setTimeout(2e3 /* milliseconds */ , function() {
console.log('timeout completed');
var obj = { name: 'timeout', message: 'I came from a timeout'};
this.write(JSON.stringify(obj));
});
将服务器和客户端服务器放在一起—首先运行服务器,以便您的套接字有一个端点可以连接到—您能够成功地与套接字连接进行通信。发起的服务器控制台将看起来像清单 2-28 ,而客户端服务器输出将看起来像清单 2-29 。
清单 2-28 。服务器输出
$ node server.js
new client connected
清单 2-29 。连接的插座输出
$ node socket.js
Connected to: 127.0.0.1
From server: hellohello Frodo
Timeout completed
From server: hello timeout
它是如何工作的
如您所见,一个net.Socket连接是一个 Node.js 对象,表示一个 TCP 或 UNIX 套接字。在 Node.js 中,这意味着它实现了一个双工流接口。node 中的一个 duplex stream 表示两个event emitters,在 Node.js 中发布事件的对象,组成 duplex stream 的两个event emitters是可读流和可写流,你可以从清单 2-30 中的 Node.js duplex stream 源码中看到。
清单 2-30 。双工流调用可读 和可写流
function Duplex(options) {
if (!(this instanceof Duplex))
return new Duplex(options);
Readable.call(this, options);
Writable.call(this, options);
if (options && options.readable === false)
this.readable = false;
if (options && options.writable === false)
this.writable = false;
this.allowHalfOpen = true;
if (options && options.allowHalfOpen === false)
this.allowHalfOpen = false;
this.once('end', onend);
}
readable streams 接口将从流缓冲区接收数据,并在套接字上将它作为数据事件发出。另一方面,可写流将以写或结束事件的形式发出数据。这些一起构成了一个插座。socket 有一些有趣的属性和方法,您在清单 2-26 和 2-27 中使用了其中的一些来创建您的 socket 通信服务器。
在第一个服务器实例中,在connectionListener 回调中,传递了 conn 参数。因为一个net.Server对象实际上是一个将监听连接的套接字,所以这个 conn 参数表示您想要使用的套接字。这个服务器做的第一件事就是向连接发出问候。这发生在conn.write('hello');中,它是一种socket.write()方法。
socket.write()方法接受一个必需的参数、要写入的数据和两个可选参数。这些可选参数是 encoding,可用于设置套接字的编码类型。编码默认为 utf8,但其他有效值为 utf-8、utf16le、ucs2、ascii 和 hex。
接下来,在服务器的connectionListener中,套接字被绑定到可读事件。这个可读事件来自流模块。每当流发送准备读取的数据时,都会触发此事件。检索通过 readable 事件发送的数据的方法是调用read()事件来读取数据。在清单 2-26 的例子中,你期望数据是一个 JSON 字符串,然后你可以解析它来显示 JSON 对象。然后通过write()方法将另一条消息发送回连接。
服务器上的最终事件绑定是通过绑定到连接上的错误事件来处理错误。如果没有这一点,服务器将在连接发生错误时崩溃。这可能是一个被终止的连接,或者任何其他错误,但是不管是哪种类型的错误,没有什么比强大的错误处理功能更好的了。
现在看看你在清单 2-27 中做的套接字连接。这显示了我们沟通故事的另一面。它从一个新的net.Socket() 的实例化开始。在这个例子中,没有参数传递给构造函数。构造函数可以接受一个 options 对象,该对象的键为 fd、type 和 allowHalfOpen。
`fd 是文件描述符,或者套接字句柄应该是什么;这默认为 null。type 键也默认为空值,但是可以采用值 tcp4、tcp6 或 unix 来确定您希望实例化的套接字的类型。同样,正如您在前面几节中看到的,allowHalfOpen 选项可以设置为允许套接字在传输初始 FIN 包后保持打开。
为了连接套接字,您调用套接字上的connect()事件,并指定主机和端口。这将初始化 TCP 或 UNIX 套接字句柄,开始连接。host 参数是可选的,示例中省略的回调函数也是可选的。示例中的回调被替换了,因为 connect 函数上的回调函数与socket.on('connect', ...)事件侦听器相同,后者绑定到我们示例中的套接字,侦听要建立的连接。
在connect事件回调中,您的解决方案做的第一件事是通过记录套接字的remoteAddress() 来获得一些关于连接的知识。在本章的下一节中,您将看到更多关于获取已连接服务器的信息。在获得这些信息之后,您创建一个包含一些信息的对象,使用JSON.stringify方法将它变成一个字符串,然后使用write()方法沿着套接字发送它。该对象必须编码为字符串;否则,写方法将失败,如清单 2-31 所示。
清单 2-31 。Node.js 网络模块中的 socket.write
Socket.prototype.write = function(chunk, encoding, cb) {
if (typeof chunk !== 'string' &&
!Buffer.isBuffer(chunk))
throw new TypeError('invalid data');
return stream.Duplex.prototype.write.apply(this, arguments);
};
然后,套接字被绑定到error事件。这个事件将处理来自套接字的所有错误,但是这里值得注意的一点是,一旦错误被处理,通过提供给on('error')监听器的回调,socket.destroy();方法被调用。destroy 方法提供了一种有用且优雅的方式来防止任何进一步的 I/O 活动发生并关闭套接字。它通过关闭套接字句柄,在销毁过程中根据需要发出任何错误回调。最后,关闭句柄后会发出关闭事件,如清单 2-32 所示。
清单 2-32 。关闭 socket.destroy( ) 内的套接字
Socket.prototype._destroy = function(exception, cb) {
debug('destroy');
var self = this;
function fireErrorCallbacks() {
if (cb) cb(exception);
if (exception && !self.errorEmitted) {
process.nextTick(function() {
self.emit('error', exception);
});
self.errorEmitted = true;
}
};
if (this.destroyed) {
debug('already destroyed, fire error callbacks');
fireErrorCallbacks();
return;
}
self._connecting = false;
this.readable = this.writable = false;
timers.unenroll(this);
debug('close');
if (this._handle) {
if (this !== process.stderr)
debug('close handle');
var isException = exception ? true : false;
this._handle.close(function() {
debug('emit close');
self.emit('close', isException);
});
this._handle.onread = noop;
this._handle = null;
}
fireErrorCallbacks();
this.destroyed = true;
if (this.server) {
COUNTER_NET_SERVER_CONNECTION_CLOSE(this);
debug('has server');
this.server._connections--;
if (this.server._emitCloseIfDrained) {
this.server._emitCloseIfDrained();
}
}
};
在套接字的错误处理程序之后,套接字被绑定到数据事件。该事件将从连接发送的可读流中产生数据,本质上是调用 stream.read()方法并将其作为数据事件发出。这为解析和处理从连接发送的信息提供了一个有用的地方。
正如您在上面看到的,对于套接字上的 write()方法,可以选择为通过套接字缓冲区发送的数据设置编码。这可以通过设置套接字上的 setEncoding(
清单 2-33 。编码的变化
# utf8
connected to: 127.0.0.1
from server: hello
from server: hello Frodo
timeout completed
from server: hello timeout
# hex
connected to: 127.0.0.1
from server: 68656c6c6f
from server: 68656c6c6f2046726f646f
timeout completed
from server: 68656c6c6f2074696d656f7574
# ucs2
connected to: 127.0.0.1
from server:
from server:
timeout completed
from server:
# ascii
connected to: 127.0.0.1
from server: hello
from server: hello Frodo
timeout completed
from server: hello timeout
# utf16le
connected to: 127.0.0.1
from server: hello
from server: hello Frodo
timeout completed
from server: hello timeout
最后,您看到了套接字可以通过使用 setTimeout 函数 来“等待”。setTimeout 接受一个参数和一个回调,该参数指示您选择等待的毫秒数。在示例应用中,这用于从套接字向连接发送消息,延迟两秒钟。为了使回调有效(如清单 2-34 所示),毫秒数必须大于零,并且是有限的,不能是数字(NaN)。如果是这种情况,Node.js 会将这个回调添加到计时器列表中,并在超时发生时发出超时事件。
清单 2-34 。socket.setTimeout
Socket.prototype.setTimeout = function(msecs, callback) {
if (msecs > 0 && !isNaN(msecs) && isFinite(msecs)) {
timers.enroll(this, msecs);
timers.active(this);
if (callback) {
this.once('timeout', callback);
}
} else if (msecs === 0) {
timers.unenroll(this);
if (callback) {
this.removeListener('timeout', callback);
}
}
};
网络上还有其他事件和参数。清单 2-27 中的套接字示例中没有包括的套接字。这些在下面的表 2-1 中进行了概述。
表 2-1 。套接字参数和事件
| 套接字参数或事件 | 描述 |
|---|---|
| socket . end([数据],[编码]) | 这个事件将 FIN 数据包发送到套接字的连接端,基本上关闭了一半的连接。如果设置了 allowHalfOpen,服务器仍然可以发送数据。您可以指定要发送的数据和编码,但这两个参数都是可选的。 |
| socket.pause() | 这正如您所期望的那样:它暂停了套接字上的数据发送。 |
| socket.resume() | 这将恢复套接字上的数据传输。 |
| socket.setNoDelay([noDelay]) | 这决定了 TCP 连接是否会在发送数据之前缓冲数据。这被称为“Nagle 算法”, noDelay 布尔参数默认为 true。 |
| socket.setKeepAlive([enable]、[initialDelay]) | 这将启用或禁用套接字的保持活动功能。这意味着在接收到最后一个数据包和初始延迟时间(默认为零)后,将会发送一个 keepalive 探测。启用布尔参数默认为 false。 |
| socket.unref() | 在 socket 上调用这个会检查 Node.js 事件系统,如果 socket 是这个系统中仅存的 socket,就允许它退出。 |
| socket.ref() | 一旦在套接字上设置了这个,如果它是唯一剩下的套接字,Node.js 中的事件系统将阻止程序退出。这与默认行为相反,默认行为会让程序退出,如果它是唯一剩下的套接字。 |
| 套接字.远程端口 | 这是套接字连接的端口。 |
| 套接字.本地地址 | 这是套接字源自的地址。 |
| 套接字.本地端口 | 这是套接字源自的端口。 |
| socket . bytes loaded | 这收集了从数据传输中读取的字节数。 |
| socket . bytes loaded | 这表示写入的字节数。 |
这些属性和事件将在本章的最后两节中详细介绍,在这两节中,您将发现如何检索有关连接的服务器的详细信息,以及如何在套接字本身中控制这些属性和详细信息。
2-6.正在检索有关已连接服务器的详细信息
问题
您希望能够在 Node.js 应用中获取有关连接的服务器和套接字的详细信息。
解决办法
要检索有关您连接的服务器的详细信息,您需要运用有关网络的知识。服务器和 net。您在前面章节中看到的插座模块。您可能有兴趣了解有关连接的许多细节,但是您可能感兴趣的是收集连接之间传输和接收的字节数。这是通过socket.bytesRead和socket.bytesWritten属性来处理的。由于各种原因,这些都是有价值的,但是许多人利用它来进行基准测试和记录应用的进度。清单 2-35 创建了一个带有循环连接的服务器,它记录了 Node.js 进程执行期间读写的字节总数。
清单 2-35 。计数字节
var net = require('net');
var PORT = 8181,
totalRead = 0,
totalWritten = 0,
connectionCount = 0;
var server = net.Server(connectionListener);
function connectionListener(conn) {
//tally the bytes on end
conn.on('end', function() {
totalRead += conn.bytesRead;
});
}
server.listen(PORT);
//Connect a socket
var socket = net.createConnection(PORT);
socket.on('connect', function() {
// plan on writing the data more than once
connectionCount++;
// My = 2 Bytes
socket.write('My', function () {
// Precious = 8 Bytes
socket.end('Precious');
});
});
// tally the bytes written on end
socket.on('end', function() {
totalWritten += socket.bytesWritten;
});
socket.on('close', function() {
// Each time we should get +=10 bytes Read and Written
console.log('total read: ' + totalRead);
console.log('total written: ' + totalWritten);
// We're gonna do this a few times
if (connectionCount < 5) {
socket.connect(PORT);
} else {
server.close();
}
});
现在,您可以访问在服务器和连接之间发送的字节数。这很好,但是现在您希望能够揭示服务器驻留在哪里以及套接字来自哪里的细节。为此,您可以使用套接字属性、remoteAddress和remotePort ( 清单 2-36 )。您可以通过在connectionListener回调函数中添加一行代码和在socket.on('connect')事件中添加另一行代码,将这些代码添加到上面的示例中。
清单 2-36 。添加一些地址和端口嗅探器
console.log(socket.remoteAddress + ":" + socket.remotePort);
它是如何工作的
获得关于连接的服务器的信息实际上很容易。在创建 Node.js 应用时,这些告诉您已经发送或接收了多少字节的数据点非常有价值。Node.js 如何构建这些数据并呈现给net模块供你消费?如果您检查 net 模块源代码,您会发现当创建新的套接字句柄时,bytesRead 值总是被设置为零,正如您所预料的那样。该值随后增加缓冲区的长度,该长度在缓冲区句柄的 onread 函数中被读取(如清单 2-37 所示)。
清单 2-37 。onread 事件—将字节增加 read 的长度
function onread(buffer, offset, length) {
var handle = this;
var self = handle.owner;
assert(handle === self._handle, 'handle != self._handle');
timers.active(self);
var end = offset + length;
debug('onread', process._errno, offset, length, end);
if (buffer) {
debug('got data');
// read success.
// In theory (and in practice) calling readStop right now
// will prevent this from being called again until _read() gets
// called again.
// if we didn't get any bytes, that doesn't necessarily mean EOF.
// wait for the next one.
if (offset === end) {
debug('not any data, keep waiting');
return;
}
// if it's not enough data, we'll just call handle.readStart()
// again right away.
self.bytesRead += length;
// Optimization: emit the original buffer with end points
var ret = true;
if (self.ondata) self.ondata(buffer, offset, end);
else ret = self.push(buffer.slice(offset, end));
if (handle.reading && !ret) {
handle.reading = false;
debug('readStop');
var r = handle.readStop();
if (r)
self._destroy(errnoException(process._errno, 'read'));
}
} else if (process._errno == 'EOF') {
debug('EOF');
if (self._readableState.length === 0)
self.readable = false;
if (self.onend) self.once('end', self.onend);
// push a null to signal the end of data.
self.push(null);
// internal end event so that we know that the actual socket
// is no longer readable, and we can start the shutdown
// procedure. No need to wait for all the data to be consumed.
self.emit('_socketEnd');
} else {
debug('error', process._errno);
// Error
self._destroy(errnoException(process._errno, 'read'));
}
}
获取 bytesWritten 值并不像通过传递给 onread 函数的 length 参数增加一个值那样简单。事实上,正如在清单 2-38 中可以看到的,bytesWritten 参数是通过读取缓冲区的块长度或实际字节长度本身来生成的。
清单 2-38 。写入的字节数
Socket.prototype.__defineGetter__('bytesWritten', function() {
var bytes = this._bytesDispatched,
state = this._writableState,
data = this._pendingData,
encoding = this._pendingEncoding;
state.buffer.forEach(function(el) {
if (Buffer.isBuffer(el.chunk))
bytes += el.chunk.length;
else
bytes += Buffer.byteLength(el.chunk, el.encoding);
});
if (data) {
if (Buffer.isBuffer(data))
bytes += data.length;
else
bytes += Buffer.byteLength(data, encoding);
}
return bytes;
});
remoteAddress 和 remotePort 参数来自套接字句柄本身。这些代表了 Node.js 句柄的 getpeername 对象之上的一个抽象(清单 2-39 ),它包含一个地址和一个端口参数。这使得 Node.js 为 remotePort 和 remoteAddress 参数定义一个 getter 变得很简单。
清单 2-39 。getpeername 方法和 remoteAddress 以及 remotePort 属性
Socket.prototype._getpeername = function() {
if (!this._handle || !this._handle.getpeername) {
return {};
}
if (!this._peername) {
this._peername = this._handle.getpeername();
// getpeername() returns null on error
if (this._peername === null) {
return {};
}
}
return this._peername;
};
Socket.prototype.__defineGetter__('remoteAddress', function() {
return this._getpeername().address;
});
Socket.prototype.__defineGetter__('remotePort', function() {
return this._getpeername().port;
});
您已经看到 Node.js 如何很好地定义了支持网络应用的服务器和套接字上的属性,使它们易于检索和使用。当您开发 Node.js 应用时,获得关于连接服务的这些细节可以提供非常需要的信息。``
三、使用文件系统
在应用中的许多情况下,您会希望使用文件系统。Node.js 通过为操作系统上的标准文件 I/O 操作创建一个包装器,使这一点变得简单明了。在 Node.js 中,这些功能以 Node.js 本地模块之一 fs 为中心。本章将举例说明如何在 Node.js 应用中使用文件系统模块。在本章中,您将学习如何执行这些操作:
- 检索目录结构
- 导航目录
- 操纵目录结构
- 监视目录的修改
- 读写文件
- 移动和链接文件
- 更改文件权限
- 监视文件的修改
注意文件系统模块包含许多方法,它们不仅是异步的,而且有一个同步的对应物。这些同步方法包含在本章的许多解决方案中,以演示如何使用它们。然而,应该注意的是,除非绝对必要,否则应该避免使用同步版本,因为使用它们通常不是最佳做法。这是因为同步版本将阻塞整个过程,直到它们完成,这可能会对您的应用造成各种形式的破坏。
3-1.正在检索目录结构
问题
您希望从 Node.js 应用中访问一个目录或一组目录的结构。
解决办法
为了掌握用于检索目录结构的 Node.js 实用程序,您必须首先通过在代码中使用require('fs') 来获得文件系统模块。然后,您希望获得一些关于目标目录的信息。让我们假设您想要打印 Node.js 应用中与当前目录相关的所有信息。首先,您可以定位当前目录,即执行 Node.js 脚本的目录,如清单 3-1 所示。
清单 3-1 。指向 Node.js 的当前目录
var fs = require('fs');
var out;
console.log(__dirname);
//read current directory asynchronously
fs.realpath(__dirname, function(err, /* [cache], */ path) {
if (err) {
console.log(err);
return;
}
console.log('realpath async: ' + path);
});
out = fs.realpathSync(__dirname);
console.log('real path sync: ' + out);
fs.stat(__dirname, function(err, stat) {
if (err) return;
var isDir = false;
fs.readdir(__dirname, function(err, contents) {
if (err) return;
contents.forEach(function(f) {
console.log('contents: ' + f);
});
});
});
//get list of what’s in the directory
out = fs.readdirSync(__dirname);
console.log('readdir sync: ' + out);
该解决方案会带来什么结果?它基于当前工作目录生成一个列表,列出该目录中包含的内容。该列表类似于清单 3-2 中的内容。
清单 3-2 。清单 3-1 的输出
$ node 3-1-1.js
/home/cgack/Dropbox/book/code/Ch03
real path sync: /home/cgack/Dropbox/book/code/Ch03
readdir sync: 3-1-1.js,3-1-2.js
contents: 3-1-1.js
contents: 3-1-2.js
realpath async: /home/cgack/Dropbox/book/code/Ch03
虽然该解决方案对于运行代码以获取应用实例化位置的目录结构是有效的,但是它使得解析任意目录的结构以及与调用 Node.js 脚本的位置相关的目录变得困难。这可以通过稍微重构清单 3-1 来解决,以允许如清单 3-3 所示的命令行参数。
清单 3-3 。重构目录摘要
var fs = require('fs');
var out;
var args;
//Normalize the arguments
args = process.argv.splice(2);
args.forEach(function(arg) {
console.log(arg);
//read current directory asynchronous
fs.realpath(arg, function(err, /* [cache], */ path) {
if (err) {
console.log(err);
return;
}
console.log('realpath async: ' + path);
});
out = fs.realpathSync(arg);
console.log('real path sync: ' + out);
fs.stat(arg, function(err, stat) {
if (err) return;
fs.readdir(arg, function(err, contents) {
if (err) return;
contents.forEach(function(f) {
console.log('contents: ' + f);
});
});
});
//get list of what’s in the directory
out = fs.readdirSync(arg);
console.log('readdir sync: ' + out);
});
你可以看到这个解决方案提供了更多。它接受一个参数列表,对它们进行规范化,然后遍历提供的目录,产生一个输出。这意味着您可以向 Node.js 应用传递两个相对路径,它将循环并产生类似于清单 3-4 中输出的结果。
清单 3-4 。多路输出
$ node 3-1-2.js ...
.
real path sync: /home/cgack/Dropbox/book/code/Ch03
readdir sync: 3-1-1.js,3-1-2.js
..
real path sync: /home/cgack/Dropbox/book/code
readdir sync: 2-6-2.js,Ch01,Ch02,Ch03
contents: 3-1-1.js
contents: 3-1-2.js
contents: 2-6-2.js
contents: Ch01
contents: Ch02
contents: Ch03
realpath async: /home/cgack/Dropbox/book/code
它是如何工作的
现在你检查所有这些是如何工作的。您会看到,一般来说,调用静态的硬编码目录和允许命令行参数传递到您的命令之间的区别是灵活性的额外好处。让我们从读取目录信息的代码开始,然后您可以检查实现中的差异。
Node.js 文件系统模块围绕标准 POSIX 命令提供了大量有用的包装器,这些命令几乎无处不在(一些操作系统在实现上有所不同)。本节使用的命令有readdir、stat和realpath。
readdir的 Node.js 实现是一个读取目录的简单命令。然而,您会注意到,在解决方案中有两个对readdir的单独调用。一个是到readdir () ,一个是到readdirSync () 。readdirSync是文件系统目录读取的同步实现,如清单 3-5 所示。
清单 3-5 。readdirSync
fs.readdirSync = function(path) {
nullCheck(path);
return binding.readdir(pathModule._makeLong(path));
};
这只是检查路径是否存在,然后返回该路径。这个调用的另一个版本是异步的(清单 3-6 ),并且如您所料,接受回调。回调接受两个参数:一个错误参数和另一个保存路径信息的参数。
清单 3-6 。readdir ()
fs.readdir = function(path, callback) {
callback = makeCallback(callback);
if (!nullCheck(path, callback)) return;
binding.readdir(pathModule._makeLong(path), callback);
};
与readdir类似,名为realpath的 Node.js 函数有同步和异步两种形式。realpath函数返回给定路径的绝对路径名。因此,这实际上是收集当前目录信息的两种方式。函数realpath检索绝对路径,而readdir检索关于其内容的信息。Readdir只能检索一个目录中的文件或目录列表,所以为了找到一个目录的更多细节,你需要一些不同的东西。这个不同的方法就是stat () 。stat()
那么,所提供的示例的两个不同版本呢?一个是基于静态路径,实际上是一个名为 __ dirname 的 Node.js 全局变量。_dirname变量是相对于每个模块的,它表示当前正在执行的 Node.js 脚本的路径。所以当你在清单 3-1 中使用它时,你是在告诉 Node.js 和你调用的文件系统模块,利用 Node.js 模块的路径作为每个文件系统调用的路径参数。
这是相当有限的,所以你可以看到在的清单 3-3 中,模块被打开以利用一组传递给模块 Node.js 的命令行参数。这是利用了包含argv元素中参数列表的全局流程对象。在清单中,您会看到这些参数被规范化以删除前两个参数—'node <file>'—and then parse,剩下的参数作为一个数组。然后,这个数组被用作每个 Node.js 文件系统方法的路径参数,为目录信息检索代码的初始实现提供了更多的功能。
3-2.浏览目录
问题
在许多使用文件系统的应用中,您可能希望以某种形式遍历目录结构。
解决办法
使用 Node.js 应用遍历机器的目录结构是通过使用fs模块完成的。这个解决方案从第 3-1 节的解决方案停止的地方开始,因为它从读取一个目录开始,然后它将相应地在整个目录中移动。目录结构的解析是递归的,并产生一个包含文件和目录的数组。这个 Node.js 应用如清单 3-7 所示。
清单 3-7 。遍历目录
var fs = require('fs');
var out;
var args;
/**
* To parse directory structure given a starting point - recursive
*/
function traverseDirectory(startDir, usePath, callback) {
if (arguments.length === 2 && typeof arguments[1] === 'function') {
callback = usePath;
usePath = false;
}
//Hold onto the array of items
var parsedDirectory = [];
//start reading a list of whats contained
fs.readdir(startDir, function(err, dirList) {
if (usePath) {
startDir = fs.realpathSync(startDir);
}
if (err) {
return callback(err);
}
//keep track of how deep we need to go before callback
var listlength = dirList.length;
if (!listlength) {
return callback(null, parsedDirectory);
}
//loop through the directory list
dirList.forEach(function(file) {
file = startDir + '/' + file;
fs.stat(file, function(err, stat) {
//note the directory or file
parsedDirectory.push(file);
//recursive if this is a directory
if (stat && stat.isDirectory()) {
//recurse
traverseDirectory(file, function(err, parsed) {
// read this directory into our output
parsedDirectory = parsedDirectory.concat(parsed);
//check to see if we've exhausted our search
if (!--listlength) {
callback(null, parsedDirectory);
}
});
} else {
//check to see if we've exhausted the search
if (!--listlength) {
callback(null, parsedDirectory);
}
}
});
});
});
}
//Normalize the arguments
args = process.argv.splice(2);
//loop through the directories
args.forEach(function(arg) {
// use provided path
traverseDirectory(arg, function(err, result) {
if (err) {
console.log(err);
}
console.log(result);
});
//use full path
traverseDirectory(arg, true, function(err, result) {
if (err) {
console.log(err);
}
console.log(result);
});
});
这个遍历产生了一个控制台输出,类似于清单 3-8 中的所示。
清单 3-8 。遍历的输出
gack∼/Dropbox/book/code/Ch03: node 3-2-1.js.
[ './3-1-1.js',
'./3-1-2.js',
'./3-2',
'./3-2-1.js',
'./3-2/file.txt',
'./3-2/sub directory',
'./3-2/sub directory/file.txt' ]
[ '/Users/gack/Dropbox/book/code/Ch03/3-1-1.js',
'/Users/gack/Dropbox/book/code/Ch03/3-1-2.js',
'/Users/gack/Dropbox/book/code/Ch03/3-2',
'/Users/gack/Dropbox/book/code/Ch03/3-2-1.js',
'/Users/gack/Dropbox/book/code/Ch03/3-2/file.txt',
'/Users/gack/Dropbox/book/code/Ch03/3-2/sub directory',
'/Users/gack/Dropbox/book/code/Ch03/3-2/sub directory/file.txt' ]
它是如何工作的
这个解决方案首先从第 3-1 节吸取教训,并通过命令行提供参数,然后这些参数被规范化。这指导应用在开始遍历目录结构时使用哪些路径,这在函数traverseDirectory中处理。
traverseDirectory函数接受一个路径(或起始目录)、一个将起始路径转换为完整路径的可选标志和一个回调函数。可选的usePath标志是通过检查是否只有两个参数被传入以及第二个参数是否是一个函数来确定的,这表明提供了回调。
if (arguments.length === 2 && typeof arguments[1] === 'function') {
callback = usePath;
usePath = false;
}
usePath标志是一个选项,如果被设置,它将使用fs.realpath函数解析提供给traverseDirectory方法的目录。因此这将转换作为“.”提供的路径,表示当前工作目录,到应用的实际路径(即“/home/username/apps/”)。
对目录结构的实际遍历是从调用fs.readdir开始的,正如你在 3-1 节中看到的,它提供了一个回调函数,给出了驻留在目录中的内容列表。然后对这个返回的列表进行检查,以确保目录中有可供解析的信息。如果不存在任何结果,函数将使用提供的回调函数退出。或者,如果目录列表中有结果,则存储该数组的长度(listlength)以跟踪目录树中要解析的剩余项。
然后循环遍历目录列表数组,将函数fs.stat应用于它包含的每一项。fs.stat函数 返回一个fs.stat对象,详细描述文件系统中某个文件的信息。然后,traverseDirectory函数将调用fs.stat对象的文件(或目录)存储到输出数组parsedDirectory.中,然后通过stat.isDirectory函数检查 stat 对象,看结果是否是一个目录。如果结果为真,那么调用traverseDirectory函数,传入目录—递归解析目录。如果 stat 不是一个目录,该函数假定它是一个文件,但是,无论它是否是一个文件,目录列表长度变量都将递减,并检查是否还有任何剩余条目,if (!—listlength)。在没有任何剩余条目的情况下,函数通过parsedDirectory数组返回回调。结果被传递给回调函数,在本例中,回调函数将结果记录到控制台。
3-3.操纵目录结构
问题
您希望通过 Node.js 应用添加和删除目录来操作目录的结构。
解决办法
该解决方案有两种形式,将分两部分进行描述。第一部分是删除目录。在 Node.js 中,这就像调用文件系统模块的 make directory 或 make directory 同步函数一样简单,fs.mkdir 和 fs.mkdirSync 。这两个函数都显示在一个例子中,清单 3-9 。
清单 3-9 。创建目录同步和异步功能
var fs = require('fs'),
dirExists = false;
//Normalize the arguments
args = process.argv.splice(2);
//loop through named args
args.forEach(function(arg) {
//mkdirSync - manually handle errors
try {
fs.mkdirSync(arg);
} catch(err) {
handleError(err);
}
//mkdir async
fs.mkdir(arg, function(err) {
if (err) handleError(err);
});
*/
});
function handleError(err) {
console.log(err);
if (err.code === 'EEXIST') {
console.log('That directory already exists');
} else {
console.log('An error occurred creating the directory');
}
}
处理目录结构的解决方案的第二部分涉及删除现有目录的 Node.js 方法。删除代码(如清单 3-10 所示)本质上是创建的反向操作,在异常处理上略有不同。
清单 3-10 。删除目录
var fs = require('fs'),
dirExists = false;
//Normalize the arguments
args = process.argv.splice(2);
//loop through named args
args.forEach(function(arg) {
//rmdir sync
try {
fs.rmdirSync(dir);
} catch(err) {
handleError(err);
}
//rmdir async
fs.rmdir(arg, function(err) {
if (err) handleError(err);
});
});
function handleError(err) {
console.log(err);
if (err.code === 'ENOENT') {
console.log('That directory does not exist');
} else if (err.code === 'ENOTEMPTY') {
console.log('Cannot remove directory because it is not empty');
} else {
console.log('An error occurred removing the directory');
}
}
它是如何工作的
使用mkdir函数创建目录。mkdir函数也接受一个只有错误参数的回调函数。当目录已经存在时抛出的错误代码为EEXIST,因此在本例中,这是在handleError函数中显式处理的。同步版本的mkdir(即mkdirSync)不提供错误回调,所以在例子中你可以看到它是在 try-catch 内部创建的,其中 catch 提供了与异步回调相同的错误处理程序。同步和异步函数accept可选的第二个参数,指定创建目录的模式,默认为0777。如果您想要限制权限,您可以将创建模式更改为任何内容(例如,0755)),限制除用户之外的所有人对目录的读取和执行权限。
目录的删除类似于创建。移除异步函数rmdir接受带有错误参数的回调。这个回调处理的常见错误是EONENT和ENOTEMPTY??。EONENT 当目录不存在,试图从目录结构中删除时抛出。ENOTEMPTY 当你试图删除一个非空的目录时抛出。这些都是通过handleError功能处理的。在本章的后面,你将看到如何移动和重命名文件,如果你想删除一个非空的文件夹,这将是必要的。
3-4.查看修改目录
问题
您希望在运行 Node.js 应用的过程中观察目录结构的变化。
解决办法
监视目录结构的解决方案遵循 3-2 节中的解决方案,遍历目录将内容读入数组。对于这个解决方案,您可以做两件事。一种方法是再次遍历目录结构,缓存目录和子目录的初始状态。然后,您将设置再次检查目录结构并比较这两个数组的输出的时间间隔。这不是最佳解决方案,因为 Node.js 在文件系统模块中有一个内置的实用程序,它将创建一个名为fs.watch 的文件系统观察器对象。清单 3-11 中显示了实现的方式。
`清单 3-11 。观察变化
/**
* Watching a directory
*/
var os = require('os'),
fs = require('fs'),
out,
args;
/**
* To parse directory structure given a starting point - recursive
*/
function traverseDirectory(startDir, usePath, callback) {
if (arguments.length === 2 && typeof arguments[1] === 'function') {
callback = usePath;
usePath = false;
}
//Hold onto the array of items
var parsedDirectory = [];
//start reading a list of what’s contained
fs.readdir(startDir, function(err, dirList) {
if (usePath) {
startDir = fs.realpathSync(startDir);
}
if (err) {
return callback(err);
}
//keep track of how deep we need to go before callback
var listlength = dirList.length;
if (!listlength) {
return callback(null, parsedDirectory);
}
//loop through the directory list
dirList.forEach(function(file) {
//WIndows is special
file = startDir + (os.platform() === 'win32' ? '\\' : '/') + file;
fs.stat(file, function(err, stat) {
//note the directory or file
parsedDirectory.push(file);
//recursive if this is a directory
if (stat && stat.isDirectory()) {
//recurse
traverseDirectory(file, function(err, parsed) {
// read this directory into our output
parsedDirectory = parsedDirectory.concat(parsed);
//check to see if we've exhausted our search
if (!--listlength) {
callback(null, parsedDirectory);
}
});
} else {
//check if we've exhausted the search
if (!--listlength) {
callback(null, parsedDirectory);
}
}
});
});
});
}
//Normalize the arguments
args = process.argv.splice(2);
//loop through the directories
args.forEach(function(arg) {
traverseDirectory(arg, true, function(err, result) {
result.forEach(function(i) {
fs.watch(i, filesystemListener);
});
});
});
function filesystemListener(e, f) {
console.log(f + ': ' + e);
}
这个解决方案非常强大,因为它还会检查单个文件的更改,只需调用一个函数。然而,正如您将看到的,fs.watch方法是不稳定的,可能无法在 Node.js 环境中按预期执行。正因为如此,监视目录结构变化的另一种方法如清单 3-12 所示。
清单 3-12 。检查目录结构变化
function checkSame(err, result) {
if (err) {
console.log(err);
}
if (initialDir.length === 0) {
initialDir = result;
} else {
secondaryDir = result;
//let’s compare these
if (secondaryDir.length !== initialDir.length) {
console.log('directory structure changed');
clearInterval(checkInt);
}
secondaryDir.sort();
initialDir.sort();
for (var i=0, ii = secondaryDir.length; i < ii; i++) {
if (secondaryDir[i] !== initialDir[i]) {
if (secondaryDir.indexOf(initialDir[i]) < 0) {
console.log(initialDir[i] + ' removed');
}
if (initialDir.indexOf(secondaryDir[i]) < 0) {
console.log(secondaryDir[i] + ' added');
}
clearInterval(checkInt);
}
}
}
}
var checkInt;
//Normalize the arguments
args = process.argv.splice(2);
//loop through the directories
args.forEach(function(arg) {
checkInt = setInterval(traverseDirectory, 2e3, arg, true, checkSame);
});
它是如何工作的
清单 3-11 中的例子展示了目录树的遍历,并添加了一个非常重要的函数。函数产生一个文件名和目录名的数组。然后循环这些结果,调用fs。watch作用于各条路径。fs.watch 函数是文件系统监视功能的一部分,这将在后面的 3-10 节中介绍。
请注意,fs.watch功能并不总是跨平台可用,并且(从 Node.js 版本 0.10.5 开始)仍然被认为是“不稳定的”因此,如清单 3-13 所示的替代实现在跨平台方面更加可靠,并且只寻找文件系统结构的变化。同样,该系统利用了traverseDirectory功能,但是间隔一段时间。这个时间间隔将每两秒钟解析一次目录结构,但是如果解析一个大的目录树需要比递归解析更长的时间,您可能需要调整这个时间间隔。在第一次迭代之后,在checkSame函数中,将原始解析的数组与当前解析的数组进行比较。如果检测到更改,则会记录下来。如果数组的长度不同,则首先检测到更改,这意味着底层结构已被修改(即文件删除)。然后对数组进行排序,然后检查每一项,看它是否还存在于另一个结果集中。
清单 3-13 。检查目录差异
if (secondaryDir.length !== initialDir.length) {
console.log('directory structure changed');
clearInterval(checkInt);
}
secondaryDir.sort();
initialDir.sort();
for (var i=0, ii = secondaryDir.length; i < ii; i++) {
if (secondaryDir[i] !== initialDir[i]) {
if (secondaryDir.indexOf(initialDir[i]) < 0) {
console.log(initialDir[i] + ' removed');
}
if (initialDir.indexOf(secondaryDir[i]) < 0) {
console.log(secondaryDir[i] + ' added');
}
clearInterval(checkInt);
}
}
在监视目录变化时使用这种方法将产生类似于清单 3-14 中输出的结果,当目录名从"that"更改为"this"时产生:
清单 3-14 。注意目录结构的变化
$ node 3-4-1.js.
/home/cgack/book/code/Ch03/3-4/now/that removed
/home/cgack/book/code/Ch03/3-4/now/this added
3-5.读取文件
问题
在构建 Node.js 应用的过程中,您需要从文件系统中访问和读取一个文件。
解决办法
当使用文件系统模块时,从文件系统中读取文件相当简单。文件系统模块提供了多种读取文件的方法。在清单 3-15 中,解决方案显示了使用文件系统在 Node.js 中读取文件的三种主要方法:readFile 、readFileSync 和createReadStream 。
`清单 3-15 。读取文件
/**
* Reading a file
*/
var fs = require('fs'),
args;
args = process.argv.splice(2);
args.forEach(function(arg){
//async read
fs.readFile(arg, 'utf8', function(err, data) {
if (err) console.log(err);
console.log(data);
});
//synchronicity
var file = fs.readFileSync(arg, 'utf8');
console.log(file);
//with a readable stream
var readstrm = fs.createReadStream(arg, {flag: 'r', encoding: 'utf8'});
readstrm.on('data', function(d) {
console.log(d);
});
});
它是如何工作的
读取 Node.js 中的文件可以采取不同的形式。首先,您可以利用标准的异步函数fs.readFile。该函数将接受一个文件名(这是必需的)、一个可选的选项参数和一个回调(也是必需的)。options 参数用于设置encoding (,它将在读取文件时设置文件缓冲区的编码,添加到 options 对象的是flag,它设置打开文件时使用的标志:它将始终是‘r’。
位于其核心的readFile函数调用函数fs.open,该函数将打开文件。标志选项总是设置为“r ”,意味着文件将被打开以供读取。在readFile的情况下,清单 3-16 中的所示的打开方法将获取文件的大小,然后创建一个与该大小相匹配的缓冲区。然后在read()功能中读取该缓冲区。
清单 3-16 。打开并读取 fs.readFile 中的()
fs.open(path, flag, 438 /*=0666*/, function(er, fd_) {
if (er) return callback(er);
fd = fd_;
fs.fstat(fd, function(er, st) {
if (er) return callback(er);
size = st.size;
if (size === 0) {
// the kernel lies about many files.
// Go ahead and try to read some bytes.
buffers = [];
return read();
}
buffer = new Buffer(size);
read();
});
});
function read() {
if (size === 0) {
buffer = new Buffer(8192);
fs.read(fd, buffer, 0, 8192, -1, afterRead);
} else {
fs.read(fd, buffer, pos, size - pos, -1, afterRead);
}
}
你可以看到fs.readFile's内部read()函数调用fs.read,指向文件描述符和被创建为打开文件大小的缓冲区。在执行afterRead函数后,结果最终被发送到fs.readFile的回调函数,文件被关闭。close 方法实际上将数据从缓冲区发送回回调。
清单 3-17 。关闭将 readFile 数据发送回调用者的事件
function close() {
fs.close(fd, function(er) {
if (size === 0) {
// collected the data into the buffers list.
buffer = Buffer.concat(buffers, pos);
} else if (pos < size) {
buffer = buffer.slice(0, pos);
}
if (encoding) buffer = buffer.toString(encoding);
return callback(er, buffer);
});
}
正如您所想象的,读取文件的下一个方法fs.readFileSync遵循与fs.readFile函数相似的模式,但是它只是同步操作。这不会导致包含从文件中读取的数据的回调,但同步版本会直接返回数据,并应用适当的编码。
if (encoding) buffer = buffer.toString(encoding);
return buffer;
最后,在使用 Node.js 读取文件的解决方案中,您创建了一个可读的流来解析文件。可读流是使用fs.createReadStream函数创建的,正如它的名字所预示的那样:它创建一个ReadStream。一个ReadStream是一个带有open事件的可读流,它返回流用来读取文件的文件描述符(fd)。传递到可读流中的选项是一个具有以下默认值的对象:
{ flags: 'r', encoding: null, fd: null, mode: 0666, bufferSize: 64 * 1024, autoClose: true }
有两个额外的选项可以传递:start 和 end。它们指定了您希望读取的文件的特定部分。
使用这些选项设置创建ReadStream,然后打开流。打开流调用fs.open,允许文件被打开和读取,如清单 3-18 所示。
清单 3-18 。ReadStream 调用 fs.open 并读取文件
ReadStream.prototype.open = function() {
var self = this;
fs.open(this.path, this.flags, this.mode, function(er, fd) {
if (er) {
if (this.autoClose) {
self.destroy();
}
self.emit('error', er);
return;
}
self.fd = fd;
self.emit('open', fd);
// start the flow of data.
self.read();
});
};
3-6.写文件
问题
您希望利用 Node.js 将内容或数据写入应用中的文件。
解决方案
从 Node.js 编写文件的解决方案类似于第 3-5 节中提到的方法。就像读取文件一样,在 Node.js 中写入文件有几种方法。有典型的异步方法(fs.writeFile ),该函数的同步版本(fs.writeFileSync ),以及写入文件的流版本(createWriteStream ))。还有一种方法,就是把数据追加到一个叫做fs.appendFile 的文件中。这些功能如清单 3-19 所示。
清单 3-19 。写文件
/**
* Writing files
*/
var fs = require('fs');
//initial write
fs.writeFile('write.txt', 'This is the contents!', function(err) {
if (err) throw err;
console.log('huzzah');
});
try {
fs.writeFileSync('./doesnotexist/newfile.txt', 'content');
} catch(err) {
console.log('unable to create a file in a non existent sub directory');
console.log(err);
}
//appending
fs.appendFile('write.txt', 'More content', function(err) {
if (err) throw err;
console.log('appended');
});
var ws = fs.createWriteStream('write.txt');
ws.write('new content\r\n', function() {
console.log('write stream hath written.');
});
清单 3-19 展示了如何使用三种不同的方法在 Node.js 中编写一个文件。同步方法有目的地针对不存在的子目录中的文件,以便演示这种情况的错误处理,并观察写入文件不会创建目录。执行该解决方案的输出将类似于清单 3-20 中所示的例子。
清单 3-20 。写文件输出
gack∼/Dropbox/book/code/Ch03: node 3-6-1.js
unable to create a file in a non existent sub directory
{ [Error: ENOENT, no such file or directory './doesnotexist/newfile.txt']
errno: 34,
code: 'ENOENT',
path: './doesnotexist/newfile.txt',
syscall: 'open' }
write stream hath written.
appended
huzzah
它是如何工作的
让我们从异步fs.writeFile开始,研究一下如何在 Node.js 中编写文件。fs.writeFile最多接受四个参数:路径、数据、选项和回调。该路径指向您希望写入的文件。该文件不需要存在,因为如果不存在,它将被创建。然而,如果你的目标是一个不存在的目录,writeFile功能不会自动为你创建目录。数据参数是您希望写入文件的数据,可以是字符串或缓冲区的形式。options 对象包含文件访问的编码、模式和标志。就像使用readFile方法一样,编码是唯一可配置的选项,因为模式和标志的设置被设置为mode: 438 /*=0666*/ and flag: 'w'。回调将传递任何错误以便处理它们。
一旦在writeFile功能中设置了默认值,就会调用fs.open。因为这个调用设置了'w'标志,它要么创建文件,要么截断文件。然后数据将作为缓冲区写入文件,如果缓冲区是提供的数据类型,字符串将被转换成缓冲区,如清单 3-21 所示。
清单 3-21 。writeFile —打开并写入数据
var flag = options.flag || 'w';
fs.open(path, options.flag || 'w', options.mode, function(openErr, fd) {
if (openErr) {
if (callback) callback(openErr);
} else {
var buffer = Buffer.isBuffer(data) ? data : new Buffer('' + data,
options.encoding || 'utf8');
var position = /a/.test(flag) ? null : 0;
writeAll(fd, buffer, 0, buffer.length, position, callback);
}
});
writeAll函数包装了fs.write函数,并将整个缓冲区写入文件。类似于readFile和readFileSync函数,writeFileSync的运行方式与writeFile相同,除了所有函数都是同步的,抛出过程中遇到的任何错误。这就是为什么示例中的代码是在 try-catch 块中编写的,以便很好地捕捉目录不存在时抛出的错误。
在许多情况下,您可能不希望在向文件中写入数据时创建或截断文件。这就是fs.appendFile函数有用的地方。这个函数是一个写文件的工具,只是把数据附加到文件中,而不是写新的数据。它通过简单地改变fs.writeFile的标志选项,然后调用如清单 3-22 所示的函数来实现。
清单 3-22 。改变标志选项
if (!options.flag)
options = util._extend({ flag: 'a' }, options);
fs.writeFile(path, data, options, callback);
如图所示,这将用fs.open打开文件。将标志设置为“a”将打开附加文件,如果文件不存在,允许创建它。
流方法为fs.createWriteStream ,创建可写流。createWriteStream方法将接受一个路径和选项。可以设置的选项有fd(一个文件描述符)、标志、模式和开始。fd 将指向要写入数据的文件句柄。该标志默认为w,,以便打开文件进行写入。模式选项默认为0666,即读写权限。start 选项告诉我们在文件中从哪里开始写数据。应该注意的是,如果您指定的起始位置超过了文件长度的末尾,您将会在文件中得到一堆缓冲区输出,而不是预期的数据或文本。
通过首先打开文件,然后调用内部 _ write函数来编写WriteStream。该函数将确保要写入的数据是正确的,并且文件确实是打开的。一旦确认,文件将使用fs.write方法写入,如清单 3-23 所示。
清单 3-23 。WriteStream 的 _write 方法
WriteStream.prototype._write = function(data, encoding, cb) {
if (!Buffer.isBuffer(data))
return this.emit('error', new Error('Invalid data'));
if (typeof this.fd !== 'number')
return this.once('open', function() {
this._write(data, encoding, cb);
});
var self = this;
fs.write(this.fd, data, 0, data.length, this.pos, function(er, bytes) {
if (er) {
self.destroy();
return cb(er);
}
self.bytesWritten += bytes;
cb();
});
if (this.pos !== undefined)
this.pos += data.length;
};
3-7.移动文件
问题
您希望能够从 Node.js 应用移动目录结构中的文件。您很可能会遇到这样的情况:由于某种原因,您需要更改文件的位置。也许您在 Node.js 应用中存储了一个临时文件缓存,您希望将它移动到一个更永久的位置。然后有一个用户表示他想存储他最喜欢的动画。gif 文件保存在更持久的位置。
解决办法
在构建访问文件系统的应用时,移动文件非常重要。在这个问题描述的情况下,你可以使用 Node.js 移动文件,如清单 3-22 所示。
清单 3-24 是用户缓存的一个文件 awesome.gif 的例子。该文件位于文件系统的临时目录3-7/tmp/中,需要移动到保存文件夹3-7/save/中。为了演示这一点,您将看到 Node.js 有多种方法可以实现这一点。其中两个利用了文件系统模块,结合了重命名和renameSync功能来移动文件。
清单 3-24 。移动文件:从命令行开始
/**
* Moving files
*/
var fs = require('fs'),
origPath,
newPath,
args = process.argv;
if (args.length !== 4) {
throw new Error('Invalid Arguments');
} else {
origPath = args[2];
newPath = args[3];
}
// move file asynchronously from tmp to save
fs.rename(origPath, newPath, function(err) {
if (err) throw err;
});
您将从一个使用命令行的示例开始,然后您将看到完成在 Node.js 中移动文件的相同任务的另外两种方法。
这是通过提供如下命令行参数来实现的:
$ node 3-7-1.js 3-7/tmp/awesome.gif 3-7/save/awesome.gif
除了通过命令行设置之外,您还可以直接在您的应用中实现fs.rename函数。这也可以设置成同步运行,如清单 3-25 所示,或者通过子进程运行,如清单 3-26 所示。
清单 3-25 。同步文件移动
//Synchronous
fs.renameSync(origPath, newPath);
清单 3-26 。使用子进程移动文件
// Child process => more in Chapter 5
var child = require('child_process');
child.exec('mv 3-7/tmp/awesome.gif 3-7/save/awesome.gif', function(err, stdout, stderr) {
console.log('out: ' + stdout);
if (stderr) throw stderr;
if (err) throw err;
});
它是如何工作的
当您开始研究这是如何工作的时候,首先要看文件系统模块。fs.rename函数执行标准的 POSIX 重命名,定义如下。
rename()函数 将改变一个文件的名称。旧参数指向要重命名的文件的路径名。新参数指向文件的新路径名。
如果旧的或新的参数命名了一个符号链接,rename()将对该符号链接本身进行操作,而不会解析该参数的最后一部分。如果旧参数和新参数解析为同一个现有文件,rename()将成功返回,并且不执行任何其他操作。
这意味着您正在利用操作系统的能力,通过改变文件的路径名来改变文件的位置。当你看到这个解决方案时,你会开始准确地理解它是如何工作的。
首先,您会看到,为了这个示例,使用了传递给 Node.js 进程的参数。您将利用这些参数告诉 Node.js 应用在移动文件时使用哪个路径名。这意味着除了标准的前两个参数node <app.js>,你还需要另外两个参数。
为了验证这些额外的参数,并防止您的 move 函数立即抛出带有错误数量的参数的错误,您需要确保您提供了正确数量的参数。这是通过检查传递的参数数量并在遇到无效数量时抛出一个适当的错误来实现的。
if (args.length !== 4) {
throw new Error('Invalid Arguments');
} else {
origPath = args[2];
newPath = args[3];
}
在这个完整性检查之后,如果您有适当数量的参数,那么您可以分配原始路径和新路径变量,它们将被传递到您的fs.rename函数。这个函数接受一个原始路径和一个新路径参数,以及一个回调。回调函数只有在重命名过程因为某种原因失败时才会接受一个错误对象。检查清单 3-27 中fs.rename 的 Node.js 源代码,你会发现该模块只是包装了操作系统的本地重命名功能。
清单 3-27 。fs.rename 源文件
fs.rename = function(oldPath, newPath, callback) {
callback = makeCallback(callback);
if (!nullCheck(oldPath, callback)) return;
if (!nullCheck(newPath, callback)) return;
binding.rename(pathModule._makeLong(oldPath),
pathModule._makeLong(newPath),
callback);
};
您可以从源文件中看到,原始路径名和新路径名必须存在,否则nullCheck函数将阻止重命名。您还必须提供文件的现有路径。重命名操作不需要文件本身存在,但是如果你提供了一个不存在的路径,将会抛出一个错误(见清单 3-28 )。
清单 3-28 。路径不存在
$ node 3-7-1.js 3-7/tmp/awesome.gif 3-7/save/does/not/exist/awesome.gif
/Users/gack/Dropbox/book/code/Ch03/3-7-1.js:18
if (err) throw err;
^
Error: ENOENT, rename '3-7/tmp/awesome.gif'
解决方案中的下一个例子实现了同步版本的fs.rename、fs.renameSync。这与fs.rename的功能相同,不同之处在于该函数等待直到重命名发生,然后返回(参见清单 3-29 )。
清单 3-29 。同步重命名
fs.renameSync = function(oldPath, newPath) {
nullCheck(oldPath);
nullCheck(newPath);
return binding.rename(pathModule._makeLong(oldPath),
pathModule._makeLong(newPath));
};
在移动文件的解决方案中,这两个示例可能是 Node.js 中最常见的方法。您还在解决方案中看到了一种利用标准终端命令来执行文件移动的方法:
'mv 3-7/tmp/awesome.gif 3-7/save/awesome.gif'
这是通过利用 Node.js 子流程模块来完成的。利用 Node.js 子流程模块的细节将在第五章中进一步讨论。但是,您可以看到,您可以通过这个模块直接执行命令。
3-8.象征性地链接文件
问题
构建 Node.js 应用时,您希望利用符号链接或文件系统中文件的链接。
解决办法
在这个解决方案中,您可以想象您的 Node.js 应用刚刚下载了一个可执行文件,然后您希望通过使用符号链接使该文件在文件系统中可用。为此,有许多方法来建立符号链接。当然,您必须从通过require('fs')导入的文件系统模块开始。然后你将会看到在文件系统中如何链接到文件,以及随后如何读取它们并知道它们链接到哪里有多种版本。
清单 3-30 。象征性地链接文件
/**
* symbolic links
*/
var fs = require('fs');
fs.link('/opt/Sublime Text 2/sublime_text', '/usr/bin/sublime', function(err) {
if (err) throw err;
});
fs.linkSync('/opt/Sublime Text 2/sublime_text', '/usr/bin/sublime');
fs.symlink('/opt/Sublime Text 2/sublime_text', '/usr/bin/sublime', function(err) {
if (err) throw err;
});
fs.symlinkSync('/opt/Sublime Text 2/sublime_text', '/usr/bin/sublime');
fs.readlink('/usr/bin/sublime', function(err, string) {
if (err) throw err;
console.log(string);
});
var rls = fs.readlinkSync('/usr/bin/sublime');
console.log(rls);
创建链接有四个函数,有两种不同的类型。首先是fs.link和fs.linkSync。另外两个是fs.symlink和fs.symlinkSync。这些是通过fs.readlink和fs.readlinkSync读取链接的方法的补充。
它是如何工作的
象征性地链接文件和文件系统中的链接文件的行为就像 Node.js 中的文件系统模块中的许多其他项目一样。也就是说,这些函数是标准操作系统命令的包装器。
首先让我们检查一下函数fs.link。这个函数本身不是一个创建符号链接的函数;相反,它是一个包装 POSIX link 命令的函数。该命令将创建一个到现有文件的链接,或通常所说的硬链接。链接函数采用三个参数:原始路径、新路径和一个回调函数,如果出现错误,回调函数将接受错误。这个函数和文件系统模块中的其他函数一样,有一个同步相关函数fs.linkSync。linkSync除了大声说出来很有趣之外,还执行和fs.link一样的硬链接操作;只是它返回结果而不是使用回调。
象征性链接文件的操作方式与fs.link类似。链接文件的功能是fs.symlink。符号链接是一种软链接,与硬链接相对。符号链接表示到另一个文件或目录的链接,就像硬链接一样,有两个明显的区别。首先,符号链接跨卷有效,而不只是作为硬链接指向本地卷。第二,符号链接可以指向任意路径,其中硬链接必须链接到文件系统上的现有文件。
除了操作系统上符号链接和硬链接实现的不同,Node.js 实现非常相似。函数fs.symlink接受三个参数:原始路径、新路径和接受任何发生的错误的回调。这与函数fs.link的签名相同。正如fs.link有一个同步副本一样,fs.symlink也有fs.symlinkSync。同步版本直接返回结果,而不是利用回调。
在文件系统上创建符号链接之前,fs.symlink和fs.symlinkSync函数确实执行了一次检查。该检查是预处理功能;清单 3-31 显示了系统如何确保在 Windows 环境下创建的符号链接使用正确的协议来解析文件路径。
清单 3-31 。符号链接预处理
function preprocessSymlinkDestination(path, type) {
if (!isWindows) {
// No preprocessing is needed on Unix.
return path;
} else if (type === 'junction') {
// Junctions paths need to be absolute and \\?\-prefixed.
return pathModule._makeLong(path);
} else {
// Windows symlinks don't tolerate forward slashes.
return ('' + path).replace(/\//g, '\\');
}
}
一旦在文件系统中创建了符号链接或硬链接,就可以利用 Node.js 来读取该链接。读取文件系统上的链接将解析到链接实际链接的位置。这是通过fs.readlink功能完成的。fs.readlink函数接受两个参数:符号链接的路径和回调。回调将包含两个参数:一个错误(如果发生的话)和符号链接或硬链接解析的文件路径的字符串。与其他方法一样,这个函数有一个同步版本,它直接返回错误或结果字符串,而不使用回调函数。
3-9.更改文件权限
问题
在 Node.js 应用中,您需要控制文件系统中文件和目录的访问和权限级别。
解决办法
为了更改文件的权限,您必须利用操作系统使用的相同功能集来执行相同的操作。文件的标准规则集由访问级别和文件的所有权决定,访问级别是通过文件在文件系统上注册的模式授予的。比方说,你有一个在你的应用中使用的文件;您可能希望使它只对操作系统上的个人可读。这很容易做到,如清单 3-32 所示。本解决方案中显示的其他示例强调了个人访问文件的几种不同模式。稍后你会看到所有不同的可能性,你可以改变文件的模式。
清单 3-32 。在 Node.js 中更改文件权限
/**
* Altering file permissions
*/
var fs = require('fs'),
file = '3-9/file.txt';
//CHANGING MODES chmod
//hidden file
//-rwSr-S--T 1 cgack cgack 4 May 5 11:50 file.txt
fs.chmod(file, 4000, function(err) {
if (err) throw err;
});
//individual write
//--w------- 1 cgack cgack 4 May 5 11:50 file.txt
fs.chmod(file, 0200, function(err) {
if (err) throw err;
});
//individual execute
//---x------ 1 cgack cgack 4 May 5 11:50 file.txt
fs.chmod(file, 0100, function(err) {
if (err) throw err;
});
//individual write + execute
//--wx------ 1 cgack cgack 4 May 5 11:50 file.txt
fs.chmod(file, 0300, function(err) {
if (err) throw err;
});
//CHANGING OWNERS chown
// requires root access
//--wx------ 1 root root 4 May 5 11:50 file.txt
fs.chown(file, 0 /* root */, 0, function(err) {
if (err) throw err;
});
//--wx------ 1 cgack cgack 4 May 5 11:50 file.txt
fs.chown(file, 1000, 1000, function(err) {
if (err) throw err;
});
清单中还显示了更改文件所有者的能力。您可以看到,通过使用fs.chown函数,文件的所有权被转移到 root,然后很容易地回到我的个人用户。
它是如何工作的
当您打算更改文件或目录的权限时,您可能首先需要更改所有权。在 Node.js 中,文件系统的所有权由fs.chown函数及其同步对应函数fs.chownSync决定。fs.chown函数接受四个参数。首先,您必须为您希望对其执行所有权变更的文件提供函数。其次,您必须提供系统上用户 ID 的整数。第三,添加该用户所属的组 ID 的整数。最后,该函数接受回调函数,回调函数将传递发生的任何错误。
文件名应该是显而易见的,因为您可能知道目标,或者在 Node.js 应用中提供一个需要更改所有权的文件。但是您可能没有记住您希望授予访问权限的所有用户的用户 ID 或组 ID。如果您想获得这些 ID 号,您可以在终端中使用以下命令。
清单 3-33 。通过终端确定用户和组 ID 号
$ id –u <username> #username user id
$ id –g <username> #username group id
使用chown会直接改变文件系统上文件的所有者。您会看到,如$ ls –l所述,将 file.txt 中的所有者更改为 root 用户,这表明该文件被更改为由 root 用户//--wx------ 1 root root 4 May 5 11:50 file.txt所拥有。当然,对于我的用户 cgack 的所有权变更来说也是如此。需要注意的是,要更改文件的所有权,您需要以 root 权限进行操作。这意味着在这个例子中,您应该以$ sudo node 3-9-1.js的身份运行应用文件。没有这个级别的权限,你会遇到一个权限错误:Error: EPERM, chown '3-9/file.txt'。
一旦更改了文件的所有权,您可能仍然希望显式设置与该文件相关联的权限。在该解决方案中,您看到示例中的文件已从隐藏文件更改为单独写入、单独执行,然后是组合的单独写入+执行权限。设置这些相当简单,因为fs.chmod函数会改变文件的访问模式。这个函数,fs.chmod,接受三个参数:文件名、八进制权限代码的整数值,以及一个回调函数来传递发生的任何错误。
决定权限的八进制代码被分成几个部分。第一个数字代表授予“其他”用户的权限。第二个数字代表组级别的访问权限。第三和第四个分别代表个人用户访问和系统级访问。除了系统级访问权限(表示文件是隐藏的、存档的还是系统文件)之外,可能的值为 1(表示执行)、2(表示写入)和 4(表示读取)。完整列表如列表 3-34 所示。
清单 3-34 。文件访问设置
4000 Hidden file
2000 System file
1000 Archive bit
0400 Individual read
0200 Individual write
0100 Individual execute
0040 Group read
0020 Group write
0010 Group execute
0004 Other read
0002 Other write
0001 Other execute
这些值都可以组合在一起,正如您在解决方案中看到的那样,通过利用0300 = 0200 and 0100来授权单独的写和执行。因此,个人的完全访问权限将设置在0700 = 0400 and 0200 and 0100。您可以看到,这个功能的全部范围,授予所有用户和组读、写和执行的完全访问权,将是0777。
有了这些工具,您应该能够在 Node.js 中改变访问级别和文件级别所有权。
3-10.查看文件修改
问题
您希望监视 Node.js 中文件的所有修改。
解决办法
如果您希望能够获得尽可能多的信息,并考虑对文件系统中的文件进行更改,那么 Node.js 的文件系统模块有一套可能行得通的解决方案。这种文件系统监控有两种方法。一种是使用fs.watchFile方法,该方法将返回您正在查看的以前和当前文件的整个 file stat 对象。第二种是较新的方法,在 3-4 节中简要提到:fs.watch。要查看这两个选项的运行情况,查看对任意文件的更改,只需看看清单 3-35 。
清单 3-35 。监视文件更改的两种方法
/**
* Watching files for modifications
*/
var fs = require('fs'),
path = '3-10/file.txt';
fs.watchFile(path, function(current, previous){
for (var key in current) {
if (current[key] !== previous[key]) {
console.log(key + ' altered. prev: ' + previous[key] + ' curr: ' + current[key]);
}
}
});
fs.watch(path, function(event, filename){
if (filename) {
console.log(filename + ' : ' + event);
} else {
//Macs don't pass the filename
console.log(path + ' : ' + event);
}
});
这将产生一个结果,如果你改变文件的内容到某种简单的程度,它将看起来类似于清单 3-36 中显示的控制台输出。
清单 3-36 。观看文件
$ node 3-10-1.js
3-10/file.txt : change
3-10/file.txt : change
size altered. prev: 14 curr: 19
atime altered. prev: Sun May 05 2013 14:04:37 GMT-0400 (EDT) curr: Sun May 05 2013 14:07:23 GMT-0400 (EDT)
mtime altered. prev: Sun May 05 2013 14:04:37 GMT-0400 (EDT) curr: Sun May 05 2013 14:07:22 GMT-0400 (EDT)
ctime altered. prev: Sun May 05 2013 14:04:37 GMT-0400 (EDT) curr: Sun May 05 2013 14:07:22 GMT-0400 (EDT)
它是如何工作的
首先,你看到的是fs.watchFile函数。这个函数接受一个路径参数和一个回调函数,回调函数将提供您正在查看的文件的当前和以前的状态。在文件上执行长轮询fs.stat调用可以做到这一点。这是由可选的第二个参数配置的,它是一个 options 对象,默认为{ persistent: true, interval: 5007 },允许连续或持久的时间间隔进行轮询。
fs.watchFile函数创建一个新的StatWatcher对象(清单 3-37 ),它在设定的时间间隔轮询文件的 stat 对象。如果在StatWatcher上发生变化,这些统计信息将在监听器回调中返回。这将返回文件 stat 的以前和当前版本。即使该文件以前不存在,它也会显示一个添加日期stat.atime,值为:Wed Dec 31 1969 19:00:00 GMT-0500(EST)(UNIX 纪元的开始)。
清单 3-37 。StatWatcher EventEmitter
function StatWatcher() {
EventEmitter.call(this);
var self = this;
this._handle = new binding.StatWatcher();
// uv_fs_poll is a little more powerful than ev_stat but we curb it for
// the sake of backwards compatibility
var oldStatus = -1;
this._handle.onchange = function(current, previous, newStatus) {
if (oldStatus === -1 &&
newStatus === -1 &&
current.nlink === previous.nlink) return;
oldStatus = newStatus;
self.emit('change', current, previous);
};
this._handle.onstop = function() {
self.emit('stop');
};
}
util.inherits(StatWatcher, EventEmitter);
StatWatcher.prototype.start = function(filename, persistent, interval) {
nullCheck(filename);
this._handle.start(pathModule._makeLong(filename), persistent, interval);
};
StatWatcher.prototype.stop = function() {
this._handle.stop();
};
在该解决方案中,您可以看到,每次侦听器进行文件归档时,都会遍历当前 stat 并与之前的 stat 进行比较。然后对于当前对象中与文件的前一个 stat 中不同的每个关键点(参见清单 3-38 )。
清单 3-38 。遍历文件以找到改变的状态。
for (var key in current) {
if (current[key] !== previous[key]) {
console.log(key + ' altered. prev: ' + previous[key] + ' curr: ' + current[key]);
}
}
在此、fs.watchFile和fs.watch之间进行选择的选项并不明确。这两种解决方案仍被认为不稳定。虽然fs.watchFile可以返回被监视文件的完整 stat 细节,但它受限于轮询功能,因此返回那个fs.watch要慢得多,您将在下面看到。
fs.watch函数创建一个FSWatcher,如清单 3-39 所示,它是一个 Node.js EventEmitter,与StatWatcher EventEmitter类似,当检测到文件修改时,它会产生并发出一个 change 事件。
清单 3-39 。创建新的 FSWatcher
watcher = new FSWatcher();
watcher.start(filename, options.persistent);
if (listener) {
watcher.addListener('change', listener);
}
FSWatcher 在文件或目录上创建一个新的FSEvent句柄。然后FSWatcher绑定到这个句柄的change事件(见清单 3-40 )。
清单 3-40 。FSWatcher
function FSWatcher() {
EventEmitter.call(this);
var self = this;
var FSEvent = process.binding('fs_event_wrap').FSEvent;
this._handle = new FSEvent();
this._handle.owner = this;
this._handle.onchange = function(status, event, filename) {
if (status) {
self._handle.close();
self.emit('error', errnoException(process._errno, 'watch'));
} else {
self.emit('change', event, filename);
}
};
}
util.inherits(FSWatcher, EventEmitter);
FSWatcher.prototype.start = function(filename, persistent) {
nullCheck(filename);
var r = this._handle.start(pathModule._makeLong(filename), persistent);
if (r) {
this._handle.close();
throw errnoException(process._errno, 'watch');
}
};
通过调用文件或目录上的fs.watch函数创建的FSWatcher将发出两个事件之一:错误或变更。在清单 3-40 中,变化事件是你的监听器函数所绑定的。这个回调提供了一个事件和一个文件名(或目录),这个事件发生在这个文件上。事件可以是"changed"或"renamed"。这缺少 fs.watchFile 函数的信息,正如您所看到的,该函数为更改的文件提供了一个完整的 stat 对象。``
四、构建 Web 服务器
Web 服务器是用 Node.js 构建的典型应用。这是由于 Node.js 的主要目标。Node.js 非常适合构建高度可伸缩的、事件驱动的、网络化的应用——web 服务器。
在本章中,你将学习和理解如何用 Node.js 构建一个 web 服务器。你将看到从简单的 web 服务器到在你的服务器上处理静态文件的主题。这些主题只是使 web 服务器正常工作的一部分。为了全面了解 web 服务器,因为它可以通过 Node.js 实现,您还将学习以下内容:
- 使用 HTTPS 创建安全套接字层(SSL)服务器
- 配置标题
- 管理 HTTP 状态代码
- 处理 HTTP 请求和响应
- 使用 HTTP 事件管理您的 web 服务器
4-1.设置 HTTP 服务器
问题
您需要创建一个简单的 web 服务器来通过 HTTP 提供内容。
解决办法
在 Node.js 中,web 服务器通常使用 HTTP 模块来设置。这提供了一个与 HTTP 协议交互的层。
假设您正在编写一个 web 服务器,当您连接到 web 服务器时,它将向客户端发送一条状态消息。在这个解决方案中,清单 4-1 ,这已经被简化为简单地写响应‘hello ’,然后结束响应。
清单 4-1 。简单 HTTP Web 服务器
/**
* Setting up an HTTP server
*/
var http = require('http');
var server = http.createServer(function(req, res) {
res.write('hello');
res.end();
});
server.listen(8080);
它是如何工作的
这个 web 服务器过于简化,因此您可以研究 HTTP 模块如何创建服务器。在这个解决方案中,您自然会从需要 http 模块开始。这个模块公开了一个函数http.createServer,它是服务器实际创建的地方。http.createServer方法实例化一个新的服务器对象。服务器对象接受一个requestListener回调函数。这将把响应和请求参数发送给 web 服务器的回调。
新的 web 服务器是一个 HTTP 服务器,它是从你在《??》第二章中看到的net.Server对象派生而来的。服务器还为事件、connection、request和clientError提供事件监听器。
清单 4-2 。由 createServer 实例化的服务器源
function Server(requestListener) {
if (!(this instanceof Server)) return new Server(requestListener);
net.Server.call(this, { allowHalfOpen: true });
if (requestListener) {
this.addListener('request', requestListener);
}
// Similar option to this. Too lazy to write my own docs.
//http://www.squid-cache.org/Doc/config/half_closed_clients/
//http://wiki.squid-cache.org/SquidFaq/InnerWorkings#What_is_a_half-closed_filedescriptor.3F
this.httpAllowHalfOpen = false;
this.addListener('connection', connectionListener);
this.addListener('clientError', function(err, conn) {
conn.destroy(err);
});
this.timeout = 2 * 60 * 1000;
}
util.inherits(Server, net.Server);
Server.prototype.setTimeout = function(msecs, callback) {
this.timeout = msecs;
if (callback)
this.on('timeout', callback);
};
exports.Server = Server;
您已经创建了您的 web 服务器。接下来,告诉服务器您想在哪里监听请求。这是通过server.listen完成的。server.listen函数接受一个端口以及一个可选的主机名、backlog 和一个回调。server.listen方法的回调函数将监听“listening’事件。提供主机名将告诉服务器您将在哪里监听给定端口的请求。
注server.listen还有另外两个签名。一种替代方法是只提供一个 UNIX 路径和一个回调。这将在路径上开始一个套接字服务器。另一种方法是提供一个句柄——一个套接字或一个服务器——它将成为新的服务器。
一旦您的服务器在监听,您就可以从服务器提供您的响应。在您提供给http.createServer方法的请求监听器回调中有两个参数。这些参数表示所提供的 HTTP 请求和 HTTP 响应。在该解决方案中,您希望创建一个 web 服务器来发送对“hello”连接的响应。这是通过流式传输一个res.write(‘hello’)函数来完成的。一旦response.end()函数被调用,这将在客户端呈现。
Response.write将响应体的块作为第一个参数发送。可选的第二个参数用于设置这个块的字符编码。您可能认为响应只需要一个response.write,但这种想法是不正确的。事实上,对于每个响应,您都需要调用response.end()函数。
4-2.使用 SSL 构建 HTTPS 服务器
问题
您创建了一个 web 服务器,但是您想通过使用 SSL 加密的连接通过 HTTPS 提供内容来增加额外的安全级别。
解决办法
为了构建一个 SSL 服务器,在开始之前,您需要准备好一些东西。首先,您的客户端和服务器必须执行传输层安全性(TLS)握手。为此,您需要生成一个证书和密钥来验证您的 HTTPS 会话。这些密钥在客户端和服务器之间交换。一旦交换了密钥,验证和确认会话的过程就开始了。一旦密钥被认为是有效的,会话就像普通的 HTTP 连接一样通过 HTTPS 继续进行,只是增加了一层安全性。
从那里,您可以使用 Node.js 中的 HTTPS 模块。该模块的行为类似于 HTTP 模块,但是连接是通过 TLS/SSL 加密的。然后通过 Node.js 创建一个 HTTPS 服务器,如清单 4-3 所示。
清单 4-3 。HTTPS 服务器
/**
* HTTPS server
*/
var https = require('https');
var fs = require('fs');
var options = {
key: fs.readFileSync('privatekey.pem'),
cert: fs.readFileSync('certificate.pem')
};
https.createServer(options, function (req, res) {
res.writeHead(200);
res.write("https!\n");
res.end();
}).listen(8080);
它是如何工作的
创建 HTTPS 连接从 TLS/SSL 开始。该协议确保客户端和服务器之间的安全通信。发生这种情况是因为客户端和服务器之间存在握手,在握手过程中,服务器向客户端公开其证书和公钥。然后,当客户端发送响应时,用服务器的公钥对响应进行加密,并进行验证。如果所有数据都被评估为有效,则会话将在 HTTPS 上继续。
但是如何获得这些证书和密钥呢?在 Node.js 中,SSL/TLS 实现利用了 OpenSSL。OpenSSL 是 SSL/TLS 的开源实现。这是一个能让你轻松实现密钥和证书的协议。为了生成这样一个密钥,你需要打开你的终端并输入如清单 4-4 所示的命令。
清单 4-4 。创建 TLS/SSL 密钥和证书
$ openssl genrsa -out privatekey.pem 1024
$ openssl req -new -key privatekey.pem -out certrequest.csr
$ openssl x509 -req -in certrequest.csr -signkey privatekey.pem -out certificate.pem
在 Windows 上这略有不同,因为默认情况下 Windows 不包含 OpenSSL 实现。您应该首先从http://openssl.org/related/binaries.html下载一个二进制发行版。默认情况下,这将安装到您计算机上的 C:\OpenSSL-Win32。在那里,您可以打开 PowerShell 并从 C:\OpenSSL-Win32\bin 目录运行以下内容。
PS C:\OpenSSL-Win32\bin> .\openssl.exe genrsa –out privatekey.pem 1024
PS C:\OpenSSL-Win32\bin> .\openssl.exe req –new –key .\privatekey.pem –out certrequest.csr
PS C:\OpenSSL-Win32\bin> .\openssl.exe x509 –req –in .\certrequest.csr –signkey .\privatekey.pem –out certificate.pem
一旦创建了证书和密钥,现在就可以创建安全的服务器了。这从https.createServer方法开始。这个函数类似于http.createServer方法,除了创建安全连接。这是通过一个选项对象完成的。本例中使用的选项为创建tls.Server?? 设置证书和密钥。你会在第六章中看到更多关于 SSL 和 TLS 的细节。为了实际读取密钥和证书文件的值,你使用文件系统读取它们,如第三章中所讨论的。一旦这些被读取,您就可以创建您的服务器。
清单 4-5 。HTTPS 服务器继承了 tls。计算机 Web 服务器
function Server(opts, requestListener) {
if (!(this instanceof Server)) return new Server(opts, requestListener);
if (process.features.tls_npn && !opts.NPNProtocols) {
opts.NPNProtocols = ['http/1.1', 'http/1.0'];
}
tls.Server.call(this, opts, http._connectionListener);
this.httpAllowHalfOpen = false;
if (requestListener) {
this.addListener('request', requestListener);
}
this.addListener('clientError', function(err, conn) {
conn.destroy(err);
});
this.timeout = 2 * 60 * 1000;
}
inherits(Server, tls.Server);
一旦创建了服务器,您应该能够通过 SSL 连接访问它。要测试这一点,只需旋转服务器地址,您应该会看到响应“https!”写入您的控制台。另一方面,如果您不尝试访问服务器的 HTTPS 版本,您将无法从服务器获得预期的结果。
清单 4-6 。使用 cURL 查看您的安全连接
$ curl –khttps://localhost:8080 # works
https!
$ curl http://localhost:8080 # nope
curl: (52) Empty response from the server
4-3.在您的服务器上处理请求
问题
你有一个 HTTP 或 HTTPS 服务器。该服务器需要处理传入的请求。
解决办法
当您构建 web 服务器时,您需要处理请求。请求的形式多种多样,包含的内容很快就会变成大量的数据。在处理请求时,您需要能够有效地筛选传入的数据,以便处理头、方法和 URL 参数。
在此解决方案中,您将创建一个处理请求的 web 服务器。它可能看起来与您熟悉的许多 web 服务器相似。该服务器将处理请求头,并按照您认为合适的方式处理它们。例如,如果请求标头包含“不要跟踪”指令,则不发送跟踪 cookie。
在正确处理了头之后,您可能想要解析请求 URL。这将通过处理传入路径来帮助您处理 404 和一般应用路由。除了路径之外,您还可能对随请求一起发送的查询字符串参数感兴趣。
最后,您将需要检查启动请求的请求方法。这就是 HTTP 方法,在你创建任何应用,或者一个具象状态转移(REST)应用 编程接口(API)的时候都会很有用。
清单 4-7 。处理请求
/**
* Processing Requests
*/
var http = require('http'),
url = require('url');
var server = http.createServer(function(req, res) {
//Handle headers
if (req.headers.dnt == 1) {
console.log('Do Not Track');
}
//Parse the URL
var url_parsed = url.parse(req.url, true);
//What type of request is this
if (req.method === 'GET') {
handleGetRequest(res, url_parsed);
} else if (['POST', 'PUT', 'DELETE'].indexOf(req.method) > -1) {
handleApiRequest(res, url_parsed, req.method);
} else {
res.end('Method not supported');
}
});
handleGetRequest = function(res, url_parsed) {
console.log('search: ' + url_parsed.search);
console.log('query: ' + JSON.stringify(url_parsed.query));
console.log('pathname: ' + url_parsed.pathname);
console.log('path: ' + url_parsed.path);
console.log('href: ' + url_parsed.href);
res.end('get\n');
};
handleApiRequest = function(res, url_parsed, method) {
if (url_parsed.path !== '/api') {
res.statusCode = 404;
res.end('404\n');
}
res.end(method);
};
server.listen(8080);
它是如何工作的
该解决方案中的 web 服务器是为处理请求而构建的。它通过检查服务器收到的请求周围的细节来做到这一点。这个请求实际上是一个名为http.IncomingMessage的对象。
http.IncomingMessage 继承了可读流接口。在此基础上,它构建了一些对 HTTP 消息有用的对象,如清单 4-8 所示。
清单 4-8 。http。传入消息
function IncomingMessage(socket) {
Stream.Readable.call(this);
this.socket = socket;
this.connection = socket;
this.httpVersion = null;
this.complete = false;
this.headers = {};
this.trailers = {};
this.readable = true;
this._pendings = [];
this._pendingIndex = 0;
// request (server) only
this.url = '';
this.method = null;
// response (client) only
this.statusCode = null;
this.client = this.socket;
this._consuming = false;
this._dumped = false;
}
util.inherits(IncomingMessage, Stream.Readable);
正如您从源代码中看到的,http.IncomingMessage带来了几个对您的解决方案很重要的对象或设置。首先,它带来了标题。请求头是直接反映随请求一起发送的键值对的对象。当我试图从我的 web 浏览器向这个服务器发送一个请求时,标题看起来如清单 4-9 所示。
清单 4-9 。典型的请求头
{ host: 'localhost:8080',
'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.7; rv:20.0) Gecko/20100101 Firefox/20.0',
accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'accept-language': 'en-us,en;q=0.5',
'accept-encoding': 'gzip, deflate',
dnt: '1',
connection: 'keep-alive' }
其次,在您的 web 服务器中,您需要处理request.url。这包含通过请求 URL 发送的所有信息。最简单的解析方法是利用 URL 模块。您可以告诉 URL 模块解析包含查询字符串的 request.url。
请求的第三部分对您的服务器有价值的是request.method . request.method将为您提供开始请求的 HTTP 方法。在该解决方案中,web 服务器被设置为模仿 web API。在这种情况下,API 方法的路由不完全由 URL 的路径决定,还由request.method决定。这些方法只是 HTTP 方法的字符串名称。您的解决方案以两种不同的方式处理这些不同的方法。首先,您服务一个 HTTP GET 请求;使用它将记录请求的一些细节,并响应该方法确实是一个 GET。第二,用其他方法模拟 API 路由方案。这些由一个单独的函数处理,该函数将检查以确保您请求的不仅是正确的方法,还有路径。正如你在解决方案中看到的,这些都是以这样的方式处理的,你可以卷曲每种类型来查看不同的结果,如你在清单 4-10 中看到的。
清单 4-10 。不同结果的卷曲
$ curl -X PUT http://localhost:8080/api
put
$ curl -X PUT http://localhost:8080/apis
404
$ curl -X DELETE http://localhost:8080/api
delete
$ curl -X TRACE http://localhost:8080/api
Method not supported
通过理解 Node.js 中伴随着http.request的信息,您能够利用它来构建您的 web 服务器来处理这些请求。接下来,您将看到如何从您的服务器发送响应。
4-4.从您的服务器发送响应
问题
您已经有了 web 服务器,但是现在您需要能够以响应的形式从服务器发送信息。
解决办法
服务器响应是作为请求事件的一部分发出的 Node.js EventEmitter对象。在这个解决方案中,您将利用响应对象直接将内容写入请求者。首先,您想要发送一个 HTML 文档。您可以通过创建一个响应并直接发送 HTML 内容来做到这一点。
清单 4-11 。HTML 的响应.写入
/**
* Sending a response from your server
*/
var http = require('http');
var server = http.createServer(function(req, res) {
res.setHeader('Content-Type', 'text/html');
res.writeHead(200, 'woot');
res.write('<!doctype html>');
res.write('<html>');
res.write('<head><meta charset="utf-8"></head>');
res.write('<body>');
res.write('<h2>Hello World</h2>');
res.write('</body></html>');
res.end();
});
server.listen(8080);
现在,您可以在回复中直接提供 HTML 内容。您可能需要能够发送其他类型的内容,以便为您的应用提供可靠的解决方案。在这种情况下,您选择发送一个 JavaScript Object Notation (JSON)编码的对象,以便客户机可以从您的服务器检索信息。这在实现上是相似的,只是有一些小的变化,您将在它的工作原理一节中看到细节。
清单 4-12 。A JSON 服务器负责人
var http = require('http');
var server = http.createServer(function(req, res) {
res.setHeader('Content-Type', 'application/json');
res.writeHead(200, 'json content');
res.write('{ "wizard": "mithrandir" }');
res.end();
});
server.listen(8080);
它是如何工作的
您现在通过使用serverResponse对象从您的 web 服务器提供内容。在本解决方案中使用的这个对象带有一些有价值的功能。requestListener回调函数中的第一行是response.setHeader.``setHeader函数顾名思义就是这样做的;它设置响应的标头。这些被设置在一个名称和值对中,res.setHeader(‘Name’, ‘Value’);.
在解决方案中,您设置 Content-Type 头来定义随请求一起发送的内容的类型。您还可以设置 cookies、自定义标头参数或来自服务器的请求标头附带的任何内容。
本解决方案中使用的另一种设置响应头的方法是response.writeHead方法。这种方法不会将您的头创建限制为单个名称和值对。此方法最多需要三个参数。第一个参数是必需的,它为响应设置 HTTP 状态代码。然后,您可以选择设置与状态代码描述相对应的自定义描述或原因短语。这可以是您希望的任何原因短语,与 HTTP 标准描述不同。
清单 4-13 。Node.js 中的 HTTP 原因短语覆盖
if (typeof arguments[1] == 'string') {
reasonPhrase = arguments[1];
headerIndex = 2;
} else {
reasonPhrase = STATUS_CODES[statusCode] || 'unknown';
headerIndex = 1;
}
第三个参数实际上是一个 header 对象,它将接受名称和值对,而不仅仅是单个的名称-值对,作为一个完整的对象。为了重构上面 JSON serverResponse的解决方案,您可以简单地调用一次response.writeHead来获得相同的结果。
清单 4-14 。在 response.writeHead 调用中组合 HTTP 状态代码、原因短语和标头
res.writeHead(200, ‘json content’, {
‘Content-Type’: ‘application/json’});
然后,使用response.write向客户机发送响应的主体。这个函数将接受一个表示响应体块的字符串。response.write的第二个参数是设置响应的编码,默认为 utf8。response.write不要求您已经通过前面提到的方法设置了标题。如果没有显式设置这个头,那么response.write方法将隐式定义一个状态码为 200 的头。然后,write 方法确保标头已经发送。如果标头尚未发送,则它们将与数据的初始写入一起发送到客户端。
清单 4-15 。如果头还没有发送,就和第一个块一起发送
if (!this._headerSent) {
if (typeof data === 'string') {
data = this._header + data;
} else {
this.output.unshift(this._header);
this.outputEncodings.unshift('ascii');
}
this._headerSent = true;
}
return this._writeRaw(data, encoding);
现在,您已经看到并执行了从 web 服务器发送响应的方法。这些选项只是 HTTP 响应的一部分。可用物品的完整列表如表 4-1 所示。
表 4-1 。HTTP serverResponse 方法
| 方法 | 描述 |
|---|---|
| response.addTrailers(标题) | 向响应中添加 HTTP 尾随标头(标头,但在消息的末尾)。仅当分块编码用于响应时,才会发出尾部;如果不是(例如,如果请求是 HTTP/1.0),它们将被无声地丢弃。 |
| response . end([数据],[编码]) | 向服务器发出信号,表明所有响应标头和正文都已发送;服务器应该认为消息是完整的。必须对每个响应调用方法 response.end(),。 |
| response.getHeader(名称) | 读出一个已经排队但没有发送到客户端的头。请注意,该名称不区分大小写。这只能在头被隐式刷新之前调用。 |
| response.headersSent | 布尔值(只读)。如果发送了头,则为真,否则为假。 |
| response.removeHeader(名称) | 移除排队等待隐式发送的标头。 |
| 响应.发送日期 | 如果为真,将自动生成日期标题,如果标题中没有日期标题,则在响应中发送。默认为真。 |
| response.setHeader(名称,值) | 为隐式标头设置单个标头值。如果该标题已经存在于待发送标题中,其值将被替换。如果需要发送多个同名的头,请在这里使用字符串数组。 |
| response.setTimeout(毫秒,回调) | 将套接字的超时值设置为毫秒。如果提供了回调,那么它将被添加为响应对象上的“超时”事件的侦听器。 |
| response.statusCode 代码 | 当使用隐式标头(不显式调用 response.writeHead()时),此属性控制标头刷新时将发送到客户端的状态代码。 |
| response.write(块,[编码]) | 发送一大块响应正文。可以多次调用此方法来提供身体的连续部分。 |
| writeContinue() | 向客户端发送 HTTP/1.1 100 Continue 消息,指示应该发送请求正文。 |
| writeHead(状态代码,[原因短语],[标题]) | 向请求发送响应标头。 |
4-5.处理标题和状态代码
问题
在构建 Node.js web 应用时,您需要能够正确地传递和处理头信息和 HTTP 状态代码。
解决办法
在为这个解决方案创建的场景中,您可以想象这样一种情况,您需要为您的 web 应用提供特定类型的文件。当您正在构建一个希望发布到托管 web 应用商店或市场(如 Chrome 或 Firefox OS 应用)的 web 应用时,可能会出现这种情况。
在这种情况下,您可以提供一个应用清单文件。这通常是 JSON 文件的形式,它设置应用的细节,以便使它可以安装在托管平台上。这需要特定的头类型,以便主机平台将该文件识别为清单。因此,在这个解决方案中,您将操作标题来适当地表示内容类型,并为您的应用处理正确的状态代码。
清单 4-16 。处理标题和状态代码
/**
* Headers and status codes
*/
var http = require('http');
url = require('url');
var server = http.createServer(function(req, res) {
if (req.headers) {
console.log('request headers', req.headers);
}
var parsedUrl = url.parse(req.url);
if (parsedUrl.path === '/manifest.webapp' && req.method === ‘GET’) {
// serving an application manifest file type
res.writeHead(200, { 'Content-Type' : 'application/x-web-app-manifest+json' });
res.write('{ "name" : "App" }');
res.write( '"description": "My elevator pitch goes here",');
res.write('"launch_path": "/",');
res.write('"icons": {');
res.write('"128": "/img/icon-128.png" },');
res.write('"developer": {');
res.write(' "name": "Your name or organization",');
res.write(' "url": "http://your-homepage-here.org" },');
res.write('"default_locale": "en" }');
res.end();
} else if (parsedUrl.path !== '/') {
res.statusCode = 404;
res.end(http.STATUS_CODES[res.statusCode]);
} else {
res.writeHead(200, { 'Content-Type': 'text/html'});
res.end('<h2>normalContent</h2>');
}
});
server.listen(8080);
它是如何工作的
该解决方案旨在做两件事。首先,它被设计为从应用的根提供静态 HTML,url path = '/'。第二,它被设计成服务于 webapp.manifest 文件,或者您将编写来打包您的应用以在应用市场上托管的内容。为了正确地做到这一点,您需要控制标题和状态代码。
状态代码很重要,因为它们提供了有关您对客户端的响应状态的信息。状态代码属于五个类别中的一个,这五个类别由每个以 100 开始的整数块分隔。100 范围内的状态代码是信息代码;200 范围内的代码是代表成功的代码;300 个范围代码表示重定向。对于客户端错误,错误由 400 范围内的状态代码表示,对于服务器错误,错误由 500 范围内的状态代码表示。
在此解决方案中,您的应用被设计为仅提供来自 web 应用根的内容,或者清单文件本身。服务器请求的其他路径将导致 404 状态代码。此状态代码是一个客户端错误,指示找不到路径。
清单 4-17 。设置 404 未找到状态码
if (parsedUrl.path !== '/') {
res.statusCode = 404;
res.end(http.STATUS_CODES[res.statusCode]);
}
这个响应是用通过response.end方法传递的数据编写的。这利用了http.STATUS_CODES对象,该对象将为传递的response.statusCode找到相应的状态代码原因描述。
您的目标 URL 都将返回 200 或“OK”状态代码。第一个是 web 应用的根。除了状态代码,您还希望将这个根目录作为 HTML 文档提供。这不仅由您提供的内容控制,也由标题控制。
当从任何类型的 web 服务器提供内容时,控制头是很重要的,因为头指示客户端如何处理内容,或者一旦内容被处理后如何处理。这方面的例子有内容类型头,指示请求如何提供内容;Cache-Control 头,它告诉客户端如何处理内容的缓存;和指示请求长度的 Content-Length。这些只是可以在 Node.js 的请求中设置的三个标准和非标准头名称。
在此解决方案中,当您发送应用清单文件时,您发送了一个自定义的非标准头:{ ' Content-Type ':' application/x-we b-app-manifest+JSON ' }。这个头表明内容属于应用清单类型,应该是一个 JSON 文件。如果您在应用的根目录下,响应会提供一个“text/html”的内容类型头,您可能会认为这是一个 html 文档。当然,您可以根据需要向这些响应添加任何额外的头,但是知道某些响应的内容类型(比如清单文件)需要精确是很重要的。
4-6.创建 HTTP 客户端
问题
您希望创建一个 Node.js 应用作为 HTTP 客户端。
解决办法
从 Node.js 应用中创建 HTTP 客户机就像创建 HTTP 服务器一样简单。在此解决方案中,您将从为客户端设置选项开始。这些选项告诉您的应用将请求发送到哪里,以及通过什么方式获取请求。您这样做是为了能够与在 4-3 节中为您的应用创建的 REST API 进行通信。这将解析一组参数,确定发送给 API 的方法和路径,然后处理http.request。
清单 4-18 。HTTP 客户端
/*
* Creating an HTTP client
*/
var http = require('http'),
args = process.argv.slice(2);
//Set defaults
var clientOptions = {
host: 'localhost',
// hostname:'nodejs.org',
port: '8080',
path: '/',
method: 'GET'
};
args.forEach(function(arg) {
switch(arg) {
case 'GET':
clientOptions.method = 'GET';
break;
case 'SUBMIT':
case 'POST':
clientOptions.method = 'POST';
clientOptions.path = '/api';
break;
case 'UPDATE':
case 'PUT':
clientOptions.path = '/api';
clientOptions.method = 'PUT';
break;
case 'REMOVE':
case 'DELETE':
clientOptions.method = 'DELETE';
clientOptions.path = '/api';
break;
default:
clientOptions.method = 'GET';
clientOptions.path = '/';
}
var clientReq = http.request(clientOptions, function(res) {
console.log('status code', res.statusCode);
switch(res.statusCode) {
case 200:
res.setEncoding('utf8');
res.on('data', function(data) {
console.log('data', data);
});
break;
case 404:
console.log('404 error');
break;
}
});
clientReq.on('error', function(error) {
throw error;
});
clientReq.end();
});
它是如何工作的
这通过利用 Node.js HTTP 模块来实现。该模块提供了一个界面,可以方便地创建一个客户端请求,http.request。在这个解决方案中,您首先利用process.argv去除启动您的应用的任何相关命令行参数。在本例中,您只需实例化传递您希望提供的 HTTP 方法的应用,应用将遍历这些方法,为每个方法创建一个请求。
$ node 4-6-1.js GET POST PUT DELETE NOTHING
如果您的目标是在第 4-3 节中创建的服务器,您可以看到与下面类似的结果,显示您成功地访问了客户端请求的 API 端点。
status code 200
data get
status code 200
data get
status code 200
data post
status code 200
data put
status code 200
data delete
以上介绍了实现的工作原理,但现在您将看到 Node.js 如何处理一个http.request。http.request有两个参数,一个选项对象和一个接收响应的回调函数。
当您调用http.request时,您初始化了一个ClientRequest对象。ClientRequest对象继承自 Node.js OutgoingMessage对象。ClientRequest对象有一整套缺省值,这些缺省值是根据传递给 options 参数的内容进行处理的。当您浏览ClientResponse对象时,您将看到这些默认设置正在被配置。
表 4-2。客户端请求对象选项
| [计]选项 | 功能 |
|---|---|
| 代理人 | 控制代理行为。当使用代理时,请求将默认为 Connection: keep-alive。 |
| 作家(author 的简写) | 基本认证(即“用户:密码”)。 |
| 头球 | 包含请求标头的对象。 |
| 宿主 | 向其发出请求的服务器的域名或 IP 地址(默认为“localhost”)。 |
| 主机名 | 为了支持 url.parse(),主机名优于主机。 |
| 本地地址 | 要为网络连接绑定的本地接口。 |
| 方法 | 指定 HTTP 请求方法的字符串(默认为 GET)。 |
| 小路 | 请求路径(默认为“/”)。应包含查询字符串(如果有)。 |
| 港口 | 远程服务器的端口(默认为 80)。 |
| 套接字路径 | Unix 域套接字(使用 host:port 或 socketPath 之一)。 |
只有当响应返回时,传递给http.request函数的回调函数才会从ClientRequest对象中调用。您还为 error 事件设置了一个事件侦听器,以便捕获请求过程中可能发生的任何错误。一旦返回了响应,您就可以处理该响应。在这个例子中,您检查statusCodes并相应地记录。您将在下一节看到更多关于处理响应的内容。
需要注意的是,为了让clientResponse工作,你必须调用request.end()函数。不管通过请求体发送的数据量有多少,这都是必要的,因为您必须表示请求的结束。
4-7.处理客户端响应
问题
您已经创建了一个 HTTP 客户端;您现在需要理解如何处理客户端响应。
解决办法
正确处理您在 HTTP 客户机上收到的响应非常重要。您需要响应诸如状态代码或应用所依赖的特定标题之类的东西。
对于这个解决方案,您可以想象一个场景,其中您的 HTTP 客户端需要查找由服务器设置的自定义头,x-ample,如果设置为适当的值,它将提醒客户端执行一个特殊的操作。然后,您将检查状态代码,以确保在执行您的操作之前有一个良好的响应。
清单 4-19 。处理响应
/**
* Processing client responses
*/
var http = require('http');
var clientOptions = {
host: 'localhost',
port: '8080',
path: '/',
method: 'GET'
};
var clientReq = http.request(clientOptions, function(res) {
//Handle custom header for something special
if (res.headers['x-ample'] === 'trigger') {
console.log('x-ample header trigger');
//work with status codes
switch(res.statusCode) {
case 200:
res.setEncoding('utf8'); // unless you can read buffer chunks
res.on('data', function(data) {
console.log('data', data);
});
break;
case 404:
console.log('404 error');
break;
default:
console.log(res.statusCode + ': ' + http.STATUS_CODES[res.statusCode]);
break;
}
} else {
console.log('required header not present');
}
});
clientReq.on('error', function(error) {
throw error;
});
clientReq.setHeader('Cache-Control', 'no-cache');
clientReq.end();
它是如何工作的
http.request函数上的回调函数是“response”事件的事件处理程序。这个事件监听器是从服务器响应接收数据的唯一方式。如果您省略了“响应”侦听器或回调,您的客户端请求将永远不会从服务器接收任何数据。
一旦您使用适当的监听器为'response'事件设置了客户端请求,您就能够从响应中获取数据。响应是一个可读的流,所以您可以通过为'data'事件添加一个侦听器,或者在流变成“readable”时调用response.read()来处理数据
在本例中,您避免直接从响应中读取数据,直到您检查了响应中的某个值。其中一个值是检查从响应发送的头。因为响应是包含 headers 对象的可读流,所以只需检查想要解析的头;将其值与应用中所需的值进行比较。
清单 4-20 。响应标题
if (res.headers['x-ample'] === 'trigger') {
console.log('x-ample header trigger');
/* . . . */
}
然后,您继续处理响应。在这个解决方案中,下一步是检查响应状态代码。如果状态代码不是 200 OK,您将无法从响应中读取数据。当然,如果一切正常,您将阅读响应正文。
清单 4-21 。响应状态代码
switch(res.statusCode) {
case 200:
res.setEncoding('utf8');
res.on('data', function(data) {
console.log('data', data);
});
break;
case 404:
console.log('404 error');
break;
default:
console.log(res.statusCode + ': ' + http.STATUS_CODES[res.statusCode]);
break;
}
读取响应分两步完成。首先,为了使响应可读,将编码设置为 UTF-8。
清单 4-22 。响应默认编码
data <Buffer 67 65 74 0a>
清单 4-23 。响应 UTF-8 编码
data get
通过有策略地检查返回到 HTTP 请求回调的响应对象,可以处理特定于 Node.js 解决方案的各种参数和任务。
4-8.处理客户端请求
问题
您已经创建了一个 HTTP 客户端,并学习了如何处理来自它的响应。现在,您需要更详细地控制您的客户端请求。
解决办法
首先构建一个 HTTP GET 请求。GET 请求可以有两种形式。首先,如果您不需要控制自定义头或何时发送request.end()事件,您可以通过使用http.get() .使用 Node.js 快速实现 HTTP GET 请求
清单 4-24 。使用 http.get()
var http = require('http');
var getReq = http.get('http://localhost:8080', function(res) {
console.log('status code', res.statusCode, ': ', http.STATUS_CODES[res.statusCode]);
});
getReq.on('error', function(err) {
console.log(err);
});
或者,如果您需要能够控制您的头的某些方面,但是您仍然只需要处理一个 HTTP GET 请求,您将希望使用完整的http.request方法来代替。
清单 4-25 。HTTP GET 使用 http.request
var http = require('http');
var clientOptions = {
host: 'localhost',
port: '8080',
path: '/',
method: 'GET',
headers: { 'Connection': 'keep-alive',
'Content-Length': 0 }
};
var clientReq = http.request(clientOptions, function(res) {
console.log('status code', res.statusCode, ': ', http.STATUS_CODES[res.statusCode]);
});
clientReq.on('continue', function(res) {
console.log('continue event due to 100-continue');
});
clientReq.on('error', function(error) {
throw error;
});
clientReq.end();
在构建 Node.js 应用时,您可能会遇到需要处理数据上传的情况。在 Node.js 中,你可以用 POST 方法处理这个http.request。这个请求然后用request.write函数写上传。
清单 4-26 。用 HTTP POST 上传
var http = require('http');
var opt = {
host : 'localhost',
port : 8080,
path : '/upload',
method : 'POST'
};
var upload = http.request(opt, function(res) {
console.log('status code', res.statusCode, ': ', http.STATUS_CODES[res.statusCode]);
});
upload.on('error', function(err) {
console.log(err);
});
upload.write('my upload stuff');
upload.end();
它是如何工作的
通过客户机请求检索内容的第一种解决方案是使用http.get()。HTTP GET 是对http.request的抽象。事实上,http.get()函数调用默认的http.request,允许设置所有的默认选项;然后它立即调用request.end()方法并完成请求。
清单 4-27 。Node.js http 模块,获取方法
exports.get = function(options, cb) {
var req = exports.request(options, cb);
req.end();
return req;
};
接下来,通过将方法选项设置为“GET”的http.request方法检索内容。这是一个标准的 GET 请求,但是您也传递了两个特定的头。值设置为“keep-alive”的连接头将告诉 Node.js 保持到服务器的连接打开,直到下一个请求。本解决方案中的另一个标题集是Content-Length标题。这个头一旦设置,将阻止 Node.js 使用默认的分块编码。在http.request选项中,有另外两个值得注意的头文件没有在这个解决方案中使用。
其中一个标题是Expect标题。设置这个头将立即发送请求头,以便考虑潜在的Expect: 100-continue头,我们将在 4-9 节处理事件时看到更多细节。
最后一个值得注意的头是当你发送一个授权头时。当配置http.request的设置时,该标题将取代利用auth选项的需要。
处理 HTTP 客户端请求的解决方案的最后一部分是演示如何处理文件上传请求。要做到这一点,必须做几件事。首先,如您所料,不要使用 HTTP GET 方法。相反,将上传的方法选项设置为 HTTP POST。然后通过http.request的 write 方法发送数据来处理上传。
现在,您已经看到了如何在 web 服务器上使用 HTTP 客户端请求来处理请求。接下来,您将看到在您的 web 服务器上发出和使用的各种事件。
4-9.响应事件
问题
您已经在 Node.js 中构建了一个 web 服务器,现在您需要处理并正确响应在您的服务器上发出或侦听的事件。
解决办法
为了恰当地描述这个解决方案,您需要理解事件的两个方面。为此,您将构建一个 HTTP web 服务器和一个 HTTP 客户端。
服务器(见清单 4-28 )是为处理不同方法的请求和事件而构建的。首先,您的 web 服务器将监听传入的请求。在出现这些请求时,您会希望用一个明文响应来欢迎请求者。
其次,您将希望通过监听连接事件来监控到该服务器的连接。这将增加与您的服务器建立的连接总数。
您希望您的服务器也能处理一些特殊事件。其中一个事件是监听发送了'Expect: 100-continue'头的传入请求的事件。这适用于希望在实际发送请求正文之前确定您的服务器是否能够接收消息的客户端连接。在这种情况下,您需要监听的事件是'checkContinue'事件。您还需要允许使用“Request: Upgrade”报头,以便通过监听服务器上的“upgrade”事件来升级请求。然后可以发送 upgrade 头,将传输升级到 TLS,或者在本例中,升级到 WebSockets。
清单 4-28 。Web 服务器事件
/**
* Responding to events
*/
var http = require('http'),
server = http.createServer(),
connections = 0;
// request event
server.on('request', function(req, res) {
console.log('request');//, req);
res.writeHead(200, { 'Content-Type': 'text/plain'});
res.end('heyo');
});
server.on('connection', function(socket) {
connections++;
console.log('connection count: ', connections);
});
server.on('checkContinue', function(req, res) {
console.log('checkContinue');
res.writeContinue();
});
server.on('upgrade', function(req, socket, head) {
console.log('upgrade');
socket.write('HTTP/1.1 101 Web Socket Protocol Handshake\r\n' +
'Upgrade: WebSocket\r\n' +
'Connection: Upgrade\r\n' +
'Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n' +
'Sec-WebSocket-Protocol: chat\r\n' +
'\r\n');
socket.pipe(socket);
});
server.listen(8080);
为了正确体验这些事件,您需要有两组连接到此服务器的客户端。您将构建的第一个客户端,清单 4-22 ,您需要在其中提供必要的事件,以便提供 Expect 头,Expect: 100-continue,并正确地响应从服务器发出的 continue 事件。
清单 4-29 。客户端事件用于处理 Expect: 100-continue
/*
* client events
*/
var http = require('http');
var clientOptions = {
host: 'localhost',
// hostname:'nodejs.org',
port: '8080',
path: '/',
method: 'GET',
headers: { 'Expect': '100-continue' }
};
var clientReq = http.request(clientOptions, function(res) {
console.log('status code', res.statusCode);
switch(res.statusCode) {
case 200:
res.setEncoding('utf8'); // unless you can read buffer chunks
res.on('data', function(data) {
console.log('data', data);
});
break;
case 404:
console.log('404 error');
break;
}
});
clientReq.on('continue', function() {
console.log('client continue');
});
clientReq.on('error', function(error) {
throw error;
});
clientReq.end();
您将创建第二个客户机来演示您的 web 服务器发出和使用的事件,它将创建一个客户机来处理对 WebSocket 服务器的升级。这发生在你在清单 4-30 中创建的客户端中,它在设计上类似于清单 4-29 。但是,它处理不同的事件以提供不同的实现。
清单 4-30 。升级客户端
/*
* client events
*/
var http = require('http');
var clientOptions = {
host: 'localhost',
// hostname:'nodejs.org',
port: '8080',
path: '/',
method: 'GET',
headers: { 'Connection': 'Upgrade',
'Upgrade': 'websocket',
'Sec-WebSocket-Key': 'dGhlIHNhbXBsZSBub25jZQ==',
'Origin' :'localhost',
'Sec-WebSocket-Protocol': 'chat',
'Sec-WebSocket-Version': 13 }
};
var clientReq = http.request(clientOptions, function(res) {
console.log('status code', res.statusCode);
switch(res.statusCode) {
case 200:
res.setEncoding('utf8'); // unless you can read buffer chunks
res.on('data', function(data) {
console.log('data', data);
});
break;
case 404:
console.log('404 error');
break;
}
});
clientReq.on('upgrade', function(res, socket, head) {
console.log('client upgrade');
});
clientReq.on('error', function(error) {
throw error;
});
clientReq.end();
它是如何工作的
事件是构建成功的 Node.js 应用的关键部分。就这一点而言,在 Node.js 中构建一个成功的 web 服务器必须包含对客户机和服务器之间发出的事件的正确处理。在本节的解决方案中,您创建了一个同时解决多个问题的 web 服务器。
该服务器开始监听请求事件。每次有对服务器的请求时,都会发出此请求事件。一旦接收到请求,就向请求者发送一个响应,在本例中是一个简单的问候。对请求事件的回调同时提供请求和响应对象;其实这个事件和直接给http.createServer函数添加回调是一样的。
您的下一个侦听器用于通过侦听何时发出连接事件来跟踪与服务器的连接。每次连接到服务器时都会发生这种情况。每当有一个连接时,当您递增计数器时,就会发生这种情况。connection 事件在回调函数中发送连接的 socket 对象,如果您愿意,允许您访问net.Socket。
在下一个事件监听器中处理'Expect: 100-continue'头。该侦听器被绑定到“checkContinue”事件。仅当请求发送 expect 标头时,才会发出此事件。如果您没有监听此事件,服务器将自己发送适当的继续响应。
清单 4-31 。当 Expect 头存在时,Node.js 发出 checkContinue
if (req.headers.expect !== undefined &&
(req.httpVersionMajor == 1 && req.httpVersionMinor == 1) &&
continueExpression.test(req.headers['expect'])) {
res._expect_continue = true;
if (EventEmitter.listenerCount(self, 'checkContinue') > 0) {
self.emit('checkContinue', req, res);
} else {
res.writeContinue();
self.emit('request', req, res);
}
}
如果您正在适当地处理这个事件,您需要向请求表明允许继续发送请求的主体。这是通过调用response.writeContinue()函数来完成的。这个函数向请求者写入适当的 HTTP 100 响应。
清单 4-32 。Response 继续发送 HTTP 100 继续响应
ServerResponse.prototype.writeContinue = function() {
this._writeRaw('HTTP/1.1 100 Continue' + CRLF + CRLF, 'ascii');
this._sent100 = true;
};
这个 continue 事件只有在你发送适当的头时才起作用,就像在来自清单 4-22 : headers: { ‘expect’ : ‘100-continue’ }的客户端请求中一样。然后监听来自客户机请求应用的 connect 事件,表示何时调用了response.writeContinue()函数并发送了 HTTP 100 响应。
最后,您的服务器被设置为处理 WebSocket 协议的升级。这个协议是通过一个握手过程启动的,这个握手过程由客户端请求和 web 服务器发出的事件处理。当客户端发送升级报头时,该过程开始:报头:{ 'Connection': 'Upgrade', 'Upgrade': 'websocket'}。除了这些头字段之外,还会发送一个 WebSocket 密钥,服务器将利用该密钥来验证请求握手是否已收到。当这个头存在时,将发出一个'upgrade’'事件,您将在您的服务器上监听这个升级事件。
服务器上的“upgrade”事件有一个回调函数,它有三个参数:请求、套接字和头。为了完成请求 WebSocket 握手,您必须发送适当的 HTTP 响应。在这种情况下,这是具有相同升级和连接头的 HTTP 101 Web Socket 协议握手。还发回了 websocket-accept 头,这是对收到来自请求的密钥的验证。一旦发送了头,您的客户机就可以接收升级事件并完成 WebSocket 升级握手。
此次升级活动还引入了。套接字流上的管道方法。流是构建许多 Node.js 应用不可或缺的一部分。这是一种以简洁和统一的方式管理流的输入和输出的方法。这可以通过获取可读的源流并将其通过管道传输到可写的目标流来实现。这导致目标流的返回。在这个升级事件回调中,你写socket.pipe(socket);。这需要您刚才调用 socket.write() 的源(或套接字)来添加 WebSocket 升级头。然后,它通过管道把它输出到代表目标流的.pipe(socket)。
4-10.通过文件系统提供静态页面
问题
您正在构建一个 web 服务器。直接从 Node.js 代码提供 HTML 是不可维护的,也是不可取的。您需要能够从驻留在文件系统本身的文件中提供内容。
解决办法
要构建提供内容的 web 服务器,您需要利用 HTTP 模块和文件系统模块,它们是 Node.js 核心的一部分。您将构建您的服务器来处理服务器上的错误,然后您可以用正确的状态代码进行响应。您还将确保随您提供的文件一起发送适当的响应头。这意味着您需要注意所提供内容的 mime 类型。为此,使用一个简单的 URL 结构来了解应用中的哪些路由将从 web 服务器请求哪些类型的文件。
清单 4-33 。静态文件 Web 服务器
/**
* serving static HTML with the file system
*/
var http = require('http'),
fs = require('fs'),
path = require('path');
//Content types map
var contentTypes = {
'.htm' : 'text/html',
'.html' : 'text/html',
'.js' : 'text/javascript',
'.json' : 'application/json',
'.css' : 'text/css'
};
var server = http.createServer(function(req, res) {
var fileStream = fs.createReadStream(req.url.split('/')[1]);
fileStream.on('error', function(error) {
if (error.code === 'ENOENT') {
res.statusCode = 404;
res.end(http.STATUS_CODES[404]);
} else {
res.statusCode = 500;
res.end(http.STATUS_CODES[500]);
}
});
//Get the extension
var extension = path.extname(req.url);
//read the extension against the content type map - default to plain text
var contentType = contentTypes[extension] || 'text/plain';
// add the content type header
res.writeHead(200, { 'Content-Type' : contentType });
// pipe the stream to the response stream
fileStream.pipe(res);
});
server.listen(8080);
现在您有了一个 web 服务器,可以从文件系统中提供静态文件。为了测试这个功能,您还需要构建两个测试文件。一个是基本 HTML 文件。第二个是 JSON 文件。这个文件表示一个假想的 API 的响应,您可以构建这个 API 来从您的应用中访问它。这些文件显示在清单 4-34 和清单 4-35 中。
清单 4-34 。要提供的基本 HTML 文件
<!doctype html>
<html>
<head>
<title>Static HTML</title>
</head>
<body>
<h2>Node.js Recipes</h2>
<p> Tasty </p>
<button id='getJSON'>Get JSON file</button>
<script type='text/javascript'>
// bind to click
var btn = document.getElementById('getJSON');
btn.addEventListener('click', getJSONContent, false);
// Send a request to the server for the JSON file
function getJSONContent() {
var xhr = new XMLHttpRequest();
xhr.onload = jsonRetrieved;
xhr.open('GET', '/4-10-1.json', true);
xhr.send();
}
// Log to the console
function jsonRetrieved() {
console.log(this.responseText);
}
</script>
</body>
</html>
清单 4-35 。要提供的示例 JSON 文件
{
'Test': 'if',
'this':'sends'
}
它是如何工作的
让我们调查一下您的全功能 web 服务器是如何工作的。首先,您为这个服务器使用 HTTP 模块。您还可以用文件系统和 URL 模块来扩充这些模块。这将允许您从 web 服务器的文件系统中获取和读取文件,并且 URL 模块允许解析 URL,以便正确地路由您的内容。
现在,您通过调用http.createServer创建一个 web 服务器,并让该服务器监听您指定的端口。这个解决方案的实质在于requestListener回调。在这个回调中,您可以处理传入的请求和传出的响应。
当您收到一个请求时,您的服务器做的第一件事就是使用 fs.createReadStream 将传入的请求 URL 读入一个流。这将允许您创建适当的错误响应代码发送到客户端。在您的情况下,如果错误代码是 ENOENT(没有这样的文件或目录),您将发送 404 not found,对于其他错误,您将返回到一般的 500 服务器错误。
然后解析来自请求 URL 的扩展。这是通过使用 Node.js 路径模块完成的,该模块有一个方法“extname ”,它将返回给定路径的扩展名。然后将它用于您创建的内容类型对象,以将给定的扩展映射到您希望从服务器提供的适当内容类型。一旦将扩展映射到内容类型,就可以将内容类型头写入响应。接下来是通过管道将文件流传送到响应。
接下来,您将研究构建到 web 服务器中的模拟 JSON API。这条路线在网址/*.json上。这表示可能调用数据库来检索信息,但是在我们的例子中,它检索的是一个 JSON 文件,该文件将在头中带有“Content-Type: application/json”。
现在,您可以为 web 服务器提供任何类型的内容。您可以通过运行您的服务器,然后导航到各种 URL 来测试这一点。如果您导航到//localhost:8080/4-10-1.html,您将看到一个 html 页面。这个页面有一个按钮,您可以按下它向 JSON API 提交一个 XMLHttpRequest,将内容记录到控制台。当然,您可以直接导航到/4-10-1.json 路径,在那里您也将收到 json。测试一个 404,你可以简单地尝试卷曲http://localhost:8080/404,你会收到预期的 404:
> GET /404 HTTP/1.1
> User-Agent: curl/7.21.4 (universal-apple-darwin11.0) libcurl/7.21.4 OpenSSL/0.9.8r zlib/1.2.5
> Host: localhost:8080
> Accept: */*
>
< HTTP/1.1 404 Not Found
< Date: Sat, 18 May 2013 19:17:55 GMT
< Connection: keep-alive
< Transfer-Encoding: chunked
五、使用事件和子进程
正如您在本书中已经看到的,Node.js 有一个健壮的框架来处理许多例程和重要任务。在这一章中,你将全面理解你在前面章节中看到的一些概念。您将首先深入了解 Node.js 的一个基石,EventEmitters。关于这些,你将看到如何创建自定义事件和为它们添加监听器的方法,以及如何创建单个事件。所有这些都将展示如何通过在 Node.js 中有策略地实现事件来减少无休止的回调噩梦。
接下来,您将揭开用子流程扩展 Node.js 流程的神秘面纱。您将看到如何产生一个子进程,以及如何执行 shell 命令和文件。然后,您将学习如何派生一个流程,这将使我们能够在 Node.js 中对流程进行集群。
5-1.创建自定事件
问题
您已经创建了一个 Node.js 应用,但是您需要通过发出一个自定义事件在其中进行通信。
解决办法
在此解决方案中,您将创建一个 Node.js 应用,演示如何创建和侦听自定义事件。您将创建一个在超时期限到期后执行的事件。这表示操作完成时应用中会出现的情况。这将调用函数doATask,该函数将返回操作是成功还是失败的状态。有两种方法可以实现这一点。
首先,您将创建特定于状态的事件。这需要检查状态并专门为该状态创建一个事件,以及绑定到那些特定的事件来处理特殊情况。这在清单 5-1 中进行了演示。
清单 5-1 。单个状态的自定义事件
/**
* Custom Events
*/
var events = require('events'),
emitter = new events.EventEmitter();
function doATask(status) {
if (status === 'success') {
emitter.emit('taskSuccess'); // Specific event
} else if (status === 'fail') {
emitter.emit('taskFail');
}
}
emitter.on('taskSuccess', function() {
console.log('task success!');
});
emitter.on('taskFail', function() {
console.log('task fail');
});
// call task with success status
setTimeout(doATask, 500, 'success');
// set task to fail
setTimeout(doATask, 1000, 'fail');
虽然您看到这有效地使事件适当地传播,但这仍然会导致您创建两个单独的事件来发出。这可以很容易地修改成更加精简和高效的,正如你将在清单 5-2 中看到的。
清单 5-2 。一个发射器来统治他们
/**
* Custom Events
*/
var events = require('events'),
emitter = new events.EventEmitter();
function doATask(status) {
// This event passes arguments to detail status
emitter.emit('taskComplete', 'complete', status);
}
// register listener for task complete
emitter.on('taskComplete', function(type, status) {
console.log('the task is ', type, ' with status ', status);
});
// call task with success status
setTimeout(doATask, 500, 'success');
// set task to fail
setTimeout(doATask, 1000, 'fail');
这是一个更精简、更高效的实现。您可以发出适用于应用中多个状态的单个事件。在这些示例中,您看到了一个实现,其中所有事件都是从同一个源文件中处理和发出的。在清单 5-3 中,你可以看到一个共享事件发射器来发射事件的例子,该事件将在当前模块之外的模块中被接收。
清单 5-3 。发射模块
/**
* Custom Events
*/
var events = require('events'),
emitter = new events.EventEmitter(),
myModule = require('./5-1-3.js')(emitter);
emitter.on('custom', function() {
console.log('custom event received');
});
emitter.emit('custom');
为应用创建自定义事件的另一种方法是利用全局流程对象。这个 Node.js 对象是一个EventEmitter,它将允许您注册将在流程中共享的事件。这种类型的事件是从清单 5-4 中的代码发出的。
清单 5-4 。在 Node.js 进程上发出一个事件
/* Module.js file */
var myMod = module.exports = {
emitEvent: function() {
process.emit('globalEvent');
}
};
它是如何工作的
在这个例子中,您看到了创建自定义事件的多种方式。这些事件可以在任何有 Node.js EventEmitter的地方发出。Node.js EventEmitter类是组成应用的模块之间和内部通信的基石之一。
当您构建一个事件时,首先遇到的是EventEmitter类。此类由事件的对象集合组成,这些事件引用已注册的不同类型的事件。类型的概念就是你给你的事件起的名字,比如taskComplete或者taskFail。当您实际使用EventEmitter's发出方法发出事件时,这很重要。
清单 5-5 。EventEmitter 的 Emit 方法
EventEmitter.prototype.emit = function(type) {
var er, handler, len, args, i, listeners;
if (!this._events)
this._events = {};
// If there is no 'error' event listener then throw.
if (type === 'error') {
if (!this._events.error ||
(typeof this._events.error === 'object' &&
!this._events.error.length)) {
er = arguments[1];
if (this.domain) {
if (!er) er = new TypeError('Uncaught, unspecified "error" event.');
er.domainEmitter = this;
er.domain = this.domain;
er.domainThrown = false;
this.domain.emit('error', er);
} else if (er instanceof Error) {
throw er; // Unhandled 'error' event
} else {
throw TypeError('Uncaught, unspecified "error" event.');
}
return false;
}
}
handler = this._events[type];
if (typeof handler === 'undefined')
return false;
if (this.domain && this !== process)
this.domain.enter();
if (typeof handler === 'function') {
switch (arguments.length) {
// fast cases
case 1:
handler.call(this);
break;
case 2:
handler.call(this, arguments[1]);
break;
case 3:
handler.call(this, arguments[1], arguments[2]);
break;
// slower
default:
len = arguments.length;
args = new Array(len - 1);
for (i = 1; i < len; i++)
args[i - 1] = arguments[i];
handler.apply(this, args);
}
} else if (typeof handler === 'object') {
len = arguments.length;
args = new Array(len - 1);
for (i = 1; i < len; i++)
args[i - 1] = arguments[i];
listeners = handler.slice();
len = listeners.length;
for (i = 0; i < len; i++)
listeners[i].apply(this, args);
}
if (this.domain && this !== process)
this.domain.exit();
return true;
};
这种方法包括两个主要部分。首先,是对“错误”事件的特殊处理。这将按照您的预期发出错误事件,除非错误事件没有侦听器。在这种情况下,Node.js 将抛出错误,该方法将返回 false。此方法的第二部分是处理非错误事件的部分。
在检查以确保指定类型的给定事件有处理程序之后,Node.js 接着检查事件处理程序是否是一个函数。如果是函数,Node.js 将解析来自 emit 方法的参数,并将这些参数应用到处理程序。这就是清单 5-2 中的将参数传递给taskComplete事件的方式。提供的额外参数在 emit 方法调用处理程序时应用。
其他解决方案都使用相同的发射方法,但是它们以不同的方式获得发射事件的结果。清单 5-4 表示一个在整个应用中共享的 Node.js 模块。这个模块包含一个函数,该函数将向应用的其余部分发出一个事件。在这个解决方案中实现这一点的方法是利用主 Node.js 进程是一个EventEmitter的知识。这意味着您只需通过调用process.emit('globalEvent')来发出事件,共享该进程的应用的一部分将接收该事件。
5-2.为自定义事件添加侦听器
问题
在上一节中,您已经发出了自定义事件,但是如果没有合适的方法绑定到这些事件,您将无法使用它们。为此,您需要向这些事件添加侦听器。
解决办法
这个解决方案是第 5-1 节的对应部分。在上一节中,您实现了EventEmitters并发出了事件。现在,您需要为这些事件添加侦听器,以便可以在您的应用中处理它们。这个过程就像发射事件一样简单,如清单 5-6 所示。
清单 5-6 。向自定义事件和系统事件添加事件监听器
/**
* Custom Events
*/
var events = require('events'),
emitter = new events.EventEmitter();
function doATask(status) {
if (status === 'success') {
emitter.emit('taskSuccess'); // Specific event
} else if (status === 'fail') {
emitter.emit('taskFail');
}
// This event passes arguments to detail status
emitter.emit('taskComplete', 'complete', status);
}
emitter.on('newListener', function(){
console.log('a new listener was added');
});
emitter.on('taskSuccess', function() {
console.log('task success!');
});
emitter.on('taskFail', function() {
console.log('task fail');
});
// register listener for task complete
emitter.on('taskComplete', function(type, status) {
console.log('the task is ', type, ' with status ', status);
});
// call task with success status
setTimeout(doATask, 2e3, 'success');
// set task to fail
setTimeout(doATask, 4e3, 'fail');
您还可以将您的EventEmitter传递给一个外部模块,然后从那个单独的代码段中监听事件。
清单 5-7 。从外部模块监听事件
/**
* External Module
*/
module.exports = function(emitter) {
emitter.on('custom', function() {
console.log('bazinga');
});
};
正如您使用 Node.js 进程EventEmitter发出事件一样,您可以将侦听器绑定到该进程并接收事件。
清单 5-8 。Node.js 进程范围侦听器
/**
* Global event
*/
var ext = require('./5-1-5.js');
process.on('globalEvent', function() {
console.log('global event');
});
ext.emitEvent();
它是如何工作的
当您检查清单 5-6 中的解决方案时,您应该很快注意到如何向事件添加监听器。这和调用EventEmitter.on()方法 一样简单。的。EventEmitter的on方法接受两个参数:一是事件类型名;第二,侦听器回调,它将接受传递给emit()事件的任何参数。的。on方法实际上只是EventEmitter addListener函数 的包装器,它采用相同的两个参数。您可以直接调用此方法来代替调用。on功能一样。
清单 5-9 。事件发射器 addListener
EventEmitter.prototype.addListener = function(type, listener) {
var m;
if (typeof listener !== 'function')
throw TypeError('listener must be a function');
if (!this._events)
this._events = {};
// To avoid recursion in the case that type === "newListener"! Before
// adding it to the listeners, first emit "newListener".
if (this._events.newListener)
this.emit('newListener', type, typeof listener.listener === 'function' ?
listener.listener : listener);
if (!this._events[type])
// Optimize the case of one listener. Don't need the extra array object.
this._events[type] = listener;
else if (typeof this._events[type] === 'object')
// If we've already got an array, just append.
this._events[type].push(listener);
else
// Adding the second element, need to change to array.
this._events[type] = [this._events[type], listener];
// Check for listener leak
if (typeof this._events[type] === 'object' && !this._events[type].warned) {
var m;
if (this._maxListeners !== undefined) {
m = this._maxListeners;
} else {
m = EventEmitter.defaultMaxListeners;
}
if (m && m > 0 && this._events[type].length > m) {
this._events[type].warned = true;
console.error('(node) warning: possible EventEmitter memory ' +
'leak detected. %d listeners added. ' +
'Use emitter.setMaxListeners() to increase limit.',
this._events[type].length);
console.trace();
}
}
return this;
};
EventEmitter.prototype.on = EventEmitter.prototype.addListener;
从源代码片段中可以看出,addListener方法完成了几项任务。首先,在验证侦听器回调是一个函数之后,addListener方法发出它自己的事件“newListener”,以表明已经添加了一个给定类型的新侦听器。
发生的第二件事是addListener函数将监听器函数推到它所绑定的事件。在上一节中,这个函数成为了每种事件类型的处理函数。根据发射器本身提供的参数数量,emit()函数将对该函数执行.call()或.apply()操作。
最后在addListener函数中,你会发现 Node.js 非常友好,试图保护你免受潜在的内存泄漏。它通过检查侦听器的数量是否超过预定义的限制(默认为 10)来实现这一点。当然,您可以通过使用setMaxListeners()方法将这个值配置为一个更高的值,当您超过这个侦听器数量时,会出现一个有用的警告。
5-3.实现一次性事件
问题
您需要在 Node.js 应用中实现一个只希望执行一次的事件。
解决办法
假设您有一个要完成重要任务的应用。这个任务需要完成,但只能完成一次。假设您有一个监听聊天室成员的事件,该成员要么退出应用,要么断开连接。这个事件只需要处理一次。将该事件向应用的其他用户广播两次是没有意义的,因此您限制了处理该事件的次数。
有两种方法可以做到这一点。一种是手动处理注册事件,然后在接收到一次事件后删除事件侦听器。
清单 5-10 。手动注册一次事件侦听器
/**
* Implementing a One time event
*/
var events = require('events'),
emitter = new events.EventEmitter();
function listener() {
console.log('one Timer');
emitter.removeListener('oneTimer', listener);
}
emitter.on('oneTimer', listener);
emitter.emit('oneTimer');
emitter.emit('oneTimer');
这需要在侦听器函数中进行二次调用,以便能够从事件中移除侦听器。随着项目的增长,这可能会变得难以处理,因此 Node.js 有一个本机实现来实现同样的效果。
清单 5-11 。emtter.once()
/**
* Implementing a One-time event
*/
var events = require('events'),
emitter = new events.EventEmitter();
/* EASIER */
emitter.once('onceOnly', function() {
console.log('one Only');
});
emitter.emit('onceOnly');
emitter.emit('onceOnly');
它是如何工作的
第一个注册一个事件侦听器只绑定一次所发出的事件的例子非常容易理解。首先用侦听器的函数回调绑定到事件。然后,在侦听器中处理该回调,并从事件中移除该侦听器。这可以防止对来自同一发射器的事件进行任何进一步的处理。
这是因为removeListener方法 的缘故,它接受一个事件类型和一个特定的监听器函数。
清单 5-12 。event 发射器 removeListener 方法
EventEmitter.prototype.removeListener = function(type, listener) {
var list, position, length, i;
if (typeof listener !== 'function')
throw TypeError('listener must be a function');
if (!this._events || !this._events[type])
return this;
list = this._events[type];
length = list.length;
position = -1;
if (list === listener ||
(typeof list.listener === 'function' && list.listener === listener)) {
this._events[type] = undefined;
if (this._events.removeListener)
this.emit('removeListener', type, listener);
} else if (typeof list === 'object') {
for (i = length; i-- > 0;) {
if (list[i] === listener ||
(list[i].listener && list[i].listener === listener)) {
position = i;
break;
}
}
if (position < 0)
return this;
if (list.length === 1) {
list.length = 0;
this._events[type] = undefined;
} else {
list.splice(position, 1);
}
if (this._events.removeListener)
this.emit('removeListener', type, listener);
}
return this;
};
removeListener函数 将通过递归搜索 events 对象来定位需要移除的特定事件,以便找到您正在搜索的类型和函数组合。然后,它将移除事件绑定,以便侦听器不再在后续事件中注册。
一个类似于手工发射一次函数的方法是EventEmitter.once方法 。
清单 5-13 。EventEmitter once 方法
EventEmitter.prototype.once = function(type, listener) {
if (typeof listener !== 'function')
throw TypeError('listener must be a function');
function g() {
this.removeListener(type, g);
listener.apply(this, arguments);
}
g.listener = listener;
this.on(type, g);
return this;
};
该方法接受您希望一次性绑定到的侦听器和事件类型。然后,它创建一个内部函数,该函数将应用侦听器。在内部函数中调用此侦听器之前,实际的侦听器将从事件中移除。这就像您的自定义一次性方法一样,因为它会在事件第一次执行时移除侦听器。
在下一节中,我们将研究如何使用这些事件和自定义事件来减少 Node.js 应用中的回调量。
5-4.使用事件减少回调
问题
您有一个 Node.js 应用,它有多个回调函数。这段代码已经变得有点笨拙,所以你想通过利用EventEmitter来减少回调。
解决办法
想象一下,一个购物应用必须访问数据库中的数据,操作这些数据,然后刷新数据库并将状态发送回客户端。这可能是获取购物车、添加商品,并让客户知道购物车已经更新。第一个例子是使用回调编写的。
清单 5-14 。使用回调的购物车
var initialize = function() {
retrieveCart(function(err, data) {
if (err) console.log(err);
data['new'] = 'other thing';
updateCart(data, function(err, result) {
if (err) console.log(err);
sendResults(result, function(err, status) {
if (err) console.log(err);
console.log(status);
});
});
});
};
// simulated call to a database
var retrieveCart = function(callback) {
var data = { item: 'thing' };
return callback(null, data );
};
// simulated call to a database
var updateCart = function(data, callback) {
return callback(null, data);
};
var sendResults = function(data, callback) {
console.log(data);
return callback(null, 'Cart Updated');
};
initialize();
首先,理解在 Node.js 中使用回调并没有错。事实上,这可能是在 Node.js 中处理异步编程的最流行的方式。然而,你也可以看到,在你有大量必须连续发生的回调的情况下,比如在清单 5-14 中,代码可能变得不那么容易理解。为了纠正这一点,您可以合并事件,以更简洁的方式管理应用流。
清单 5-15 。使用事件代替回调
/**
* Reducing callbacks
*/
var events = require('events');
var MyCart = function() {
this.data = { item: 'thing' };
};
MyCart.prototype = new events.EventEmitter();
MyCart.prototype.retrieveCart = function() {
//Fetch Data then emit
this.emit('data', this.data);
};
MyCart.prototype.updateCart = function() {
// Update data then emit
this.emit('result', this.data);
};
MyCart.prototype.sendResults = function() {
console.log(this.data);
this.emit('complete');
};
var cart = new MyCart();
cart.on('data', function(data) {
cart.data['new'] = 'other thing';
cart.updateCart();
});
cart.on('result', function(data) {
cart.sendResults(data);
});
cart.on('complete', function() {
console.log('Cart Updated');
});
cart.retrieveCart();
对于同一任务的不同解决方案,两者都使用了相似数量的代码,但是通过转移到事件驱动的模块而不是回调流,回调的数量已经大大减少了。
它是如何工作的
当您想要检查对应用至关重要的代码时,独占使用回调可能会成为一种负担。这也会让参与项目的开发人员感到头疼,因为他们可能不太熟悉项目,不知道给定的回调函数嵌套在哪里。当您希望重构应用以添加另一个要在回调期间执行的方法时,这也会成为一个问题。这些都是您可能选择迁移到事件驱动模型的原因。
在清单 5-11 的事件驱动解决方案中,首先创建一个名为MyCart的新对象。你可以假设MyCart用存储在其中的一个项目MyCart初始化。data。然后你的MyCart对象继承了events。EventEmitter对象。这意味着MyCart可以通过 Node.js 事件模块发送和接收数据。
既然您的对象可以发出和监听事件,您可以通过使用EventEmitter的方法来扩充您的对象。例如,您创建了一个retrieveCart方法,它将从数据存储中获取数据;一旦完成,就会发出']'事件,传递从购物车中检索到的任何数据。类似地,您创建一个updateCart方法和一个sendResults方法,这两个方法将提醒客户端更新的结果。
然后实例化一个新的MyCart实例。这个新的购物车现在可以绑定到将从MyCart对象发送的事件。您有一个单独的函数来处理每个事件。这使得代码更易于维护,并且在许多情况下更易于扩展。例如,假设您需要为MyCart添加另一个日志功能。现在,您可以将其绑定到每个事件并记录交互,而无需重写整个回调流。
5-5.生下一个孩子。产卵
问题
您需要创建一个子进程来执行 Node.js 应用中的辅助操作。
解决办法
您希望从 Node.js 应用中派生出一个子进程的原因有很多。其中几个可以简单地执行命令行任务,而不需要为应用要求或构建整个模块。在这个解决方案中,您将突出显示两个命令行应用和第三个解决方案,它们将从 spawn 方法执行另一个 Node.js 进程。
清单 5-16 。产卵的孩子
/**
* .spawn
*/
var spawn = require('child_process').spawn,
pwd = spawn('pwd'),
ls = spawn('ls', ['-G']),
nd = spawn('node', ['5-4-1.js']);
pwd.stdout.setEncoding('utf8');
pwd.stdout.on('data', function(data) {
console.log(data);
});
pwd.stderr.on('data', function(data) {
console.log(data);
});
pwd.on('close', function(){
console.log('closed');
});
ls.stdout.setEncoding('utf8');
ls.stdout.on('data', function(data) {
console.log(data);
});
nd.stdout.setEncoding('utf8');
nd.stdout.on('data', function(data) {
console.log(data);
});
第一个 spawn 是在当前目录下运行'pwd'命令;第二个是列出该目录中的所有文件。这些只是内置于操作系统中的命令行实用程序。但是,此解决方案中的第三个示例执行命令来运行 Node.js 文件;然后,像前面的例子一样,将输出记录到控制台。
它是如何工作的
产卵是调用 Node.js 中子进程的一种方法,也是child_process模块的一种方法。child_process模块创建了一种通过stdout、stdin和stderr传输数据流的方式。由于这个模块的性质,这可以以非阻塞的方式完成,很好地适应 Node.js 模型。
child_process spawn 方法将实例化一个ChildProcess对象,这是一个 Node.js EventEmitter。与ChildProcess对象相关的事件如表 5-1 所示。
表 5-1 。ChildProcess 事件
| 事件 | 详述 |
|---|---|
| '消息' | 传输一个消息对象,它是 JSON 或一个值。这也可以将套接字或服务器对象作为可选的第二个参数进行传输。 |
| '错误' | 将错误传输到回调。当子进程无法生成、无法终止或消息传输失败时,会发生这种情况。 |
| '关闭' | 当子进程的所有 stdio 流都完成时发生。这将发送退出代码和与之一起发送的信号。 |
| '断开连接' | 方法终止连接时发出。子对象(或父对象)上的 disconnect()方法。 |
| '退出' | 在子进程结束后发出。如果进程正常终止,code 是进程的最终退出代码,否则为 null。如果进程因收到信号而终止,signal 是信号的字符串名称,否则为 null。 |
除了这些ChildProcess事件,产生的孩子也是一个流,正如你在上面看到的。该流包含来自子进程的标准 I/O 的数据。在表 5-2 中列出了与这些流相关的方法,以及子流上的其他方法。
表 5-2 。子进程和其他方法的流事件
| 方法 | 描述 |
|---|---|
| 。标准输入设备 | 代表子进程 stdin 的可写流。 |
| 。标准输出 | 表示子进程的标准输出的可读流。 |
| 。标准错误 | 子进程的 stderr 的可读流。 |
| 。pid | 子进程进程标识符(PID)。 |
| 。杀 | 终止一个进程,可以选择发送终止信号。 |
| 。拆开 | 断开与父级的连接。 |
| 。派遣 | 向. fork 进程发送消息。(在第 5-8 节中有更多的细节。) |
现在您对什么是ChildProcess以及它如何适应child_process模块有了更多的了解,您可以看到您的生成子流程的解决方案直接调用了流程。您创建了三个衍生进程。其中每一个都以命令参数开始。这个参数,'pwd、' 'ls、' ']'是将被执行的命令,就像您在终端应用的命令行上运行它一样。child_process.spawn方法中的下一个参数是传递给命令参数的可选参数数组。
您会看到,本例中衍生的进程与在终端命令行中运行以下内容是一样的:
$ pwd &
$ ls –G &
$ node 5-4-1.js &
您也可以从您派生的进程中读取这些命令的输出。这是通过监听child_process.stdout流实现的。如果绑定到数据事件,您会看到这些命令的标准输出,就像在终端中运行命令一样。在第三个 spawn 的例子中,您可以看到本章前一节中整个模块的输出。
还有一个可选的第三个参数可以出现在child_process.spawn方法中。该参数表示帮助设置衍生进程的一组选项。这些选项的值如表 5-3 所示。
表 5-3 。繁殖选项
| [计]选项 | 类型 | 描述 |
|---|---|---|
| 粗木质残体 | 线 | 子进程的当前工作目录。 |
| 刺痛 | 数组或字符串 | 子进程的 stdio 配置。 |
| 自定义 Fds | 排列 | 不推荐使用的功能。 |
| 包封/包围(动词 envelop 的简写) | 目标 | 环境键值对。 |
| 分离的 | 布尔代数学体系的 | 这个子进程将成为一个组长。 |
| 用户界面设计(User Interface Design 的缩写) | 数字 | 设置进程的用户标识。 |
| 眩倒病 | 数字 | 设置进程的组标识。 |
5-6.使用运行 Shell 命令。执行
问题
您希望从 Node.js 应用中直接执行一个 shell 命令作为子进程。
解决办法
在上一节中,您看到了如何通过使用child_process模块轻松地产生子进程。这可能是一个长时间运行的流程,您希望访问流程中可用的stdio流。与此类似的是child_process.exec法。这两种方法的区别在于。spawn 方法将以流的形式返回所有数据,而。exec 方法将数据作为缓冲区返回。使用这种方法,您可以直接从 Node.js 应用中执行 shell 命令,在 Windows 中执行cmd.exe,或者在其他地方执行/bin/sh。使用本节中的解决方案,您将列出一组文件,并将该操作的结果记录到控制台。然后,您将在系统中搜索包含单词 node 的所有正在运行的进程,再次将输出记录到您的控制台。
清单 5-17 。。高级管理人员
/**
* Running Shell commands with .exec
*/
var exec = require('child_process').exec;
exec('ls -g', function(error, stdout, stderr) {
if (error) console.log(error);
console.log(stdout);
});
exec('ps ax | grep node', function(error, stdout, stderr) {
if (error) console.log(error);
console.log(stdout);
});
它是如何工作的
这个解决方案通过利用child_process.spawn方法和child_process.execFile方法的各个方面来工作,您将在下一节中研究这两个方法。本质上,当您告诉child_process使用exec方法时,您是在告诉它运行一个将/bin/sh文件(Windows 上的cmd.exe)作为可执行文件的进程。
清单 5-18 。子进程。执行功能
exports.exec = function(command /*, options, callback */) {
var file, args, options, callback;
if (typeof arguments[1] === 'function') {
options = undefined;
callback = arguments[1];
} else {
options = arguments[1];
callback = arguments[2];
}
if (process.platform === 'win32') {
file = 'cmd.exe';
args = ['/s', '/c', '"' + command + '"'];
// Make a shallow copy before patching so we don't clobber the user's
// options object.
options = util._extend({}, options);
options.windowsVerbatimArguments = true;
} else {
file = '/bin/sh';
args = ['-c', command];
}
return exports.execFile(file, args, options, callback);
};
事实上,这个函数调用了execFile方法。您将在下一节中看到,这意味着该进程是根据传递给函数的文件参数生成的。
var
child
=
spawn(file, args, {
cwd
:
options.cwd,
env
:
options.env,
windowsVerbatimArguments
: !!
options.windowsVerbatimArguments
});
这意味着您想在命令行中运行的任何东西,都可以通过exec来运行。这就是为什么当您试图以ps ax | grep node的身份运行exec函数来识别所有包含单词 node 的正在运行的进程时,您会看到stdout结果,就像在 shell 中运行它一样。
17774 s001 S+ 0:00.06 node 5-6-1.js
17776 s001 S+ 0:00.00 /bin/sh -c ps ax | grep node
17778 s001 S+ 0:00.00 grep node
11503 s002 S+ 0:00.07 node
5-7.使用执行外壳文件。execFile
问题
在您的应用中,您需要将一个文件作为 Node.js 进程的子进程来执行。
解决办法
您已经对child_process模块的这个方法有些熟悉了。在这个解决方案中,您有一个 shell 脚本,其中包含您希望从 Node.js 应用中执行的几个步骤。这些可以在 Node.js 中直接完成,要么生成它们,要么调用.exec方法。然而,通过将它们作为一个文件调用一次,你可以将它们组合在一起,并且仍然可以将它们的组合输出缓冲到execFile的回调函数中。您可以在接下来的两个清单中看到示例 Node.js 应用和将要执行的文件。
清单 5-19 。使用。execFile
/**
* execFile
*/
var execFile = require('child_process').execFile;
execFile('./5-7-1.sh', function(error, stdout, stderr) {
console.log(stdout);
console.log(stderr);
console.log(error);
});
清单 5-20 。要执行的外壳文件
#!/bin/sh
echo "running this shell script from child_process.execFile"
# run another node process
node 5-6-1.js
# and another
node 5-5-1.js
ps ax | grep node
它是如何工作的
当您开始研究execFile方法如何工作时,您会很快意识到它是.spawn方法的衍生物。这个方法非常复杂,为了执行一个文件要做很多事情。首先,execFile函数将接受四个参数。第一个是文件,它是查找要执行的文件和路径所必需的。
第二个是 args 数组,它将把参数传递给要执行的文件;第三个由在衍生进程上设置的特定选项组成;第四个是回调。如你所见,这些选项默认为通用设置,如 utf8 编码,超时设置为零,以及其他如清单 5-21 所示的设置。这个回调就像来自child_process.exec的回调一样,它将一组缓冲的error、stdout和stderr传递给函数,您可以直接从回调中使用这些流。
清单 5-21 。设置 execFile 的文件、参数和选项
exports.execFile = function(file /* args, options, callback */) {
var args, optionArg, callback;
var options = {
encoding: 'utf8',
timeout: 0,
maxBuffer: 200 * 1024,
killSignal: 'SIGTERM',
cwd: null,
env: null
};
// Parse the parameters.
if (typeof arguments[arguments.length - 1] === 'function') {
callback = arguments[arguments.length - 1];
}
if (Array.isArray(arguments[1])) {
args = arguments[1];
options = util._extend(options, arguments[2]);
} else {
args = [];
options = util._extend(options, arguments[1]);
}
Node.js 现在通过传入 options 对象来产生子进程。然后,产生的子 Node 通过各种事件监听器和回调函数传递,以便在返回子 Node 本身之前将stdio流聚合到提供给execFile方法的回调函数中,如清单 5-22 所示。这与。spawn 方法将直接返回stdout和stderr流。这里使用。exec方法返回一个从stdout和stderr流创建的缓冲区。
清单 5-22 。正在生成 execFile
var child = spawn(file, args, {
cwd: options.cwd,
env: options.env,
windowsVerbatimArguments: !!options.windowsVerbatimArguments
});
var stdout = '';
var stderr = '';
var killed = false;
var exited = false;
var timeoutId;
var err;
function exithandler(code, signal) {
if (exited) return;
exited = true;
if (timeoutId) {
clearTimeout(timeoutId);
timeoutId = null;
}
if (!callback) return;
if (err) {
callback(err, stdout, stderr);
} else if (code === 0 && signal === null) {
callback(null, stdout, stderr);
} else {
var e = new Error('Command failed: ' + stderr);
e.killed = child.killed || killed;
e.code = code;
e.signal = signal;
callback(e, stdout, stderr);
}
}
function errorhandler(e) {
err = e;
child.stdout.destroy();
child.stderr.destroy();
exithandler();
}
function kill() {
child.stdout.destroy();
child.stderr.destroy();
killed = true;
try {
child.kill(options.killSignal);
} catch (e) {
err = e;
exithandler();
}
}
if (options.timeout > 0) {
timeoutId = setTimeout(function() {
kill();
timeoutId = null;
}, options.timeout);
}
child.stdout.setEncoding(options.encoding);
child.stderr.setEncoding(options.encoding);
child.stdout.addListener('data', function(chunk) {
stdout += chunk;
if (stdout.length > options.maxBuffer) {
err = new Error('stdout maxBuffer exceeded.');
kill();
}
});
child.stderr.addListener('data', function(chunk) {
stderr += chunk;
if (stderr.length > options.maxBuffer) {
err = new Error('stderr maxBuffer exceeded.');
kill();
}
});
child.addListener('close', exithandler);
child.addListener('error', errorhandler);
return child;
};
5-8.使用。fork 用于进程间通信
问题
您需要在 Node.js 中创建一个子流程,但是您还需要能够在这些子流程之间轻松地进行通信。
解决办法
使用 fork 方法在进程间通信的解决方案非常简单。您将构建一个创建 HTTP 服务器的主进程。该流程还将派生一个子流程,并将服务器对象传递给该子流程。即使服务器不是在子进程上创建的,子进程也能够处理来自该服务器的请求。服务器对象和所有消息都通过。send()法。
清单 5-23 。父进程
/**
* .fork main
*/
var cp = require('child_process');
http = require('http');
var child = cp.fork('5-8-2.js');
var server = http.createServer(function(req, res) {
res.end('hello');
}).listen(8080);
child.send('hello');
child.send('server', server);
清单 5-24 。分叉过程
/**
* forked process
*/
process.on('message', function(msg, hndl) {
console.log(msg);
if (msg === 'server') {
hndl.on('connection', function() {
console.log('connected on the child');
});
}
});
它是如何工作的
正如你在第 5-5 节看到的,创建一个分叉的进程和创建一个衍生的进程几乎是一样的。主要区别是通过child.send方法实现的跨进程通信。
这个send事件发送一个消息字符串和一个可选的句柄。手柄可以是五种类型之一:net.Socket, net.Server, net.Native, dgram.Socket, or dgram.Native。乍一看,要适应这些不同类型的方法可能令人望而生畏。幸运的是,Node.js 会为您转换句柄类型。这种处理也适用于衍生进程的响应。
消息发送到子流程时发生的事件是'message'事件。在这个解决方案中,您看到'message'事件包含消息的命名类型。首先,您发送了一条问候消息。接下来,您发送了一个服务器对象。一旦事件被确定为服务器,这个对象就被绑定到“connection”事件。然后,您可以像在单个流程模块中一样处理连接。
摘要
在本章中,您研究并实现了 Node.js 固有的两个重要模块的解决方案:事件和子流程。
在 events 模块中,您首先创建了一个自定义事件,然后解决了如何使用侦听器绑定到该事件。之后,您研究了当您只需要一个绑定时添加一次性事件侦听器的特殊情况。最后,您可以看到如何利用 Node.js 事件模块,通过使用事件来驱动功能,可以非常明显地减少回调的数量。
在本章的第二部分,您检查了子流程模块。您首先看到了如何生成一个子进程来运行主进程之外的命令。然后您看到了如何通过使用exec和execFile方法直接运行 shell 命令和文件。这些都是从 spawn 进程派生出来的,正如.fork()进程一样,spawn 是 spawn 的一个特例,它允许简单的进程间通信,为多进程 Node.js 应用提供了无限的可能性。
六、实现安全性和加密
在这一章中,你将研究任何应用的一个关键部分,不管你选择使用什么样的框架来开发它:安全性。然而,您不仅将研究如何将安全特性融入到您的应用中,还将获得关于 Node.js 如何在其核心中以及通过第三方实现来处理安全性和加密的独特视角。
您将研究 Node.js 中可用的各种类型的散列算法,分析它们是如何工作的,以及它们可能比其他算法更适合哪些应用。您将看到使用哈希算法验证文件完整性的解决方案。哈希算法在构建应用时很常见,因为它们被设计为接受任意数量的数据,并将其转换为该数据的可管理的固定长度表示形式,就像原始数据的签名一样。这在应用中很常见,用于密码存储等场景,在这些场景中,您应该存储可用于验证文本的引用,而不是密码的实际文本。哈希算法对于构建也是不可或缺的。您将利用基于哈希的消息验证码(HMAC)来验证和验证应用的消息。除了哈希算法之外,还有加密算法,用于在加密后实际检索原始数据。与哈希算法的区别在于哈希算法仅限于单向哈希,这意味着您不打算对哈希后的消息进行解哈希。
接下来,您还将研究可用于加密数据并保证其安全的 OpenSSL 密码。然后,您将在此基础上研究如何使用 Node.js 传输层安全性(TLS)模块来保护您的服务器。最后,通过 crypto 模块和第三方身份验证模块对凭证进行加密,您将完成 Node.js 安全领域的旅程。
6-1.分析哈希算法的类型
问题
您有一个命令行界面,通过它可以访问 Node.js 用户可以使用的所有散列算法,以便为他们的数据创建散列。在发布这个应用之前,您需要更好地理解每个算法。
解决办法
这个问题的解决方案其实很简单。您将构建 Node.js 中当前可用散列算法的列表。然后,您将在“如何工作”一节中看到这些散列的设计数量以及它们的常见用途。首先,您将构建一个散列列表,如清单 6-1 中的所示。
清单 6-1 。构建 Node.js 中可用的散列列表
/**
* Hashes
*/
var crypto = require('crypto'),
hashes = crypto.getHashes();
console.log(hashes.join(', '));
现在运行这段代码来获取 Node.js 的可用散列的完整列表。
清单 6-2 。Node.js 中可用的哈希
$ node 6-1-1.js
DSA-SHA1-old, dsa, dsa-sha, dsa-sha1, dsaEncryption, dsaWithSHA, dsaWithSHA1, dss1, ecdsa-with-SHA1, md4, md4WithRSAEncryption, md5, md5WithRSAEncryption, mdc2, mdc2WithRSA, ripemd, ripemd160, ripemd160WithRSA, rmd160, rsa-md4, rsa-md5, rsa-mdc2, rsa-ripemd160, rsa-sha, rsa-sha1, rsa-sha1-2, rsa-sha224, rsa-sha256, rsa-sha384, rsa-sha512, sha, sha1, sha1WithRSAEncryption, sha224, sha224WithRSAEncryption, sha256, sha256WithRSAEncryption, sha384, sha384WithRSAEncryption, sha512, sha512WithRSAEncryption, shaWithRSAEncryption, ssl2-md5, ssl3-md5, ssl3-sha1, whirlpool
其中一些已被弃用(例如“DSA-SHA1-old”),或者不是真正的加密哈希函数,而是其他加密有用的实现。也就是说,RSA 加密并不是真正的散列函数,但它可以以利用散列的方式被利用。在本节中,您将重点关注数字签名算法(DSA)、消息摘要(MD4、MD5 等。)、安全哈希算法(SHA)和 WHIRLPOOL 哈希函数、它们的用途以及潜在的漏洞。
它是如何工作的
这通过利用 Node.js 加密模块来实现。构建此模块的目的是为您在构建 Node.js 应用时可能遇到的许多加密需求提供一个健壮的实现。getHashes方法是列出所有可用散列的快捷方式,它是运行 Node.js 的平台上可用的 OpenSSL 散列的列表。
在您开始在您的应用中使用这些散列之前,注意它们是如何工作的以及它们有什么好处是很重要的;以下小节将分解最常见的算法及其特性。一般来说,加密哈希是一种将数据或消息加密为固定长度摘要(称为哈希)的方法。这种固定长度的摘要将作为代表散列的原始数据的签名或指纹,而不会泄露原始数据的内容。下面是常用算法及其功能的列表。
目录系统代理(Directory System Agent)
这种类型的加密可以对最初由国家标准与技术研究所为 DSS (数字签名标准)提出的数据进行编码。因此,这两个缩写有时可以互换使用。应当注意,DSA 不是直接的散列,而是利用散列函数来生成加密值。这个散列函数最初被设计成利用 SHA-1,但是 SHA-2 也被使用;稍后您将会读到更多关于这些散列函数的内容。
MD4
MD4 散列仍在使用,但在许多情况下,它已被 MD5 和其他更高级的散列算法所取代。它已被指定为过时。MD4 旨在快速执行。它所做的是接受一个消息并将其加密成一个 128 位的摘要。
MD4 安全性不强。在创建后不久,发现很有可能会出现哈希冲突。这意味着,即使原始消息中的微小差异通常会创建一个唯一的散列,但有几种证据和方法可以从多个消息中创建相同的散列。因此,在 MD5 规范中改进了算法。
讯息摘要 5
MD5 是 MD4 的发展,目的是提高 hash 的安全性。它再次产生一个 128 位的散列,但是它牺牲了算法的速度,尽管是轻微的。您将看到的与 MD4 的主要区别是 MD5 引入了第四个辅助函数,用于处理散列的中间步骤。这些函数还包含一些额外的常数和与 MD4 的细微差异,以使散列更加安全。
尽管如此,MD5 散列仍然是不安全的,因为它仍然容易发生冲突,因此容易受到冲突攻击。然而,它仍然是非常流行的验证文件完整性,或检查文件中的变化。MD5 还有各种其他用途,包括通用唯一标识符(UUID 版本 3)和 CRAM-MD5(一种挑战-响应认证)等等。如上所述,它仍然是一种可靠的哈希算法,但由于其安全漏洞,对于强化的安全应用或保护 SSL 连接等操作,应该避免使用它。
重复一遍
RIPEMD 消息摘要有几种变体,其最初设计基于 MD4 算法。最常见的 RIPEMD 实现是 RIPEMD-160。它是原始 128 位散列的下一代变体,创建了一个 160 位散列,就像 SHA-1 一样。RIPEMD-160 目前没有任何碰撞漏洞,因为它也有望在未来十年内保持安全。RIPEMD-160 比 SHA-1 稍慢,这可能是它没有被广泛使用的一个原因。然而,它被用于相当好的隐私(PGP)加密。RIPEMD 未被广泛使用的另一个原因是,它不像 SHA-1 那样作为事实上的标准由国家标准与技术研究所进行推广。
恒星时角
SHA 有几种变体,其中大部分都可以在 Node.js 中找到。
最初的 SHA 算法现在被称为 SHA-0,在 Node.js getHashes函数中作为 SHA 提供,它是一个 160 位的哈希算法,已知它可能会发生冲突。由于这个原因,它已经不再流行,被该算法的较新版本所取代。
在 SHA-0 之后出现了 SHA-1 ,它仍然是当今计算中使用最广泛的加密哈希函数之一。像之前的 SHA-0 一样,SHA-1 也创建了一个 160 位的摘要。如今,几乎所有最流行的安全软件协议中都使用了 SHA-1。它用于安全套接字层(SSL)、安全外壳(SSH)、TLS 和 IP 安全(IPsec)协议,以及成千上万个在 Git 版本控制系统中包含散列文件的其他实现。然而,理论上已经表明 SHA-1 具有冲突漏洞,因此已经努力创建基于 SHA-1 的甚至更安全的散列算法。
SHA-2 是 SHA-256 (256 位摘要)、SHA-224 (224 位摘要)、SHA-384 (384 位摘要)和 SHA-512 (512 位摘要)的信封名称,所有这些都可以在 Node.js 中使用。这些代表了 SHA-1 算法的发展。224 位的变体是 256 位的截断;同样,384 是 512 的一个截断。
SHA-2 散列已经在许多与 SHA-1 相同的地方实现了,包括 SSL、TLS、PGP 和 SSH。它也是比特币哈希方法以及许多平台上的下一代密码哈希方法的一部分。
漩涡
WHIRLPOOL 算法 是 Node.js 中提供的另一种哈希算法。该算法产生 512 位哈希,目前尚未发现有冲突漏洞。它已被国际标准组织作为一种标准采用,但它没有 MD5 和 SHA 系列得到那么多的支持。
在编写 Node.js 应用时,您可以使用各种散列算法,每种算法都有自己的加密算法和潜在的漏洞。在保护您的应用时,您应该仔细考虑每一个散列,并确保在评估加密散列时仔细检查每一件事情。
6-2.使用 createHash 散列数据
问题
您已经检查了 Node.js 中可用的不同类型的散列,现在您想要实现一个散列来保护消息。
解决办法
在这个解决方案中,您将通过查看 Node.js 如何散列两种不同形式的输入来检查可用的加密散列函数:空字符串和非空字符串(参见清单 6-3 )。清单 6-4 展示了一个 SHA-1 和 MD5 散列算法的例子,它们有不同的编码。
清单 6-3 。使用各种散列算法
/**
* Analyzing types of data
*/
var crypto = require('crypto'),
hashes = crypto.getHashes();
hashes.forEach(function(hash) {
['', 'The quick brown fox jumped over the lazy dog.'].forEach(function(txt) {
var hashed;
try {
hashed =crypto.createHash(hash).update(txt).digest('hex');
} catch (ex) {
if (ex.message === 'Digest method not supported') {
// not supported for this algo
} else {
console.log(ex, hash);
}
}
console.log(hash, hashed);
});
});
清单 6-4 。使用不同摘要编码的散列法
/**
* Different Encodings
*/
var crypto = require('crypto'),
message = 'this is a message';
console.log('sha1');
console.log(crypto.createHash('sha1').update(message).digest('hex'));
console.log(crypto.createHash('sha1').update(message).digest('base64'));
console.log(crypto.createHash('sha1').update(message).digest('binary'));
console.log('md5');
console.log(crypto.createHash('md5').update(message).digest('hex'));
console.log(crypto.createHash('md5').update(message).digest('base64'));
console.log(crypto.createHash('md5').update(message).digest('binary'));
算法之间的哈希差别很大。例如,使用 MD5 的空字符串的散列看起来像d41d8cd98f00b204e9800998ecf8427e,而使用 SHA-1 散列的相同字符串是da39a3ee5e6b4b0d3255bfef95601890afd80709。当然,当使用不同的编码时,它们有不同的外观,但是散列是相同的。
它是如何工作的
为了理解 Node.js 中这些函数的散列,您必须首先检查生成散列摘要的过程,然后您可以查看 crypto 模块的createHash方法的源代码。
在上面的解决方案中,您首先看到了如何跨 Node.js 中可用的各种哈希算法实现createHash方法。createHash函数接受一个算法,该算法必须是创建哈希可接受的算法。例如,如果您要使用不存在的“cheese”算法,您将得到错误“不支持摘要方法”。
在将算法传递给createHash方法之后,现在就有了 Node.js 散列类的一个实例。这个类是一个可读写的流。现在您需要向 Hash 类的update()方法发送一条您想要散列的消息。更新功能接收消息和(可选)该消息的编码。如果不是,则定义输入编码,可以是'utf8'、' ?? '、' ?? ';则假设输入是缓冲器。这意味着当新数据被读入流中时,这个方法可能被多次调用。
在散列流上调用 update 方法之后,现在就可以创建摘要,或者散列的实际输出。这个函数将接受一个输出编码,如果没有提供,则再次默认为一个缓冲区。可用的编码类型有'hex'、' ?? '、' ?? '。通常在哈希算法中,比如检查shasum (SHA-1 哈希),正如你将在下一节中看到的,更典型的是利用“hex”,但是在清单 6-4 中,你看到了一个使用多种输出编码的例子。
现在您可以检查 Node.js 源代码中的createHash方法和hash对象的源代码,如清单 6-5 所示。
清单 6-5 。创建哈希源
exports.createHash = exports.Hash = Hash;
function Hash(algorithm, options) {
if (!(this instanceof Hash))
return new Hash(algorithm, options);
this._binding = new binding.Hash(algorithm);
LazyTransform.call(this, options);
}
util.inherits(Hash, LazyTransform);
Hash.prototype._transform = function(chunk, encoding, callback) {
this._binding.update(chunk, encoding);
callback();
};
Hash.prototype._flush = function(callback) {
var encoding = this._readableState.encoding || 'buffer';
this.push(this._binding.digest(encoding), encoding);
callback();
};
Hash.prototype.update = function(data, encoding) {
encoding = encoding || exports.DEFAULT_ENCODING;
if (encoding === 'buffer' && typeof data === 'string')
encoding = 'binary';
this._binding.update(data, encoding);
return this;
};
Hash.prototype.digest = function(outputEncoding) {
outputEncoding = outputEncoding || exports.DEFAULT_ENCODING;
return this._binding.digest(outputEncoding);
};
您可以注意上面讨论的createHash、update 和 digest 函数,但是要特别注意binding.Hash(algorithm)方法。这是 Node.js 绑定到驱动 Node.js 核心的 C++的地方。这实际上是处理 OpenSSL 散列的地方,也是发生诸如算法不存在时抛出错误之类的事情的地方。除此之外,您会看到 hash 流检查可选的编码值,适当地设置它们,然后将关于 C++绑定方法的请求发送给调用者。
现在,您已经看到了如何在 Node.js 中对消息进行散列,方法是选择一种算法,用消息更新散列,然后用您选择的编码创建摘要。在下一节中,您将研究如何使用常见的哈希函数来验证文件的完整性。
6-3.使用哈希验证文件完整性
问题
您需要利用加密哈希算法来验证 Node.js 应用中使用的文件的完整性。
解决办法
您可能经常会遇到这样的情况,即您需要检查在 Node.js 应用中访问的文件的内容是否与以前的版本有所不同。您可以使用 Node.js 加密模块通过生成文件内容的散列来实现这一点。
在本解决方案中,您将创建一个应用,演示四种哈希算法处理读取文件内容任务的能力。在清单 6-6 中,你将传入你想要散列的文件,然后你的应用将读取文件流,当流被读取时更新消息,然后记录每个算法的结果散列。
清单 6-6 。检查文件完整性
/**
* Checking File Integrity
*/
var fs = require('fs'),
args = process.argv.splice('2'),
crypto = require('crypto');
var algorithm = ['md5', 'sha1', 'sha256', 'sha512'];
algorithm.forEach(function(algo) {
var hash = crypto.createHash(algo);
var fileStream = fs.ReadStream(args[0]);
fileStream.on('data', function(data) {
hash.update(data);
});
fileStream.on('end', function() {
console.log(algo);
console.log(hash.digest('hex'));
});
});
您可以想象在 Node.js 中构建一个部署流程,在该流程中,您从远程服务器发送或检索文件,并且您希望确保这些文件与它们来自的内容的预期散列相匹配。
您可能还需要下载一个文件。下载文件后,您可能需要检查该文件的完整性,根据您知道的准确值进行验证。在清单 6-7 的中,您下载了 Windows 0 . 10 . 10 版本的 Node 可执行文件,并对照这个散列的已知值检查shasum。
清单 6-7 。下载文件并检查 shasum
/**
* Verifying file integrity
*/
var http = require('http'),
fs = require('fs'),
crypto = require('crypto');
var node_exe = fs.createWriteStream('node.exe');
var req = http.get('http://nodejs.org/dist/v0.10.10/node.exe', function(res) {
res.pipe(node_exe);
res.on('end', function() {
var hash = crypto.createHash('sha1');
readr = fs.ReadStream('node.exe');
readr.on('data', function(data) {
hash.update(data);
});
readr.on('end', function() {
// Should match 419fc85e5e16139260f7b2080ffbb66550fbe93f node.exe
// from http://nodejs.org/dist/v0.10.10/SHASUMS.txt
var dig = hash.digest('hex');
if (dig === '419fc85e5e16139260f7b2080ffbb66550fbe93f') {
console.log('match');
} else {
console.log('no match');
}
console.log(dig);
});
});
});
它是如何工作的
清单 6-6 中的解决方案首先需要加密和文件系统模块,两者都是实现所需要的。然后,创建一组不同的算法用于散列。正如您在前面几节中看到的,最常用的两种是 MD5 和 SHA-1,但是 SHA-2 的变体也越来越流行。
当您遍历每个算法时,您可以使用crypto.getHash创建一个新的散列,传递要使用的算法。然后使用fs.ReadStream创建一个文件流,在命令行上传递一个作为参数提供的文件名。您可以轻松地传递任何要从应用中读取的文件名。当 Node.js 读取文件时,会发出数据事件。
在对数据事件侦听器的回调中,您开始处理散列的消息部分。这是通过将数据直接传递给散列的update()函数来实现的。文件读取完毕后,将发出“end”事件。在这个回调中,您将实际生成散列摘要并将结果记录到控制台。
当您对文件进行哈希处理时,很容易看出内容的细微变化会导致不同的哈希。例如,仅包含字符串“这是文本”的文件将生成一个shasum,它完全独立于包含字符串‘This is test’的类似文件的shasum。
在第二个例子中,清单 6-7 ,你创建了一个非常实用的解决方案,一旦一个文件被下载到你的服务器上,你就用一个shasum来检查它的完整性。为此,您需要导入 http、文件系统和加密模块。这个实现从在您的文件系统上创建一个可通过fs.createWriteStream写入的文件开始。接下来,向文件源发出一个 HTTP GET 请求;在这种情况下,您将检索 0.10.10 版 Windows 的 Node.js 可执行文件。您知道文件应该匹配的正确的shasum,因为它可以从这个版本的下载页面免费获得。
来自这个 GET 请求的响应随后被传送到您使用response.pipe(file)创建的文件中。一旦响应完成,用‘end’事件表示,您就可以从文件系统中读取文件了。就像在前面的例子中一样,您使用给定的算法(在本例中是 SHA-1 算法)创建一个散列,并在文件读取器流的“数据”事件上更新消息。一旦读取完成,您就可以通过对散列调用 digest('hex ')方法来生成散列。现在,您已经获得了下载文件的 shasum,您可以将它与预期值进行比较,以确保下载是完整的并且没有损坏。
检查系统上文件的完整性是至关重要的,但是如果你只是想获得远程文件的散列,你可以通过直接散列响应流来生成文件的 shasum,如清单 6-8 所示。
清单 6-8 。散列 HTTP 响应流
/**
* Verifying file integrity
*/
var http = require('http'),
fs = require('fs'),
crypto = require('crypto');
var req = http.get('http://nodejs.org/dist/v0.10.10/node.exe', function(res) {
var hash = crypto.createHash('sha1');
res.on('data', function(data) {
hash.update(data);
});
res.on('end', function() {
console.log(hash.digest('hex'));
});
});
这消除了文件写入,并在响应流发出“数据”事件时简单地更新散列,最终在响应结束时生成摘要。
6-4.使用 HMAC 来验证和认证消息
问题
您需要使用 HMAC 对 Node.js 应用中的消息进行身份验证。
解决办法
要生成 HMAC,您需要对本章前面几节中概述的加密哈希函数有一个基本的了解。Node.js 提供了一个类似于createHash函数的方法,可以让你轻松地用 HMAC 保护你的消息。清单 6-9 展示了如何在 Node.js 中实现一个基于 SHA-1 的 HMAC,并直接与 SHA-1 散列法进行了比较。
清单 6-9 。使用 HMAC
/**
* Using Hash-Based Message Authentication
*/
var crypto = require('crypto')
secret = 'not gonna tell you';
var hash = crypto.createHash('sha1').update('text to keep safe').digest('hex');
var hmac = crypto.createHmac('sha1', secret).update('text to keep safe').digest('hex');
console.log(hash);
console.log(hmac);
这将生成两个 160 位编码哈希的输出,但即使它们编码的是相同的文本,它们也有很大的不同,因为 HMAC 使用密钥来验证哈希。
SHA-1·哈希对 SHA-1·HMAC
f59de5dd5f2d5c49e45e1317448031baa38ab7e9
c6b314dbebdd4ff17d0fc84e9ee0d5ab5821df5f
它是如何工作的
HMAC 创建了一种验证消息完整性的方法,就像任何加密散列一样,而且由于集成了密钥,它还允许您验证消息的真实性。这可能不是加密函数的圣杯,但它超越了简单散列消息的安全性,因为它确实提供了一种额外的验证方法。这将有助于您断言您的消息在传输过程中没有被更改,提供了比更高级别的加密技术更高的保证级别。
理论上,当您创建消息的 SHA-1 散列时,通过将秘密与消息连接起来,您可以生成附有密钥的有效消息,但是这种结构仍然存在漏洞。这是因为,如果攻击者能够确定消息,他或她就可以通过编程方式提取密钥,并访问他或她尚未遇到的消息上的受保护信息。这很危险。
基本上,SHA-1 的 HMAC 实际上是在两次传达 SHA-1 的信息。这自动意味着您的散列更好,但仍然没有被认证。然而,在 HMAC 中,密钥被添加到消息中,这样 HMAC 就成为可认证的摘要。您知道您的 HMAC 的发送者是实际的发送者,因为当您比较 HMAC 的摘要时,传递的密钥被自动验证为可信的。这是独立构建的,因此不管您遇到了什么消息,以及传递了什么密钥,您都无法确定即将到来的秘密的信息。如上所述,这提供了额外的保证,即您的消息没有被更改。
在您的解决方案中,您通过调用createHmac函数来创建 HMAC。该函数接受您将在 HMAC 中使用的哈希算法类型,并且它还将在构造函数参数中获取密钥。在实例化之后,crypto.Hmac被创建,具有相同方法的流程作为散列对象被附加到原型。区别在于 C++绑定指向HMAC绑定,而不是散列,这意味着处理实现了正确的 HMAC 算法。然后,C++解析 HMAC 的参数,然后通过利用 OpenSSL HMAC 实现来处理 HMAC,以生成最终结果。
Node.js HMAC 实现
//hmac in crypto.js
exports.createHmac = exports.Hmac = Hmac;
function Hmac(hmac, key, options) {
if (!(this instanceof Hmac))
return new Hmac(hmac, key, options);
this._binding = new binding.Hmac();
this._binding.init(hmac, toBuf(key));
LazyTransform.call(this, options);
}
util.inherits(Hmac, LazyTransform);
Hmac.prototype.update = Hash.prototype.update;
Hmac.prototype.digest = Hash.prototype.digest;
Hmac.prototype._flush = Hash.prototype._flush;
Hmac.prototype._transform = Hash.prototype._transform;
//hmac in node_crypto.cc
void Hmac::Initialize(v8::Handle<v8::Object> target) {
HandleScope scope(node_isolate);
Local<FunctionTemplate> t = FunctionTemplate::New(New);
t->InstanceTemplate()->SetInternalFieldCount(1);
NODE_SET_PROTOTYPE_METHOD(t, "init", HmacInit);
NODE_SET_PROTOTYPE_METHOD(t, "update", HmacUpdate);
NODE_SET_PROTOTYPE_METHOD(t, "digest", HmacDigest);
target->Set(FIXED_ONE_BYTE_STRING(node_isolate, "Hmac"), t->GetFunction());
}
void Hmac::New(const FunctionCallbackInfo<Value>& args) {
HandleScope scope(node_isolate);
Hmac* hmac = new Hmac();
hmac->Wrap(args.This());
}
void Hmac::HmacInit(const char* hash_type, const char* key, int key_len) {
HandleScope scope(node_isolate);
assert(md_ == NULL);
md_ = EVP_get_digestbyname(hash_type);
if (md_ == NULL) {
return ThrowError("Unknown message digest");
}
HMAC_CTX_init(&ctx_);
if (key_len == 0) {
HMAC_Init(&ctx_, "", 0, md_);
} else {
HMAC_Init(&ctx_, key, key_len, md_);
}
initialised_ = true;
}
void Hmac::HmacInit(const FunctionCallbackInfo<Value>& args) {
HandleScope scope(node_isolate);
Hmac* hmac = ObjectWrap::Unwrap<Hmac>(args.This());
if (args.Length() < 2 || !args[0]->IsString()) {
return ThrowError("Must give hashtype string, key as arguments");
}
ASSERT_IS_BUFFER(args[1]);
const String::Utf8Value hash_type(args[0]);
const char* buffer_data = Buffer::Data(args[1]);
size_t buffer_length = Buffer::Length(args[1]);
hmac->HmacInit(*hash_type, buffer_data, buffer_length);
}
bool Hmac::HmacUpdate(const char* data, int len) {
if (!initialised_) return false;
HMAC_Update(&ctx_, reinterpret_cast<const unsigned char*>(data), len);
return true;
}
void Hmac::HmacUpdate(const FunctionCallbackInfo<Value>& args) {
HandleScope scope(node_isolate);
Hmac* hmac = ObjectWrap::Unwrap<Hmac>(args.This());
ASSERT_IS_STRING_OR_BUFFER(args[0]);
// Only copy the data if we have to, because it's a string
bool r;
if (args[0]->IsString()) {
Local<String> string = args[0].As<String>();
enum encoding encoding = ParseEncoding(args[1], BINARY);
if (!StringBytes::IsValidString(string, encoding))
return ThrowTypeError("Bad input string");
size_t buflen = StringBytes::StorageSize(string, encoding);
char* buf = new char[buflen];
size_t written = StringBytes::Write(buf, buflen, string, encoding);
r = hmac->HmacUpdate(buf, written);
delete[] buf;
} else {
char* buf = Buffer::Data(args[0]);
size_t buflen = Buffer::Length(args[0]);
r = hmac->HmacUpdate(buf, buflen);
}
if (!r) {
return ThrowTypeError("HmacUpdate fail");
}
}
bool Hmac::HmacDigest(unsigned char** md_value, unsigned int* md_len) {
if (!initialised_) return false;
*md_value = new unsigned char[EVP_MAX_MD_SIZE];
HMAC_Final(&ctx_, *md_value, md_len);
HMAC_CTX_cleanup(&ctx_);
initialised_ = false;
return true;
}
void Hmac::HmacDigest(const FunctionCallbackInfo<Value>& args) {
HandleScope scope(node_isolate);
Hmac* hmac = ObjectWrap::Unwrap<Hmac>(args.This());
enum encoding encoding = BUFFER;
if (args.Length() >= 1) {
encoding = ParseEncoding(args[0]->ToString(), BUFFER);
}
unsigned char* md_value = NULL;
unsigned int md_len = 0;
bool r = hmac->HmacDigest(&md_value, &md_len);
if (!r) {
md_value = NULL;
md_len = 0;
}
Local<Value> rc = StringBytes::Encode(
reinterpret_cast<const char*>(md_value), md_len, encoding);
delete[] md_value;
args.GetReturnValue().Set(rc);
}
6-5.回顾 OpenSSL 密码和安全性
问题
作为 Node.js 开发人员,您需要对 OpenSSL 密码有一个高层次的理解。
解决办法
Node.js 为 OpenSSL 密码提供了一个包装器。因此,您可以使用的密码是通过 OpenSSL 获得的。要查看这些可用的密码,您可以运行一个简单的程序(参见清单 6-10 )来输出 Node.js 中可用的各种密码。
清单 6-10 。Node.js 可用的密码
/**
* Reviewing ciphers
*/
var crypto = require('crypto');
var ciphers = crypto.getCiphers();
console.log(ciphers.join(', '));
在清单 6-11 的中,您可以看到getCiphers()函数的输出。这些是所有可用的密码,其中几个将在它如何工作一节中讨论。
清单 6-11 。crypto.getCiphers()
[ 'CAST-cbc', 'aes-128-cbc', 'aes-128-cbc-hmac-sha1', 'aes-128-cfb', 'aes-128-cfb1', 'aes-128-cfb8', 'aes-128-ctr', 'aes-128-ecb', 'aes-128-gcm', 'aes-128-ofb', 'aes-128-xts', 'aes-192-cbc', 'aes-192-cfb', 'aes-192-cfb1', 'aes-192-cfb8', 'aes-192-ctr', 'aes-192-ecb', 'aes-192-gcm', 'aes-192-ofb', 'aes-256-cbc', 'aes-256-cbc-hmac-sha1', 'aes-256-cfb', 'aes-256-cfb1', 'aes-256-cfb8', 'aes-256-ctr', 'aes-256-ecb', 'aes-256-gcm', 'aes-256-ofb', 'aes-256-xts', 'aes128', 'aes192', 'aes256', 'bf', 'bf-cbc', 'bf-cfb', 'bf-ecb', 'bf-ofb', 'blowfish', 'camellia-128-cbc', 'camellia-128-cfb', 'camellia-128-cfb1', 'camellia-128-cfb8', 'camellia-128-ecb', 'camellia-128-ofb', 'camellia-192-cbc', 'camellia-192-cfb', 'camellia-192-cfb1', 'camellia-192-cfb8', 'camellia-192-ecb', 'camellia-192-ofb', 'camellia-256-cbc', 'camellia-256-cfb', 'camellia-256-cfb1', 'camellia-256-cfb8', 'camellia-256-ecb', 'camellia-256-ofb', 'camellia128', 'camellia192', 'camellia256', 'cast', 'cast-cbc', 'cast5-cbc', 'cast5-cfb', 'cast5-ecb', 'cast5-ofb', 'des', 'des-cbc', 'des-cfb', 'des-cfb1', 'des-cfb8', 'des-ecb', 'des-ede', 'des-ede-cbc', 'des-ede-cfb', 'des-ede-ofb', 'des-ede3', 'des-ede3-cbc', 'des-ede3-cfb', 'des-ede3-cfb1', 'des-ede3-cfb8', 'des-ede3-ofb', 'des-ofb', 'des3', 'desx', 'desx-cbc', 'id-aes128-GCM', 'id-aes192-GCM', 'id-aes256-GCM', 'idea', 'idea-cbc', 'idea-cfb', 'idea-ecb', 'idea-ofb', 'rc2', 'rc2-40-cbc', 'rc2-64-cbc', 'rc2-cbc', 'rc2-cfb', 'rc2-ecb', 'rc2-ofb', 'rc4', 'rc4-40', 'rc4-hmac-md5', 'seed', 'seed-cbc', 'seed-cfb', 'seed-ecb', 'seed-ofb' ]
它是如何工作的
密码是一种通过使用一套算法来加密和解密数据的方法。正如你在本节的解答中看到的,有许多算法可供你使用。其中许多是块密码,或作用于固定数据块的密码,而不是作用于数据流并将明文转换为加密形式或密文的密码。每种密码都有自己的实现,我将详细讨论。
注本节讨论一些相对复杂的关于各种密码的算法和实现的材料。这里定义了一些在提到加密算法时常见的术语,供您参考。
攻击媒介:一组针对安全漏洞的恶意代码。
分组:通常在分组密码中使用的一组特定大小的比特。
分组密码:一种在单个分组内运行的密码,在不同的分组上进行排列,直到得到最终的密文。
密文:使用密码加密明文的最终结果。
置换:数据的一轮处理或转换。
相关密钥攻击:一种攻击手段,目标是使用多个数学上相关的密钥的密码。这些密码的结果可用于推断密码和泄露加密值。
是吗
DES 代表数据加密标准,是 20 世纪 70 年代 IBM 最初设计的一种分组密码。DES 使用 64 位的密码块大小,密钥大小也是 64 位。该算法将采用 64 位明文块,对该块进行初始排列,将该块分成两个 32 位的半块,然后通过对密钥的一部分进行异或运算,以交替的方式处理它们。这个过程重复 16 轮,直到出现最终的排列。结果是 DES 密文。
像其他密码一样,DES 容易受到强力攻击,攻击者可以对所有可能的密钥进行检查。因为 DES 中的密钥长度是 56 位(64 减去奇偶校验的最后 8 位),所以密钥相对较短,从而使得暴力攻击可行。然而,尽管 DES 易受攻击,但直到它在市场上出现了 20 多年后,攻击才得以成功演示。
由于其脆弱性,DES 不被许多应用所青睐;然而,有一种替代实现仍然被广泛使用:三重 DES。
三重 DES 是一种增加 DES 算法密钥大小的方法,它实际上是将该过程运行三次。整体设计相同,但选择了三个键。第一个密钥用于加密明文。然后使用第二密钥来解密第一加密。最后,第三个密钥再次运行 DES 以生成密文。这些密钥可以全部相同,一个不同,或者全部三个不同,它们的强度根据密钥的不同而不同,因为本质上是由您决定密码的密钥长度。虽然仍有已知的对三重 DES 的攻击,但它比 DES 本身更安全。
RC2〔??〕
RC2(或 Rivest Cipher 2),也是一种分组密码,是由 RSA 著名的 Ron Rivest 在 20 世纪 80 年代末创造的。像 DES 一样,RC2 密码由 64 位块组成,在算法中包含 18 轮。有 16 轮“混合”和 2 轮“捣碎”RC2 算法的密钥大小在 8 到 128 位之间变化,默认为 64 位。这种密码存在一个已知的相关密钥攻击漏洞。
RC4
RC4 是一个流密码,也是由罗恩·里维斯特在 20 世纪 80 年代末设计的。众所周知,它的速度和简单。这种密码的工作原理是生成一个用于加密的近似随机位流。这分两步进行:首先,有一个数组生成步骤,然后是伪随机生成步骤。通过一次两个地循环遍历数组的半随机字节,交换数组中每个字节的值,然后以 256 为模对这些值进行处理,从而生成输出。结果用于在字节数组中查找该操作的总和。
RC4 已被广泛应用于许多应用中,如 TLS、有线等效保密(WEP)和保护无线电脑网络安全系统(WPA)。然而,部分由于伪随机值,攻击向量并不是不可穿透的。正因为如此,在 2001 年,无线网络的 WEP 加密受到攻击,这促使了无线加密的后续实现。
投
CAST 是分组密码。它广泛用于 PGP 和 GNU 隐私保护(GPG)加密版本。该算法本身利用 40 到 128 位的密钥大小,并且将运行 12 轮或 16 轮,尽管 12 轮仅在密钥大小小于 80 位时出现。底层函数由八个 32 位替换框组成,这些替换框基于其他各种算法,如 XOR、模加法、bent 函数和旋转。CAST 密码中使用了三种不同的循环函数。round 函数的第一个版本用于第 1、4、7、10、13、16 轮;第二个在第 2、5、8、11、14 轮;第三次是第 3、6、9、12 和 15 轮。
山茶
山茶花密码 是另一种 128 位分组密码,其分组大小为 16 字节。密钥大小在 128、192 和 256 位之间变化。Camellia 是另一种 Feistel 密码,如果使用 128 位密钥,它将使用 18 轮,如果使用更大的密钥,它将使用 24 轮。像 CAST 一样,Camellia 使用替换盒。对于 Camellia,这些盒是 8 位乘 8 位的盒,并且使用了其中的四个。每六个回合有一个特殊的变换应用于这个密码。
河豚
河豚密码 是布鲁斯·席耐尔设计的分组密码。尽管它容易受到包括差分攻击在内的传播媒介的攻击,但它仍然受到高度重视。块大小为 64 位,密钥可以是 32 到 448 位之间的任何值。它使用 16 发子弹和大型 S 盒。在 150 Hz 的 Pentium 上,该算法的速度为 8.3 MB/s。
有几个著名的密码管理产品利用河豚。其中包括 1 密码、密码保险箱和密码钱包等。它也用于 GPG 和许多文件和磁盘加密软件。
俄歇电子能谱
AES (又名 Rijndael),或高级加密标准,是一种旨在取代 DES 的加密算法。AES 的块大小为 128 位,密钥大小可以是 128、192 或 256 位。AES 对 128 位密钥进行 10 轮运算,对 192 位密钥进行 12 轮运算,对 256 位密钥进行 14 轮运算。对 4 字节乘 4 字节的矩阵进行 AES 密码运算的过程称为“状态”该过程首先通过使用 Rijndael 密钥表来扩展密钥,然后可以开始回合。
第一轮被称为“AddRoundKey”,它提取一个子密钥,并通过使用 XOR 来组合状态中的一个字节。
这将开始剩余的回合,但不包括最后一轮。这些循环从执行“子字节”步骤开始,该步骤通过 8 位替换框替换“状态”中的每个字节。接下来是“ShiftRows”步骤,该步骤将按照设定的量移动所有行的值。每行的数量不同。下一步是“混合列”步骤。在该步骤中,通过使用可逆线性变换来组合“状态”中的列。通过这一步,每一列通过与一个已知的多项式或矩阵相乘来进行转置,从而得到最终的混合列。然后还有另一个“AddRoundKey”步骤。
轮次完成后,除了省略“混合列”步骤之外,最后一轮次的操作方式与前面的轮次相同。结果是 AES 密文。
AES 容易受到相关密钥攻击、区分攻击和密钥恢复攻击。然而,这些攻击的复杂性是不容忽视的,AES 仍然是基本安全的。事实上,它可以说是当今实践中最广泛使用的加密密码。
它用于加密 7Zip、RAR 和 WinZip 实例中的文件归档。使用 AES 的其他地方是像 BitLocker 这样的磁盘加密技术。使用 AES 的还有 GPG、IPsec、IronKey、OpenSSL(node . js 的 crypto 派生的包装器)、Pidgin 和 Linux 内核 Crypto API。当然,今天有更多的地方使用 AES,但这只是少数。
当您构建 Node.js 应用时,有多种密码可供您使用。您应该选择适合您的解决方案的特定需求的实现,并跟上不断变化的标准和新的实现。
6-6.使用 OpenSSL 密码加密数据
问题
您已经对 Node.js 中可用的 OpenSSL 密码有了一些了解,现在您需要利用这些密码来加密数据。
解决办法
了解如何在您的代码中实现密码非常重要。为此,您将构建一个解决方案,它将获取一个密钥和一个文本字符串,然后利用 AES-256 算法从您的明文创建一个密文,并再次解密它,如清单 6-12 中的所示。
清单 6-12 。从明文创建密文
/**
* encrypting data
*/
var crypto = require('crypto'),
algo = 'aes256',
key = 'cheese',
text = 'the itsy bitsy spider went up the water spout';
var cipher = crypto.createCipher(algo, key);
var encrypted = cipher.update(text, 'utf8', 'hex') + cipher.final('hex');
console.log(encrypted);
var decipher = crypto.createDecipher(algo, key);
var decrypted = decipher.update(encrypted, 'hex', 'utf8') + decipher.final('utf8');
if (decrypted === text) {
console.log('success!');
}
在应用中加密明文是很重要的,但是在某些情况下,您可能希望加密整个文件的内容,然后再解密该文本。这个解决方案需要使用文件系统模块,以便您能够读取文件的内容。
清单 6-13 。加密文件的内容
/**
* using ciphers on files
*/
var crypto = require('crypto'),
fs = require('fs'),
algo = 'aes256',
key = 'cheese';
var text = fs.readFileSync('6-6-1.txt', { encoding: 'utf8' });
var cipher = crypto.createCipher(algo, key);
var encrypted = cipher.update(text, 'utf8', 'hex') + cipher.final('hex');
console.log(encrypted);
var decipher = crypto.createDecipher(algo, key);
var decrypted = decipher.update(encrypted, 'hex', 'utf8') + decipher.final('utf8');
if (decrypted === text) {
console.log('success!');
console.log(text);
}
它是如何工作的
在 Node.js 中使用 crypto 模块时,createCipher函数是必不可少的。createCipher方法将接受一个算法和一个密码或密钥。此方法的结果是创建一个密码对象。密码对象是一个流,有三种方法。
Node.jscrypto.createCipher 对象
exports.createCipher = exports.Cipher = Cipher;
function Cipher(cipher, password, options) {
if (!(this instanceof Cipher))
return new Cipher(cipher, password, options);
this._binding = new binding.CipherBase(true);
this._binding.init(cipher, toBuf(password));
this._decoder = null;
LazyTransform.call(this, options);
}
util.inherits(Cipher, LazyTransform);
Cipher.prototype._transform = function(chunk, encoding, callback) {
this.push(this._binding.update(chunk, encoding));
callback();
};
Cipher.prototype._flush = function(callback) {
this.push(this._binding.final()); callback();
};
如本节中的解决方案一样,要加密数据或数据串,可以使用 update 函数。update 方法接受您希望加密的数据、输入编码和输出编码。这种方法的结果是加密的数据,并且像加密散列一样,它可以在一个密码上被多次调用。一旦你完成了加密,你将调用接受输出编码的cipher.final函数,给你留下加密的结果。
密码更新和最终输出
Cipher.prototype.update = function(data, inputEncoding, outputEncoding) {
inputEncoding = inputEncoding || exports.DEFAULT_ENCODING;
outputEncoding = outputEncoding || exports.DEFAULT_ENCODING;
var ret = this._binding.update(data, inputEncoding);
if (outputEncoding && outputEncoding !== 'buffer') {
this._decoder = getDecoder(this._decoder, outputEncoding);
ret = this._decoder.write(ret);
}
return ret;
};
Cipher.prototype.final = function(outputEncoding) {
outputEncoding = outputEncoding || exports.DEFAULT_ENCODING;
var ret = this._binding.final();
if (outputEncoding && outputEncoding !== 'buffer') {
this._decoder = getDecoder(this._decoder, outputEncoding);
ret = this._decoder.end(ret);
}
return ret;
};
最后,您可以通过使用 setAutoPadding(false)函数将密码输入数据的填充覆盖到块大小中。这个需要之前调用。最终()。
Cipher.prototype.setAutoPadding = function(ap) {
this._binding.setAutoPadding(ap);
return this;
};
正如您可能已经猜到的,用于逆转加密过程的crypto.createDecipher函数的工作方式与createCipher函数类似。确实如此,因为它在实例化过程中创建了一个解密对象。一旦创建了它,您就可以访问与 cipher 对象相同的 API。
node . js crypto . create deciper
exports.createDecipher = exports.Decipher = Decipher;
function Decipher(cipher, password, options) {
if (!(this instanceof Decipher))
return new Decipher(cipher, password, options);
this._binding = new binding.CipherBase(false);
this._binding.init(cipher, toBuf(password));
this._decoder = null;
LazyTransform.call(this, options);
}
util.inherits(Decipher, LazyTransform);
Decipher.prototype._transform = Cipher.prototype._transform;
Decipher.prototype._flush = Cipher.prototype._flush;
Decipher.prototype.update = Cipher.prototype.update;
Decipher.prototype.final = Cipher.prototype.final;
Decipher.prototype.finaltol = Cipher.prototype.final;
Decipher.prototype.setAutoPadding = Cipher.prototype.setAutoPadding;
在您的解决方案中,您创建了两个密文示例,并在同一个应用中破译了该文本。对于文件中的明文字符串的情况,这通过使用密码密钥‘cheese’并在createCipher方法中使用它来实现。这实际上与第二个示例没有什么不同,在第二个示例中,您使用文件系统读取文件的内容,按照 UTF-8 编码,并解密得到的密文以找到预期的结果。在这个例子中,您使用了 AES-256 密码算法,但是任何可接受的 OpenSSL 算法都可以。例如,您可以很容易地用“cast”或“camellia256”替换 aes256,前提是您在加密和解密数据之间保持一致。
6-7.使用 Node.js 的 TLS 模块保护您的服务器
问题
您有一个正在传输信息的 Node.js 服务器,您希望通过利用 Node.js 的 TLS 模块来确保传输的安全性。
解决办法
在 Node.js 中构建 TLS 服务器看起来很熟悉。它类似于你在第四章中创建的 HTTPS 服务器。这是因为 HTTP 服务器对象的基础架构是从 TLS 模块继承的。
要创建 TLS 服务器,您需要从 TLS 模块本身开始。然后,您将构建一个对您的服务器密钥和证书文件的引用,并将它们作为选项传递,如清单 6-14 所示。
清单 6-14 。创建 TLS 服务器
/**
* using TLS
*/
var tls = require('tls'),
fs = require('fs');
var options = {
key: fs.readFileSync('srv-key.pem'),
cert: fs.readFileSync('srv-cert.pem')
};
tls.createServer(options, function(s) {
s.write('yo');
s.pipe(s);
}).listen(8888);
使用有效的密钥和证书创建了安全的 TLS 服务器后,您需要创建一个能够连接到它的客户端。这也是 Node.js 的一项功能。事实上,如清单 6-15 所示,它几乎等同于网络模块创建连接的能力;但是,您需要注意指向证书颁发机构和凭证,以便对安全传输进行身份验证。
清单 6-15 。TLS 连接
/**
* tls connection
*/
var tls = require('tls'),
fs = require('fs');
var options = {
key: fs.readFileSync('privatekey.pem'),
cert: fs.readFileSync('certificate.pem'),
ca: fs.readFileSync('srv-cert.pem')
};
var connection = tls.connect(8888, options, function() {
if (connection.authorized) {
console.log('authorized');
} else {
console.log(':( not authorized');
}
});
connection.on('data', function(data) {
console.log(data);
});
它是如何工作的
TLS 是一种加密发送到服务器和从服务器接收的数据的方法。在这个解决方案中,您创建了一个服务器,它利用了一个密钥和证书,该密钥和证书是通过终端中的 OpenSSL 命令生成的,正如您在第四章中看到的那样。
清单 6-16 。生成 OpenSSL 密钥
$ openssl genrsa -out srv-key.pem 1024
$ openssl req -new –key srv-key.pem -out src-crt-request.csr
$ openssl x509 -req -in srv-crt-request.csr -signkey srv-key.pem -out srv-cert.pem
一旦有了密钥和证书,就可以在调用 tls.createServer 时将它们传递给 options 对象,并告诉它监听端口 8888。createServer 函数不仅接受 options 参数,还接受回调。这个回调在到服务器的连接上发出,并通过函数沿着安全流传递。在您的解决方案中,您向流中写入一个字符串,然后通过管道将其输出。
当您创建服务器时,有更多的选项可用,例如为握手设置超时或拒绝未授权的连接。所有这些都被考虑在内,以保护您的服务器。
ca:可信证书的字符串或缓冲区的数组。cert:包含保密增强邮件(PEM)格式的服务器证书密钥的字符串或缓冲区。(必需)ciphers:描述要使用或排除的密码的字符串。crl:PEM 编码的证书撤销列表(CRL)的字符串或字符串列表handshakeTimeout:如果 SSL/TLS 握手在一定的毫秒数内没有完成,则中止连接。默认值为 120 秒。honorCipherOrder:选择密码时,使用服务器的偏好设置,而不是客户端的偏好设置。key:包含 PEM 格式的服务器私钥的字符串或缓冲区。(必需)NPNProtocols:可能的下一个协议协商(NPN)协议的数组或缓冲区。(协议应根据其优先级排序。)passphrase:私钥(或 pfx)的字符串或密码短语。pfx:包含 PFX(或 PKCS #12)格式的服务器的私钥、证书和认证机构(CA)证书的字符串或缓冲区。(这与key、cert,和ca选项是互斥的。)rejectUnauthorized:如果为真,服务器将拒绝任何未经所提供的 ca 列表授权的连接。(该选项仅在requestCert为真时有效;默认值为 false。)requestCert:如果为真,服务器将向连接的客户端请求证书,并尝试验证该证书。(默认为false。)sessionIdContext:包含会话恢复不透明标识符的字符串。SNICallback:如果客户端支持服务器名称标识(SNI) TLS 扩展,将调用的函数。
有趣的是,这些值成为用来标识服务器的凭证的一部分。这是通过将相关参数传递给crypto.createCredentials函数来实现的。
清单 6-17 。创建服务器方法凭据
var sharedCreds = crypto.createCredentials({
pfx: self.pfx,
key: self.key,
passphrase: self.passphrase,
cert: self.cert,
ca: self.ca,
ciphers: self.ciphers || DEFAULT_CIPHERS,
secureProtocol: self.secureProtocol,
secureOptions: self.secureOptions,
crl: self.crl,
sessionIdContext: self.sessionIdContext
});
现在您有了一个安全的 TLS 服务器。你需要连接到它。要测试连接,只需打开一个终端窗口并连接。
注意在 Windows 上,默认不包含 OpenSSL。通过在http://openssl.org/related/binaries.html下载一个二进制文件,你可以很容易地把它添加到你的机器上。这将安装到您计算机上的 C:\OpenSSL-Win32。然后,您可以从 PowerShell 的 C:\OpenSSL-Win32\bin 目录中运行 OpenSSL。
清单 6-18 。连接到安全服务器
$ openssl s_client –connect localhost:8888
然而,更健壮的客户机,比如用 Node.js 构建的客户机,是可能的。为了在您的解决方案中构建您的客户端连接,您首先使用tls.connect创建一个连接。向其传递端口(和可选的 URL)。然后是 options 对象,您会注意到除了“ca”选项之外,它看起来与服务器的选项非常相似。这是服务器 CA 的值。因为服务器的凭证是自签名的,所以识别它的唯一方法是通过它本身。一旦连接,您就可以访问连接流。这个流有一个属性,它会告诉你是否真的通过了服务器的认证。从那里,一旦您被授权,您就可以执行通常在网络应用中可能执行的客户机-服务器交互,但是您现在有了 TLS 的附加安全性。
6-8.使用加密模块加密用户凭证
问题
您有一个 Node.js 应用,它需要对服务器进行身份验证,您需要确保它是加密的。
解决办法
如果您打算用 Node.js 构建任何类型的安全应用,您可能需要一种向数据库验证用户身份的方法。例如,假设您有一个在线购物车,您想让您的用户注册一个帐户,以便更快地结账,并向他们发送促销信息。您可以轻松地在数据存储中实现一个用户表或文档,它将保存用户名和密码,但是存储明文密码不是一个好主意。这也是完全不必要的,因为正如您将看到的,在使用 Node.js 加密模块时很容易增加安全性。
为了构建这个解决方案,假设您的用户刚刚向您的站点提交了一个密码,现在您希望将其存储为一个散列。这很棒,因为您根本不需要存储明文密码,而且通过使用 salt,您能够轻松地验证后续登录。清单 6-19 展示了如何创建这个实现的一个版本来存储凭证。
清单 6-19 。创建安全凭据
/**
* user credentials
*/
var crypto = require('crypto'),
password = 'MySuperSecretPassword';
function getHmac(password, salt) {
var out = crypto.createHmac('sha256', salt).update(password).digest('hex');
return out;
}
function getHash(password, salt) {
var out = crypto.createHash('sha256').update(salt + password).digest('hex');
return out;
}
function getSalt() {
return crypto.randomBytes(32).toString('hex');
}
var salt = getSalt();
var hmac = getHmac(password, salt);
var hash = getHash(password, salt);
console.log('my pwd: ', password, ' salted: ', salt, ' and hashed: ', hash);
console.log('hmac: ' , hmac);
它是如何工作的
当你看一看这是如何工作的,你会注意到这个解决方案基本上只有两个部分。首先,你生成一个随机的 salt,当你用createHash或createHmac散列你的密码时,它将成为密钥;第二,你散列你的密码。
关于保护你的密码的正确方法有不同的观点。一些人认为像SHA-256 (SHA256[password])这样的东西足够安全。然而,大多数其他人会认为您需要对您的散列加盐,如本解决方案所示。接下来的争论是,你的散列应该有多大,它是否有必要是一个密码安全的伪随机数,或者是否任何随机字节的集合都可以工作。对于这个解决方案,您的代码获取一组随机的 32 字节。
您可以在getSalt函数中看到这一点。在这里,您可以访问加密模块和randomBytes函数,它实际上是一个加密安全的伪随机字节集合。Node.js 源代码中的randomBytes调用是从 JavaScript 到 C++实现的直接绑定。
现在您有了盐,您可以使用它来安全地散列您的密码。这在本解决方案中以两种方式进行了演示。一个是通过getHmac功能。该函数将使用您生成的盐来创建您的基于 SHA-256 的 HMAC。然后用 HMAC 更新密码,并生成十六进制编码的摘要。
使用Hmac方法的替代方法是在密码后面添加 salt,然后 SHA-256 得到结果,如getHash函数所示。
对于这两种方法来说,重要的是不要对每个用户使用相同的盐。如果您使用相同的 salt,您将容易受到字典反向查找攻击。这意味着,如果你和你的朋友使用相同的密码,他们的散列将是相同的,因为盐保持不变。狡猾的恶意来源将能够确定这些模式,并最终提取安全数据。然而,使用不同的盐,你不会碰到这种情况;因为生成的 salt 在加密上是可靠的,所以它们在相同的 salt 和密码散列上发生冲突的可能性被最小化到几乎不重要的程度。
6-9.使用第三方认证模块
问题
您需要在 Node.js 中对您的用户进行身份验证,为此,您需要利用适当的第三方模块进行身份验证。
解决办法
您可以想象您有一个在上一节中讨论过的基于 Node.js 的购物车。您知道需要一种安全的方式来存储用户登录和身份验证数据,但是您可能不愿意使用自己的身份验证模块。为了减轻这一点,你做了大量的研究,并找到了一些解决方案。其中一个在方法上类似于 rolling your own,但是它在 npm 中打包为一个名为“bcrypt”的模块。这个模块将允许随机盐生成,散列您的密码,并访问这些值,以便您能够将它们安全地存储在您的数据存储。这个实现如清单 6-20 所示。这个实现只是一个片段,你可以想象它是一个更大规模的应用。这个代码片段是一个应用中注册路由的示例,它将获取用户数据,如果没有找到用户,它将利用 bcrypt 为用户生成一个 salt 和 hash,并将其保存到数据存储中。
清单 6-20 。使用 bcrypt 散列密码
app.post("/register", function(req, res) {
var usrnm = req.body.name;
User.findOne({username: usrnm}, function(err, usrData) {
if (usrData === null) {
//create
bcrypt.genSalt(10, function(err, salt) {
bcrypt.hash(req.body.pwd, salt, function(err, hash) {
var newUser = new User({ username: usrnm, email: req.body.email, pwHash: hash });
newUser.save(function(err) {
if (err) {
res.send({name: usrnm, message: "failure", error: err});
return;
}
res.send({name: usrnm, message: "success"});
});
});
});
} else {
//emit to client
res.send({name: usrnm, message: "failure", error: "User already exists"});
}
});
});
您随后能够发现的另一种方法是构建在 express.js 框架上的方法。但这是 Mozilla Persona 身份提供者的扩展。
注 Express.js 是一个非常流行的构建 web 应用的框架,你将在本书的后面读到更详细的内容。Mozilla Persona 是利用您的电子邮件地址作为您的身份提供者的一种方式,消除了您的用户对您的站点拥有特定密码的需要。
你可以通过安装一个与 Express.js 协同工作的模块“express-persona”来实现它,如清单 6-21 所示。
清单 6-21 。使用角色进行身份验证
require('express-persona')(app, {
audience: 'http://localhost:3000', // Must match your browser's address bar
verifyResponse: function(error, req, res, email) {
var out;
if (error) {
out = { status: 'failure', reason: error };
res.json(out);
} else {
models.user.findOrCreate(email, function(result) {
if (result.status === 'okay' ) {
out = { status: 'okay', user: result.user };
} else {
out = { status: 'failure', reason: 'mongodb failed to find or create user' };
}
res.json(out);
});
}
}
});
You have seen two possible implementations of third-party authentication modules, but there is a seemingly limitless supply if you examine the npm registry. It is important to scrutinize all security implementations that you will utilize on your server or in your Node.js application.
它是如何工作的
您首先实现了一个利用了bcrypt模块的解决方案。这是通过使用命令npm install bcrypt安装的,然后需要代码中的模块。bcrypt着手解决哈希密码中易受字典攻击的任何潜在漏洞。
bcrypt溶液来源于河豚。它根据特定的密码实现密钥调度。在此之后,它通过创建一个缓慢的自适应散列算法,超越了正常的加盐和散列。速度慢是好事,因为这会导致攻击者无法执行同样多的操作,从而大幅增加随后破解密码所需的时间。
这个 Node.js 模块中的基本实现只需要创建一个 salt,然后将它传递给一个散列函数,得到一个散列密码。
清单 6-22 。用 bcrypt 生成 Salt 和 Hash
bcrypt.genSalt(10, function(err, salt) {
bcrypt.hash(req.body.pwd, salt, function(err, hash) {
//store password
});
});
genSalt函数接受轮数(默认为 10)、种子长度(默认为 20)和回调,这是必需的。如果发生错误,回调将提供错误以及 salt 值。从这个 salt 中,您应该使用bcrypt.hash函数创建散列。这需要您希望加密的明文密码、salt 和一个回调。回调将产生密码的散列,然后您可以将它存储在您的数据存储中。
要在登录时解密密码并将其与数据存储中的值进行比较,可以调用bcrypt.compare方法。它接受要验证的密码作为第一个参数,然后接受来自数据库的哈希值。这将返回一个布尔值—如果匹配则为真,否则为假。
在解决方案的第二部分中,您看到了如何实现 Mozilla Persona 进行用户身份验证。这是通过使用适合 Express.js 应用的“express-persona”实现的。Mozilla Persona 还有其他实现,包括 persona-id,它不依赖于特定的框架,或者您可以推出自己的实现。
您会看到,在您的解决方案中,您需要该模块,然后向它传递您的 express.js 应用和一个对象。该对象包含目标受众,即您的应用的 URL。它还包含一个 verifyResponse 函数,该函数将在验证成功或失败时生成一个路径,允许您将用户信息存储在数据库中。这个 Node.js 实现的补充是客户端。
为了让客户端与服务器通信,您需要在源代码中包含 login.persona.org/include.js 脚本。然后,您需要为navigator.id.login()和logout()事件注册事件。
清单 6-23 。绑定到角色登录和注销
document.querySelector("#login").addEventListener("click", function() {
navigator.id.request();
}, false);
document.querySelector("#logout").addEventListener("click", function() {
navigator.id.logout();
}, false);
Persona 还需要您实现一个监视功能来监听这些事件。当检测到一个时,它将向 Express.js 应用中的/persona/verify 或/persona/logout 路由发送一个 XMLHttpRequest。
清单 6-24 。角色导航器观察方法
navigator.id.watch({
onlogin: function(assertion) {
var xhr = new XMLHttpRequest();
xhr.open("POST", "/persona/verify", true);
xhr.setRequestHeader("Content-Type", "application/json");
xhr.addEventListener("loadend", function(e) {
var data = JSON.parse(this.responseText);
if (data && data.status === "okay") {
console.log("You have been logged in as: " + data.email);
}
}, false);
xhr.send(JSON.stringify({
assertion: assertion
}));
},
onlogout: function() {
var xhr = new XMLHttpRequest();
xhr.open("POST", "/persona/logout", true);
xhr.addEventListener("loadend", function(e) {
console.log("You have been logged out");
});
xhr.send();
}
});
如上所述,用户识别有很多第三方模块。这些都应该仔细实施,以确保正确保护用户的登录凭证。当然,您可以自己实现这些模块中涉及的安全原则。
摘要
Node.js 完全有能力创建和其他框架一样安全的应用。加密模块是 OpenSSL 哈希、密码和加密功能的包装,为生成安全哈希和密文以及保护数据安全提供了顶层支持。
Node.js 还提供了一个框架来保证与 TLS 模块的服务器通信安全。这允许您在客户端和服务器之间创建安全的连接,并对 HTTPS 上的 HTTP 流量进行加密。
您还看到了如何构建一个用户身份验证模块,以更安全的方式存储用户凭证。最后,您看到了如何在现有框架的基础上创建一种通过第三方模块进行身份验证的安全方法。
保护应用不是一项简单的任务,需要时间和研究来获得正确的解决方案和实施。您还需要保持最新并留意最佳实践的变化,它们如何影响您的应用,以及如何通过 Node.js 加密模块增强您的安全机制。
七、探索其他 Node.js 模块
您已经看到了许多不同的 Node.js 模块,您可以在希望构建的 Node.js 应用中利用这些模块。但是,在构建 Node.js 应用时,可以使用 Node.js 核心的许多其他模块和部分。这一章将涉及到一些本地 Node.js 模块,并扩展它们的实现,让你对这些模块有更好的理解。理解这些模块在构建 Node.js 应用中起着至关重要的作用是很重要的。
在本章中,您将使用域名系统(DNS)模块来解析远程服务器的主机名和 IP 地址。通过使用缓冲区,您将获得更好的流处理,并且您将看到应用的集群化。您将使用全局流程对象,利用计时器,并处理服务器请求上的查询字符串。您还将看到 Node.js 控制台中公开的内容,以及 Node.js 中可用的调试器 URL。
7-1.使用 DNS 创建简单的 DNS 服务器
问题
您希望能够在 Node.js 应用中从远程服务器获取信息。这些信息可以是 IP 地址或域名。
解决办法
Node.js 为您提供了一种访问远程服务器的域名、IP 地址和域名的方法。这可以通过创建一个接受域名的简单 Node.js 命令行应用(如清单 7-1 所示)来实现。结果是与该域名相关联的所有 IP 地址的列表。
清单 7-1 。DNS 查找命令行工具
/**
* DNS
*/
var dns = require('dns'),
args = process.argv.splice(2),
domain = args[0];
dns.resolve(domain, function (err, addresses) {
if (err) throw err;
addresses.forEach(function (address) {
getDomainsReverse('resolve', address);
});
});
dns.lookup(domain, function(err, address, family) {
if (err) console.log(err);
getDomainsReverse('lookup', address);
});
function getDomainsReverse(type, ipaddress) {
dns.reverse(ipaddress, function(err, domains) {
if (err) {
console.log(err);
} else if (domains.length > 1) {
console.log(type + ' domain names for ' + ipaddress + ' ' + domain);
} else {
console.log(type + ' domain name for ' + ipaddress + ' ' + domain);
}
});
}
利用这个命令行工具将产生类似于清单 7-2 中的输出,显示查询的结果以及使用了哪种 Node.js DNS 工具来收集结果。
清单 7-2 。使用 Node.js DNS 查找命令行工具
$ node 7-1-1.js g.co
resolve domain name for 173.194.46.37 g.co
resolve domain name for 173.194.46.38 g.co
resolve domain name for 173.194.46.39 g.co
resolve domain name for 173.194.46.40 g.co
resolve domain name for 173.194.46.46 g.co
resolve domain name for 173.194.46.32 g.co
resolve domain name for 173.194.46.33 g.co
resolve domain name for 173.194.46.34 g.co
resolve domain name for 173.194.46.36 g.co
lookup domain name for 74.125.225.78 g.co
resolve domain name for 173.194.46.41 g.co
resolve domain name for 173.194.46.35 g.co
它是如何工作的
Node.js 实现了一个 DNS 版本,它是“C-ares”的包装版本。这是一个为异步 DNS 请求而构建的 C 库。该模块是 Node.js ' dns'模块,是上述解决方案所必需的。
上面的解决方案允许您运行 Node.js 命令,并传递您希望解析或查找的域名。这将从 Node.js 进程的参数中解析出来,传递给查询该域名的两个方法。DNS 对象上的这两个方法是dns.resolve()和dns.lookup()。其中每一个都执行类似的任务,因为它们都将从 DNS 服务器获取与传递给该函数的域名相关联的 IP 地址。然而,实现却大不相同。
dns.lookup()函数将接受一个域名和一个回调。有一个可选的第二个参数,可以向其中传递族参数。family 参数将是 4 或 6,表示您希望查询哪个 IP 系列的地址。对dns.lookup()函数的回调将提供错误、地址和族参数,如果它们可用的话。从下面查找函数的源代码中可以看出,一旦配置了初始参数,Node.js 就会调用 C-ares DNS 模块 cares.getaddrinfo的本机包装器,并返回包装后的结果。
清单 7-3 。来自 Node.js dns.js 源的 Dns.lookup 方法
exports.lookup = function(domain, family, callback) {
// parse arguments
if (arguments.length === 2) {
callback = family;
family = 0;
} else if (!family) {
family = 0;
} else {
family = +family;
if (family !== 4 && family !== 6) {
throw new Error('invalid argument: `family` must be 4 or 6');
}
}
callback = makeAsync(callback);
if (!domain) {
callback(null, null, family === 6 ? 6 : 4);
return {};
}
if (process.platform == 'win32' && domain == 'localhost') {
callback(null, '127.0.0.1', 4);
return {};
}
var matchedFamily = net.isIP(domain);
if (matchedFamily) {
callback(null, domain, matchedFamily);
return {};
}
function onanswer(addresses) {
if (addresses) {
if (family) {
callback(null, addresses[0], family);
} else {
callback(null, addresses[0], addresses[0].indexOf(':') >= 0 ? 6 : 4);
}
} else {
callback(errnoException(process._errno, 'getaddrinfo'));
}
}
var wrap = cares.getaddrinfo(domain, family);
if (!wrap) {
throw errnoException(process._errno, 'getaddrinfo');
}
wrap.oncomplete = onanswer;
callback.immediately = true;
return wrap;
};
本解决方案中使用的第二个函数是 dns.resolve()函数,用于解析域名。这个函数也接受一个域和一个回调,并带有可选的第二个参数。回调函数提供了一个错误和一个已经解决的地址数组。如果该方法导致错误,它将是表 7-1 中显示的代码之一。
表 7-1 。DNS 错误代码
| dns 格式的错误。错误) | 描述 |
|---|---|
| ADDRGETNETWORKPARAMS | 找不到 GetNetworkParams 函数 |
| 坏家庭 | 不支持的地址族 |
| BADFLAGS | 指定了非法标志 |
| 错误提示 | 指定了非法的提示标志 |
| 坏名 | 域名格式不正确 |
| 坏查询 | 格式错误的 DNS 查询 |
| 巴德雷普 | 格式错误的 DNS 回复 |
| 巴德斯特勒 | 格式错误的字符串 |
| 取消 | 取消 DNS 查询 |
| 经济复兴 | 无法联系 DNS 服务器 |
| 毁灭 | 频道被破坏了 |
| 文件结束 | 文件结尾 |
| 文件 | 读取文件时出错 |
| 前任的 | DNS 服务器声明查询格式不正确 |
| LOADIPHLPAPI | 加载 iphlpapi.dll 时出错 |
| 无数据 | DNS 服务器返回了一个没有数据的答案 |
| 叫什么名字 | 被遗忘 |
| 无名 | 给定的主机名不是数字 |
| 找不到 | 找不到域名 |
| 白色祛皱美眼笔 | DNS 服务器没有执行请求的操作 |
| 未初始化 | c-ares 未初始化 |
| 拒绝 | DNS 服务器拒绝查询 |
| SERVFAIL(服务失败) | DNS 服务器返回一般故障 |
| 超时 | 联系 DNS 服务器时超时 |
与dns.lookup()方法不同,该方法可选的第二个参数是一个记录类型,表示您试图解析的 DNS 记录的类型。记录类型有以下几种:“A”、“AAAA”、“MX”、“TXT”、“SRV”、“PTR”、“NS”和“CNAME”。dns.resolve()方法可以将这七种记录类型中的任何一种作为参数;但是,如果没有提供,则默认为“A”类型或 IPv4。
与这种方法相关,但没有显示在解决方案中的是七种记录类型的包装器。这些都是无需传递可选参数就能获得您想要的精确分辨率的简便方法。这些包括dns.resolve4, dns.resolve6, dns.resolveMx, dns.resolveTxt, dns.resolveSrv, dns.resolvePtr, dns.resolveNs和dns.resolveCname。
清单 7-4 。Dns.resolve 和来自 dns.js 的亲戚
var resolveMap = {};
exports.resolve4 = resolveMap.A = resolver('queryA');
exports.resolve6 = resolveMap.AAAA = resolver('queryAaaa');
exports.resolveCname = resolveMap.CNAME = resolver('queryCname');
exports.resolveMx = resolveMap.MX = resolver('queryMx');
exports.resolveNs = resolveMap.NS = resolver('queryNs');
exports.resolveTxt = resolveMap.TXT = resolver('queryTxt');
exports.resolveSrv = resolveMap.SRV = resolver('querySrv');
exports.resolveNaptr = resolveMap.NAPTR = resolver('queryNaptr');
exports.reverse = resolveMap.PTR = resolver('getHostByAddr');
exports.resolve = function(domain, type_, callback_) {
var resolver, callback;
if (typeof type_ == 'string') {
resolver = resolveMap[type_];
callback = callback_;
} else {
resolver = exports.resolve4;
callback = type_;
}
if (typeof resolver === 'function') {
return resolver(domain, callback);
} else {
throw new Error('Unknown type "' + type_ + '"');
}
};
对于解决方案的最后一部分,您构建了一个getDomainsReverse函数。这是一个用于dns.reverse函数的包装器,它被设计用来接受一个 IP 地址并找到所有与提供的 IP 地址匹配的域。在您的解决方案中,您将它抽象出来,这样dns.lookup()和dns.resolve()方法都可以重用该函数。然后将检索结果记录到控制台。
您可以看到,DNS 模块为您提供了许多机会来收集离您的位置很远的域和服务器的信息。在解析主机问题的解决方案中,您能够利用dns.lookup、dns.resolve和dns.reverse方法来完成这项任务。
7-2.用缓冲区处理流
问题
您需要使用缓冲区对象来更好地处理流。
解决办法
缓冲区是类似于数组而不仅仅是字符串的二进制数据形式的数据。Buffer 是一个 Node.js 全局对象,用于处理缓冲区。这意味着你可能永远不需要require('buffer'),尽管这是可能的,因为对象的全局性质。为了检查 Buffer 对象的能力,您将创建一个 Node.js 文件,如清单 7-5 所示,它将执行 Buffer 可用的大部分功能。这将使您更好地理解如何处理 Node.js 生态系统中的许多可用缓冲区。
清单 7-5 。Node.js 中的缓冲区
/**
* Buffer
*/
var buffer = new Buffer(16);
console.log('size init', buffer.toString());
buffer = new Buffer([42, 41, 41, 41, 41, 41, 41, 42, 42,4, 41, 41, 0, 0, 7, 77], 'utf-8');
console.log('array init', buffer.toString());
buffer = new Buffer('hello buffer', 'ascii');
console.log(buffer.toString());
buffer = new Buffer('hello buffer', 'ucs2');
console.log(buffer.toString());
buffer = new Buffer('hello buffer', 'base64');
console.log(buffer.toString());
buffer = new Buffer('hello buffer', 'binary');
console.log(buffer.toString());
console.log(JSON.stringify(buffer));
console.log(buffer[1]);
console.log(Buffer.isBuffer('not a buffer'));
console.log(Buffer.isBuffer(buffer));
// allocate size
var buffer = new Buffer(16);
// write to a buffer
console.log(buffer.write('hello again', 'utf-8'));
// append more starting with an offset
console.log(buffer.write(' wut', 11, 'utf8'));
console.log(buffer.toString());
// slice [start, end]
buf = buffer.slice(11, 15);
console.log(buf.toString());
console.log(buffer.length);
console.log(buffer.readUInt8(0));
console.log(buffer.readUInt16LE(0));
console.log(buffer.readUInt16BE(0));
console.log(buffer.readUInt32LE(0));
console.log(buffer.readUInt32BE(0));
console.log(buffer.readInt16LE(0));
console.log(buffer.readInt16BE(0));
console.log(buffer.readInt32LE(0));
console.log(buffer.readInt32BE(0));
console.log(buffer.readFloatLE(0));
console.log(buffer.readFloatBE(0));
console.log(buffer.readDoubleLE(0));
console.log(buffer.readDoubleBE(0));
buffer.fill('4');
console.log(buffer.toString());
var b1 = new Buffer(4);
var b2 = new Buffer(4);
b1.fill('1');
b2.fill('2');
console.log(b1.toString());
console.log(b2.toString());
b2.copy(b1, 2, 2, 4);
console.log(b1.toString());
清单 7-5 中的解决方案强调了当你在 Node.js 中使用缓冲区时,你可以使用的许多功能。接下来你将看到一个使用“net”模块的例子,以及当你在客户机和服务器之间通信时,你如何以缓冲区的形式发送数据。
清单 7-6 。使用缓冲区
var net = require('net');
var PORT = 8181;
var server = net.Server(connectionListener);
function connectionListener(conn) {
conn.on('readable', function() {
//buffer
var buf = conn.read();
if (Buffer.isBuffer(buf)) {
console.log('readable buffer: ' , buf);
conn.write('from server');
}
});
conn.on('end', function() {
});
}
server.listen(PORT);
//Connect a socket
var socket = net.createConnection(PORT);
socket.on('data', function(data) {
console.log('data recieved: ', data.toString());
});
socket.on('connect', function() {
socket.end('My Precious');
});
for (var i = 0; i < 2000; i++) {
socket.write('buffer');
}
socket.on('end', function() {
});
socket.on('close', function() {
server.close();
});
它是如何工作的
缓冲区是在 Node.js 中处理八位字节流的最佳方式。它们表示从 Node.js 应用内部传输的原始数据,并且可以在 Node.js 中的多个位置找到它们。缓冲区非常通用。正如你在清单 7-5 中看到的,他们有很多方法可以为你提供给定工作的最佳解决方案。
当您希望创建缓冲区时,有几种方法可以使用。在这个解决方案中,您首先通过为缓冲区分配大小var buffer = new Buffer(16);来创建一个缓冲区。然后,您可以通过将一个数组直接传递给缓冲区的构造函数var buffer = new Buffer([42, 42]...);来生成一个新的缓冲区。创建新缓冲区的第三种方法是将一个字符串直接传递给构造函数var buffer = new Buffer('hello world');。构造函数也接受一种编码类型,设置为字符串。如果没有传递编码,那么编码将默认为 utf8。
一旦创建了缓冲区,现在就可以操作缓冲区了。缓冲区对象本身有一些直接可用的方法。这些方法包括Buffer.isEncoding(encoding)、Buffer.isBuffer(object)和Buffer.byteLength(buffer),它们分别评估编码是否按预期设置,查看给定对象是否是缓冲区,并返回缓冲区的字节长度。
除了这些类本身的缓冲方法,还有一些方法,在表 7-2 中列出,你可以在使用缓冲时使用。
表 7-2 。缓冲方法和事件
| 方法(缓冲。 |
描述 |
|---|---|
| 写入(字符串,[偏移量],[长度],[编码]) | 按照给定的偏移量和编码将字符串写入缓冲区。 |
| toString([编码]、[开始]、[结束]) | 从开始到结束,将缓冲区转换为给定范围内具有给定编码的字符串。 |
| toJSON() | 返回缓冲区的 JSON 化版本。 |
| 长度 | 以字节为单位返回缓冲区的大小。 |
| copy([目标缓冲区]、[目标启动]、[sourceStart]、[sourceEnd]) | 将缓冲区数据从源复制到目标:var b1 = new Buffer('1111'); var b2 = new Buffer('2222'); b2.copy(b1, 2, 2, 4); //b2 == 1121 |
| 切片([开始],[结束]) | 在开始和结束参数之间分割缓冲区。产生新的缓冲区。 |
| readUInt8(偏移量,[noassert]) | 从偏移量开始,以无符号 8 位整数形式读取缓冲区。 |
| readUInt16LE(偏移,无修正) | 从偏移量开始,以无符号 16 位整数 little endian 形式读取缓冲区。 |
| readUInt16BE(偏移量[noassert]) | 从偏移量开始,以无符号 16 位整数 big endian 形式读取缓冲区。 |
| readUInt32LE(偏移,无修正) | 从偏移量开始,以无符号 32 位整数小端格式读取缓冲区。 |
| readUInt32BE(偏移量,[noassert]) | 读取缓冲区,从偏移量开始,为无符号 32 位整数,大端格式。 |
| readInt8(偏移量,[noassert]) | 从偏移量开始,以 8 位整数形式读取缓冲区。 |
| readInt16LE(偏移,[no adjustment]) | 从偏移量开始,以 16 位小端方式读取缓冲区。 |
| readInt16BE(偏移量,[noassert]) | 以 16 位 big endian 形式从偏移量开始读取缓冲区。 |
| readInt32LE(位移,[no water]) | 从偏移量开始,以 32 位小端方式读取缓冲区。 |
| readInt32BE(偏移量,[noassert]) | 从偏移量开始,以 32 位大端顺序读取缓冲区。 |
| readFloatLE(offset, [noassert]) | 以 float,little endian 形式从偏移量开始读取缓冲区。 |
| readFloatBE(偏移量,[noassert]) | 读取缓冲区,从偏移量开始,作为一个浮点,大端。 |
| read doublel(offset,[noassert]) | 从偏移量开始,以双精度小端方式读取缓冲区。 |
| readDoubleBE(偏移,[no adjustment]) | 从偏移量开始,以双精度大端方式读取缓冲区。 |
| writeUInt8(值,偏移量,[noassert] | 从偏移量开始,将一个无符号 8 位整数写入缓冲区。 |
| writeUInt16LE(值,偏移量,[noassert]) | 从偏移量 little endian 开始,将一个无符号 16 位整数写入缓冲区。 |
| writeUInt16BE(值,偏移量,[noassert]) | 从偏移量 big endian 开始,将一个无符号 16 位整数写入缓冲区。 |
| writeUInt32LE(值,偏移量,[noassert]) | 从偏移量 little endian 开始,将一个无符号 32 位整数写入缓冲区。 |
| writeUInt32BE(值,偏移量,[noassert]) | 从偏移量 big endian 开始,将一个无符号 32 位整数写入缓冲区。 |
| writeInt8(值,偏移量,[noassert]) | 从偏移量开始,将一个 8 位整数写入缓冲区。 |
| writeInt16LE(值,偏移,[no improved]) | 从偏移量 little endian 开始,将一个 16 位整数写入缓冲区。 |
| writeInt16BE(值,偏移量,[noassert]) | 从偏移量 big endian 开始,将一个 16 位整数写入缓冲区。 |
| writeInt32LE(值、偏移、无改善) | 从偏移量 little endian 开始,将一个 32 位整数写入缓冲区。 |
| writeInt32BE(值,偏移量,[noassert]) | 从偏移量 big endian 开始,将一个 32 位整数写入缓冲区。 |
| writeFloatLE(值,偏移量,[noassert]) | 从偏移量 little endian 开始,将一个浮点值写入缓冲区。 |
| siwriteFloatBE(值,偏移量,[no asset]) | 从偏移量 big endian 开始,将一个浮点值写入缓冲区。 |
| writeDoubleLE(value,offset,[no asset]) | 从偏移量 little endian 开始,将一个 double 值写入缓冲区。 |
| writeDoubleBE(值,偏移量,[no asset]) | 从偏移量 big endian 开始,将一个 double 值写入缓冲区。 |
| fill(值,[偏移量],[结束]) | 用从偏移量到结束范围指定的值填充缓冲区。 |
缓冲区中有许多方法可用于非常特殊的目的。例如,如果您需要以 little endian 格式读写无符号 32 位整数,缓冲区可以做到这一点。虽然这些方法非常灵活,但在大多数情况下,你会使用如清单 7-4 所示的缓冲区。这是一个“网络”服务器和客户端互相发送数据的例子。数据变成一个可读的流,这是一个缓冲区。您将能够对任何缓冲区执行一节中描述的方法,从而允许您操作应用中使用的数据和流。
7-3.使用 Node.js 进行聚类
问题
您希望构建一个进程集群来更有效地运行您的应用。
解决办法
Node.js 为集群提供了一个解决方案。在撰写本文时,该特性仍处于试验阶段,但它能够将单线程 Node.js 应用转变为在您的机器上利用多个内核的应用。通过这种方式,您可以将 Node.js 任务委托给不同的线程,从而实现更大的可伸缩性。在这个解决方案中,您将生成一个使用集群模块的 Node.js 应用。第一个例子是一个独立的解决方案,它将分割一个简单的 HTTP 服务器,并将各种集群方法的结果记录到您的控制台。
清单 7-7 。使聚集
/**
* Clustering
*/
var cluster = require('cluster'),
http = require('http'),
cpuCount = require('os').cpus().length;
if (cluster.isMaster) {
for (var i = 0; i < cpuCount; i++) {
cluster.fork();
}
cluster.on('fork', function(worker) {
console.log(worker + ' worker is forked');
});
cluster.on('listening', function(worker, address) {
console.log(worker + ' is listening on ' + address);
});
cluster.on('online', function(worker) {
console.log(worker + ' is online');
});
cluster.on('disconnect', function(worker) {
console.log(worker + ' disconnected');
});
cluster.on('exit', function(worker, code, signal) {
console.log('worker ' + worker.process.pid + ' died');
});
} else {
// Workers can share any TCP connection
// In this case it is an HTTP server
http.createServer(function(req, res) {
res.writeHead(200);
res.end("hello world\n");
}).listen(8000);
}
现在,您将配置集群来执行第二个 Node.js 文件,对机器上的每个内核执行一次。
清单 7-8 。集群化 Node.js 流程
/**
* Clustering
*/
var cluster = require('cluster'),
cpuCount = require('os').cpus().length;
cluster.setupMaster({
exec: '7-3-3.js'
});
if (cluster.isMaster) {
for (var i = 0; i < cpuCount; i++) {
cluster.fork();
}
cluster.on('fork', function(worker) {
console.log(worker + ' worker is forked');
});
cluster.on('listening', function(worker, address) {
console.log(worker + ' is listening on ' + address);
});
cluster.on('online', function(worker) {
console.log(worker + ' is online');
});
cluster.on('disconnect', function(worker) {
console.log(worker + ' disconnected');
});
cluster.on('exit', function(worker, code, signal) {
console.log('worker ' + worker.process.pid + ' died');
});
}
清单 7-9 ,工作进程,如下所示。
清单 7-9 。工作进程
var http = require('http');
http.createServer(function(req, res) {
console.log(req.url);
res.writeHead(200);
res.end("hello world\n");
}).listen(8000);
它是如何工作的
Node.js 中的集群本质上是利用一个 Node.js 模块和利用child_process.fork()函数分割工作进程的解决方案,同时维护主进程和工作进程之间的引用和通信。工作进程可以是 TCP 或 HTTP 服务器,请求由主进程处理。然后,这个主进程利用循环负载平衡在服务器中分配负载。它通过监听连接,然后调用 distribute 方法并将处理交给工作进程来完成。
清单 7-10 。主监听,然后分配负载
this.server.once('listening', function() {
self.handle = self.server._handle;
self.handle.onconnection = self.distribute.bind(self);
self.server._handle = null;
self.server = null;
});
RoundRobinHandle.prototype.distribute = function(handle) {
this.handles.push(handle);
var worker = this.free.shift();
if (worker) this.handoff(worker);
};
RoundRobinHandle.prototype.handoff = function(worker) {
if (worker.id in this.all === false) {
return; // Worker is closing (or has closed) the server.
}
var handle = this.handles.shift();
if (typeof handle === 'undefined') {
this.free.push(worker); // Add to ready queue again.
return;
}
var message = { act: 'newconn', key: this.key };
var self = this;
sendHelper(worker.process, message, handle, function(reply) {
if (reply.accepted)
handle.close();
else
self.distribute(handle); // Worker is shutting down. Send to another.
self.handoff(worker);
});
};
在您的解决方案中,您主要使用主流程。在这两种情况下,您都有一个简单的 HTTP 服务器,当向服务器地址发出请求时,它会以“hello world”作为响应。导入集群模块后,使用cluster.isMaster 检查该进程是否是集群的主进程。现在,您可以通过使用“os”模块检查您的计算机拥有的内核数量来查看您应该在您的计算机上创建多少个集群。对于每个 CPU,您派生一个名为cluster.fork()的新工作进程。因为底层框架仍然生成一个新的子进程,它仍然是 v8 的一个新实例,所以您可以假设每个 worker 的启动时间在大多数情况下将大于 0 并且小于 100 ms。它还会在启动时为每个进程生成大约 10 MB 的内存消耗。
您现在知道这是主流程,因此您能够绑定到将被传递到主流程的事件。感兴趣的事件是“fork”、“listening”、“exit”、“online”和“setup”。“fork”事件在工作进程被成功分叉时发出,并为被分叉的进程提供工作对象。一旦创建并运行分叉流程,就会发生“在线”事件。一旦工作者开始监听,就发送“监听”事件。当工作进程终止时,将发出“exit”事件。如果发生这种情况,您可能需要调用。fork()方法来替换关闭的工作线程。
集群是一个强大的模块,它允许您在机器上的多个进程之间分配服务器的负载。随着 Node.js 应用的增长,该功能在创建可伸缩应用时会变得很重要。
7-4.使用查询字符串
问题
您用 Node.js 构建了一个 web 服务器,您希望巧妙地处理通过 HTTP 请求传递给应用的查询字符串的各种差异。
解决办法
Node.js 有一个查询字符串模块,允许您为 Node.js 应用正确解析和编码查询字符串参数。
清单 7-11 。使用查询字符串模块
/**
* querystrings
*/
var qs = require('querystring');
var incomingQS = [ 'foo=bar&foo=baz',
'trademark=%E2%84%A2',
'%7BLOTR%7D=frodo%20baggins'];
incomingQS.forEach(function(q) {
console.log(qs.parse(q));
});
var outgoingQS = { good: 'night', v: '0.10.12', trademark: '™'};
console.log(qs.stringify(outgoingQS));
var newQS = qs.stringify(outgoingQS, '|', '∼');
console.log(newQS);
console.log(qs.parse(newQS));
您可以将任意查询字符串作为输入,然后将其解析为一个对象。您还能够获取任意对象,并将其解析为 URL 安全查询字符串。
它是如何工作的
虽然查询字符串模块不是一个巨大的模块,但它只导出四个方法。它为处理查询字符串提供了一个非常有用的解决方案。在这个解决方案中,您首先需要'querystring'模块。这个模块提供了几种方法来帮助处理应用中的查询字符串。
首先,querystring.parse函数接受一个字符串,并可选地覆盖分隔符,默认为&和等于。还有一个 options 对象,允许您覆盖要处理的 1000 个最大键(maxKeys)的缺省值。对于查询字符串中的每个键,‘querystring’模块将尝试通过首先使用 JavaScript 自带的decodeURIComponent()函数来解析值。如果这产生了一个错误,那么这个模块将会转移到它自己的被称为querystring.unescape的实现上。
其次,querystring.stringify函数将接受您希望编码到querystring中的对象。或者,该方法还将允许您覆盖默认的&号和等号分隔符。querystring.stringify方法将解析对象,将其转换成字符串,然后调用模块的QueryString.escape方法。这个方法只是 JavaScript 的encodeURIComponent()的一个包装器。
7-5.用“进程”处理事件
问题
您希望能够在 Node.js 应用中全局处理事件。
解决办法
Node.js 流程模块是一个全局对象,可以在 Node.js 应用中的任何地方访问。在这个解决方案中,您可以想象一种情况,其中您有一个模块和一个辅助模块。主模块绑定到初始化事件,并开始调用辅助模块的某些方法。主模块看起来像清单 7-12 中所示的模块,辅助模块看起来像清单 7-13 中所示的模块。
清单 7-12 。主模块,处理流程
/**
* using the process
*/
function log(msg) {
if (typeof msg === 'object') {
msg = JSON.stringify(msg);
}
process.stdout.write(msg + '\n');
}
//add listeners
process.on('power::init', function() {
log('power initialized');
});
process.on('power::begin', function() {
log('power calc beginning');
});
process.on('exit', function() {
log(process.uptime());
log('process exiting...');
});
process.on('uncaughtException', function(err) {
log('error in process ' + err.message + '\n');
});
log(process.cwd());
process.chdir('..');
log(process.cwd());
log(process.execPath);
log(process.env.HOME);
log(process.version);
log(process.versions);
log(process.config);
log(process.pid);
log(process.platform);
log(process.memoryUsage());
log(process.arch);
var pow = new require('./power');
var out = pow.power(42, 42);
log(out);
// throws
setTimeout(pow.error, 1e3);
清单 7-13 。辅助模块
/**
* power module
*/
process.emit('power::init');
exports.power = function(base, exponent) {
var result = 1;
process.emit('power::begin');
for (var count = 0; count < exponent; count++)
result *= base;
return result;
};
它是如何工作的
Node.js 流程对象可以为您的应用获取有价值的信息和实用程序。流程对象是全局的,是一个EventEmitter。这就是为什么在上面的例子中,您能够从 power.js 文件中发出'power::init'。你也通过调用process.on('power::init', callback)来绑定到这个。然后绑定到其他事件。首先,绑定到另一个自定义事件,该事件是在开始执行 power.js 模块的 power 函数时发出的。
另外两个事件是 Node.js 流程的内置事件。首先,绑定到'exit'事件。这将在流程准备退出时触发,给你最后一次机会记录错误或通知用户流程即将结束。您侦听的另一个内置事件是'uncaughtException'事件。该事件由任何异常触发,否则这些异常会出现在控制台上并使您的应用崩溃。在该解决方案中,您可以通过尝试调用 power.js 模块上不存在的方法来触发该事件。
流程模块不仅仅处理事件。事实上,它可以提供大量与您当前 Node.js 流程相关的信息,其中许多信息您可以在创建解决方案时加以利用。表 7-3 详述了流程对象上的这些其他方法和属性。
表 7-3 。过程对象方法和属性
| 方法 | 描述 |
|---|---|
| 中止() | 此方法将中止该过程。 |
| 拱门 | 您系统的架构。 |
| 阿尔戈夫 | 这是实例化 Node.js 流程的参数。在解析传递给应用的参数时,您已经看到了这一点。 |
| 总监(主任) | 更改您的进程当前工作的目录。 |
| 配置 | 列出了 Node.js 应用的配置。 |
| cwd() | 打印进程的当前工作目录。 |
| 包封/包围(动词 envelop 的简写) | 将系统中的环境变量作为对象列出。 |
| execPath | 这是系统上 Node.js 可执行文件的路径。 |
| 退出([代码]) | 用指定的代码发出退出事件。 |
| 格吉德() | 获取进程的组 ID。在 Windows 上不可用。 |
| getgroups() | 获取进程补充组的组 ID 数组。在 Windows 上不可用。 |
| 它是() | 获取进程的用户 ID。在 Windows 上不可用。 |
| hrtime([hrtime]) | 过去任意时期以来的高分辨率时间数组(秒、纳秒)。这可以与先前的 hrtime 读数一起使用,以获得差值。 |
| 初始组 | 读取/etc/groups。在 Windows 上不可用。 |
| kill(进程 id,[信号]) | 向进程 ID 发送信号。 |
| maxTickDepth | 您可以使用它来设置在允许事件循环处理之前要运行的节拍数。这阻止了使用。锁定 I/O 后的下一个时钟周期 |
| memoryUsage() | 进程中使用的内存字节数。 |
| nextTick(回调) | 下一次在事件循环中,回调将被执行。可以这样做: |
function definitelyAsync(arg, cb) {
if (arg) {
process.nextTick(cb);
return;
}
fs.stat('file', cb);
} |
| pid | 进程标识符。 |
| 平台 | 列出运行进程的平台。 |
| setgid() | 设置进程的组 ID。在 Windows 上不可用。 |
| 集合组 | 为进程设置组数组。在 Windows 上不可用。 |
| setuid() | 设置进程的用户 ID。在 Windows 上不可用。 |
| 标准错误 | 标准误差;这是一个可写流。 |
| 标准输入设备 | 表示标准输入的可读流。
function log(msg) {
if (typeof msg === 'object') {
msg = JSON.stringify(msg);
}
process.stdout.write(msg + '\n');
} |
| 标准输出 | 这是您的流程的标准输出,并且是一个可写流。您可以通过创建您的 log()函数来重新创建控制台日志记录:
function log(msg) {
if (typeof msg === 'object') {
msg = JSON.stringify(msg);
}
process.stdout.write(msg + '\n');
} |
| 标题 | 流程的标题。 |
| umask([mask]) | 进程的文件模式创建掩码的 Setter 和 getter。 |
| 正常运行时间() | Node 已经运行的秒数(不是毫秒)。 |
| 版本 | 打印进程正在使用的 Node.js 版本。 |
| 版本 | 列出包含 Node.js 版本及其依赖项的对象。 |
7-6.使用计时器
问题
您希望能够在 Node.js 应用中利用计时器来控制流。
解决办法
控制任何应用中特定进程的时间是非常关键的,包括那些用 Node.js 构建的应用。如果您在 web 应用中使用过计时器,那么在 Node.js 中使用计时器应该很熟悉,因为有些方法在浏览器中也可以使用。
在这个解决方案中,您将创建一个应用,它将利用计时器来轮询一个虚构的远程资源。这个解决方案将代表一个场景,其中您需要从远程队列获取数据。有几种解决方案可以按时间间隔进行轮询,或者简单地利用计时器在事件循环中有效地调用方法。
清单 7-14 。使用计时器
/**
* Using Timers
*/
var count = 0;
var getMockData = function(callback) {
var obj = {
status: 'lookin good',
data: [
"item0",
"item1"
],
numberOfCalls: count++
};
return callback(null, obj);
};
var onDataSuccess = function(err, data) {
if (err) console.log(err);
if (data.numberOfCalls > 15) clearInterval(intrvl);
console.log(data);
};
// getMockData(onDataSuccess);
setImmediate(getMockData, onDataSuccess);
var tmr = setTimeout(getMockData, 2e3, onDataSuccess);
tmr.unref();
var intrvl = setInterval(getMockData, 50, onDataSuccess);
它是如何工作的
Node.js 中有几个可以使用的计时器。首先,有一组可以在 web 浏览器中找到的计时器。这些是setTimeout和setInterval及其相应的clearTimeout和clearInterval功能。
setTimeout是一种在给定时间延迟后安排一次性事件的方式。一个setTimeout调用的结构至少有两个参数。第一个参数是您希望在计时器触发时执行的回调。第二个是等待回调执行的毫秒数。或者,您可以向函数添加额外的参数,这些参数将在计时器执行时应用于回调。通过调用clearTimeout并传递一个对初始超时定时器的引用,可以取消setTimeout。
注意由于 JavaScript Node.js 事件循环,您无法直接依赖回调执行的时间。Node.js 将尝试在接近规定的时间执行回调,但它可能不会在精确的时间间隔执行。
setInterval是setTimeout的亲戚。它通过提供一种将功能的执行延迟一段设定时间的机制,以类似的方式发挥作用。然而,使用setInterval,该函数将在相同的时间间隔内重复执行,直到clearInterval被调用。在上面的解决方案中,这是在长时间运行的流程中用于轮询的情况。理论上,您可以在 30 秒、3 分钟或每小时的长时间轮询中运行一个时间间隔,并让该过程继续运行。然而在解决方案中,您以很短的时间间隔(< 1 秒)运行该间隔,并在它执行了 15 次后清除它。
setInterval和setTimeout方法都附加了两个额外的方法。这些是unref()和ref()。如果计时器是 event.loop 上剩下的唯一计时器,那么unref()方法允许 Node.js 进程终止。而ref()方法则相反,它会暂停进程,直到计时器执行完毕。要看到这一点,您可以注意到,在解决方案中的两秒延迟setTimeout方法之后,您立即调用该计时器的unref()方法。这意味着这个定时器永远不会执行。因为从setInterval开始的时间间隔在两秒钟过去之前就已经结束并被清除,所以事件循环中不再有其他计时器,该过程优雅地退出。
setImmediate是 Node.js 的另一个计时机制。这对于调用近即时方法很有用,类似于process.nextTick()函数的操作方式。setImmediate会将函数排在当前事件循环中任何 I/O 绑定回调函数的后面进行处理。这与nextTick的操作略有不同,因为它会将其执行推到事件循环的前面。这意味着setImmediate是一种更好的方式来执行一个不会锁定 I/O 进程的方法。如果您正在运行一个需要一定程度的 CPU 使用率的递归函数,这将特别有用,因为这不会阻止这些操作在回调之间发生。
7-7.使用 V8 调试器
问题
您需要单步调试 Node.js 应用。
解决办法
Node.js 运行在 Google 的 V8 上,V8 有内置的调试机制。因此,Node.js 允许您利用该工具调试源代码。您将创建一个解决方案,帮助您了解调试器是如何工作的,以及它能为您的代码带来哪些启示。清单 7-15 显示了一个简单的 HTTP 服务器,它需要第二个模块(如清单 7-16 所示)。
清单 7-15 。HTTP 服务器
/**
* Debugging
*/
var http = require('http'),
mod = require('./7-7-2');
server = http.createServer(function(req, res) {
if (req.url === '/') {
debugger;
mod.doSomething(function(err, data) {
if (err) res.end('an error occured');
res.end(JSON.stringify(data));
});
} else {
res.end('404');
}
});
server.listen(8080);
清单 7-16 。所需模块
/**
* Debugging
*/
exports.doSomething = function(callback) {
debugger;
callback(null, { status: 'okay', data: ['a', 'b', 'c']});
};
有些东西可能与典型的 Node.js 应用略有不同;特别是你可以看到一些“调试器”陈述。这些指令告诉 V8 调试机制暂停程序的执行。这个过程从用“debug”标志启动 Node.js 应用开始。
它是如何工作的
Google 设计了支持 Node.js 的 V8 JavaScript 引擎,允许调试引擎中执行的 JavaScript。Node.js 以两种方式支持 V8 调试。一种方法是以创建调试器的方式实现 V8 调试器协议,监听 TCP 端口。如果您正在创建或使用协调使用该协议的第三方调试工具,这将非常有用。为此,使用命令$ node --debug 7-7-1.js启动 Node.js 应用 7-7-1.js。这将启动调试器,并在localhost:5858上监听调试器的挂钩。这允许创建与调试器通信的调试客户端。幸运的是,Node.js 自带了自己的 V8 调试器客户端。您可以通过在控制台中键入$ node debug 来访问在调试模式下使用- -debug 标志启动的应用。
通过使用“debug”参数启动 Node.js 应用,可以访问 Node.js 内置调试器。
清单 7-17 。启动 Node.js 调试 CLI
$ node debug 7-7-1.js
这将启动您的应用,但附加了调试器。控制台中的输出将显示调试器已经开始监听,并且它将显示 JavaScript 代码的第一行,默认情况下调试器将中断该行。
清单 7-18 。调试器的初始状态
< debugger listening on port 5858
connecting... ok
break in 7-7-1.js:5
3 */
4
5 var http = require('http'),
6 mod = require('./7-7-2');
7
debug>
您现在有一个“debug>”提示。这是调试器的命令行界面。您可以按照下面的步骤完成调试的基础。首先,您可以向应用中的对象或属性添加观察器。要做到这一点,你可以输入“watch ”,然后输入任何你想观看的表情。
debug> watch('expression')
因此,在您的解决方案中,您可以通过使用 watch 命令并传递“req.url”作为表达式来监视请求 URL。
debug> watch('req.url')
您还可以列出调试器会话中当前活动的所有观察器。结果会将活动的观察器及其值打印到控制台。当前值被赋予 JavaScript 代码暂停的直接上下文。
debug> watchers
0: req.url = "<error>"
回想一下,在您的应用代码中,您创建了两个名为'debugger;'的地方。它将在应用中的这些点暂停执行。但是,有时您可能不希望添加调试器语句,而只想在代码中设置断点。为此,调试器有几个可用的断点方法。要在当前行设置断点,只需在调试控制台中键入setBreakpoint()。或者,您可以使用简写的sb()来设置断点。setBreakpoint方法也接受一个行号,因此您可以预先确定一行来中断。您可以在代码中通过在server.listen(8080)方法上设置一个断点来做到这一点。
debug> sb(21)
1 /**
2 * Debugging
3 */
4
5 var http = require('http'),
6 mod = require('./7-7-2');
7
8 server = http.createServer(function(req, res) {
9 if (req.url === '/') {
10 debugger;
您还可以中断将加载到您的应用中的另一个文件。为此,将文件名和行号传递给setBreakpoint方法。
debug> sb('7-7-2.js', 5)
Warning: script '7-7-2.js' was not loaded yet.
1 /**
2 * Debugging
3 */
4
5 var http = require('http'),
6 mod = require('./7-7-2');
7
8 server = http.createServer(function(req, res) {
9 if (req.url === '/') {
10 debugger;
在这里您可以看到,您已经在文件 7-7-2.js 中的第一行代码上设置了断点。一旦您继续执行程序,一旦命中该行代码,断点将再次暂停程序的执行。
此时,您已经准备好使用调试器在应用中导航了。与大多数调试器一样,调试器公开允许您逐句通过并继续执行代码的方法。最细化的方法是命令中的步骤。这是通过键入'step'或's'来调用的,简称为。从调试实例执行的开始,如果您单步执行,它会将您移动到下一个执行区域。在这个实例中,它已经移动到 module.js 文件中,并开始在源代码中添加您需要的模块。
debug> s
break in module.js:380
Watchers:
0: req.url = "<error>"
378
379 function require(path) {
380 return self.require(path);
381 }
382
从这里你会想继续。继续执行将一直运行,直到遇到下一个断点。如果没有其他断点,应用将正常运行,直到您用pause命令手动暂停它。可以通过“cont'”或“c'”来触发延续。在您的示例中,这将引导您完成模块导入代码,并到达断点,断点是您在“7-7-2.js”文件的第 5 行设置的。
debug> c
break in 7-7-2.js:5
Watchers:
0: req.url = "<error>"
3 */
4
5 exports.doSomething = function(callback) {
6 debugger;
7 callback(null, { status: 'okay', data: ['a', 'b', 'c']});
debug> c
break in 7-7-1.js:21
Watchers:
0: req.url = "<error>"
19 });
20
*21 server.listen(8080);
22
23
再继续一次,会碰到你在‘7-7-1 . js’第 21 行设置的断点;这是您设置的最后一个断点。但是,一旦与 HTTP 服务器建立了连接,就会遇到一些调试器语句。继续完成后,您可以向您的 web 服务器发出请求,'``http://localhost:8080/’。因为有了debugger;语句,这将在连接监听器回调的精确位置暂停执行。
debug> c
break in 7-7-1.js:10
Watchers:
0: req.url = "/"
8 server = http.createServer(function(req, res) {
9 if (req.url === '/') {
10 debugger;
11 mod.doSomething(function(err, data) {
12 if (err) res.end('an error occured');
从这里,你可以进入下一次执行。这是使用调试器中的“next”或“n”命令完成的。执行两次“??”,你就会在调试器中结束;' 7-7-2.js '模块中的语句。
debug> n
break in 7-7-1.js:11
Watchers:
0: req.url = "/"
9 if (req.url === '/') {
10 debugger;
11 mod. doSomething (function(err, data) {
12 if (err) res.end('an error occured');
13
debug> n
break in 7-7-2.js:6
Watchers:
0: req.url = "<error>"
4
5 exports.doSomething = function(callback) {
6 debugger;
7 callback(null, { status: 'okay', data: ['a', 'b', 'c']});
8 };
现在,您可以使用'out'或(' ')'命令跳出此方法。
debug> o
break in 7-7-1.js:19
Watchers:
0: req.url = "/"
17 res.end('404');
18 }
19});
20
21 server.listen(8080);
除了单步、下一步、继续和退出,还有'pause'命令。这将暂停当时正在运行的任何代码的执行。
当您单步执行代码时,有时需要获得更多关于应用中发生的事情的信息。调试器对此也有实用工具。首先,当你在一个断点处暂停时,如果你想看到周围更多的代码,你可以通过使用'list(n)'命令来实现。这将显示当前暂停位置前后由“n”行包围的代码,这对于收集调试器中当前正在发生的事情的更多上下文非常有用。另一个有用的特性是'backtrace'(' ?? ')命令。这将显示程序中当前点的执行路径的轨迹。
清单 7-19 。7-7-2.js 模块的 doSomething 方法中的回溯示例
debug> bt
#0 exports.doSomething 7-7-2.js:6:2
#1 7-7-1.js:11:7
您也可以使用'scripts'命令查看加载的文件。重要的是,如果您需要更深入地研究代码,您可以通过使用'repl'命令来使用调试器的读取-评估-打印循环(REPL)模块。
使用内置命令行界面调试 Node.js 应用被视为高优先级,以便使用 V8 调试器调试您的应用。当您跟踪代码中的异常和错误时,您会发现这些工具非常有用。
7-8.解析 URL
问题
您希望能够解析 Node.js HTTP 服务器应用中的 URL。
解决办法
Node.js 附带了一个 URL 模块,可以用来解析 URL 并收集其中包含的信息。看看这是如何工作的一个解决方案(见清单 7-20 )将告诉你如何解析一个任意的 URL。
清单 7-20 。解析任意 URL
/**
* parse url
*/
var url = require('url');
var theurl = 'http://who:ami@hostname:1234/a/b/c/d/?d=e#f=g';
var urlParsed = url.parse(theurl, true, true);
console.log('protocol', urlParsed.protocol);
console.log('slashes', urlParsed.slashes);
console.log('auth', urlParsed.auth);
console.log('host', urlParsed.host);
console.log('port', urlParsed.port);
console.log('hostname', urlParsed.hostname);
console.log('hash', urlParsed.hash);
console.log('search', urlParsed.search);
console.log('query', urlParsed.query);
console.log('pathname', urlParsed.pathname);
console.log('path', urlParsed.path);
console.log('href', urlParsed.href);
console.log(url.resolve('/a/b/c/', 'd'));
7-20 的结果
$ node 7-8-2.js
protocol http:
slashes true
auth who:ami
host hostname:1234
port 1234
hostname hostname
hash #f=g
search ?d=e
query { d: 'e' }
pathname /a/b/c/d/
path /a/b/c/d/?d=e
href http://who:ami@hostname:1234/a/b/c/d/?d=e#f=g
/a/b/c/d
在实践中使用它,你可以想象一个 HTTP 服务器,不像清单 7-21 ,它需要 URL 被解析,这样你就可以协调正确的文件来服务于HTTP.response()中的客户端。
清单 7-21 。使用 URL 模块
/**
* Parsing URLS
*/
var http = require('http'),
fs = require('fs'),
url = require('url');
var server = http.createServer(function(req, res) {
var urlParsed = url.parse(req.url,true, true);
fs.readFile(urlParsed.path.split('/')[1], function(err, data) {
if (err) {
res.statusCode = 404;
res.end(http.STATUS_CODES[404]);
}
var ursplit = urlParsed.path.split('.');
var ext = ursplit[ursplit.length - 1];
switch(ext) {
case 'htm':
case 'html':
res.writeHead(200, {'Content-Type': 'text/html'});
res.end(data);
break;
case 'js':
res.writeHead(200, {'Content-Type': 'text/javascript'});
res.end(data);
break;
case 'css':
res.writeHead(200, {'Content-Type': 'text/css'});
res.end(data);
break;
case 'json':
res.writeHead(200, {'Content-Type': 'application/json'});
res.end(data);
break;
default:
res.writeHead(200, {'Content-Type': 'text/plain'});
res.end(data);
}
});
}).listen(8080);
它是如何工作的
使用 URL 模块为您提供了三种处理 URL 的方法。首先你使用了url.parse()方法。该方法接受一个 URL 字符串,并返回一个经过解析的 URL 对象。解析后的 URL 对象可以采用表 7-4 中所示的属性。
表 7-4 。解析的 URL 对象属性
| 财产 | 描述 |
|---|---|
| 。作家(author 的简写) | URL 的授权部分。用户名:密码 |
| 。混杂 | URL 中存在的任何片段。 |
| 。宿主 | URL 的完整主机名和端口。 |
| 。主机名 | URL 中主机的全名。 |
| 。超链接 | 完整的网址。 |
| 。小路 | 路径名和搜索相结合。 |
| 。路径名 | URL 的主机名和端口部分后面的完整路径名。 |
| 。港口 | URL 中指定的端口。 |
| 。草案 | 请求的协议。 |
| 。询问 | 不带“?”的查询字符串。可以被解析为一个对象。 |
| 。搜索 | URL 的查询字符串部分。 |
HTTP 服务器示例中使用了解析的对象来解析路径,以便服务器可以读入文件类型并为内容提供适当的 mime 类型。URL 路由也有很好的用途,可以从解析 URL 中受益。
如果您正在处理一个解析的 URL,并且您想要将该对象转换回一个正确的 URL,无论您是将 URL 返回给客户端还是出于其他目的,您都可以通过调用该对象上的url.format()函数从一个解析的 URL 对象创建一个 URL。这将重新格式化对象,不包括返回到 URL 的href。
第三种可以使用的方法是url.resolve(from, to)函数。该函数将尝试解析路径,就像 web 浏览器一样。
可以看到,如果要在 Node.js 应用中处理 URL,应该利用 URL 模块内置的特性。它提供了解析和格式化应用中需要的任何 URL 所需的工具。
7-9.使用控制台
问题
您希望利用控制台来记录 Node.js 应用中的细节、指标和断言。
解决办法
您可能熟悉一些控制台函数,因为大多数人在构建 Node.js 应用时至少会使用console.log()函数,并且您已经在本书其他部分的许多示例中看到了它的使用。为了了解如何使用控制台,清单 7-22 展示了 Node.js 开发人员可以使用的所有不同方法。
清单 7-22 。使用控制台
/**
* Console
*/
console.log('console usage in Node.js');
console.info('console.info writes the', 'same as console.log');
console.error('same as console.log but writes to stderr');
console.warn('same as console.err');
console.time('timer');
setTimeout(console.timeEnd, 2e3, 'timer');
console.dir({ name: 'console.dir', logs: ['the', 'string representation', 'of objects']});
var yo = 'yo';
console.trace(yo);
try {
console.assert(1 === '1', 'one does not equal one');
} catch(ex) {
console.error('an error occured: ', ex.message);
}
控制台结果
7|⇒ node 7-9-1.js
console usage in Node.js
console.info writes the same as console.log
same as console.log but writes to stderr
same as console.err
{ name: 'console.dir',
logs: [ 'the', 'string representation', 'of objects' ] }
Trace: yo
at Object.<anonymous> (/Users/gack/Dropbox/book/code/7/7-9-1.js:20:9)
at Module._compile (module.js:456:26)
at Object.Module._extensions..js (module.js:474:10)
at Module.load (module.js:356:32)
at Function.Module._load (module.js:312:12)
at Function.Module.runMain (module.js:497:10)
at startup (node.js:119:16)
at node.js:901:3
an error occured: one does not equal one
timer: 2001ms
它是如何工作的
Node.js 中的 console 对象是写入stdout和stderr 的方法。最常见的控制台功能有console.log、console.err、console.warn、console.dir和console.info。这些函数直接处理日志信息以向用户提供信息,它们直接处理 Node.js 进程的stdout和stderr。
控制台对象源
Console.prototype.log = function() {
this._stdout.write(util.format.apply(this, arguments) + '\n');
};
Console.prototype.info = Console.prototype.log;
Console.prototype.warn = function() {
this._stderr.write(util.format.apply(this, arguments) + '\n');
};
Console.prototype.error = Console.prototype.warn;
Console.prototype.dir = function(object) {
this._stdout.write(util.inspect(object, { customInspect: false }) + '\n');
};
在 Node.js 中实现这些方法并不重要。但是,还有一些其他的控制台方法可能会有用。
其中之一就是一对console.time和console.timeEnd。每个函数都有一个标签,告诉 Node.js 跟踪从调用console.time('label')到console.timeEnd('label')之间的时间。曾经的console。它将记录事件之间经过的毫秒数。
Console.prototype.time = function(label) {
this._times[label] = Date.now();
};
Console.prototype.timeEnd = function(label) {
var time = this._times[label];
if (!time) {
throw new Error('No such label: ' + label);
}
var duration = Date.now() - time;
this.log('%s: %dms', label, duration);
};
Console.trace() 是一个打印当前堆栈跟踪的函数,应用于作为标签传递的参数。这是通过基于当前堆栈创建一个新的错误对象并设置细节来实现的。
Console.prototype.trace = function() {
// TODO probably can to do this better with V8's debug object once that is
// exposed.
var err = new Error;
err.name = 'Trace';
err.message = util.format.apply(this, arguments);
Error.captureStackTrace(err, arguments.callee);
this.error(err.stack);
};
Console.assert ,是assert.ok()的包装器,如果断言失败,它将抛出一个错误。在这个解决方案中,您创建了一个您知道会失败的断言,并且在捕获到异常时记录了错误消息。
八、创建 WebSocket 服务器
这一章开始与书中前几章不同。以前,这些章节主要集中在 Node.js 核心及其功能上。这是为了更好地理解 Node.js 中包含的基础架构和平台可用性。然而,Node.js 之所以取得巨大成功,是因为第三方模块的生态系统以及它们提供的可扩展性。本章以及接下来的章节将会让你体验 Node.js 社区,以及它能为你的应用开发提供什么。本章从讨论 WebSockets 开始。
在 WebSockets 出现之前,客户端和服务器之间有许多类似 WebSocket 的通信方式。其中许多都使用了某种形式的从客户端到服务器的轮询,客户端连接到服务器,然后服务器要么直接用一个状态进行响应,要么长时间保持 HTTP 连接打开以等待事件。这会产生许多 HTTP 请求,并且不是客户端和服务器之间的完全双向通信。因此,HTML 5 规范起草了 WebSocket 协议,以允许这种具有持久连接的双向通信。
WebSockets 基于 WebSocket 协议,定义为与远程主机的双向通信,或 TCP 上的双向通信。WebSocket 通信是基于消息的,这使得它比 TCP 流等通信机制更容易处理。乍一看,WebSocket 实现可能看起来像一个 HTTP 实例,但接口的 HTTP 部分只是为了在客户端和服务器之间创建一个握手,并随后将连接升级到 WebSocket 协议。一旦握手成功,客户端和服务器都能够向对方发送消息。WebSocket 消息由帧组成,根据协议,帧是确定发送何种类型消息的信息部分。这些可以是内容的类型(二进制或文本),也可以是用于发出连接应该关闭的信号的控制帧。通过使用安全套接字层 (SSL) 连接的ws:// URI 方案和wss://来访问 WebSocket 端点。
WebSocket 之所以在 Node.js 中蓬勃发展,是因为 Node.js 的事件驱动特性以及手动或通过第三方工具快速高效地创建 web socket 服务器的能力。由于与 Node.js 的这种天然匹配,进入 WebSockets 世界的障碍使得用 Node.js 创建支持 WebSocket 的服务器变得很容易
您在第四章中简要地看到了如何创建一个升级的 WebSocket 连接,但是本章将展示如何利用不同的框架和技术来构建一个完整的 WebSocket 应用。您将涉及的一些主题包括:
- 使用第三方模块构建 WebSocket 服务器
- 监听客户端上的事件
- 用 WebSockets 构建 API
- 使用 WebSockets 从服务器传递事件
- 在浏览器中处理这些事件并创建双向通信
- 用 WebSockets 构建多用户应用
如果您不想创建自己的服务器,作为 Node.js 开发人员,您可以使用几种 WebSocket 实现。通过不创建自己的服务器,你会牺牲一些东西而获得另外一些东西。你牺牲了从概念到产品对服务的完全控制,但是如果你正在使用的模块得到很好的支持,你会得到围绕该模块的社区。本章将关注其中的两个模块:WebSocket-Node 和 Socket。IO 。两者都有强大的社区,开发人员可以向其寻求可靠的实现;然而,插座。IO 已经成为许多 WebSocket 开发者的首选。
8-1.用 WebSocket-Node 实现 WebSocket 服务器
问题
您想要开始使用 WebSocket-Node 模块 来创建 WebSocket 服务器。
解决办法
当您第一次转向 WebSocket-Node 来满足您的 WebSocket 需求时,您会发现您有机会利用一个关于如何格式化 WebSocket 的框架,因为它主要是 WebSocket 协议的 JavaScript 实现。
要开始使用这个 Node.js 模块,您首先需要从 npm 注册表安装,'npm install websocket]。'一旦你安装了这个,你就可以像清单 8-1 中所示的那样使用它,你可以看到你扩展了一个 web 服务器来利用升级后的 WebSockets 连接。
清单 8-1 。升级 Web 服务器以使用 WebSockets
/**
* using WebSocket-Node
*/
var http = require('http'),
fs = require('fs'),
url = require('url'),
WebSocketServer = require('websocket').server;
var server = http.createServer(function(req, res) {
var urlParsed = url.parse(req.url,true, true);
fs.readFile(urlParsed.path.split('/')[1], function(err, data) {
if (err) {
res.statusCode = 404;
res.end(http.STATUS_CODES[404]);
}
res.statusCode = 200;
res.end(data);
});
}).listen(8080);
var serverConfig = {
httpServer: server,
autoAcceptConnections: false
};
var wsserver = new WebSocketServer();
wsserver.mount(serverConfig);
wsserver.on('connect', function(connection) {
console.log('connected');
connection.send('yo');
});
wsserver.on('request', function(req) {
console.log('request');
var connection = req.accept('echo-protocol', req.origin);
connection.on('message', function(message) {
if (message.type === 'utf8') {
console.log(message.utf8Data);
}
else if (message.type === 'binary') {
console.log(message.binaryData);
}
});
connection.on('close', function(reasonCode, description) {
console.log('connection closed', reasonCode, description);
});
});
wsserver.on('close', function(conn, reason, description) {
console.log('closing', reason, description);
});
它是如何工作的
当您使用 WebSocket-Node 创建 WebSocket 服务器时,您可以用一个简单易用的 API 完成很多事情。首先,您正在创建一个 HTTP 服务器。这是一个要求,因为 HTTP 连接必须升级,以便通过握手过程成功创建 WebSocket 连接。然后您需要在您的解决方案中创建一个 WebSocket 服务器配置对象serverConfig。
这个配置 将用于确定您的服务器将处理的 WebSocket 通信的类型。该配置上可供设置的选项如表 8-1 所示。这些默认值与您在 WebSocket 服务器中设置和使用的选项合并。
表 8-1 。WebSocket 服务器配置选项
| [计]选项 | 描述 |
|---|---|
| 集合碎片 | 这告诉服务器自动组装分段的消息,然后在“消息”事件中发出完整的消息。如果这不是真的,那么帧将在“帧”事件中发出,客户端需要自己将这些帧组装在一起。默认值:真 |
| 。自动接受连接 | 这告诉服务器是否接受任何 WebSocket 连接,而不管客户端指定的路径或协议。在大多数情况下应该避免这种情况,因为您最好检查请求以检查允许的来源和协议。默认值:false |
| 。关闭超时 | 这是发送关闭帧后等待的毫秒数,以查看在关闭套接字之前是否返回了确认。默认值:5000 |
| 。禁用算法 | 这将决定是否使用 Nagle 算法。该算法允许通过在传输前插入一小段延迟来将较小的数据包聚合在一起。默认值:真(无延迟) |
| 。dropConnectionOnKeepaliveTimeout | 这将告知 WebSocket 服务器断开与无法在. keepaliveGracePeriod 内响应 keepalive ping 的客户端的连接。默认值:true |
| 。分割阈值 | 如果传出帧大于这个数字,那么它将被分段。默认值:0x4000 (16KB) |
| 。fragmentOutgoingMessages | 此设置决定是否对超过 fragmentationThreshold 选项的邮件进行分段。默认值:真 |
| 。http server(http 服务器) | 这是您将要升级 WebSocket 协议连接的服务器。此选项是必需的。默认值:空 |
| 保持活力 | 此计时器将在每个指定的. keepaliveInterval 向所有客户端发送 ping 命令。默认值:true |
| 。keepaliveGracePeriod | 这是在发送 keepalive ping 后断开连接前等待的时间,以毫秒为单位。默认值:10000 |
| 。keepaliveInterval | 向连接的客户端发送 keepalive ping 的时间(毫秒)。默认值:20000 |
| 。maxReceivedFrameSize | 此选项用于设置 WebSocket 消息帧的最大帧大小阈值。默认值:0x10000(十六进制)= 64 千字节 |
| 。maxReceivedMessageSize | 这是为了设置邮件的最大大小。这仅适用于以下情况。assembleFragments 设置为 true。默认值:0x100000 (1 MB) |
| 。useNativeKeepalive | 这将告诉服务器使用 TCP keepalive,而不是 WebSocket ping 和 pong 数据包。不同的是,TCP keepalive 略小,减少了带宽。如果设置为 true,那么。keepaliveGracePeriod 和。dropConnectionOnKeepaliveTimeout 被忽略。默认值:false |
一旦使用 HTTP 服务器设置了配置,就可以通过调用new WebSocketServer([config])来实例化一个新的 WebSocket 服务器,这里的【config】表示您可以选择传入配置选项。在您的解决方案中,然后调用新 WebSocket 服务器的.mount()方法,这将合并选项并绑定到 HTTP 服务器的“upgrade”事件。
WebSocket 服务器可用的另一种方法是unmount(),它将取消从 HTTP 服务器升级到 WebSocket 协议的能力,但不会影响任何现有的连接。closeAllConnections() 是另一种方法,即优雅地关闭所有连接;shutdown()关闭所有连接并从服务器卸载。
有几个事件你也可以听。在您的示例中,您使用了“request”、“connect”和“close”事件。
当您没有将配置选项'autoAcceptConnections '设置为真时,将发出'request'事件。这将使您有机会检查传入的 WebSocket 请求,以保证您的目标是连接到所需的源和协议。然后,您可以选择accept()或reject()请求。你可以看到在这个例子中,accept()方法带参数。accept()方法可以接受三个参数:协议、来源和 cookies。该协议将只接受来自同一协议的 WebSocket 连接的数据。origin 允许您将 WebSocket 通信限制到指定的主机。参数中的 cookies 必须是名称/值对伴随请求的数组。
一旦请求被接受,服务器就会发出'connect'事件。然后,该事件将在已处理事件的回调中传递要处理的WebSocketConnection对象。
当与 WebSocket 服务器的连接因任何原因关闭时,会发出'close'事件。它不仅会传递WebSocketConnection对象,还会将关闭原因和描述传递给事件处理程序的回调。
您已经看到了如何使用 WebSocket-Node 创建到 WebSocket 服务器的连接,WebSocket-Node 是 web socket 实现的第三方模块。现在,您将研究两种与 WebSocket 服务器通信的方法,一种是使用 Node.js 客户机,另一种是从 web 应用上的客户机。
注意 WebSockets 并不完全适用于所有的浏览器。直到 Internet Explorer 版本 10,Internet Explorer 才实现该协议。Opera Mini(通过 7.0 版)和 Android 浏览器(通过 4.2 版)不支持该协议。除此之外,其他浏览器的一些旧版本不支持最新的实现。更多信息,请查看http://caniuse.com/#feat=websockets。
8-2.在客户端监听 WebSocket 事件
问题
您希望能够作为客户机与 WebSocket 服务器通信。
解决办法
有几种方法可以连接到 WebSocket 连接,您将在本解决方案中看到其中的两种方法。实现这一点的一种方法是利用第三方框架 WebSocket-Node 来创建一个客户端应用,它将连接到 WebSocket 服务器并在两个端点之间进行通信。这在清单 8-2 中显示,并且不同于更典型的利用网页(你将在第 8-5 节中更详细地介绍)连接到 WebSocket 服务器并继续使用该协议进行通信的方法。
清单 8-2 。使用 WebSocket-Node 创建 WebSocket 客户端
/**
* A WebSocket Client
*/
var WebSocketClient = require('websocket').client;
var client = new WebSocketClient();
client.on('connectFailed', function(error) {
console.log('Connect Error: ' + error.toString());
});
client.on('connect', function(connection) {
console.log('woot: WebSocket client connected');
connection.on('error', function(error) {
console.log(error);
});
connection.on('close', function() {
console.log('echo-protocol Connection Closed');
});
connection.on('message', function(message) {
switch (message.type) {
case 'utf8':
console.log('from server: ', message.utf8Data);
break;
default:
console.log(JSON.stringify(message));
break;
}
});
connection.send('heyo');
});
client.connect('ws://localhost:8080/', 'echo-protocol');
作为清单 8-2 中 WebSocket-Node 实现的替代方案,您可以创建一个 WebSocket 客户端,它将使用类似于清单 8-3 中所示的 HTML 页面进行连接。
清单 8-3 。WebSocket 客户端 HTML 页面
<!doctype html>
<html>
<head>
</head>
<body>
<h3>WebSockets!</h3>
<script>
window.onload = function() {
var ws = new WebSocket('ws://localhost:8080', 'echo-protocol');
ws.onopen = function() {
console.log('opened');
};
ws.onmessage = function(event) {
console.log(event);
ws.send(JSON.stringify({ status: 'ok'}));
};
};
</script>
</body>
</html>
它是如何工作的
首先,您使用 WebSocket-Node 附带的 Node.js 应用中可用的WebSocketClient创建了一个客户机。当您调用new WebSocketClient();时,这个客户端为您创建到 WebSocket 服务器的升级连接。该构造函数将接受一个选项对象并用默认选项扩展该对象,如表 8-2 所示。
表 8-2 。WebSocketClient 选项
| [计]选项 | 描述 |
|---|---|
| 。集合碎片 | 这告诉客户端自动将碎片帧组装成一个完整的消息。默认值:真 |
| 。关闭超时 | 这是等待的时间,以毫秒为单位,直到连接在没有收到响应后关闭。默认值:5000 |
| 。禁用算法 | 这表明是否禁用 Nagle 算法,该算法将在发送消息之前设置一个小的延迟,以减少 HTTP 流量。默认值:真 |
| 。fragmentOutgoingMessages | 这将导致传出的消息大于集合。fragmentation 要分段的阈值。默认值:真 |
| 。分割阈值 | 这是将帧分割成片段的大小限制。默认值:16KB |
| 。webSocketVersion | 这是在此连接中使用的 WebSocket 协议的指定版本。默认值:13 |
| 。maxReceivedFrameSize | 这将设置通过 WebSocket 协议接收的帧的最大大小。默认值:1 MB |
| 。maxReceivedMessageSize | 这是通过协议接收的消息的最大大小。仅当。assembleFragments 选项设置为 true。默认值:8 MB |
| 。选项 | 该对象可以包含用于安全连接的传输层安全性(TLS)信息。 |
一旦创建了 WebSocket 客户端,就可以监听通过连接传输的事件和消息。在你的解决方案中,你监听一个'connect'事件。该事件将在回调中接收连接对象,然后您将使用该对象向服务器发送和接收数据。连接是通过调用。connect()web socket 客户端上的功能。这将接受您希望将端点绑定到的 URL 和协议。
为了向 WebSocket 服务器传输消息,您利用了connection.send()方法。该方法将接受两个参数:第一个是您希望发送的数据,第二个是回调函数(可选)。数据将被处理以检查数据是否是缓冲区。如果数据是缓冲区,它们将通过调用。sendBytes()连接的方法;否则,它将尝试使用连接的。sendUTF()方法如果数据可以用。toString()方法。那个。sendBytes()或。sendUTF()方法是传递回调的地方。您可以在清单 8-4 中看到 send 方法的 WebSocket-Node 实现的内部工作方式。
清单 8-4 。WebSocketClient 发送方法
WebSocketConnection.prototype.send = function(data, cb) {
if (Buffer.isBuffer(data)) {
this.sendBytes(data, cb);
}
else if (typeof(data['toString']) === 'function') {
this.sendUTF(data, cb);
}
else {
throw new Error("Data provided must either be a Node Buffer or implement toString()")
}
};
您还可以收听“消息”活动。这个事件是从 WebSocket 服务器发出的,在您的示例中,您检查了收到的消息类型。检查类型允许您适当地处理消息,无论它是 utf8 字符串还是其他格式。使用 WebSocket-Node 附带的WebSocketClient是为 Node.js 应用构建进程间通信的好方法。但是,您可能希望使用 HTML 页面来创建 WebSocket 客户端。
通过利用 web 浏览器中 WebSocket 对象中可用的WebSocketClient或本地 WebSockets,您可以创建一个到 WebSocket 服务器的有用的客户端连接。
8-3.构建 WebSocket API
问题
您希望构建一个利用 WebSockets 的应用,但是您需要创建一个非常适合 WebSocket 范例的 API。
解决办法
用 WebSockets 创建一个 API 似乎与另一个 API 方法不同,比如表述性状态转移(REST) 。这是因为,虽然你可以想象在你的应用中有多条路由,但是使用 WebSockets 你无法访问在 RESTful 设计中指示动作的 HTTP 动词。有几种方法仍然可以构建一个有组织的 API。在清单 8-5 中,你可以看到你构建了一个 WebSocket 服务器,与本章第一节中创建的服务器没有什么不同,它包含了一些额外的处理来自客户端的消息中发送的数据。
清单 8-5 。使用 WebSocket 服务器进行路由处理
/**
* using WebSocket-Node
*/
var http = require('http'),
fs = require('fs'),
url = require('url'),
WebSocketServer = require('websocket').server;
var server = http.createServer(function(req, res) {
var urlParsed = url.parse(req.url,true, true);
fs.readFile(urlParsed.path.split('/')[1], function(err, data) {
if (err) {
res.statusCode = 404;
res.end(http.STATUS_CODES[404]);
}
res.statusCode = 200;
res.end(data);
});
}).listen(8080);
var serverConfig = {
httpServer: server,
autoAcceptConnections: false
};
var wsserver = new WebSocketServer();
wsserver.mount(serverConfig);
wsserver.on('connect', function(connection) {
connection.send('yo');
});
wsserver.on('request', function(req) {
if (req.requestedProtocols[0] == 'echo-protocol') {
var connection = req.accept('echo-protocol', req.origin);
connection.on('message', function(message) {
if (message.type === 'utf8') {
var rt = JSON.parse(message.utf8Data);
switch (rt.path) {
case 'route_a':
console.log('something cool on route a');
break;
case 'route_b':
console.log('something cool on route b', rt);
break;
default:
console.log('something awesome always can happen');
break;
}
}
else if (message.type === 'binary') {
console.log(message.binaryData);
}
});
connection.on('close', function(reasonCode, description) {
console.log('connection closed', reasonCode, description);
});
} else {
console.log('protocol not acceptable');
}
});
wsserver.on('close', function(conn, reason, description) {
console.log('closing', reason, description);
});
一旦您在您的服务器上创建了这个路由处理,您就可以构建一个更符合逻辑的模型,通过 WebSocket 连接从客户端发送消息,如清单 8-6 中的所示。
清单 8-6 。手动路线
<!doctype html>
<html>
<head>
</head>
<body>
<h3>WebSockets!</h3>
<script>
window.onload = function() {
var ws = new WebSocket('ws://localhost:8080', 'echo-protocol');
ws.onopen = function() {
console.log('opened');
};
ws.onmessage = function(event) {
console.log(event);
ws.send(JSON.stringify({ status: 'ok', path: 'route_a'}));
ws.send(JSON.stringify({ status: 'ok', path: 'route_b', action: 'update'}));
};
};
</script>
</body>
</html>
虽然这通常是使用 WebSockets 实现某种路由或 API 设计的成功策略,但是这种对象路由概念也有替代方案。一种替代方法是利用 WebSocket-Node WebSocketRouter对象。该对象允许您在基于 WebSocket 的 Node.js 应用中轻松地为不同的路径或协议指定单独的路由。这种类型的服务器如清单 8-7 所示。
清单 8-7 。一个 WebSocketRouter 服务器
/**
* WebSockets API
*/
var http = require('http'),
fs = require('fs'),
url = require('url'),
WebSocketServer = require('websocket').server,
WebSocketRouter = require('websocket').router;
var server = http.createServer(function(req, res) {
var urlParsed = url.parse(req.url,true, true);
fs.readFile(urlParsed.path.split('/')[1], function(err, data) {
if (err) {
res.statusCode = 404;
res.end(http.STATUS_CODES[404]);
}
res.statusCode = 200;
res.end(data);
});
}).listen(8080);
var serverConfig = {
httpServer: server,
autoAcceptConnections: false
};
var wsserver = new WebSocketServer();
wsserver.mount(serverConfig);
var router = new WebSocketRouter();
router.attachServer(wsserver);
router.mount('*', 'echo-protocol', function(request) {
console.log('mounted to echo protocol');
var conn = request.accept(request.origin);
conn.on('message', function(message) {
console.log('routed message');
});
conn.send('hey');
});
router.mount('*', 'update-protocol', function(request) {
console.log('mounted to update protocol');
var conn = request.accept(request.origin);
conn.on('message', function(message) {
console.log('update all the things');
});
});
清单 8-8 展示了如何在 HTML 页面中构建一个 HTTP 客户端,它将展示如何从WebSocketRouter服务器指定路由。
清单 8-8 。WebSocketRouter HTTP 客户端
<!doctype html>
<html>
<head>
</head>
<body>
<h3>WebSockets!</h3>
<script>
window.onload = function() {
var ws = new WebSocket('ws://localhost:8080', 'echo-protocol');
ws.onopen = function() {
console.log('opened');
};
ws.onmessage = function(event) {
console.log(event);
ws.send(JSON.stringify({ status: 'ok', path: 'route_a'}));
};
var wsupdate = new WebSocket('ws://localhost:8080', 'update-protocol');
wsupdate.onopen = function() {
wsupdate.send('update');
};
};
</script>
</body>
</html>
它是如何工作的
在研究了 WebSockets 的这两种 API 实现之后,您会立即注意到这些解决方案并没有什么过于复杂的地方。两者的基础都是在给定来自服务器的特定消息的情况下,指定要解决的动作。
在清单 8-4 中,您创建了一个服务器,处理从客户端传递到服务器的 JavaScript 对象符号(JSON) 对象。这要求您设计希望在 API 中提供的路线和动作。如果您愿意,您甚至可以通过相应地提供路线和动作来模仿 REST API。例如,如果您有一个希望通过 API 访问的用户配置文件,您可以构建一组如下所示的对象:
{ route: '/user/profile', action: 'GET' }
{ route: '/user/profile', action: 'POST' }
{ route: '/user/profile', action: 'PUT' }
{ route: '/user/profile', action: 'DELETE'}
这将由您的服务器通过解析传入的消息来处理,然后像您在解决方案的switch(rt.path) {...}中所做的那样处理路由。您可以看到,这个构建 WebSocket API 的解决方案非常适合许多需求,尤其是如果您只实现一个协议来处理 API 指令的话。当然,您可以隔离由不同 WebSocket 协议处理的路由。为此,WebSocket-Node 中有一个特性使得用一个WebSocketServer实例访问不同的协议变得更加容易。
清单 8-6 中的解决方案证明了这一点。在这里,您再次创建了您的服务器,但是您包括了。WebSocket-Node 模块中的路由对象。要利用这个特性,首先要创建 HTTP 服务器。然后,和以前一样,您必须告诉您的新 WebSocket 服务器,您希望使用这个 HTTP 服务器进行连接。然而,您现在可以传递WebSocketServer来绑定到一个WebSocketRouter实例,而不是之前看到的消息和连接处理。这个WebSocketRouter实例将允许您将路由的处理从您的客户端分离到特定的路径和/或协议。
在您的解决方案中,您构建了一个路由,它可以处理从客户端提供给它的任何路径(' * '),但是可以通过单独处理不同的协议来处理不同的路由。这意味着,如果您的应用中有一个逻辑分离,比如一个用于用户更新的 API 和一个用于产品更新的 API,您可以用一个单独的协议轻松地将它们分开。您只需在客户机上创建一个新的 WebSocket,它指向您的服务器并为每一项传递特定的协议。
var users = new WebSocket('ws://my.wsserver.co', 'users_protocol');
var products = new WebSocket('ws://my.wsserver.co', 'product_protocol');
从这里开始,您不再关心数据中路由的所有细节,尽管您仍然需要知道您希望通过 WebSocket 连接驱动的动作和特定事件,但是您知道如果您正在访问特定的 WebSocket 协议,您将被隔离到应用逻辑集。事实上,正如您在示例中看到的,整个路由在服务器上是隔离的。显然,分离对象类型是一种方法,但是您可以想象分离每种类型的更新/获取消息的可能性也是可能的。对于大多数情况来说,这可能太细了,但是在一个聊天室的例子中,您可能有一个'sendmessage_protocol'和一个'getmessage_protocol',并且完全独立地处理 get 和 send 操作。
在 Node.js 应用中,围绕 WebSocket 连接构建 API 的方式基本上是无限的,这允许您自由地创建自己认为合适的应用。
到目前为止,本章的大部分内容都基于 WebSocket-Node 模块及其实现。从这里开始,您将研究 Socket。IO,这是另一个非常流行的框架,用于构建基于 WebSocket 的 Node.js 应用。
8-4.使用插座。WebSocket 通信的 IO
问题
您希望通过利用套接字来构建基于 WebSocket 的 Node.js 应用。IO 模块。
解决办法
插座。IO 是一个完全可操作且非常受欢迎的框架,它将 WebSockets 与 Socket 的 Node.js.Implementations 结合使用。IO 可以采取多种形式,但最流行的是以类似于 Node.js 事件模型的方式在客户机和服务器之间传递消息。首先要安装 Socket。使用“$ npm install socket.io”命令通过 npm 进行 IO。来构建套接字。IO 服务器,您可以遵循清单 8-9 中所示的示例,该示例展示了在您实现 Node.js 套接字时可以使用的各种方法。IO 服务器。
清单 8-9 。实现套接字。IO 服务器
/**
* Socket.io Server
*/
var app = require('http').createServer(connectHandler),
io = require('socket.io').listen(app),
fs = require('fs');
app.listen(8080);
function connectHandler (req, res) {
fs.readFile(__dirname + '/8-4-1.html',
function (err, data) {
if (err) {
res.writeHead(500);
return res.end('Error loading 8-4-1.html');
}
res.writeHead(200);
res.end(data);
});
}
// General
io.sockets.on('connection', function (socket) {
socket.broadcast.emit('big_news'); // Emits to all others except this socket.
socket.emit('news', { hello: 'world' });
socket.on('my other event', function (data) {
console.log(data);
});
});
//namespaced
var users = io.of('/users').on('connection', function(socket) {
socket.emit('user message', {
that: 'only',
'/users': 'will get'
});
users.emit('users message', {
all: 'in',
'/users': 'will get'
});
});
插座的原因之一。IO 变得如此流行是因为它有一个嵌入的客户端模块,您可以在绑定到您的服务器的 HTML 页面中使用它。这允许毫不费力地连接到使用 Socket.IO 创建的 WebSocket 服务器。实现此连接需要添加一个 JavaScript 文件引用,然后使用套接字绑定到 WebSocket 服务器。IO 特定绑定,与 web 标准中的new WebSocket()实例化相反。
清单 8-10 。一个插座。IO 客户端
<!doctype html>
<html>
<head>
<script src="/socket.io/socket.io.js"></script>
<script>
var socket = io.connect('http://localhost');
socket.on('news', function (data) {
console.log(data);
socket.emit('my other event', { my: 'data' });
});
socket.on('big_news', function(data) {
console.log('holy cow!');
});
var users = io.connect('http://localhost/users');
users.on('connect', function() {
users.emit('users yo');
});
</script>
</head>
<body>
</body>
</html>
它是如何工作的
插座。在构建 WebSocket 服务器时,IO 可以为您做很多事情,但是它也提供了构建 Node.js 应用时所需的灵活性。就像 WebSocket-Node 一样,它抽象出握手来创建升级的 WebSocket 连接,不仅在服务器上,而且在包含套接字的客户端上。HTML 中的 IO JavaScript 文件。Socket.IO 也是独一无二的,因为它不仅利用了 WebSocket 协议的强大功能,而且还依赖于其他双向通信方法,比如 Flash sockets、long-polling 和 iframes。它将这样做,以便您可以构建您的应用,创建 WebSocket 通信结构 ,并且仍然能够依赖套接字。IO 通信,即使在旧的浏览器或不支持 WebSocket 协议的浏览器上。
在清单 8-8 中,你使用 Socket.IO 创建了一个 WebSocket 服务器。IO 包。然后,您使用本机 Node.js http 模块创建了一个简单的 HTTP 服务器;这是为了提供来自清单 8-9 的 HTML 页面,您计划用它来连接 WebSocket 服务器。
实例化套接字时。在您的代码中,您可以通过告诉新对象在 HTTP 服务器上进行监听来做到这一点,但是您也可以只传入一个监听端口。现在您可以访问插座了。IO API。在服务器上,有几个您用来与客户端通信的事件。
首先,您监听了“连接”事件 ,该事件在服务器接收到来自客户端的连接时发出。从该事件的回调中,您可以访问绑定到该特定连接的单个套接字。这个套接字是您的 WebSocket 通信可以发生的地方。
您执行的第一个通信是广播消息 。该消息通过调用socket.broadcast.emit('big_news');,来触发,调用socket.broadcast.emit('big_news');,会将消息'big_news''发送到连接到套接字的所有套接字。IO 服务器,发送广播的连接除外。接下来你通过使用socket.emit('news', { hello: 'world' });方法发出一个事件‘新闻’。可以在客户端监听该事件,然后可以在客户端处理与消息一起传输的数据。这类似于WebSocket.send()方法,您将在下一节中看到更详细的内容。您在“连接”事件回调中使用的最后一个方法是绑定到从客户端发出的任意事件消息。这与绑定到任何事件的方式相同。
然后,创建了一个绑定到名称空间的 WebSocket 连接。这将有助于创建类似于上一节中概述的示例的 API。您可以通过调用io.of('/path')绑定到名称空间。这将把该路径上的所有连接路由到指定的处理程序。您可以像在解决方案中一样命名这些名称空间var users = io.on('/users');.这很有用,因为您可以只在用户的名称空间上调用事件,比如当您通过调用users.emit('users message'...).向所有用户发出消息时
要在客户机上接收和传输消息,只需向 socket.io.js 文件添加一个 JavaScript 引用。这将为您提供对 I/O 对象的访问,然后您可以使用该对象连接到您的 Socket.IOserver。同样,就像服务器名称空间一样,您可以通过使用路径:var users = io.connect(' http://localhost/users ');连接到特定的路由。
通过这个实现,您可以看到如何利用套接字。IO 构建 Node.js WebSocket 服务器和客户端。IO 利用自己的 API 来发送和接收消息。但是,如果你选择使用 WebSocket 标准.send()而不是.emit()方法,Socket。木卫一将支持这一点。在接下来的几节中,您将进一步了解如何利用 WebSocket 对象跨连接发送和处理消息。
8-5.在浏览器中处理 WebSocket 事件
问题
您希望在浏览器中利用 WebSocket 事件。
解决办法
在本章的前面,您看到了如何使用 WebSocket-Node 模块构建 WebSocket 客户端。您还看到了可以在 web 浏览器中或通过使用 Socket 建立这些 WebSocket 连接的情况。浏览器中的 IO。清单 8-11 展示了如何在网络浏览器中直接使用 WebSocket API。在这种情况下,你应该运行一个类似于清单 8-1 所示的 WebSocket 服务器。
清单 8-11 。浏览器中的 WebSocket API
<!doctype html>
<html>
<head>
</head>
<body>
<h3>WebSockets!</h3>
<script>
window.onload = function() {
var ws = new WebSocket('ws://localhost:8080', 'echo-protocol');
ws.onopen = function() {
console.log('opened');
};
ws.onmessage = function(event) {
console.log(event);
ws.send(JSON.stringify({ status: 'ok'}));
console.log(ws.binaryType);
console.log(ws.bufferedAmount);
console.log(ws.protocol);
console.log(ws.url);
console.log(ws.readyState);
};
ws.onerror = function() {
console.log('oh no! an error has occured');
}
ws.onclose = function() {
console.log('connection closed');
}
};
</script>
</body>
</html>
它是如何工作的
您可以通过简单地绑定到 WebSocket 服务器的端点并请求正确的协议来创建一个 HTML 格式的 WebSocket 客户端,在您的例子中,该协议被称为‘echo-protocol’。这是通过在网页的 JavaScript 中创建一个new WebSocket(<url>, <protocol>);对象来实现的。这个新的 WebSocket 对象可以访问几个事件和属性。可用的 WebSocket 方法有。close()和.send(),分别关闭连接或发送消息。您在解决方案中绑定到的事件是.onmessage和.onopen。那个。onopen一旦连接打开,就发出事件,这意味着连接准备好发送和接收数据。那个。onmessage事件是接收到来自 WebSocket 服务器的消息。其他可用的事件侦听器有。onerror,它将接收发生的任何错误。onclose事件,当状态变为关闭时发出。
浏览器中的 WebSocket 对象也可以访问几个属性。这些包括用于传输信息的 URL 和协议,以及服务器提供的状态和任何扩展。WebSocket 连接还可以查看通过该连接传输的数据类型。这是通过.binaryType属性访问的,它可以根据传输的数据报告“blob”或“arraybuffer”。浏览器上 WebSocket 的最后一个属性是。bufferedAmount房产。这告诉您通过使用。send()法。
8-6.通过 WebSockets 通信服务器事件
问题
您已经看到了如何实现 WebSocket 框架和模块。现在,您想使用 WebSockets 发送服务器信息。
解决办法
当您构建 WebSocket 服务器时,这种双向信息高速公路的一个非常吸引人的用例是能够以低延迟的方式发送服务器状态的更新或来自服务器的事件。这与标准 web 服务器相反,在标准 web 服务器中,您需要轮询信息;相反,您可以简单地按需发送信息,并在消息到达时绑定到消息。
您可以想象一个类似于您在前面章节中看到的情况,但是您已经创建了一个 WebSocket 服务器,它正在向客户端传输数据,包括连接和客户端驱动的消息传递。在这里,您将处理 WebSocket 连接,并定期向所有连接的客户端发送更新。
清单 8-12 。发送服务器事件
/**
* server events
*/
var http = require('http'),
fs = require('fs'),
url = require('url'),
WebSocketServer = require('websocket').server;
var server = http.createServer(function(req, res) {
var urlParsed = url.parse(req.url,true, true);
fs.readFile(urlParsed.path.split('/')[1], function(err, data) {
if (err) {
res.statusCode = 404;
res.end(http.STATUS_CODES[404]);
}
res.statusCode = 200;
res.end(data);
});
}).listen(8080);
var serverConfig = {
httpServer: server,
autoAcceptConnections: false
};
var wsserver = new WebSocketServer();
wsserver.mount(serverConfig);
var conns = [];
wsserver.on('connect', function(connection) {
console.log('connected');
conns.push(connection);
setInterval(pingClients, 5e3);
});
wsserver.on('request', function(req) {
console.log('request');
var connection = req.accept('echo-protocol', req.origin);
connection.on('message', function(message) {
if (message.type === 'utf8') {
console.log(message.utf8Data);
}
else if (message.type === 'binary') {
console.log(message.binaryData);
}
});
connection.on('close', function(reasonCode, description) {
console.log('connection closed', reasonCode, description);
});
});
wsserver.on('close', function(conn, reason, description) {
console.log('closing', reason, description);
for (var i = 0; i < conns.length; i++) {
if (conns[i] === conn) {
conns.splice(i, 1);
}
}
});
function pingClients() {
for (var i =0; i < conns.length; i++) {
conns[i].send('ping');
}
}
它是如何工作的
在这个解决方案中,服务器本身的创建方式与您在 WebSocket-Node 中看到的许多其他方式相同。清单 8-11 突出显示了这种差异,显示了当建立连接时,它们被添加到一个到服务器的连接数组中。这样,您就不必为了向连接发送消息而留在连接内部或请求回调。在这个解决方案中,您创建了一个pingClients()方法,它将遍历数组中的所有连接,并每隔一段时间向它们发送一条消息;您可以想象这样一种情况,您有一个关键的服务器事件要传递,您能够以类似的方式将其分发到连接的套接字。
conns 数组包含对整个WebSocketConnection对象的引用。这意味着您能够从数组中挑选出一个连接并发送消息。您可以在pingClients函数中看到这一点,在这里您迭代数组并调用。send()法在每个单独的插座上。要在连接关闭后进行清理,只需在数组中找到关闭的连接,然后用splice()方法删除它。
8-7.与 WebSockets 的双向通信
问题
您需要能够利用 WebSockets 进行双向通信。
解决办法
这个解决方案将允许你用 WebSockets 创建一个简单的聊天室。您将使用套接字创建服务器。IO 并利用它在客户端与服务器和客户端之间传输数据。服务器如清单 8-13 所示,客户端网页如清单 8-14 所示。
清单 8-13 。插座。IO 聊天服务器
/**
* two-way communications
*/
var app = require('http').createServer(connectHandler),
io = require('socket.io').listen(app),
fs = require('fs');
app.listen(8080);
function connectHandler (req, res) {
fs.readFile(__dirname + '/8-7-1.html',
function (err, data) {
if (err) {
res.writeHead(500);
return res.end('Error loading 8-7-1.html');
}
res.writeHead(200);
res.end(data);
});
}
var members = [];
io.sockets.on('connection', function (socket) {
socket.on('joined', function(data) {
var mbr = data;
mbr.id = socket.id;
members.push(mbr);
socket.broadcast.emit('joined', data);
console.log(data.name, 'joined the room');
});
socket.on('message', function(data) {
// store chat now
socket.broadcast.emit('message', data);
});
socket.on('disconnect', function() {
for (var i = 0; i < members.length; i++) {
if (members[i].id === socket.id) {
socket.broadcast.emit('disconnected', { name: members[i].name });
}
}
});
});
清单 8-14 。聊天客户端
<!doctype html>
<html>
<head>
<script src="/socket.io/socket.io.js"></script>
</head>
<body>
<div id="messages">
</div>
<form id="newChat">
<textarea id="text"></textarea>
<input type="submit" id="sendMessage" value="Send" />
</form>
<script>
var socket = io.connect('http://localhost');
var who;
socket.on('connect', function() {
var chatter = prompt('Please enter your name');
chatter = (chatter === "" || chatter === null) ? "anon" : chatter;
addChatter("you", "Joined");
who = chatter;
socket.emit('joined', { name: chatter});
});
function addChatter(name, message) {
var chat = document.getElementById("messages");
chat.innerHTML += "<div>" + name + " - " + message + "</div>";
}
socket.on('joined', function(data) {
console.log(data);
addChatter(data.name, ' joined');
});
socket.on('disconnected', function(data) {
addChatter(data.name, 'disconnected');
});
socket.on('message', function(data) {
addChatter(data.name, data.message);
});
var chat = document.getElementById("newChat");
chat.onsubmit = function() {
var msg = document.getElementById("text").value;
socket.emit("message", { name: who, message: msg });
document.getElementById("text").value = "";
addChatter(who, msg);
return false;
}
</script>
</body>
</html>
它是如何工作的
这个解决方案首先创建一个套接字。IO 服务器。该服务器将充当您连接的聊天客户端之间的中继,如果您要在生产环境中使用它,您可能希望添加一些持久层来将聊天存储在数据库中。
你的插座。IO server 为三个事件执行中继:加入聊天室、发送消息和断开套接字。
当你在客户端输入你的名字时,加入一个聊天室是被控制的。然后,客户端将通过socket.emit('joined', { name: <username> });发送一条消息,告诉服务器有一个加入事件,以及用户的名字。然后在服务器上接收,并立即向其他客户端发送广播事件。然后,这些客户端绑定到来自服务器的“joined”消息,该消息包含它们需要的数据,以便知道谁加入了房间。然后将其添加到网页的 HTML 中。
加入房间后,您可以向房间中的其他用户发送消息。这从客户端开始,您可以在文本区输入聊天消息,然后发送消息。这发生在socket.emit('message', {name: <user>, message: <text>});中,并且这再次被立即广播到文本所在的其他连接,并且用户被添加到 HTML 中。
最后,你想知道和你聊天的人是否已经离开了房间。为此,您绑定到套接字上的'disconnect'事件,并找到正在断开的套接字的用户名;这是通过将用户数据存储在服务器上的一个members[]数组中来实现的。然后,您向连接到服务器的其余客户端广播这一离开。
这是一个基本的聊天服务器,但它非常清楚地说明了如何使用 WebSockets 在客户端和服务器以及客户端之间进行低延迟的双向通信。在下一节中,您将看到如何使用类似的方法构建一个多用户白板,允许许多用户通过使用 WebSockets 以协作的方式共享绘制的坐标。
8-8.使用 WebSockets 构建多用户白板
问题
现在您已经理解了 WebSockets 的双向通信,您想要构建一个多用户白板应用,以便实时共享绘图。
解决办法
清单 8-15 展示了如何构建一个 WebSocket 服务器,作为 HTML 画布绘制客户端之间的媒介。这些客户端(HTML 如清单 8-16 所示)和(JavaScript 如清单 8-17 所示)将发送和接受 WebSocket 消息,该消息将提供跨客户端实例共享协作绘图程序的能力。
清单 8-15 。带有 WebSocket-Node 的绘图 WebSocket 服务器
var WebSocketServer = require('websocket').server,
http = require('http'),
sox = {},
idx = 0;
var server = http.createServer(function(request, response) {
response.writeHead(404);
response.end();
});
server.listen(8080, function() {
});
ws = new WebSocketServer({
httpServer: server,
autoAcceptConnections: false
});
function originIsAllowed(origin) {
//Check here to make sure we're on the right origin
return true;
}
var getNextId = (function() {
var idx = 0;
return function() { return ++idx; };
})();
ws.on('request', function(request) {
if (!originIsAllowed(request.origin)) {
request.reject();
console.log((new Date()) + ' Connection from origin ' + request.origin + ' rejected.');
return;
}
var connection = request.accept('draw-protocol', request.origin);
connection.socketid = getNextId();
connection.sendUTF("socketid_" + connection.socketid);
console.log(connection.socketid);
sox[connection.socketid] = connection;
connection.on('message', function(message) {
if (message.type === 'utf8') {
sendToAll(JSON.parse(message.utf8Data), 'utf8');
}
else if (message.type === 'binary') {
connection.sendBytes(message.binaryData);
}
});
connection.on('close', function(reasonCode, description) {
delete sox[connection.socketid];
});
});
function sendToAll(drawEvt, type) {
for (var socket in sox) {
if (type === 'utf8' &&drawEvt.socketid !== socket) {
sox[socket].sendUTF(JSON.stringify(drawEvt));
}
}
}
清单 8-16 。绘图画布和 HTML 标记
<!doctype html>
<html>
<head>
<title>whiteboard</title>
<link rel="stylesheet" type="text/css" href="style.css" />
<script src="jquery_1.10.2.js" type="text/javascript"></script>
<script src="drawings.js" type="text/javascript"></script>
</head>
<body>
<div id="wrapper">
<div class="menu">
<ul>
<li>
<a id="clear">Clear</a>
</li>
<li>
<li>
<a id="draw">Draw</a>
<ul id="colors">
<li style="background-color:white;">
<a>White</a>
</li>
<li style="background-color:red;">
<a>Red</a>
</li>
<li style="background-color:orange;">
<a>Orange</a>
</li>
<li style="background-color:yellow;">
<a>Yellow</a>
</li>
<li style="background-color:green;">
<a>Green</a>
</li>
<li style="background-color:blue;">
<a>Blue</a>
</li>
<li style="background-color:indigo;">
<a>Indigo</a>
</li>
<li style="background-color:violet;">
<a>Violet</a>
</li>
<li style="background-color:black;">
<a>Black</a>
</li>
</ul>
</li>
<label for="sizer">Line Size:</label>
<input name="sizer" id="sizer" type="number" min="5" max="100" step="5" />
</ul>
</div>
<canvas id="canvas" ></canvas>
<canvas id="remotecanvas"></canvas>
</div>
</body>
</html>
清单 8-17 。绘图应用:WebSockets 和 Canvas
$(document).ready(function() {
var canvas = document.getElementById("canvas"),
ctx = canvas.getContext("2d"),
remotecanvas = document.getElementById("remotecanvas"),
remotectx = remotecanvas.getContext("2d"),
$cvs = $("#canvas"),
top = $cvs.offset().top,
left = $cvs.offset().left,
wsc = new WebSocket("ws://localhost:8080", "draw-protocol"),
mySocketId = -1;
var resizeCvs = function() {
ctx.canvas.width = remotectx.canvas.width = $(window).width();
ctx.canvas.height = remotectx.canvas.height = $(window).height();
};
var initializeCvs = function () {
ctx.lineCap = remotectx.lineCap = "round";
resizeCvs();
ctx.save();
remotectx.save();
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
remotectx.clearRect(0,0, remotectx.canvas.width, remotectx.canvas.height);
ctx.restore();
remotectx.restore();
};
var draw = {
isDrawing: false,
mousedown: function(ctx, coordinates) {
ctx.beginPath();
ctx.moveTo(coordinates.x, coordinates.y);
this.isDrawing = true;
},
mousemove: function(ctx, coordinates) {
if (this.isDrawing) {
ctx.lineTo(coordinates.x, coordinates.y);
ctx.stroke();
}
},
mouseup: function(ctx, coordinates) {
this.isDrawing = false;
ctx.lineTo(coordinates.x, coordinates.y);
ctx.stroke();
ctx.closePath();
},
touchstart: function(ctx, coordinates){
ctx.beginPath();
ctx.moveTo(coordinates.x, coordinates.y);
this.isDrawing = true;
},
touchmove: function(ctx, coordinates){
if (this.isDrawing) {
ctx.lineTo(coordinates.x, coordinates.y);
ctx.stroke();
}
},
touchend: function(ctx, coordinates){
if (this.isDrawing) {
this.touchmove(coordinates);
this.isDrawing = false;
}
}
};
// create a function to pass touch events and coordinates to drawer
function setupDraw(event, isRemote){
var coordinates = {};
var evt = {};
evt.type = event.type;
evt.socketid = mySocketId;
evt.lineWidth = ctx.lineWidth;
evt.strokeStyle = ctx.strokeStyle;
if (event.type.indexOf("touch") != -1 ){
evt.targetTouches = [{ pageX: 0, pageY: 0 }];
evt.targetTouches[0].pageX = event.targetTouches[0].pageX || 0;
evt.targetTouches[0].pageY = event.targetTouches[0].pageY || 0;
coordinates.x = event.targetTouches[0].pageX - left;
coordinates.y = event.targetTouches[0].pageY - top;
} else {
evt.pageX = event.pageX;
evt.pageY = event.pageY;
coordinates.x = event.pageX - left;
coordinates.y = event.pageY - top;
}
if (event.strokeStyle) {
remotectx.strokeStyle = event.strokeStyle;
remotectx.lineWidth = event.lineWidth;
}
if (!isRemote) {
wsc.send(JSON.stringify(evt));
drawevent.type;
} else {
drawevent.type;
}
}
window.addEventListener("mousedown", setupDraw, false);
window.addEventListener("mousemove", setupDraw, false);
window.addEventListener("mouseup", setupDraw, false);
canvas.addEventListener('touchstart',setupDraw, false);
canvas.addEventListener('touchmove',setupDraw, false);
canvas.addEventListener('touchend',setupDraw, false);
document.body.addEventListener('touchmove',function(event){
event.preventDefault();
},false);
$('#clear').click(function (e) {
initializeCvs(true);
$("#sizer").val("");
});
$("#draw").click(function (e) {
e.preventDefault();
$("label[for='sizer']").text("Line Size:");
});
$("#colors li").click(function (e) {
e.preventDefault();
$("label[for='sizer']").text("Line Size:");
ctx.strokeStyle = $(this).css("background-color");
});
$("#sizer").change(function (e) {
ctx.lineWidth = parseInt($(this).val(), 10);
});
initializeCvs();
window.onresize = function() {
resizeCvs();
};
wsc.onmessage = function(event) {
if (event.data.indexOf("socketid_") !== -1) {
mySocketId = event.data.split("_")[1];
} else {
var dt = JSON.parse(event.data);
setupDraw(dt, true);
}
};
});
它是如何工作的
这个解决方案再次从一个 WebSocket Node WebSocket 服务器的简单实现开始。该服务器将只接受“draw-protocol ”的连接。一旦建立了这些连接,就必须创建一个新的套接字标识符,以便稍后将消息传递给套接字。然后,您绑定到将从连接到达的消息事件。从这里开始,假设您将收到包含坐标的消息,这些坐标将从一个客户端向另一个客户端复制绘图。然后,通过遍历包含所有已连接套接字的对象,将这些消息发送给所有已连接的客户端。
function sendToAll(text, type) {
for (var socket in sox) {
if (type === 'utf8' && text.socketid !== socket) {
sox[socket].sendUTF(JSON.stringify(text));
}
}
}
在客户端,您创建了一个具有一些功能的画布绘制应用,但是您可以通过某种方式对其进行扩展,以便能够模拟从一个客户端到另一个客户端的整套鼠标或触摸运动。当然,您首先要绑定到 WebSocket 服务器的 URL,并利用该服务器所需的“draw-protocol”。然后在 JavaScript 中构建一个setupDraw函数。这将解析发生在画布上的鼠标或触摸事件,并将它们发送到画布上进行实际绘制。如果实例化绘图的事件在客户端开始,那么您将把坐标、样式和事件发送到 WebSocket 服务器进行调度。
if (!isRemote) {
wsc.send(JSON.stringify(evt));
drawevent.type;
} else {
drawevent.type;
}
然后在客户端上接收发送的绘画事件。这将再次调用setupDraw函数;只是这次您告诉绘图工具您的数据来自远程,这意味着您不需要将 stringified 事件发送回 WebSocket 服务器。
wsc.onmessage = function(event) {
if (event.data.indexOf("socketid_") !== -1) {
mySocketId = event.data.split("_")[1];
} else {
var dt = JSON.parse(event.data);
setupDraw(dt, true);
}
};
九、使用 Web 服务器框架
Node.js 非常适合构建 web 服务器。正因为如此,许多开发人员构建了 web 服务器应用。其中一些已经成为开源框架,可供任何希望开发 Node.js web 服务器的开发人员使用。在本章中,你将看到利用这些框架来构建 Node.js web 服务器的例子。
首先,您将研究最流行的 Node.js 框架之一 Express。您将了解如何利用 Express 构建 web 应用。您还将看到如何利用 Express 附带的工具快速构建应用。Express 还提供了一种路由请求的简单方法,允许您创建逻辑路由,并使用框架构建 API。
除了 Express 之外,本章还将介绍其他几个框架,它们允许您创建基于 Node.js 的 web 应用。您将研究的下一个框架在实现表达上略有不同,但是您将看到创建 Node.js 应用的各种方法。其中包括以下内容:
- 盖迪
- 雅虎!莫吉托(鸡尾酒的一种)
- 熨斗
9-1.快速入门
问题
您想要启动并运行 Express Node.js 应用框架。
解决办法
有几种方法可以帮助您开始使用 Express。在这个解决方案中,清单 9-1 ,您将使用 Express 构建一个 web 服务器来执行几个任务。
首先,您的服务器将使用 Express 中间件来记录向服务器发出的请求。您的服务器还将从脚本执行的目录中提供静态文件。这些静态文件将在提供时用 gzip 压缩。除了这些操作之外,您的 Express 服务器将能够执行简单的身份验证,并对静态页面无法提供服务的任何地址提供回退响应。
您还将看到如何获取和设置许多快速设置,以及如何启用和禁用它们。您还将创建一个方法,根据您是在开发模式还是在生产环境中提供内容,为您的应用设置不同的配置。要开始使用,您必须首先使用 npm 安装 Express framework。这可以通过运行命令$ npm install express 或全局执行$ npm install –g express 来完成;然后,您将能够开始使用 Express。
清单 9-1 。快速入门
/**
* Getting started with ExpressJS
*/
var express = require('express'),
app = express();
// use middleware
app.use(express.logger());
app.use(express.compress());
app.use(express.static(__dirname));
app.use(express.basicAuth(function(username, password) {
return username == 'shire' & password == 'baggins';
}));
// a simple route
app.get('/blah', function(req, res) {
res.send(app.get('default'));
});
// a default handler
app.use(function(req, res) {
res.send(app.get('default'));
});
// settings
console.log(app.get('env')); // development
console.log(app.get('trust proxy')); // undefined
app.disable('trust proxy');
console.log(app.get('trust proxy')); // false
console.log(app.get('jsonp callback name'));
console.log(app.get('json replacer'));
console.log(app.get('json spaces'));
console.log(app.get('case sensitive routing'));
console.log(app.get('strict routing'));
console.log(app.get('view cache'));
console.log(app.get('view engine'));
console.log(app.get('views'));
// configurations
app.configure('development', function() {
app.set('default', 'express development site');
});
app.configure('production', function() {
app.set('default', 'express production site');
});
// app.engine('jade', require('jade').__express);
app.listen(8080); // same as http.server.listen
它是如何工作的
Express 是一个为 Node.js 设计的应用框架。它是一个高度灵活的框架,允许您根据自己的需要构建 Node.js 应用。在清单 9-1 的解决方案中,你创建了一个执行几项任务的 web 服务器。为了实现这一点,首先必须安装 Express Node.js 模块。这可以用$ npm install express在你正在做的项目的本地完成,也可以用$ npm install –g express在全局完成。
一旦您安装了 Express,您就可以将它合并到您的项目中。您必须包含框架—require('express'),然后您告诉 express 通过实例化 Express 对象来创建一个应用。这将在您的解决方案中创建一个对 app 变量的引用,这将提供对 Express API 的访问。
这个解决方案首先让你看到的是对app.use() 的一系列调用(见清单 9-2 )。这个函数来自于 Express 的主要依赖项之一——Connect,它是 Sencha Labs 为 Node.js 构建的一个应用中间件框架。下面的片段来自于app.use()的快速实现。这向你展示了。use()调用是从 Connect 扩展而来的。
清单 9-2 。express/lib/application.js 中的 app.use
app.use = function(route, fn){
var app;
// default route to '/'
if ('string' != typeof route) fn = route, route = '/';
// express app
if (fn.handle && fn.set) app = fn;
// restore .app property on req and res
if (app) {
app.route = route;
fn = function(req, res, next) {
var orig = req.app;
app.handle(req, res, function(err){
req.__proto__ = orig.request;
res.__proto__ = orig.response;
next(err);
});
};
}
connect.proto.use.call(this, route, fn);
// mounted an app
if (app) {
app.parent = this;
app.emit('mount', this);
}
return this;
};
从这段代码中可以看出,app.use()方法是通过一些定制逻辑运行的,其中包括确保方法签名是预期的,请求和响应被正确传递,所有这些都是在调用 Connect 的.use()方法之前。
这意味着,当您在解决方案中调用app.use(<function>)时,您要么使用特殊的中间件功能提供一个通用路由,要么设置一个显式路由。在解决方案中,您首先使用它来调用express.logger()。
当添加到 Express 应用中时,express.logger()中间件用于记录服务器上的每个请求。在这个解决方案中,当您运行 Express 应用并导航到您的 Express 站点时,express.logger() 应用将记录类似如下的内容:
清单 9-3 。express.logger()在运行中
< 127.0.0.1 - - [Tue, 16 Jul 2013 00:26:22 GMT] "GET / HTTP/1.1" 401 - "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:22.0) Gecko/20100101 Firefox/22.0"
< 127.0.0.1 - - [Tue, 16 Jul 2013 00:26:27 GMT] "GET / HTTP/1.1" 200 24 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:22.0) Gecko/20100101 Firefox/22.0"
您使用的下一个中间件是express.compress() 。这个中间件将使用 gzip 压缩内容。例如,当添加了express.compress()中间件时,一个 1.3 KB(未压缩)的 JavaScript 文件大约需要 518 B。
在express.compress()之后,您将中间件express.static()添加到您的应用中。Express 使用express.static指向您希望用作静态文件处理程序的目录。例如,您可以使用它来提供静态 JavaScript、HTML 和 CSS 文件。关于express.static() 的案例对于发现app.use()的用法很有意思。如解决方案所示,您只需使用app.use()的功能处理程序,并将静态中间件放在那里。但是,您希望提供的静态内容可能位于应用中一个不知名的目录中,或者您只是希望将静态内容的路径重命名为其他名称。只需为静态处理程序命名一条路由,就可以轻松做到这一点。例如,如果您的静态内容驻留在子目录'/content/shared/static/'中,并且您希望将其作为'/static'来提供,那么您的app.use()将更改如下:
//Original
app.use(express.static(__dirname + '/content/shared/static'));
//Altered route
App.use('/static',express.static(__dirname + '/content/shared/static'));
您连接到 Express 的下一个中间件是express.basicAuth()中间件。这个中间件将允许您以基本的方式实现身份验证,以授予对 Express 应用的访问权限。在这个解决方案中,您提供了一个回调,然后直接检查这个回调中由basicAuth()中间件提供的凭证。
解决方案中的最后一个例子app.use()是为您的应用设置通用路由的默认响应。这是为回调提供请求和响应的地方。在解决方案部分,您还可以看到app.get()的使用。
app.get() 的表达方法有两个主要作用。第一个角色是路由角色。这是app.HTTP_VERB,意味着您将处理任何 HTTP GET 请求。您将在后面的章节中看到更多关于app.get()的用法。第二个角色,正如您在默认路由的回调中看到的,是通过使用app.set()方法检索您为 Express 应用设置的设置。
方法用于改变应用中的设置。签名是app.set(<name>, <value>)。在这个解决方案中,您设置了几个变量。一个是“default”变量,它设置您希望从 web 服务器中的默认路由提供的文本。
有一些设置用作 Express 应用的环境变量。有些,比如'jsonp callback name',默认情况下被设置为' callback ',但是可以设置为您希望的 JSON with padding (JSONP)方法的任何值。其他的,比如'trust proxy',显示设置的状态。在示例中,您可以看到默认的“trust proxy”设置是未定义的。然后利用app.disable('trust proxy')方法将该值设置为 false。这也可以使用app.enable()设置为真。还有一个env变量,它是 Node.js 进程运行的环境。然后,您可以使用来配置希望在开发和生产环境中保持不同的选项。
这是 Express 提供的基本 API。您可以将它用作 web 服务器框架,如本例所示,也可以利用 Express 的命令行功能来生成一个应用,您将在下面几节中看到。
9-2.使用 Express 生成应用
问题
您希望利用 Express 的命令行界面来快速生成应用支架。
解决办法
Express 不仅附带了您在上一节中看到的 API,还允许您创建能够处理 Node.js 服务器中所需的许多样板方法的 web 服务器,而且它还可以用作命令行应用生成器。清单 9-4 展示了快速应用生成的几种方法。
清单 9-4 。快递申请代
> npm install -g express
> mkdir myapp
> cd myapp
> express -h
Usage: express [options]
Options:
-h, --help output usage information
-V, --version output the version number
-s, --sessions add session support
-e, --ejs add ejs engine support (defaults to jade)
-J, --jshtml add jshtml engine support (defaults to jade)
-H, --hogan add hogan.js engine support
-c, --css <engine> add stylesheet <engine> support (less|stylus) (defaults to plain css)
-f, --force force on non-empty directory
> express
destination is not empty, continue? y
create : .
create : ./package.json
create : ./app.js
create : ./public
create : ./routes
create : ./routes/index.js
create : ./routes/user.js
create : ./public/images
create : ./public/javascripts
create : ./views
create : ./views/layout.jade
create : ./public/stylesheets
create : ./public/stylesheets/style.css
install dependencies:
$ cd . && npm install
run the app:
$ node app
> express --sessions --ejs --css less --force
create : .
create : ./package.json
create : ./app.js
create : ./public
create : ./routes
create : ./routes/index.js
create : ./routes/user.js
create : ./public/images
create : ./public/javascripts
create : ./views
create : ./views/index.ejs
create : ./public/stylesheets
create : ./public/stylesheets/style.less
install dependencies:
$ cd . && npm install
run the app:
$ node app
它是如何工作的
这个命令行实用程序首先使用npm install –g express将 Express 模块安装到您的机器上。从这里,您现在可以访问 express 命令行实用程序。当您键入“express”时,您运行的 JavaScript 文件可以在源文件的 bin 文件中找到。
首先,您想知道在 Express 命令行工具中可以访问什么。您可以通过键入express –h或express --help来完成此操作。这将打印可以伴随express命令的命令参数列表。前两个是帮助文本和您正在使用的 Express 版本的一般输出。
其他选项是通过解析命令行参数在应用中设置的,这些参数通过使用名为“commander”的模块传递并添加到名为“program”的对象中。
清单 9-5 。使用 commander 解析命令行参数
program
.version(version)
.option('-s, --sessions', 'add session support')
.option('-e, --ejs', 'add ejs engine support (defaults to jade)')
.option('-J, --jshtml', 'add jshtml engine support (defaults to jade)')
.option('-H, --hogan', 'add hogan.js engine support')
.option('-c, --css <engine>', 'add stylesheet <engine> support (less|stylus) (defaults to plain css)')
.option('-f, --force', 'force on non-empty directory')
.parse(process.argv);
接下来,您将创建一个 Express 应用,默认设置是不使用任何东西。这可以通过导航到您希望创建应用的目录并运行不带任何参数的express命令来完成。您还可以添加一个单独的参数来命名一个应用名express myapp,它将根据这个名称创建一个子目录,脚本将在这个目录中执行。
当 express 命令行应用执行时,除了如上所述解析参数之外,它还将立即调用一个函数,该函数将传入生成应用的路径。
清单 9-6 。生成应用
(function createApplication(path) {
emptyDirectory(path, function(empty){
if (empty || program.force) {
createApplicationAt(path);
} else {
program.confirm('destination is not empty, continue? ', function(ok){
if (ok) {
process.stdin.destroy();
createApplicationAt(path);
} else {
abort('aborting');
}
});
}
});
})(path);
这将检查您创建应用的目录是否为空。它通过利用 Node.js 和文件系统模块来实现这一点。
function emptyDirectory(path, fn) {
fs.readdir(path, function(err, files){
if (err && 'ENOENT' != err.code) throw err;
fn(!files || !files.length);
});
}
如果目录不为空,Express 将向您显示警告“目的地不为空,是否继续?”您只需输入“y”并继续。应用将根据您提供的参数生成应用结构并搭建您的应用。这是通过createApplicationAt()功能完成的。
该方法首先创建应用的根目录,然后创建应用所需的所有目录。您可以看到,这将利用您设置的标志来创建应用目录树。
mkdir(path, function(){
mkdir(path + '/public');
mkdir(path + '/public/javascripts');
mkdir(path + '/public/images');
mkdir(path + '/public/stylesheets', function(){
switch (program.css) {
case 'less':
write(path + '/public/stylesheets/style.less', less);
break;
case 'stylus':
write(path + '/public/stylesheets/style.styl', stylus);
break;
default:
write(path + '/public/stylesheets/style.css', css);
}
});
mkdir(path + '/routes', function(){
write(path + '/routes/index.js', index);
write(path + '/routes/user.js', users);
});
mkdir(path + '/views', function(){
switch (program.template) {
case 'ejs':
write(path + '/views/index.ejs', ejsIndex);
break;
case 'jade':
write(path + '/views/layout.jade', jadeLayout);
write(path + '/views/index.jade', jadeIndex);
break;
case 'jshtml':
write(path + '/views/layout.jshtml', jshtmlLayout);
write(path + '/views/index.jshtml', jshtmlIndex);
break;
case 'hjs':
write(path + '/views/index.hjs', hoganIndex);
break;
}
});
在设置好目录结构之后,将会生成根应用 JavaScript 文件以及正确配置的package.json文件。这可以通过简单地替换基于您在命令行上传递给 Express generator 的设置而设置的令牌来实现。
// CSS Engine support
switch (program.css) {
case 'less':
app = app.replace('{css}', eol + 'app.use(require(\'less-middleware\')({ src: __dirname + \'/public\' }));');
break;
case 'stylus':
app = app.replace('{css}', eol + 'app.use(require(\'stylus\').middleware(__dirname + \'/public\'));');
break;
default:
app = app.replace('{css}', ");
}
// Session support
app = app.replace('{sess}', program.sessions
? eol + 'app.use(express.cookieParser(\'your secret here\'));' + eol + 'app.use(express.session());'
: ");
// Template support
app = app.replace(':TEMPLATE', program.template);
// package.json
var pkg = {
name: 'application-name'
, version: '0.0.1'
, private: true
, scripts: { start: 'node app.js' }
, dependencies: {
express: version
}
}
if (program.template) pkg.dependencies[program.template] = '*';
// CSS Engine support
switch (program.css) {
case 'less':
pkg.dependencies['less-middleware'] = '*';
break;
default:
if (program.css) {
pkg.dependencies[program.css] = '*';
}
}
write(path + '/package.json', JSON.stringify(pkg, null, 2));
write(path + '/app.js', app);
现在,您已经有了一个通过应用的命令行界面工具构建的正常运行的 Express 应用。您可以看到,Express 附带的默认模板呈现利用了一个名为 Jade 的框架。在下一节中,您将看到如何利用这个工具来为您的 Express 应用创建最小且干净的 HTML 模板。
9-3.用 Jade 渲染 HTML
问题
您已经使用命令$ express生成了一个快速应用。默认情况下,这个应用将利用 Jade HTML 模板框架,因此您需要能够理解并在 Node.js 应用中利用这个框架。
解决办法
Jade 是专门为 Node.js 创建的模板语言,默认情况下它与 Express 一起使用,所以对它有所了解是很重要的。它是作为一个极简的模板引擎构建的;它可以用来从一个非常简洁的模板构建 HTML。
在此解决方案中,您将检查并构建使用 Express 生成的默认模板。这些位于
清单 9-7 。/views/layout.jade
doctype 5
html
head
title= title
link(rel='stylesheet', href='/stylesheets/style.css')
//if lt IE 8
script(src='/old_ie.js')
body
block content
清单 9-8 。/views/index.jade
extends layout
block content
h1= title
p.a_class Welcome to #{title}
#an_id this is a div which has an ID
label A range slider:
input(type='range')
#ckbx
label A checkbox:
input(type='checkbox', checked)
ul
li.odd: a(href='#', title='one') one
li.even: a(href='#', title='two') two
li.odd: a(href='#', title='three') three
case flag
when 0: #zero there is no flag
when 1: #one there is a single flag
default: #other other
- if (items.length)
ul
- items.forEach(function(item){
li= item
- })
清单 9-9 。Routes/index.js
/*
* GET home page.
*/
exports.index = function(req, res){
res.render('index', { title: 'Express', flag: 0, items: ['a', 'b', 'c'] });
};
它是如何工作的
Jade 的工作原理是解析自己的语法,然后将其转换成相应的 HTML 输出。看看你的 layout.jade 文件。这个文件声明了一个 doctype,特别是 HTML5 doctype。这可以通过键入“doctype 5”或“!!!5'.这些是文档类型的简写符号。虽然对于简洁的 HTML5 文档类型来说,这不是一个太极端的快捷方式,但是从清单 9-10 中可以看出,如果您在 Express 应用中使用一个过渡文档类型,这将变得非常有用。
清单 9-10 。Express.js 的可用文档类型
var doctypes = exports.doctypes = {
'5': '<!DOCTYPE html>',
'default': '<!DOCTYPE html>',
'xml': '<?xml version="1.0" encoding="utf-8" ?>',
'transitional': '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">',
'strict': '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">',
'frameset': '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Frameset//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-frameset.dtd">',
'1.1': '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">',
'basic': '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML Basic 1.1//EN" "http://www.w3.org/TR/xhtml-basic/xhtml-basic11.dtd">',
'mobile': '<!DOCTYPE html PUBLIC "-//WAPFORUM//DTD XHTML Mobile 1.2//EN" "http://www.openmobilealliance.org/tech/DTD/xhtml-mobile12.dtd">'
};
接下来,您会看到 HTML 元素是以速记的方式编写的,这允许高度可读的标记。创建元素,内部内容由两个空格或一个制表符嵌套,以指示它们相对于父元素。您可以看到,嵌套在 head 元素下面的 title 元素明显体现了这一点。元素中的属性(如 script 元素中的“src ”)在括号中设置。注释从单行 C 风格注释转换为 HTML 注释,因此您可以看到添加条件参数来测试旧版本的 Internet Explorer 并不复杂。一旦完成了这个 HTML 块的所有 Jade 呈现,直到元素,您可以看到这个相对简短的标记被翻译成下面的 HTML:
<!DOCTYPE html><html>
<head>
<title>Express</title>
<link rel="stylesheet" href="/stylesheets/style.css">
<!--[if lt IE 8]>
<script src=”/zomg_old_ie.js”></script>
<![endif]-->
</head>
<body>
</body>
</html>
在检查 index.jade 文件时,您会看到许多与 layout.jade 文件相同的内容。您可以看到,您已经用“扩展布局”命名了要扩展的 Jade 文件。您还有一个命名块“块内容”,它与布局文件上的相应块相匹配。
这个文件中还展示了 Jade 的模板呈现功能的一个示例。您有一个“h1= title”代码段。该代码片段将在您的代码中创建一个 H1 元素,但也会从 routes/index.js 文件中的索引呈现代码中获取对象,并将“标题”添加到 HTML 标记中。您创建的模板对象的其他部分是“flag”属性和“items”数组。这些项目也被解析成 HTML。如您所见,您在代码中的“case”语句中使用的标志。这允许条件标记。您还可以看到,您能够遍历项目数组,并为每个项目呈现一个列表项目。当进行这种迭代时,您必须在您用来迭代的代码前面加上一个连字符。然后你可以在这个连字符后面写 JavaScript 来生成你想要的布局。当您构建大型列表时,这可以极大地改进标记的生成,并在开发周期中节省时间。另一种遍历这个数组的方法是写“items 中的每一项”,然后写“li= item”(下面缩进)。
Jade 也有向 HTML 标签添加类和 id 的简写方法。您可以看到p.a_class将在哪里生成一个
标签,并将类“
a_class”添加到该标签中。类也可以链接在一起,允许任意数量的类名绑定到一个标签上。向标签添加 ID 也同样简单。只需在文本字符串前加上#就可以了,该文本字符串将成为标签的 ID。但是,您也可以创建一个带有 ID 的标记,而不命名该标记。只需添加'#an_id,您就可以在标记中生成一个<div id='an_id'>标记,而无需键入额外的三个字符来命名 div。
如果您使用 Express 和 Jade 创建 Node.js 应用,您就可以使用这个强大的模板引擎创建干净的标记。
9-4.使用 Express 路由
问题
您已经使用 Express 创建了一个应用。您的应用中的 URL 结构变得越来越复杂,因此您需要一个强大的路由机制来将这些 URL 连接到适当的处理程序。
解决办法
Express 提供了一种在 Node.js 应用中调用正确路由的简单方法。您可以在 Express 应用的路由处理程序中将路由作为外部模块或内联回调来处理。清单 9-11 给出了这两种路由行为的例子。这建立在第 9-2 节的服务器文件上。您可以看到已经添加的路由在这个列表中以粗体突出显示。
清单 9-11 。快速路由
/**
* Module dependencies.
*/
var express = require('express')
, routes = require('./routes')
, user = require('./routes/user')
, http = require('http')
, path = require('path');
var app = express();
// all environments
app.set('port', process.env.PORT || 3000);
app.set('views', __dirname + '/views');
app.set('view engine', 'jade');
app.use(express.favicon());
app.use(express.logger('dev'));
app.use(express.bodyParser());
app.use(express.methodOverride());
app.use(app.router);
app.use(express.static(path.join(__dirname, 'public')));
// development only
if ('development' == app.get('env')) {
app.use(express.errorHandler());
}
app.get('/', routes.index);
app.get('/user/:id', function(req, res) {
res.send('you supplied param ' + req.params.id);
});
app.get('/users', user.list);
app.post('/user/:id', function(req, res) {
console.log('update user' + req.params.id);
res.send('successfully updated');
});
http.createServer(app).listen(app.get('port'), function(){
console.log('Express server listening on port ' + app.get('port'));
});
它是如何工作的
Express 通过利用动词是 HTTP 动词的app.VERB范例来处理路由。这意味着你可以为特定类型的 HTTP 请求路由一个应用,当你在 9-6 节用 Express 构建一个 API 时,你会看到更多。
app.VERB方法的行为都类似于你在 9-1 节看到的中间件路由。它们各自接收一个路由,该路由指向请求的访问 URL,相对于应用根。它们还接收一个回调函数。如果您希望以特定的方式处理特定路由的所有请求,不管提供的 HTTP 动词是什么,您可以使用app.all()函数来实现。
在此解决方案中,有多种路线可供使用。首先,您会看到“/”的根路径有一个路由。这指向一个路由模块和驻留在那里的索引方法。这个索引方法有一个请求和响应对象,然后通过发送一个响应来处理它们,在本例中,响应是使用 Jade 呈现的,但不一定是 HTTP 响应可以发送的任何内容。
您还可以看到路由请求可以提供通用参数。这在路由“users/:id”中很明显。这里的':id '是您将传递给路由的参数。这不限于单一参数;事实上,您可以从清单 9-12 中的源代码片段中看到,任何数量的参数都可以添加到一个路由中。
清单 9-12 。找到一条匹配的路由,然后将剩余的路由解析为参数的一部分
Route.prototype.match = function(path){
var keys = this.keys
, params = this.params = []
, m = this.regexp.exec(path);
if (!m) return false;
for (var i = 1, len = m.length; i < len; ++i) {
var key = keys[i - 1];
var val = 'string' == typeof m[i]
? decodeURIComponent(m[i])
: m[i];
if (key) {
params[key.name] = val;
} else {
params.push(val);
}
}
return true;
};
这个逻辑依赖于给定路径的正则表达式,正如您在 app.js 文件中定义的那样。对于' users/:id '路由,构建了这个正则表达式,它将成为下面显示的表达式。
清单 9-13 。“user/:id”路由的正则表达式
/^\\/user\\/(?:([^\\/]+?))\\/?$/
如果您提供了一个 ID,这将创建['/user/1 ',' 1']的正则表达式匹配。如果您要向这个路由添加一个额外的参数(例如' user/:role/:id '或类似的东西),那么正则表达式将会如下所示。
清单 9-14 。“/user/:role/:id”的正则表达式
/^\\/user\\/(?:([^\\/]+?))\\/(?:([^\\/]+?))\\/?$/
这个路由产生['/user/eng/1 ',' eng ',' 1']的正则表达式匹配。然后,这些匹配被添加到路由的 params 数组中。
您可以使用 Express 来建立布线和参数化布线。在下一节中,您将大致了解如何在 Node.js 应用中处理失败的请求。
9-5.处理应用中失败的请求
问题
您已经在 Node.js 中构建了一个 Express 应用,甚至还指定了路由和请求处理。但是,您需要能够在应用中正确处理失败的请求。
解决办法
Express 是一个健壮的框架,允许开发人员以自己的方式处理应用开发过程的许多方面。当您在构建快速路线或通过向 Node.js 应用添加中间件来扩充设置时,这一点很明显。处理失败的请求也是 Express 允许的事情,但它不会对此过于固执己见。
这个失败的请求处理显示在列表 9-15 中。这些处理程序必须遵循一定的模式,但是可以在需要的地方适合您的应用。
清单 9-15 。Express 中失败的请求处理
/**
* Getting started with ExpressJS
*/
var express = require('express'),
app = express();
// use middleware
app.use(express.logger());
app.use(express.compress());
app.use(express.static(__dirname));
app.use(express.basicAuth(function(username, password) {
return username == 'shire' & password == 'baggins';
}));
// a simple route
app.get('/blah', function(req, res, next) {
next(new Error('failing route'));
res.send(app.get('blah'));
});
// a default handler
app.use(function(req, res) {
res.send(app.get('default'));
});
app.use(function(err, req, res, next){
console.error(err.stack);
res.send(500, 'Oh no! Something failed');
});
// configurations
app.configure('development', function() {
app.set('default', 'express development site');
app.set('blah', 'blah blah blah');
});
app.configure('production', function() {
app.set('default', 'express production site');
});
// app.engine('jade', require('jade').__express);
app.listen(8080); // same as http.server.listen
它是如何工作的
你用与你在第 9-1 节中构建的初始例子相似的方式构建了这个解决方案。清单 9-15 中突出显示了两个重要部分。
首先,为了模拟失败的路由,您在路由'/blah '上构建一个错误。这会调用next()路由处理程序,并向其传递一个新的Error()对象。这允许失败的请求路由工作,因为如果没有这个处理程序,站点将会崩溃。
失败的请求路由是通过使用app.use()函数建立的。然后使用包含错误参数的默认回调。当调用next()函数出错时,Express 将利用这个错误处理程序,并将其作为失败的请求处理程序。通过这种方式,您可以优雅地处理 Node.js 服务器上的错误。
9-6.用 ExpressJS 设计 RESTful API
问题
在设计 API 时,很多时候你希望利用 HTTP 并为你的应用创建一个表述性状态转移(REST)架构。
解决办法
在前面的章节中,您已经了解了如何在 Express 中构建有用的路线。因为 Express API 允许路由以特定的 HTTP 动词为目标,所以它非常适合类似 REST 的 API。
在这个解决方案中,您将创建一个简单的 REST API 来与由产品组成的数据模型进行交互。在本例中,这些产品由一个简单的 JavaScript 对象表示,但是它们可以很容易地成为一个对象集并从数据存储中检索。该示例如清单 9-16 所示。
清单 9-16 。休息界面
/**
* Module dependencies.
*/
var express = require('express')
, routes = require('./routes')
, http = require('http')
, path = require('path');
var app = express();
// all environments
app.set('port', process.env.PORT || 3000);
app.set('views', __dirname + '/views');
app.set('view engine', 'jade');
app.use(express.favicon());
app.use(express.logger('dev'));
app.use(express.bodyParser());
app.use(express.methodOverride());
app.use(app.router);
app.use(express.static(path.join(__dirname, 'public')));
// development only
if ('development' == app.get('env')) {
app.use(express.errorHandler());
}
var products = [
{ id: 0, name: 'watch', description: 'Tell time with this amazing watch', price: 30.00 },
{ id: 1, name: 'sandals', description: 'Walk in comfort with these sandals', price: 10.00 },
{ id: 2, name: 'sunglasses', description: 'Protect your eyes in style', price: 25.00 }
];
app.get('/', routes.index);
// curl -X GET http://localhost:3000/products
app.get('/products', function(req, res) {
res.json(products);
});
// curl -X GET http://localhost:3000/products/2
app.get('/products/:id', function(req, res) {
if (req.params.id > (products.length - 1) || req.params.id < 0) {
res.statusCode = 404;
res.end('Not Found');
}
res.json(products[req.params.id]);
});
// curl -X POST -d "name=flops&description=sandals&price=12.00" http://localhost:3000/products
app.post('/products', function(req, res) {
if (typeof req.body.name === 'undefined') {
res.statusCode = 400;
res.end('a product name is required');
}
products.push(req.body);
res.send(req.body);
});
// curl -X PUT -d "name=flipflops&description=sandals&price=12.00" http://localhost:3000/products/3
app.put('/products/:id', function(req, res) {
if (req.params.id > (products.length -1) || req.params.id < 0) {
res.statusCode = 404;
res.end('No product found for that ID');
}
products[req.params.id] = req.body;
res.send(req.body);
});
// curl -X DELETE http://localhost:3000/products/2
app.delete('/products/:id', function(req, res) {
if (req.params.id > (products.length - 1) || req.params.id < 0) {
req.statusCode = 404;
res.end('No product found for that ID');
}
products.splice(req.params.id, 1);
res.json(products);
});
http.createServer(app).listen(app.get('port'), function(){
console.log('Express server listening on port ' + app.get('port'));
});
它是如何工作的
REST 是一种应用架构模式,可以依赖 HTTP 请求来执行操作。这些操作通常采用服务器上常见的创建、读取、更新和删除方法的形式。
典型的 REST 接口至少会使用 HTTP GET 和 POST 方法 在服务器上执行这些动作。然而,在这个解决方案中,您实际上使用了 GET、PUT、POST 和 DELETE HTTP 动词来创建您的 API。您用来简单地从资源中检索数据的 GET 方法。POST 方法用于创建数据,而 PUT 方法用于更新现有数据。DELETE 方法将从数据存储中移除数据。
您从创建产品的数据存储开始,这是代码中包含三个条目的对象数组。一旦你的服务器开始运行,你就可以通过发送 HTTP GET 到http://localhost:3000/products .来获取这些数据
$ CURL -X GET http://localhost:3000/products
[
{
"name": "watch",
"description": "Tell time with this amazing watch",
"price": 30
},
{
"name": "sandals",
"description": "Walk in comfort with these sandals",
"price": 10
},
{
"name": "sunglasses",
"description": "Protect your eyes in style",
"price": 25
}
]
现在您已经创建并获取了数据,您可以向产品数据存储中插入新记录了。在 REST API 中,您只需向产品路线发送一个 POST 请求。如果成功,并且它必须至少包括产品的名称,它将返回您刚刚添加的项目:
$ CURL -X POST -d "name=flops&description=sandal%20things&price=12.00"http://localhost:3000/products
{
"name": "flops",
"description": "sandal things",
"price": "12.00"
}
您现在已经创建了一个新记录,但是您意识到您给产品起的名字不正确。要更新产品的名称,您需要向服务器发送一个 PUT 请求,但是您还必须知道您希望将它发送到的:id。在您的例子中,您知道这是数组中的第四项,所以:id 变成了“3”为了更新名称,您现在可以发送请求,该请求将使用数据存储中的更新值进行响应:
$ curl -X PUT -d "name=flip%20flops&description=sandals&price=12.00" http://localhost:3000/products/3
{
"name": "flip flops",
"description": "sandals",
"price": "12.00"
}
在创建和更新这个新记录后,您意识到产品“凉鞋”在您的产品系列中不再是必需的。因此,您可以通过向该项发送 DELETE HTTP 请求 来删除它,这将把它从数据存储中完全删除。这也将返回删除此项目后现在可用的产品列表:
$ curl -X DELETE http://localhost:3000/products/1
[
{
"name": "watch",
"description": "Tell time with this amazing watch",
"price": 30
},
{
"name": "sunglasses",
"description": "Protect your eyes in style",
"price": 25
},
{
"name": "flip flops",
"description": "sandals",
"price": "12.00"
}
]
9-7.和盖迪一起行动
问题
您希望利用 Node.js 的 Geddy 应用框架来构建您的产品。
解决办法
要开始使用 Geddy,首先需要通过 npm 安装该模块。如果你全局安装它,如清单 9-17 所示,你将可以从你机器上的任何目录访问应用生成器。
清单 9-17 。安装 Geddy
$ npm install –g geddy
清单 9-18 。在指定的目录中生成应用
$ geddy gen app geddyapp
$ cd geddyapp
清单 9-19 。运行应用
$ geddy
清单 9-20 。为应用生成一个新模型并运行应用
$ geddy gen scaffold products name:default description price
$ geddy
您可以通过在浏览器中导航到http://localhost:4000来查看正在运行的应用。
它是如何工作的
Geddy 通过安装一个有用的命令行工具来工作,该工具将允许您快速有效地生成应用和构建新功能。一旦安装了这个命令行工具,您就可以通过键入命令$ geddy gen app <appname>来构建您的第一个应用。这将在appname目录中为您生成一个新的应用。目录结构将如下所示:
appname
-app
-controllers
-helpers
-models
-views
-config
-lib
-log
-node_modules
-public
-css
-img
-js
-test
-controllers
-models
您可以看到,这搭建出了一个工作应用,它在应用中有默认的模型、视图、控制器和助手,允许在默认情况下测试您的应用。为了运行应用,您使用了命令$ geddy,这将启动服务器运行。使用包含在 config 目录中的 production.js 或 development.js 文件配置服务器。
清单 9-21 。配置文件
var config = {
detailedErrors: true
, debug: true
, hostname: null
, port: 4000
, model: {
defaultAdapter: 'memory'
}
, sessions: {
store: 'memory'
, key: 'sid'
, expiry: 14 * 24 * 60 * 60
}
};
module.exports = config;
这为 HTTP 服务器的标准特性(如端口和主机名)创建了几个配置选项,但是它添加了一些特性,如数据模型适配器,您会看到这些特性默认为内存中的。Geddy 非常健壮,因为除了内存选项之外,它还提供了一组通用的适配器用于存储。这些选项包括 PostgreSQL、MongoDB 和 Riak。一旦应用开始运行,您将会在您的控制台中看到对服务器的任何请求。
[Sat, 20 Jul 2013 19:47:42 GMT] 127.0.0.1 - - [Sat Jul 20 2013 15:47:42 GMT-0400 (Eastern Daylight Time)] "GET / 1.1" 200 2645 "-" "Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/30.0.1568.2 Safari/537.36"
接下来,您在应用中为产品模型生成了一个支架。这是通过命令$ geddy gen scaffold products name:default description price完成的。这将在每个app/models、app/views/和app/controllers目录中生成相应的 JavaScript 或 HTML 视图文件。您会注意到,您用属性“default”设置了产品模型的“name”参数。这将它设置为应用中的必填字段,当您查看产品模型文件时,这一点变得很明显。
清单 9-22 。产品模型文件
var Product = function () {
this.defineProperties({
name: {type: 'string', required: true},
description: {type: 'string'},
price: {type: 'string'}
});
};
exports.Product = Product;
Geddy 搭建了一个完整的创建、读取、更新和删除(CRUD)控制器。通过这个简单的命令,您现在可以控制应用可能需要的所有 CRUD 操作。控制器将指导应用存储、删除或重定向您的视图到您试图到达的模型的任何部分。
清单 9-23 。产品控制员
var Products = function () {
this.respondsWith = ['html', 'json', 'xml', 'js', 'txt'];
this.index = function (req, resp, params) {
var self = this;
geddy.model.Product.all(function(err, products) {
self.respond({params: params, products: products});
});
};
this.add = function (req, resp, params) {
this.respond({params: params});
};
this.create = function (req, resp, params) {
var self = this
, product = geddy.model.Product.create(params);
if (!product.isValid()) {
this.flash.error(product.errors);
this.redirect({action: 'add'});
}
else {
product.save(function(err, data) {
if (err) {
self.flash.error(err);
self.redirect({action: 'add'});
}
else {
self.redirect({controller: self.name});
}
});
}
};
this.show = function (req, resp, params) {
var self = this;
geddy.model.Product.first(params.id, function(err, product) {
if (!product) {
var err = new Error();
err.statusCode = 404;
self.error(err);
}
else {
self.respond({params: params, product: product.toObj()});
}
});
};
this.edit = function (req, resp, params) {
var self = this;
geddy.model.Product.first(params.id, function(err, product) {
if (!product) {
var err = new Error();
err.statusCode = 400;
self.error(err);
}
else {
self.respond({params: params, product: product});
}
});
};
this.update = function (req, resp, params) {
var self = this;
geddy.model.Product.first(params.id, function(err, product) {
product.updateProperties(params);
if (!product.isValid()) {
this.flash.error(product.errors);
this.redirect({action: 'edit'});
}
else {
product.save(function(err, data) {
if (err) {
self.flash.error(err);
self.redirect({action: 'edit'});
}
else {
self.redirect({controller: self.name});
}
});
}
});
};
this.destroy = function (req, resp, params) {
var self = this;
geddy.model.Product.remove(params.id, function(err) {
if (err) {
self.flash.error(err);
self.redirect({action: 'edit'});
}
else {
self.redirect({controller: self.name});
}
});
};
};
exports.Products = Products;
控制器执行 CRUD 操作,在本例中是在内存中,但是有了可用的适配器,您可以轻松地存储到 PostgreSQL 或 MongoDB 中以实现持久性。有几个对 self.redirect 的调用。这个函数将你的应用重定向到控制器中描述的视图。默认情况下,这些视图是用 EmbeddedJS (EJS)模板创建的,但是在生成应用时,您可以通过将它作为命令行参数传递来利用 Jade。
EmbeddedJS 基本上就是听起来那样;您可以在视图模板中直接嵌入 JavaScript,这将控制页面的最终布局。嵌入的 JavaScript 简单地放在标记中的'< % % >'标记内,当视图呈现时,它将被剥离、解析和执行。如果您检查添加产品的模板,您会看到清单 9-24 中显示的内容,以及嵌入子模板的能力。
清单 9-24 。EJS 模板:add.html.ejs
<div class="hero-unit">
<form id="product-form" class="form-horizontal" action="/products" method="POST">
<fieldset>
<legend>Create a new Product</legend>
<% if(params.errors) { %>
<div class="control-group">
<ul>
<% for(var err in params.errors) { %>
<li><%= params.errors[err]; %></li>
<% } %>
</ul>
</div>
<% } %>
<%- partial('form', {product: {}}) %>
<div class="form-actions">
<%- contentTag('input', 'Add', {type: 'submit', class: 'btn btn-primary'}) %>
</div>
</fieldset>
</form>
</div>
清单 9-25 。分部子模板 .html.ejs
<div class="control-group">
<label for="name" class="control-label">name</label>
<div class="controls">
<%- contentTag('input', product.name, {type:'text', class:'span6', name:'name'}) %>
</div>
</div>
<div class="control-group">
<label for="description" class="control-label">description</label>
<div class="controls">
<%- contentTag('input', product.description, {type:'text', class:'span6', name:'description'}) %>
</div>
</div>
<div class="control-group">
<label for="price" class="control-label">price</label>
<div class="controls">
<%- contentTag('input', product.price, {type:'text', class:'span6', name:'price'}) %>
</div>
</div>
这里的 EJS 模板利用一个函数从产品模型中创建内容。例如,为了创建将成为产品名称的输入字段,EJS 看起来像<%- contentTag('input', product.name, {type:'text', class:'span6', name:'name'}) %>,它使用模型项的名称,并在表单被提交时将它分配回该模型。
除了在应用中自动生成这些 CRUD 方法的脚手架之外,您还可以使用$ geddy gen resource <resourcename>命令。resource命令不太固执己见,因为它不为模型上的每个操作创建特定的视图,控制器更一般化,如下所示:
var Products = function () {
this.respondsWith = ['html', 'json', 'xml', 'js', 'txt'];
this.index = function (req, resp, params) {
this.respond({params: params});
};
this.add = function (req, resp, params) {
this.respond({params: params});
};
this.create = function (req, resp, params) {
// Save the resource, then display index page
this.redirect({controller: this.name});
};
this.show = function (req, resp, params) {
this.respond({params: params});
};
this.edit = function (req, resp, params) {
this.respond({params: params});
};
this.update = function (req, resp, params) {
// Save the resource, then display the item page
this.redirect({controller: this.name, id: params.id});
};
this.destroy = function (req, resp, params) {
this.respond({params: params});
};
};
exports.Products = Products;
使用资源生成器可能更适合您的应用,因为它不那么固执己见,并且您可以轻松地添加您自己的特定于应用的逻辑,而无需使用scaffold命令中的 CRUD 样板文件。
最后,正如您在使用 Geddy 的测试中已经看到的,Geddy 提供了一个灵活的路由。该路由位于 config/router.js 文件中。它可以匹配路由,类似于 9-4 节中的快速路由。这意味着,如果您有一个特定的产品,并且您知道该 ID,并且想要在该路由上执行特定的操作,您可以将它添加到 router.js 文件中作为router.match('products/:id', 'GET').to(products.handleId);这将把特定的 ID 路由到products.handleId控制器方法。您不需要在router.match查询中指定 HTTP 动词,事实上,您可以将它写成router.get('products/:id').to(products.handleId);,它会以同样的方式执行。在此解决方案中,您利用了完全基于资源的路由,因此您的路由将显示如下:
var router = new geddy.RegExpRouter();
router.get('/').to('Main.index');
router.resource('users');
router.resource('products');
exports.router = router;
您可以使用 Geddy 快速启动并运行应用。这个框架对于启动一个简单的 CRUD 应用来说非常快,并且仍然允许您的 Node.js 应用的更有创造性的实现。
9-8.使用雅虎!莫吉托(鸡尾酒的一种)
问题
您希望通过使用 Yahoo!Mojito 框架。
解决办法
雅虎!Mojito 是另一个 Node.js 应用开发框架。由雅虎创建!,它允许您“使用 Node.js 构建在客户机和服务器上运行的高性能、独立于设备的 HTML5 应用。”
开始使用 Yahoo!Mojito 你首先必须安装命令行界面。这是通过国家预防机制完成的。从那时起,你可以利用命令行界面来构建你的应用,如清单 9-26 所示。
清单 9-26 。安装雅虎!Mojito 和创建应用
$ npm install –g mojito-cli
$ mojito create app mojitoapp
$cd mojitoapp
$ mojito create mojit testmojit
$ mojito test
$ mojito start
http://localhost:8666/@testmojit/index
它是如何工作的
当您通过 npm 安装 Mojito 时,您是在全局范围内这样做的,以获得对整个机器的命令行界面的访问。然后通过键入$ mojito create app mojitoapp创建一个应用。这将创建一个名为“mojitoapp”的目录,其结构如下。
mojitoapp
- artifacts
- assets
- mojits
- node_modules
application.json
package.json
routes.json
server.js
server.js 文件控制 Mojito 应用,并在您运行mojito start时启动。在这个文件中,您可以看到似乎是 Node.js HTTP 服务器的 Mojito 版本。
/*jslint anon:true, sloppy:true, nomen:true*/
process.chdir(__dirname);
/*
* Create the MojitoServer instance we'll interact with. Options can be passed
* using an object with the desired key/value pairs.
*/
var Mojito = require('mojito');
var app = Mojito.createServer();
// ---------------------------------------------------------------------------
// Different hosting environments require different approaches to starting the
// server. Adjust below to match the requirements of your hosting environment.
// ---------------------------------------------------------------------------
module.exports = app.listen();
从这一点上来说,你创造了一个“魔咒”“mojit”是一个 Mojito 术语,表示名称“模块”和“小部件”的混搭。这意味着当你创建一个 mojit 时,你可以把它看作是构建一个模块。默认情况下,Mojito 应用中的 mojit 可以通过路径http://localhost:8666/@mojitname/index访问。当您在应用中导航到这个位置时,您将看到默认的 mojit 页面,它是 Mojito 模型-视图-控制器(MVC)框架的一部分。
Mojito 中的 MVC 架构以所谓的“动作上下文”为中心,你会在代码中的大多数地方看到“ac”。当您查看 controller.server.js 文件的源代码时,每个 mojit 的操作上下文变得很明显。这个文件向应用注册 mojit,并控制模型视图的行为。
/*jslint anon:true, sloppy:true, nomen:true*/
YUI.add('testmojit', function(Y, NAME) {
/**
* The testmojit module.
*
* @module testmojit
*/
/**
* Constructor for the Controller class.
*
* @class Controller
* @constructor
*/
Y.namespace('mojito.controllers')[NAME] = {
/**
* Method corresponding to the 'index' action.
*
* @param ac {Object} The ActionContext that provides access
* to the Mojito API.
*/
index: function(ac) {
ac.models.get('testmojitModel').getData(function(err, data) {
if (err) {
ac.error(err);
return;
}
ac.assets.addCss('./index.css');
ac.done({
status: 'Mojito is working.',
data: data
});
});
}
};
}, '0.0.1', {requires: ['mojito', 'mojito-assets-addon', 'mojito-models-addon', 'testmojitModel']});
在这个 mojit 的索引处理程序中提供了Y.namespace回调中的动作上下文。它将使用合适的型号ac.models.get('testmojitModel')。。。然后添加资产并通过ac.done()处理器发送数据。
模型服务器位于每个 mojit 的模型目录中,您可以看到它遵循类似的 Yahoo!用户界面(YUI ) 模式生成模型并通过Y.namespace将其添加到应用中。这也是您不仅用一个配置初始化模型,而且然后添加诸如getData之类的方法的地方。
/*jslint anon:true, sloppy:true, nomen:true*/
YUI.add('testmojitModel', function(Y, NAME) {
/**
* The testmojitModel module.
*
* @module testmojit
*/
/**
* Constructor for the testmojitModel class.
*
* @class testmojitModel
* @constructor
*/
Y.namespace('mojito.models')[NAME] = {
init: function(config) {
this.config = config;
},
/**
* Method that will be invoked by the mojit controller to obtain data.
*
* @param callback {function(err,data)} The callback function to call when the
* data has been retrieved.
*/
getData: function(callback) {
callback(null, { some: 'data', even: 'more data' });
}
};
}, '0.0.1', {requires: []});
在您了解这些视图如何与 Mojito 一起工作之前,您应该首先理解您并不局限于将您的 URL 作为默认的.../@mojitname/index路径。这些 URL 在你的应用中是没问题的,但是如果用户需要记住这些 URL,那就不是很好的体验了。有一种方法可以让这些 URL 看起来更干净、更友好。
首先,您需要通过将名称添加到应用根目录中的 application.json 文件来命名您的新端点。在这个文件中,您将一个规范命名为“test”,并将其指向您之前创建的“testmojit”类型。然后,您需要在 routes.json 文件中命名这个新路由。这是通过命名“test index”并告诉“/”路径上的 HTTP GET 方法解析。testmojit mojit 的索引处理程序。这些文件显示在以下示例中。
清单 9-27 。应用. json
[
{
"settings": [ "master" ],
"appPort": "8666",
"specs": {
"test": {
"type": "testmojit"
}
}
},
{
"settings": [ "environment:development" ],
"staticHandling": {
"forceUpdate": true
}
}
]
清单 9-28 。Routes.json
[{
"settings": [ "master" ],
"test index": {
"verbs": ["get"],
"path": "/",
"call": "test.index"
}
//^^ convert http://localhost:8666/@testmojit/index to http://localhost:8666/
}]
$ mojito start
http://localhost:8666/
现在,您已经在您想要的 URL 上提供了您的 mojit,您可以修改这些模板了。Mojito 的模板默认使用手柄模板语言。Handlebars 允许您使用简单的表达式从模型中插入对象,就像您在 testmojit 模板中看到的那样。
清单 9-29 。用车把做模板
<div id=" {{mojit_view_id}} ">
<dl>
<dt>status</dt>
<dd id="dd_status"> {{status}} </dd>
<dt>data</dt>
<dd id="dd_data">
<b>some:</b> {{#data}}{{some}}{{/data}}
<span>event:</b> {{#data}}{{even}}{{/data}}
</dd>
</dl>
</div>
使用 Yahoo!构建 Node.js 应用 Mojito 应用框架可以支持多功能的 MVC 应用。
9-9.构建熨斗应用
问题
您希望通过利用 Flatiron 应用框架来构建 Node.js 服务器。
解决办法
要开始使用 Flatiron,就像你在本章中看到的许多其他框架一样,你需要安装这个框架,这样做可以全局地允许对命令行界面的通用访问。命令行界面,正如你在清单 9-30 中看到的,允许你快速搭建一个 Flatiron 应用。
清单 9-30 。安装熨斗和生成应用
$ npm install -g flatiron
$ flatiron create flatironapp
一旦创建了应用,就可以从生成的目录$ cd flatironapp中安装依赖项。然后,为了使用您的 Flatiron 应用,您只需启动 app.js 文件。
$ npm install
$ node app.js # starts your app on localhost:3000
它是如何工作的
当您第一次用 Flatiron 生成一个应用时,您利用了创建该应用的cli/create.js文件。这将提示您输入作者和您要创建的应用的描述。
$ flatiron create flatironapp
info: Creating application flatironapp
info: Using http scaffold.
prompt: author: cgack
prompt: description: test application
test application
prompt: homepage:
info: Creating directory config
info: Creating directory lib
info: Creating directory test
info: Writing package.json
info: Writing file app.js
info: Writing file config/config.json
info: Application flatiron is now ready
提供这些信息后,您现在已经生成了 app.js 文件、配置文件和 package.json 文件。package.json 文件包含运行应用所需的依赖项。这意味着您需要做的就是导航到您的应用目录并安装 npm 的依赖项。这将使您能够访问主 Flatiron 应用,默认情况下,它将使用 HTTP 插件 flatiron.plugins.http。
var flatiron = require('flatiron'),
path = require('path'),
app = flatiron.app;
app.config.file({ file: path.join(__dirname, 'config', 'config.json') });
app.use(flatiron.plugins.http);
app.router.get('/', function () {
this.res.json({ 'hello': 'world' })
});
app.start(3000);
这个应用内置了一个路由,它使用语法app.router.VERB('/path', callback);来提供路由和这些路由的处理程序。在 Flatiron 应用中,您的视图没有默认的模板语言,但是文档建议您可以使用语言“Plates–NPM install Plates–save-dev”来创建简单的、与模板语言无关的模板。这些模板只在唯一的 HTML ID 属性之间使用绑定,看起来与这里显示的类似。
var flatiron = require('flatiron'),
path = require('path'),
plates = require('plates'),
app = flatiron.app;
app.config.file({ file: path.join(__dirname, 'config', 'config.json') });
app.use(flatiron.plugins.http);
app.router.get('/', function () {
this.res.json({ 'hello': 'world' })
});
app.router.get('/test', function() {
var html = '<div id="bind"></div>';
var data = { "bind": "Bound data" };
var output = plates.bind(html, data);
this.res.end(output);
});
app.start(3000);
Flatiron 具有类似于 Express 框架的开放性和 API 语法。它可以很快上手并运行,您应该会发现它是灵活的和可扩展的。像你在本章中看到的许多框架一样,它提供了一种构建 Node.js web 服务器应用的方式,这种方式允许快速生成和灵活的可伸缩性。
十、连接到数据存储
如果您正在用 Node.js 构建一个应用,您几乎不可避免地需要某种形式的数据存储。这可以是简单的内存存储或任何数量的数据存储解决方案。Node.js 社区为您在应用开发中可能遇到的几乎所有数据存储创建了许多驱动程序和连接桥。在本章中,您将研究如何使用 Node.js 来连接其中的许多组件,包括:
- 关系型数据库
- 搜寻配置不当的
- 一种数据库系统
- MongoDB
- 数据库
- 使用心得
- 卡桑德拉
本章重点介绍 Node.js 与这些数据库的通信,而不是每个数据库的安装和初始化。因为本书旨在关注 Node.js 以及它如何在各种用例以及所有这些数据库类型中运行,所以鼓励您找到适合您特定数据库需求的方法。
10-1.连接到 MySQL
问题
许多开发人员最初是通过 MySQL 接触数据库编程的。正因为如此,许多人也希望将这种熟悉感(或桥梁)从现有应用带到 Node.js 应用中。因此,您希望能够从 Node.js 代码中连接到 MySQL 数据库。
解决办法
当您开始将 MySQL 集成到您的应用中时,您必须决定您希望用作 MySQL 驱动程序的 Node.js 框架。如果您选择使用 npm 并搜索 mysql,您可能会看到 MySQL 包列在顶部或顶部附近。然后用$ npm install mysql 命令安装这个包。
一旦安装了这个包,就可以在 Node.js 应用中使用 MySQL 了。为了连接和执行查询,你可以使用一个类似于你在清单 10-1 中看到的模块。在这个例子中,您可以利用 MySQL 示例数据库 Sakila,您可以根据在http://dev.mysql.com/doc/sakila/en/sakila-installation.html找到的说明来安装它。
清单 10-1 。连接和查询 MySQL
/**
* mysql
*/
var mysql = require('mysql');
var connectionConfig = {
host: 'localhost',
user: 'root',
password: '',
database: 'sakila'
};
var connection = mysql.createConnection(connectionConfig);
connection.connect(function(err) {
console.log('connection::connected');
});
connection.query('SELECT * FROM actor', function(err, rows, fields) {
if (err) throw err;
rows.forEach(function(row) {
console.log(row.first_name, row.last_name);
});
});
var actor = { first_name: 'Wil', last_name: 'Wheaton' };
connection.query('INSERT INTO actor SET ?', actor, function(err, results) {
if (err) throw err;
console.log(results);
});
connection.end(function(err) {
console.log('connection::end');
});
它是如何工作的
使用 mysql 模块连接到 MySQL 从一个连接配置对象开始。您的解决方案中的连接对象只是提供您希望连接的主机、用户、密码和数据库。这些是基本的设置,但是还有其他选项可以在该对象上配置,如您在表 10-1 中所见。
表 10-1 。MySQL 的连接选项
| [计]选项 | 描述 |
|---|---|
| bigNumberStrings | 当与 supportBigNumbers 一起使用时,大数字将由 JavaScript 中的字符串表示。默认值:False |
| 字符集 | 命名要用于连接的字符集。默认值:UTF8_GENERAL_CI |
| 数据库ˌ资料库 | 列出 MySQL 服务器上的数据库名称。 |
| 调试 | 使用 stdout 打印详细信息。默认值:False |
| 旗帜 | 列出要使用的非默认连接标志。 |
| 圣体 | 提供您要连接的数据库服务器的主机名。默认值:本地主机 |
| 安全认证 | 允许连接到不安全的(旧的)服务器验证方法。默认值:False |
| 多重陈述 | 允许每个查询有多个语句。这可能会导致 SQL 注入的袭击。默认值:False |
| 密码 | 列出 MySQL 用户的密码。 |
| 港口 | 给出 MySQL 服务器实例所在机器的端口号。默认值:3306 |
| 查询格式 | 创建自定义查询函数。 |
| 套接字路径 | 提供 Unix 套接字的路径。这将导致主机和端口被忽略。 |
| stringifyObjects | 将字符串化对象,而不是转换它们的值。默认值:False |
| 支持 BigNumbers | 在列中使用 BIGINT 或 DECIMAL 时使用此选项。默认值:False |
| 时区 | 列出本地日期的时区。默认值:本地 |
| 分配担任特定类型角色 | 将类型转换为本机 JavaScript 类型。默认值:真 |
| 用户 | 列出用于身份验证的 MySQL 用户。 |
一旦创建了连接对象,就可以实例化一个到 MySQL 服务器的新连接。这是通过调用mysql.createConnection(config)来完成的,然后它将实例化连接对象并向其传递ConnectionConfig()对象。
你可以在清单 10-2 中看到,连接对象将实际尝试在协议模块中创建连接,该模块执行必要的 MySQL 握手以连接到服务器。
清单 10-2 。在 MySQL 模块中连接
module.exports = Connection;
Util.inherits(Connection, EventEmitter);
function Connection(options) {
EventEmitter.call(this);
this.config = options.config;
this._socket = options.socket;
this._protocol = new Protocol({config: this.config, connection: this});
this._connectCalled = false;
this.state = "disconnected";
}
现在您已经连接到 MySQL 服务器,您可以使用该连接进行查询。在该解决方案中,您能够执行两种不同类型的查询。
第一个查询是来自数据库中 actor 表的显式 select 语句。这只需要将查询正确地形成为connection.query方法的第一个参数的字符串。connection.query方法最多可以接受三个参数:sql、值和回调。如果 values 参数不存在,则通过检查它是否是一个函数来检测它,然后只有 SQL 被排队以在服务器上执行。一旦查询完成,回调将被返回。
在第二个查询中,传递一些您希望在数据库中设置的值。这些值是 JavaScript 对象的形式,它们被传递给“?”插入查询上的占位符。使用这种方法的一个好处是,mysql 模块会尝试安全地对您加载到数据库中的所有数据进行转义。它这样做是为了减轻 SQL 注入的攻击。mysql 模块中有一个转义矩阵,会对不同的值类型执行不同类型的转义(见表 10-2 )。
表 10-2 。逃离矩阵
| 值类型 | 它是如何转换的 |
|---|---|
| 数组 | 转向列表['a ',' b'] => 'a ',' b ' |
| 布尔代数学体系的 | True' / 'false '字符串 |
| 缓冲 | 十六进制字符串 |
| 日期 | ' YYYY-mm-dd HH:ii:ss '字符串 |
| NaN/无穷大 | 因为 MySQL 没有将它们转换成 |
| 嵌套数组 | 分组列表[['a ',' b'],['c ',' d']] => ('a ',b '),(' c ',' d ') |
| 民数记 | 没有人 |
| 目标 | 生成键-'值'对;嵌套对象变成字符串 |
| 用线串 | 安全逃脱 |
| 未定义/空 | 空 |
这只是一个使用 mysql 模块连接和执行 MySQL 查询的基本示例。你也可以使用其他方法。您可以对查询的响应进行流式处理,并绑定到事件,以便在返回某一行时对该行执行特定的操作,然后再继续处理下一行。这方面的一个例子如清单 10-3 所示。
清单 10-3 。流式传输一个查询
/**
* mysql
*/
var mysql = require('mysql');
var connectionConfig = {
host: 'localhost',
user: 'root',
password: '’,
database: 'sakila'
};
var connection = mysql.createConnection(connectionConfig);
connection.connect(function(err) {
console.log('connection::connected');
});
var query = connection.query('SELECT * FROM actor');
query.on('error', function(err) {
console.log(err);
}).on('fields', function(fields) {
console.log(fields);
}).on('result', function(row) {
connection.pause();
console.log(row);
connection.resume();
}).on('end', function(err) {
console.log('connection::end');
});
在这里,您可以看到查询本身并没有改变;我们不是向查询方法传递回调,而是绑定到查询执行时发出的事件。因此,在解析字段时,会对它们进行处理。然后,对于每一行,在移动到下一条记录之前,处理该数据。这是通过使用 connection.pause()函数,然后执行您的动作,接着是connection.resume()方法来完成的。
当您使用 mysql 模块这样的框架时,在 Node.js 中连接和使用 MySQL 非常简单。如果 MySQL 是您选择的数据库,它不应该限制您选择 Node.js 作为数据访问服务器的能力。
10-2.连接到微软 SQL 服务器
问题
您希望将 Node.js 应用集成到 Microsoft SQL Server 实例中。
解决办法
就像 MySQL 一样,使用 Node.js 为 Microsoft SQL Server 寻找驱动程序有几种解决方案。其中最受欢迎的一个包是“乏味的”,以连接到 SQL Server 的表格数据流(TDS) 协议命名。您首先使用$ npm install tedious命令通过 npm 安装这个包。
然后,构建一组与 SQL Server 交互的模块。这个解决方案的第一部分,清单 10-4 ,利用 dravoid 创建一个到 SQL Server 实例的连接。第二部分,如清单 10-5 所示,是包含与 SQL Server 实例上的数据交互的模块。
注意 SQL Server 是微软的产品。因此,只有当您的服务器运行 Windows 和 SQL Server 时,以下实现才有效。
清单 10-4 。将连接到您的 SQL Server 实例
/*
* Using MS SQL
*/
var TDS = require('tedious'),
Conn = TDS.Connection,
aModel = require('./10-2-1.js');
var conn = new Conn({
username: 'sa',
password: 'pass',
server: 'localhost',
options: {
database: 'Northwind',
rowCollectionOnRequestCompletion: true
});
function handleResult(err, res) {
if (err) throw err;
console.log(res);
}
conn.on('connect', function(err) {
if (err) throw err;
aModel.getByParameter(conn, 'parameter', handleResult);
aModel.getByParameterSP(conn, 'parameter', handleResult);
});
清单 10-5 。正在查询微软 SQL 服务器
var TDS = require('tedious'),
TYPES = TDS.TYPES,
Request = TDS.Request;
var aModel = module.exports = {
// Use vanilla SQL
getByParameter: function(conn, parm, callback) {
var q = 'select * from model (NOLOCK) where identifier = @parm';
var req = new Request(q, function(err, rowcount, rows) {
callback( err, rows );
});
req.addParameter('parm', TYPES.UniqueIdentifierN, parm);
conn.execSql(req);
},
// Use a Store Procedure
getByParameterSP: function(conn, parm, callback) {
var q = 'exec sp_getModelByParameter @parm';
var req = new Request(q, function(err, rowcount, rows) {
callback( err, rows );
});
req.addParameter('parm', TYPES.UniqueIdentifierN, parm);
conn.execSql(req);
}
};
它是如何工作的
当您第一次使用繁琐的模块连接到 Microsoft SQL Server 时,首先需要创建一个连接。这是通过使用TDS.Connection对象并用配置对象实例化它来完成的。在您的解决方案中,要创建连接,您需要发送用户名、密码、服务器名和一组用于连接的选项。有许多选项可以传递给这个对象,如表 10-3 所示。
表 10-3 。TDS。连接配置
| 环境 | 描述 |
|---|---|
| 选项.取消超时 | 取消请求超时前的时间。默认值:5 秒 |
| 选项. connectTimeout | 等待连接尝试超时的时间。默认值:15 秒 |
| 选项.加密凭证详细信息 | 对象,该对象将包含加密所需的任何凭据。默认值:空对象“{ 0 }” |
| 选项.数据库 | 要连接的数据库的名称 |
| 选项.调试.数据 | 布尔值,表示是否发送关于数据包数据的调试信息。默认值:False |
| 选项. debug.packet | 布尔值,表示是否发送关于数据包的调试信息。默认值:False |
| 选项. debug.payload | 布尔值,表示是否发送有关数据包有效负载的调试信息。默认值:False |
| 选项. debug.token | 布尔值,表明是否发送有关流标记的调试信息。默认值:False |
| 选项.加密 | 设置是否加密请求。默认值:False |
| 选项。实例 Name | 要连接的命名实例。 |
| 选项。isolationLevel | 服务器上的隔离级别,或者服务器何时允许从另一个操作中看到数据。默认值:未提交读(这被称为“脏读”,或最低级别的隔离。给定的事务可以看到来自另一个事务的未提交的事务。) |
| 选项. packetSize | 发送到服务器和从服务器接收的数据包的大小限制。默认值:4 KB |
| 选项.端口 | 要连接的端口。此选项与 options.instanceName 互斥。默认值:1433 |
| 选项. requestTimeout | 给定请求超时前的时间。默认值:15 秒 |
| 选项. rowcollectionondone | 一个布尔值,表示当发出“done”、“doneInProc”和“doneProc”事件时将接收行集合。默认值:False |
| options . rowcollectiononrequestcompletion | 布尔值,为真时,将在请求回调中提供行集合。默认值:False |
| options.tdsVersion | 连接要使用的 TDS 协议的版本。默认值:7_2 |
| options.textsize | 为文本数据类型设置任何列的最大宽度。默认:2147483647 |
| 。密码 | 与用户名关联的密码 |
| 。计算机 Web 服务器 | 您希望连接的服务器的名称或 IP 地址 |
| 。用户名 | 用于连接到 MS SQL Server 实例的用户名(注意:不支持 Windows 身份验证连接。) |
一旦将这些选项传递给连接对象,即 Node.js EventEmitter ,就可以绑定到“连接”事件。有几种方法可以从连接中发出“连接”事件,如下所述:
- 成功的连接
- 登录失败
- 在
connectTimeout过去之后 - 在连接过程中出现套接字错误后
一旦成功连接到 SQL Server,就可以调用包含您的请求的模块。TDS.Request是一个EventEmitter,它允许您通过普通的 T-SQL 字符串或存储过程来执行 SQL。该请求还接受回调,要么直接调用回调,要么将结果应用到'requestCompleted'事件。
正如许多 SQL Server 实现一样,您可以将参数传递给希望执行的 SQL。在您的解决方案的两个示例中(一个是 SQL 文本,一个是存储过程),您传递了一个命名参数。这个命名参数通过使用Request.addParameter()方法被添加到请求中。addParameter()方法最多接受四个参数:名称、类型、值和一个选项对象。添加参数时使用的类型可以是允许作为参数一部分的TDS.Types对象中的任何类型。它们是 Bit、TinyInt、SmallInt、Int、BigInt、Float、Real、SmallDateTime、DateTime、VarChar、Text、NVarChar、Null、UniqueIdentifier 和 UniqueIdentifierN。
一旦创建了请求对象,并添加了所需的参数,就可以通过调用connection.execSql(<Request>)来执行 SQL 语句,将请求传递给方法。当请求完成时,您的回调执行,您可以相应地处理结果和行。
现在,您已经了解了如何使用 Node.js 和繁琐的包来管理 TDS 连接,从而实现到 MS SQL Server 的连接。
10-3.通过 Node.js 使用 PostgreSQL】
问题
您将在数据库中使用 PostgreSQL ,并且需要在 Node.js 应用中利用它。
解决办法
有几个软件包可用于连接 PostgreSQL。这个解决方案将利用 node-postgres 模块,这是 PostgreSQL 的一个低级实现。清单 10-6 显示了一个简单的例子,连接到一个 PostgreSQL 实例,执行一个简单的查询,然后记录结果。
清单 10-6 。连接到 PostgreSQL 并执行查询
/**
* PostgreSQL
*/
var pg = require('pg');
var connectionString = 'tcp://postgres:pass@localhost/postgres';
var client = new pg.Client(connectionString);
client.connect(function(err) {
if (err) throw err;
client.query('SELECT EXTRACT(CENTURY FROM TIMESTAMP "2011-11-11 11:11:11")', function(err, result) {
if (err) throw err;
console.log(result.rows[0]);
client.end();
});
});
它是如何工作的
这个解决方案从使用$ npm install pg安装 node-postgres 模块开始。然后可以将它添加到 Node.js 代码中。然后,通过实例化一个新的客户机来创建到 PostgreSQL 实例的连接。客户端构造器可以解析连接字符串参数,然后你可以创建一个连接,如清单 10-7 所示。
清单 10-7 。node-postgres 的客户端构造
var Client = function(config) {
EventEmitter.call(this);
this.connectionParameters = new ConnectionParameters(config);
this.user = this.connectionParameters.user;
this.database = this.connectionParameters.database;
this.port = this.connectionParameters.port;
this.host = this.connectionParameters.host;
this.password = this.connectionParameters.password;
var c = config || {};
this.connection = c.connection || new Connection({
stream: c.stream,
ssl: c.ssl
});
this.queryQueue = [];
this.binary = c.binary || defaults.binary;
this.encoding = 'utf8';
this.processID = null;
this.secretKey = null;
this.ssl = c.ssl || false;
};
一旦创建了这个连接,接下来就要执行一个查询。这是通过调用client.query()并传递一个 SQL 字符串作为第一个参数来完成的。第二个参数可以是应用于查询的一组值,就像你在 10-1 节看到的那样,也可以是回调函数。回调函数将传递两个参数、一个错误(如果存在)或查询结果。如您所见,结果将包含一个返回行的数组。一旦您处理了结果,您就可以通过调用client.end()来关闭客户端连接。那个。end()方法将通过connection.end()方法关闭连接。
您的示例使用明文 SQL 语句来执行 node-postgres。使用 node-postgres 执行查询还有另外两种方法:参数化和预处理语句。
参数化查询允许您向查询传递参数,例如'select description from products where name=$1', ['sandals']'。通过使用参数化查询,您可以针对 SQL 注入攻击提供更高级别的保护。它们的执行速度也比纯文本查询慢,因为在每次执行之前,这些语句都要准备好,然后再执行。
使用 node-postgres 可以执行的最后一种查询是预处理语句。其中一个将被准备一次,然后对于到 postgres 的每个会话连接,这个 SQL 查询的执行计划被缓存,这样如果它被执行多次,它将成为使用 node-postgres 执行 SQL 的最有效的方式。像参数化查询一样,预处理语句也为 SQL 注入攻击提供了类似的屏障。准备好的语句是通过将一个对象传递给具有名称、文本和 values 属性的查询方法来创建的。然后,您可以通过您为它们提供的名称来调用这些准备好的语句。
利用 node-postgres 允许您从 Node.js 应用中直接高效地与 PostgreSQL 进行交互。接下来的部分将脱离 Node.js 的传统 SQL 接口,您将开始研究几种用于连接 Node.js 的非 SQL 选项。
10-4.使用 Mongoose 连接到 MongoDB
问题
您希望能够在 Node.js 应用中利用 MongoDB 。为此,您选择与 Mongoose 集成。
解决办法
当您在 Node.js 应用中使用 MongoDB 时,有许多驱动程序可供您选择来连接到您的数据存储。然而,最广泛使用的解决方案可能是将您的 MongoDB 实例与 Mongoose 模块集成。用$ npm install mongoose安装后,您可以使用清单 10-8 中列出的连接方法创建一个到 MongoDB 的连接。
清单 10-8 。使用 Mongoose 连接到 MongoDB
/**
* Connecting to MongoDB with Mongoose
*/
var mongoose = require('mongoose');
// simple connection string
// mongoose.connect('mongodb://localhost/test');
mongoose.connect('mongodb://localhost/test', {
db: { native_parser: false },
server: { poolSize: 1 }
// replset: { rs_name : 'myReplicaSetName' },
// user: 'username',
// pass: 'password'
});
// using authentication
// mongoose.connect('mongodb://username:password@host/collection')
mongoose.connection.on('open', function() {
console.log('huzzah! connection open');
});
mongoose.connection.on('connecting', function() {
console.log('connecting');
});
mongoose.connection.on('connected', function() {
console.log('connected');
});
mongoose.connection.on('reconnected', function() {
console.log('reconnected');
});
mongoose.connection.on('disconnecting', function() {
console.log('disconnecting');
});
mongoose.connection.on('disconnected', function() {
console.log('disconnected');
});
mongoose.connection.on('error', function(error) {
console.log('error', error);
});
mongoose.connection.on('close', function() {
console.log('connection closed');
});
它是如何工作的
一般来说,连接到 MongoDB 并不复杂。它需要一个特定于 MongoDB 的统一资源标识符(uniform resource identifier,URI)方案,该方案将指向一个(或多个)可以托管您的 MongoDB 数据的服务器。在 Mongoose 中,使用了相同的 URI 模式,如清单 10-9 所示,增加了几个选项,如清单 10-8 所示。
清单 10-9 。MongoDB 连接字符串
mongodb://[username:password@]host[:port][[,host2[:port2]...[,hostN[:portN][/database][?options]
对于猫鼬,你用mongoose.connect(<uri>, <options>)的方法。您在 Mongoose 中设置的选项可以像表 10-4 中列出的任何选项一样进行设置。
表 10-4 。猫鼬连接选项
| [计]选项 | 描述 |
|---|---|
| 。作家(author 的简写) | 身份验证机制选项,包括要使用的机制的来源和类型。 |
| 。 | 传递给连接。数据库实例(例如,{native_parser: true}将使用本机二进制 JSON [BSON]解析)。 |
| .莽哥 | 布尔值,表示为您的. mongos 使用高可用性选项。如果连接到多个 Mongoose 实例,则应设置为 true。 |
| 。及格 | 与用户名关联的密码。 |
| 。replset | 这是要使用的副本集的名称,假设您要连接的 Mongoose 实例是副本集的成员。 |
| 。计算机 Web 服务器 | 传递给连接服务器实例(例如,{poolSize: 1}个池)。 |
| 。用户 | 用于身份验证的用户名。 |
connection 对象继承了 Node.js EventEmitter ,因此您可以从您的解决方案中看到,您可以使用 Mongoose 订阅许多事件。这些事件在表 10-5 中进行了概述和描述。
表 10-5 。猫鼬连接事件
| 事件 | 描述 |
|---|---|
| '关闭' | 在所有连接上执行 disconnected 和“onClose”后发出。 |
| '已连接' | 成功连接到数据库后发出。 |
| '正在连接' | 对连接执行 connection.open 或 connection.openSet 时发出。 |
| '断开连接' | 断开连接后发出。 |
| '断开连接' | 执行 connection.close()事件时发出。 |
| '错误' | 当错误发生时(即当 Mongo 实例被删除时)发出。 |
| '完整设置' | 当所有 Node 都连接时,在副本集中发出。 |
| '打开' | 一旦打开到 MongoDB 实例的连接就发出。 |
| '重新连接' | 在后续连接后发出。 |
这是使用 Mongoose 连接到 MongoDB 的基本场景和设置。在下一节中,您将研究如何使用 Mongoose 智能地建模数据存储并在 MongoDB 中检索它。
10-5.猫鼬的建模数据
问题
您希望在 Node.js 应用中使用 Mongoose 对 MongoDB 数据建模。
解决办法
用 Mongoose 建模数据时,需要利用mongoose.model()方法 。您不一定需要mongoose.Schema方法,但是对于在清单 10-10 中创建的模型,它被用来构建模型的模式。
清单 10-10 。用猫鼬创建模型
/**
* Modeling data with Mongoose
*/
var mongoose = require('mongoose'),
Schema = mongoose.Schema,
ObjectId = Schema.ObjectId;
mongoose.connect('mongodb://localhost/test');
var productModel = new Schema({
productId: ObjectId,
name: String,
description: String,
price: Number
});
var Product = mongoose.model('Product', productModel);
var sandal = new Product({name: 'sandal', description: 'something to wear', price: 12});
sandal.save(function(err) {
if (err) console.log(err);
console.log('sandal created');
});
Product.find({name: 'sandal'}).exec(function(err, product) {
if (err) console.log(err);
console.log(product);
});
它是如何工作的
对于在 Node.js 应用中使用 MongoDB 的人来说,mongose 成为高优先级的原因之一是因为 mongose 自然地对数据建模。这意味着您只需要为您的数据生成一个模式模型,然后就可以使用该模型从 MongoDB 获取、更新和删除数据。
在您的解决方案中,首先导入mongoose.Schema对象。模式是通过传递一个 JavaScript 对象来创建的,该对象包含您希望建模的实际模式,以及一个可选的第二个参数,该参数包含您的模型的选项。该模式不仅允许您在模型中创建字段的名称,还为您提供了为模式中的值命名特定类型的机会。模式实例化中实现的类型显示为{ <fieldname> : <DataType> }。可用类型如下:
- 排列
- 布尔缓冲区
- 日期
- 混合的
- 数字
- ObjectId(对象 Id)
字符串您为您的模式创建的选项是在表 10-6 中显示的任何选项。
表 10-6 。猫鼬的选择。计划
| [计]选项 | 描述 |
|---|---|
| 自动索引 | 决定 MongoDB 是否自动生成索引的布尔值。默认值:真 |
| buffer 命令 | 一个布尔值,它决定当连接丢失时命令是否被缓冲,直到重新连接发生。默认值:真 |
| 脱帽致意 | 将 MongoDB 设置为有上限——这意味着集合的大小是固定的。默认值:False |
| 募捐 | 设置集合名称的字符串。 |
| 编号 | 返回文档的 _id 字段,或对象的十六进制字符串。如果设置为 false,这将是未定义的。默认值:真 |
| _id | 告知 MongoDB 是否会在创建模型对象时创建 _id 字段。默认值:真 |
| 阅读 | 在架构上设置 query.read 选项。这个字符串决定了您的应用是从复制集中的主要、辅助还是最近的 Mongo 读取。选项:'主要' '主要优先' '次要' '次要优先' '最近' |
| 安全的 | 布尔值,它设置是否将错误传递给回调。默认值:真 |
| 分布式 | 设置以哪个分片集合为目标。 |
| 严格的 | 确保传递给构造函数的非模型值不被保存的布尔值。默认值:真 |
| 托杰森 | 将模型转换为 JavaScript 对象表示法(JSON)。 |
| 图征物件 | 将模型转换为普通的 JavaScript 对象。 |
| 版本密钥 | 创建模型时设置模式的版本。默认值:__v: 0 |
当您创建您的“产品”模式时,您创建了一个包含一个productId的简单对象,它导入产品的ObjectId或hexString。您的模型还将为您希望存储和检索的产品创建一个字符串形式的名称、一个字符串形式的描述和一个数字形式的价格。
从 schema 对象中,您现在实际上创建了一个 Mongoose 模型,方法是使用mongoose.model()方法并传递您为模型选择的名称和 schema 模型本身。现在,您可以使用这个新产品模型来创建产品。您可以通过在 MongoDB 服务器上传递您希望在文档中建模的对象来做到这一点。在此解决方案中,您将创建一个凉鞋对象。然后通过使用接受回调的sandal.save()方法来保存它。
您还可以从您的模型中查找和删除数据。在这个解决方案中,您使用Product.find({ name: 'sandal' }),查询您的模型,它将搜索所有名为“sandal”的产品,并在 exec()回调中返回这些产品。从回调中,您可以访问名为“sandal”的所有产品的数组。如果您希望删除全部或部分结果,您可以遍历这些结果并逐个删除它们,如清单 10-11 所示。
清单 10-11 。使用猫鼬删除记录
Product.find({name: 'sandal'}).exec(function(err, products) {
if (err) console.log(err);
console.log(products);
for (var i = 0; i < products.length; i++) {
if (i >= 3) {
products[i].remove(function() {
console.log('removing');
});
}
}
});
您已经看到了如何在 Node.js 应用中使用 Mongoose 连接和实现一个模式。Mongoose 对象模型允许将您的模型干净地实现到 MongoDB 文档数据库。
10-6.连接到 CouchDB
问题
您希望在 Node.js 应用中利用 CouchDB 。
解决办法
CouchDB 是一个数据库,它利用 JSON 文档、HTTP 的应用编程接口(API)和 JavaScript 的 MapReduce。正因为如此,成为了很多 Node.js 开发者的天然契合点。有几个模块可用于使用 CouchDB 构建 Node.js 应用。在这个解决方案中,您将利用 Nano,这是一个支持 CouchDB 的轻量级模块。可以使用$ npm install nano 进行安装。
在清单 10-12 中,您将创建一个数据库并将一个文档插入到该数据库中。接下来,您将更新数据库中文档。然后,您将在清单 10-13 中检索文档并将其从数据库中删除。
清单 10-12 。用 Nano 在 CouchDB 中创建数据库和文档
/**
* CouchDB
*/
var nano = require('nano')('http://localhost:5984');
nano.db.create('products', function(err, body, header) {
if (err) console.log(err);
console.log(body, header);
});
var products = nano.db.use('products', function(err, body, header) {
if (err) console.log(err);
console.log(body, header);
});
products.insert({ name: 'sandals', description: 'for your feet', price: 12.00}, 'sandals', function(err, body, header) {
if (err) console.log(err);
console.log(body, header);
});
products.get('sandals', {ref_info: true}, function(err, body, header) {
if (err) console.log(err);
console.log(body, header);
});
// Updating in couchDB with Nano.
products.get('sandals', function(err, body, header) {
if (!err) {
products.insert({name: 'sandals', description: 'flip flops', price: 12.50, _rev: body._rev }, 'sandals', function(err, body, header) {
if (!err) {
console.log(body, header);
}
});
}
});
清单 10-13 。从 CouchDB 数据库中删除文档
var nano = require('nano')('http://localhost:5984');
var products = nano.db.use('products');
// deleting in couchDB with Nano.
products.get('sandals', function(err, body, header) {
if (!err) {
products.destroy( 'sandals', body._rev, function(err, body, header) {
if (!err) {
console.log(body, header);
}
nano.db.destroy('products');
});
}
});
您还可以通过一个单一的nano.request()接口使用 Nano 创建对 CouchDB 的请求,如清单 10-14 所示。
清单 10-14 。使用 nano.request()
nano.request({
db: 'products',
doc: 'sandals',
method: 'get'
}, function(err, body, header) {
if (err) console.log('request::err', err);
console.log('request::body', body);
});
它是如何工作的
使用 CouchDB 是许多 Node.js 应用的天然选择。Nano 模块被设计为“Node.js 的极简 CouchDB 驱动程序”,不仅是 Nano 极简,而且它还支持使用管道,您可以从 CouchDB 直接访问错误。
在清单 10-12 中,您首先需要 Nano 模块并连接到您的服务器。连接到服务器就像指向包含正在运行的 CouchDB 实例的主机和端口一样简单。接下来,在 CouchDB 服务器上创建一个数据库,它将保存您希望创建的所有产品。当您使用 Nano 调用任何方法时,您可以添加一个回调来接收错误、主体和头参数。当你第一次用 Nano 创建数据库时,你会看到主体和请求响应 JSON,看起来像清单 10-15 。
清单 10-15 。创建“产品后回调
Body: { ok: true }
Header: { location: 'http://localhost:5984/products',
date: 'Sun, 28 Jul 2013 14:34:01 GMT',
'content-type': 'application/json',
'cache-control': 'must-revalidate',
'status-code': 201,
uri: 'http://localhost:5984/products' }
一旦创建了数据库,就可以创建第一个产品文档。通过将包含产品信息的 JavaScript 对象传递给products.insert() 方法,可以创建“sandals”文档。第二个参数是您希望与该文档相关联的名称。正文和标题的响应会让你知道插入是正确的,如清单 10-16 中的所示。
清单 10-16 。插入产品
Body: { ok: true,
id: 'sandals',
rev: '1-e62b89a561374872bab560cef58d1d61' }
Header: { location: 'http://localhost:5984/products/sandals',
etag: '"1-e62b89a561374872bab560cef58d1d61"',
date: 'Sun, 28 Jul 2013 14:34:01 GMT',
'content-type': 'application/json',
'cache-control': 'must-revalidate',
'status-code': 201,
uri: 'http://localhost:5984/products/sandals' }
如果您想用 Nano 更新 CouchDB 中的一个文档,您需要获得您希望更新的特定文档的修订标识符,然后再次调用nano.insert()函数,将修订标识符传递给您希望更新的特定文档。在您的解决方案中,您通过使用nano.get()方法,然后使用来自回调的 body._rev修订标识符来更新文档(参见清单 10-17 )。
清单 10-17 。更新现有文档
products.get('sandals', function(err, body, header) {
if (!err) {
products.insert({name: 'sandals', description: 'flip flops', price: 12.50, _rev: body._rev
}, 'sandals', function(err, body, header) {
if (!err) {
console.log(body, header);
}
});
}
});
创建、插入和更新文档后,您可能希望能够不时地删除项目。为此,您还需要一个对您计划删除的文档修订的引用。这意味着您可以首先用nano.get()获取文档,并使用来自回调的body._rev标识符传递给nano.destroy()方法。这将从数据库中删除该文档。但是,如果您想删除您的数据库,您可以通过调用nano.db.destroy(<DBNAME>);来销毁整个数据库。
Nano 框架的关键在于所有这些函数实际上都是你在清单 10-14 中看到的nano.request()方法的包装器。在清单 10-14 中,您的请求是以 HTTP GET 的形式向“产品”数据库中的“凉鞋”文档发出的。这个和nano.get()一样。销毁操作的等效操作是使用 HTTP 动词 DELETE,因此在您的nano.db.destroy('products')的实例中,您实际上是在编写nano.request({db: 'products', method: 'DELETE'}, callback);.
10 比 7。使用 Redis
问题
您希望在 Node.js 应用中利用 Redis 键值存储。
解决办法
Redis 是一个非常强大和流行的键值数据存储。因为它太受欢迎了,所以 Node.js 有很多实现可供选择。一些被用来连接到 Express web 服务器框架,而其他的只是指定的。Redis 网站上推荐的一个实现是 node_redis,位于https://github.com/mranney/node_redis。要安装 node_redis,可以按如下方式利用 NPM:$ npm install redis。
对于熟悉 redis 的人来说,使用 redis_node 很简单,因为 API 是相同的。您的所有 get、set、hget 和 hgetall 命令都可以直接从 Redis 本身执行。清单 10-18 中显示了一个获取和设置值和哈希值的简单示例。
清单 10-18 。用 node_redis 获取和设置字符串和散列键值对
/**
* Redis
*/
var redis = require("redis"),
client = redis.createClient();
client.on("error", function (err) {
console.log("Error " + err);
});
client.set("key", "value", redis.print);
client.hset("hash key", "hashtest 1", "some value", redis.print);
client.hset(["hash key", "hashtest 2", "some other value"], redis.print);
client.hkeys("hash key", function (err, replies) {
console.log(replies.length + " replies:");
replies.forEach(function (reply, i) {
console.log(" " + i + ": " + reply);
});
client.quit();
});
client.hgetall('hash key', function(err, replies) {
replies.forEach(function(reply) {
console.log(reply);
});
});
client.get("key", function(err, reply) {
if (err) console.log(err);
console.log(reply);
});
其他时候,您可能希望实现一个松散耦合的发布和订阅范例,而不是仅仅为会话级的键值存储存储散列。对于许多 Node.js 应用的开发人员来说,这可能是一种非常熟悉的方法,他们已经熟悉了事件驱动的开发,但是希望利用 Redis 来实现这些目的。清单 10-19 中显示了一个使用发布和订阅的例子。
清单 10-19 。发布和订阅示例
/**
* Pub/Sub
*/
var redis = require("redis"),
subscriber = redis.createClient(),
publisher = redis.createClient();
subscriber.on("subscribe", function (topic, count) {
publisher.publish("event topic", "your event has occured");
});
subscriber.on("message", function (topic, message) {
console.log("message recieved:: " + topic + ": " + message);
subscriber.end();
publisher.end();
});
subscriber.subscribe("event topic");
它是如何工作的
一旦你通过$ npm install redis安装了 node_redis,你就可以访问 Node.js 中 redis 的完整实现。正如你在清单 10-18 中看到的,你可以很容易地利用redis.createClient()创建一个新的客户端。createClient()方法将创建一个到 Redis 实例的端口和主机的连接,默认为http://127.0.0.1:6379,然后将实例化一个 RedisClient 对象,如清单 10-20 所示。
清单 10-20 。Node_redis 创建客户端
exports.createClient = function (port_arg, host_arg, options) {
var port = port_arg || default_port,
host = host_arg || default_host,
redis_client, net_client;
net_client = net.createConnection(port, host);
redis_client = new RedisClient(net_client, options);
redis_client.port = port;
redis_client.host = host;
return redis_client;
};
RedisClient 继承了 Node.js EventEmitter,会发出几个事件,如 表 10-7 所示。
表 10-7 。再贴现事件
| 事件 | 描述 |
|---|---|
| '连接' | 此事件将与“就绪”同时发出,除非客户端选项“no_ready_check”设置为 true,在这种情况下,只有在建立连接后才会发出此事件。然后你就可以自由地向 Redis 发送命令了。 |
| '排水' | 当到 Redis 服务器的传输控制协议(TCP)连接已经缓冲但再次可写时,RedisClient 将发出“drain”。 |
| '结束' | 一旦到 Redis 服务器的客户端连接关闭,就会发出此事件。 |
| '错误' | 当 Redis 服务器出现异常时,RedisClient 将发出“error”。 |
| “闲置” | 一旦没有等待响应的未完成消息,RedisClient 将发出“idle”。 |
| 准备好了吗 | 一旦建立了到 Redis 服务器和的连接,客户端将发出“就绪”事件,服务器报告它已准备好接收命令。如果您在“就绪”事件之前发送命令,它们将在该事件发出之前排队并执行。 |
在您的解决方案中,然后设置一个字符串值和一个哈希值。使用client.set和client.get设置和检索字符串值。为了处理散列,您还使用了client.hset、client.hkeys和client.hgetall。这些方法直接等同于直接输入命令(见清单 10-21 )。
清单 10-21 。雷迪斯集、get、hset、hkeys 和 hgetall
> set key value
OK
> get key
"value"
> hset 'hash key' 'hashtest 1' 'blah'
(integer) 0
> hset 'hash key' 'hashtest 2' 'cheese'
(integer) 0
> hkeys 'hash key'
1) "hashtest 1"
2) "hashtest 2"
> hgetall 'hash key'
1) "hashtest 1"
2) "blah"
3) "hashtest 2"
4) "cheese"
然后,您创建了一个发布和订阅解决方案。这可以与 Node.js 的事件模型一起使用,以便在应用的隔离部分之间创建一个松散耦合的集成。首先,您创建了两个名为 publisher 和 subscriber 的RedisClients。首先,在您希望收听的主题上调用subscriber.subscribe(),然后一旦订户的‘subscribe’事件被发出,就使用publisher.publish(<event name>)实际发出该事件。然后,您可以将订阅者绑定到消息事件,并在该事件发布后执行各种操作。
现在,您已经利用 Redis 存储了键-值对,以及带有 node_redis 的数据存储中的散列键。您还使用 Redis 执行了发布和订阅方法来支持这些消息。
10-8.连接到卡珊德拉
问题
您正在利用 Cassandra 来记录来自您的应用的事件,并且您希望用 Node.js 来实现这种日志记录。
解决办法
在不同的编程语言中,Cassandra 有许多不同的驱动程序。对于 Node.js,与$ npm install helenus一起安装的包“helenus”处于最前沿,因为它提供了对 thrift 协议和 Cassandra 查询语言(CQL) 的绑定。
在清单 10-22 中,您将创建一个日志机制来记录 Node.js 服务器上发生的事件。
清单 10-22 。使用 helenus 为 Cassandra 创建一个日志应用
var helenus = require('helenus'),
pool = new helenus.ConnectionPool({
hosts : ['127.0.0.1:9160'],
keyspace : 'my_ks',
user : 'username',
password : 'pass',
timeout : 3000//,
//cqlVersion : '3.0.0' // specify this if you're using Cassandra 1.1 and want to use CQL 3
});
var logger = module.exports = {
/**
* Logs data to the Cassandra cluster
*
* @param status the status event that you want to log
* @param message the detailed message of the event
* @param stack the stack trace of the event
* @param callback optional callback
*/
log: function(status, message, stack, callback) {
pool.connect(function(err, keyspace){
console.log('connected');
keyspace.get('logger', function(err, cf) {
var dt = Date.parse(new Date());
//Create a column
var column = {};
column['time'] = dt;
column['status'] = status;
column['message'] = message;
column['stack'] = stack;
var timeUUID = helenus.TimeUUID.fromTimestamp(new Date());
cf.insert(timeUUID, column, function(err) {
if (err) {
console.log('error', err);
}
Console.log('insert complete');
if (callback) {
callback();
} else {
return;
}
});
});
});
}
};
它是如何工作的
helenus 模块是连接到 Cassandra 数据库的健壮解决方案。在您的解决方案中,导入 helenus 模块后,您连接到 Cassandra。这是通过向helenus.connectionPool()传递一个简单的对象来实现的。创建这个连接池的对象包含几个选项,如 表 10-8 所示。
表 10-8 。连接池选项
| [计]选项 | 描述 |
|---|---|
| 。cqlVersion | 命名您希望使用的 CQL 版本。 |
| 。主机 | 提供一个值数组,这些值是群集中所有 Cassandra 实例的 IP 地址和端口。 |
| 。keyspace(键空间) | 列出您希望最初连接到的 Cassandra 集群上的密钥空间。 |
| 。密码 | 提供您希望用来连接到 Node 的密码。 |
| 。超时 | 超时时间,以毫秒为单位。 |
| 。用户 | 给出连接到 Node 的用户名。 |
一旦建立了连接,您就可以调用pool.connect()。一旦连接发生,回调将提供对您在连接池中配置的默认键空间的引用。然而,有另一种方法可以通过使用pool.use('keyspacename', function(err, keyspace) {});方法连接到一个键空间。
现在,您可以访问 Cassandra 集群上的密钥空间。要访问 logger 列族,您可以调用keyspace.get('logger'...),它将获取列族并返回一个引用,这样您就可以直接对列族进行操作。
现在您已经获得了对希望写入数据的列族的访问权,您可以创建想要插入的列了。在这个解决方案中,假设您的 logger 列族有一个 TimeUUID 类型的行键,为每个条目创建一个惟一的时间戳。 Helenus 允许您轻松使用这种类型的键,因为 TimeUUID 是一种内置类型。您可以访问这个类型,并通过在对象上使用fromTimestamp方法创建一个新的 TimeUUID,如清单 10-23 中的所示。您还将看到,如果需要,helenus 提供了一种生成 UUID 类型的方法。
清单 10-23 。创建新的 TimeUUID
> helenus.TimeUUID.fromTimestamp(new Date());
c19515c0-f7c4-11e2-9257-fd79518d2700
> new helenus.UUID();
7b451d58-548f-4602-a26e-2ecc78bae57c
除了 logger 列族中的行键之外,您只需传递希望记录的事件的时间戳、状态、消息和堆栈跟踪。这些都成为您命名为“列”的对象的一部分现在已经有了行键和列值,可以通过调用列族上的cf.insert方法将它们插入到 Cassandra 中。
这个解决方案利用 JavaScript 和对象生成一个类似模型的实现,该实现被转换成 Cassandra Thrift 协议,以便插入数据。Helenus 允许通过使用 CQL 语言插入数据的其他方法。与清单 10-22 中的类似的实现,但是使用了 CQL ,如清单 10-24 中的所示。检索列族的步骤被省略了,因为 CQL 直接在键空间上操作。
清单 10-24 。使用 CQL 将数据记录到卡珊德拉
var helenus = require('helenus'),
pool = new helenus.ConnectionPool({
hosts : ['127.0.0.1:9160'],
keyspace : 'my_ks',
user : 'username',
password : 'pass',
timeout : 3000//,
//cqlVersion : '3.0.0' // specify this if you're using Cassandra 1.1 and want to use CQL 3
});
var logger = module.exports = {
/**
* Logs data to the Cassandra cluster
*
* @param status the status event that you want to log
* @param message the detailed message of the event
* @param stack the stack trace of the event
* @param callback optional callback
*/
log: function(status, message, stack, callback) {
pool.connect(function(err, keyspace){
keyspace.get('logger', function(err, cf) {
var dt = Date.parse(new Date());
//Create a column
var column = {};
column['time'] = dt;
column['status'] = status;
column['message'] = message;
column['stack'] = stack;
var timeUUID = helenus.TimeUUID.fromTimestamp(new Date());
var cqlInsert = 'INSERT INTO logger (log_time, time, status, message,stack)' +
'VALUES ( %s, %s, %s, %s, %s )';
var cqlParams = [ timeUUID, column.time, column.status, column.message, column.stack ];
pool.cql(cqlInsert, cqlParams, function(err, results) {
if (err) logger.log('ERROR', JSON.stringify(err), err.stack);
});
});
});
}
};
var queueObj = {};
var timeUUID = helenus.TimeUUID.fromTimestamp(new Date()) + '';
var cqlInsert = 'INSERT INTO hx_services_pha_card (card_id, card_definition_id, pig_query, display_template, tokens, trigger)' +
'VALUES ( %s, %s, %s, %s, %s, %s )';
var cqlParams = [ timeUUID, queueObj.card_definition_id, queueObj.pig_query, queueObj.display_template, tokens.join(','), queueObj.trigger ];
pool.cql(cqlInsert, cqlParams, function(err, results) {
if (err) logger.log('ERROR', JSON.stringify(err), err.stack);
});
10-9.对 Node.js 使用 Riak
问题
您希望能够在 Node.js 应用中利用高度可伸缩的分布式数据库 Riak。
解决办法
Riak 是为分布式系统的高可用性而设计的。它被设计为快速和可伸缩的,这使得它非常适合许多 Node.js 应用。在清单 10-25 中,您将再次创建一个数据存储,它将创建、更新和检索您的产品数据。
注意 Riak 目前在 Windows 机器上不支持。下面的实现应该可以在 Linux 或 OSX 上工作。
清单 10-25 。通过 Node.js 使用 Riak
/**
* RIAK
*/
var db = require('riak-js').getClient();
db.exists('products', 'huaraches', function(err, exists, meta) {
if (exists) {
db.remove('products', 'huaraches', function(err, value, meta) {
if (err) console.log(err);
console.log('removed huaraches');
});
}
});
db.save('products', 'flops', { name: 'flip flops', description: 'super for your feet', price: 12.50}, function(err) {
if (err) console.log(err);
console.log('flip flops created');
process.emit('prod');
});
db.save('products', 'flops', { name: 'flip flops', description: 'fun for your feet', price: 12.00}, function(err) {
if (err) console.log(err);
console.log('flip flops created');
process.emit('prod');
});
db.save('products', 'huaraches', {name: 'huaraches', description: 'more fun for your feet', price: 20.00}, function(err) {
if (err) console.log(err);
console.log('huaraches created');
process.emit('prod');
db.get('products', 'huaraches', function(err, value, meta) {
if (err) console.log(err);
console.log(value);
});
});
process.on('prod', function() {
db.getAll('products', function(err, value, meta) {
if (err) console.log(err);
console.log(value);
});
});
它是如何工作的
为了创建这个解决方案,您从使用$ npm install riak-js安装的 Node.js 模块 riak-js 开始。然后你通过使用getClient()方法连接到服务器。这种方法在不使用的情况下,会发现默认的客户端运行在本地机器上,但是也可以用 options 对象进行配置。
现在,您通过使用 db 对象连接到了 Riak 实例。首先,当你遇到db.exists(<bucket>, <key>, callback)时,你看到 API 是简洁的。如果这个键存在于 Riak Node 上的 bucket 中,这个回调将返回 true 值。如果存储桶键确实存在,您只需指向该存储桶键并使用db.remove()方法就可以删除该特定数据集。
接下来,使用db.save方法将一些数据保存到 Riak Node。该方法接受一个桶、一个键和一个您希望为桶键设置的值。这可以是一个 JavaScript 对象、一个数字或一个您希望为键值存储的字符串。与 riak-js 的所有请求一样,您也可以访问回调函数。回调有三个值:一个错误(如果发生的话)、一个作为 riak-js 方法的结果传递的值和一个元对象。
在您将 huaraches 保存到产品存储桶之后,您可以通过使用db.get()功能来检索这个密钥。同样,这个方法使用桶和键来确定您希望检索 Node 上的哪些数据。回调可以包含与数据相关联的值和元。使用 riak-js 还有一种方法可以访问数据。这用于检索给定存储桶的所有值。回调中的结果值将是一个与桶相关联的数据数组。
您已经使用 riak-js 与 Node.js 中的 riak 集群进行了交互。Riak 是一个强大的分布式数据库解决方案,除了这些简单的任务之外,还可以通过类似的 API 使用 map 和 reduce 函数执行更复杂的搜索。为此,您可以通过运行以下命令来搜索您的 Node 中的所有产品(参见清单 10-26 )。
清单 10-26 。使用 riak-js 减少地图
db.mapreduce
.add('products')
.map(function(v) {
return [Riak.mapValuesJson(v)[0]];
})
.run(function(err, value, meta) {
console.log(value);
});
十一、在 Node.js 中测试
测试让您安心地知道您编写的代码实际上正在执行它想要做的操作。Node.js 为编写某种形式的单元测试提供了本机实现,Node.js 社区已经创建了几个健壮的库来帮助您进行测试驱动的开发过程。
在本章中,您将研究如何为 Node.js 应用决定一个测试框架,并且您还将看到各种各样的技术和测试机制,您可以使用它们来创建一个完全经过测试的应用。一旦您构建了 Node.js 应用所需的测试,您将学习如何报告您已经创建的测试并将它们集成到您的工作流中。
当您创建 Node.js 测试实现时,有很多选项可供您使用。在本章中,您将有机会看到一些框架的小样本,包括以下内容:
- Node.js 断言
- Node 单元
- 摩卡
- 柴网
- 誓言. js
- 应该. js
本章中使用的那些在社区中更为突出,但无论如何这不是一个详尽的列表。本章旨在向您提供足够的信息,以便为 Node.js 应用创建一个完整的测试套件,但是我将尝试对您最终可能选择的框架保持不可知的态度。
11-1.选择测试驱动开发或行为驱动开发的框架
您希望在 Node.js 开发过程中实现测试。要做到这一点,您需要找到满足您测试需求的最佳测试解决方案。
当你选择一个测试框架时,你应该考虑的第一件事是你要做的测试的类型。有两个通用的类别来描述测试风格,它们可以被分成更小的类别。
首先,有经典的测试驱动开发(TDD) 。TDD 是有用的,因为你接受一个代码需求,为需求的那个方面写一个失败的测试,然后创建允许测试通过的代码。TDD 的演变是行为驱动开发(BDD)。 BDD 不仅采用可测试的代码功能单元,还采用可测试的业务用例,保护代码和最终用户的体验。
您的框架选择不仅取决于您喜欢的测试风格,还取决于您希望遇到的测试类型。您可能希望仅对应用中的关键值进行一些简单的断言测试。您可能希望提供一个代码覆盖率总结,让您知道可以在哪里重构并从应用中删除无关的代码。
在本章中,您将看到许多实现,每个实现都提供了 Node.js 中测试的独特方式。这不是一个详尽的列表,但旨在帮助您了解如何选择一个适合您的测试框架。
有些与异步代码无缝协作;其他人在大规模测试项目中表现更好。这些决定取决于您,即开发人员,来决定哪一个适合您的需要。应该选择这些框架中的任何一个还是都不选择,都不如将测试引入工作流并构建一个 Node.js 应用重要,因为您知道该应用已经过很好的测试。
11-2.使用 Node.js 断言模块创建测试
问题
您希望利用 Node.js 的本机 assert 模块来测试您的 Node.js 应用。
解决办法
在 Node.js 中创建基本测试有许多选项。许多测试的基础都源自 Node.js assert 模块。这个模块在内部用于 Node.js 测试,是 TDD 的一个很好的模型。
清单 11-1 展示了如何用 Node.js 断言模块实现一组基本的测试。
清单 11-1 。Node.js 断言基础
var assert = require('assert');
/*11-2*/
var three = 3;
assert.equal(three, '3', '3 equals "3"');
assert.strictEqual('3', three.toString(), '3 and "3" are not strictly equal');
assert.notEqual(three, 'three', '3 not equals three');
// assert.ok(false, 'not truthy ');
assert.ok(true, 'truthy');
Node.js 断言模块也可以用来测试异步代码,如清单 11-2 所示。
清单 11-2 。测试异步平方一个数字的函数
var squareAsync = function(a, cb) {
result = a * a;
cb(result);
};
var assert = require(‘assert’);
squareAsync(three, function(result) {
assert.equal(result, 9, '3 squared is nine');
});
squareAsync('three', function(result) {
assert.ok(isNaN(result), '"Three squared is NaN');
});
通常在 Node.js 中,您创建的代码是异步的,如上所示。然而,有时您可能有一些想要测试的同步性质的代码。你可以按照清单 11-3 中的例子用断言模块测试同步代码。
清单 11-3 。与 Node.js 断言同步
var square = function(a) {
if (typeof a !== 'number') return false;
return a * a;
};
var assert = require('assert');
assert.equal(square(three), 9, '3 squared equals 9');
assert.equal(square('three'), false, 'should fail because "three" is not a number');
它是如何工作的
使用 require('assert ')导入的 Node.js 断言测试模块用于内部 Node.js 测试,并已成为许多第三方测试实现的基础。assert 模块是以这样一种方式创建的,如果不满足测试需要的特定条件,它将抛出一个断言错误。如果全部通过,Node.js 进程将按预期退出。
在此解决方案中,您看到了可以通过使用 assert 模块的应用编程接口(API) 的一小部分来验证真值或假值、等式和非等式。API 能够断言的完整列表如表 11-1 所示。
表 11-1 。断言模块的 API 方法
| 方法 | 描述 |
|---|---|
| assert.deepEqual(实际、预期、消息) | 深度相等的测试。这将测试' === '运算符以外的内容,并检查比较中的日期、正则表达式、对象和缓冲区。 |
| assert.doesNotThrow(块,[错误],[消息]) | 期望提供的代码块不会引发错误。 |
| assert.equal(实际、预期、消息) | 使用“==”运算符测试实际值是否等于预期值。 |
| assert.fail(实际、预期、消息、运算符) | 通过显示消息以及由运算符分隔的实际值和期望值来引发断言异常。 |
| assert.ifError(值) | 测试该值是否不为假,如果为假,则引发断言异常。这用于测试回调是否提供了错误参数。 |
| assert.notDeepEqual(实际,预期,消息) | 深度非质量测试。这超出了测试的范围!== '运算符,并在比较中检查日期、正则表达式、对象和缓冲区。 |
| assert.notEqual(实际,预期,消息) | 使用“!= '运算符。 |
| assert.notStrictEqual(实际,预期,消息) | 与相同。notEqual 的不同之处在于,它将这些值与“!== '运算符。 |
| assert.ok(值,消息) | 测试传递的值是否为真值。否则,消息将被记录为断言异常。 |
| assert.strictEqual(实际、预期、消息) | 与相同。等于,不同之处在于它使用“===”运算符来比较这些值。 |
| assert.throws(块,[错误],[消息]) | 期望提供的块引发错误。 |
11-3.使用 Node 单元创建测试
问题
您希望利用 nodeunit 单元测试模块来测试 Node.js 应用。
解决办法
nodeunit 测试模块建立在 assert 模块的 API 之上,如清单 11-4 所示。
清单 11-4 。使用 Node 单元进行测试
var test = require('nodeunit');
module.exports = {
'11-2': {
'equal': function(test) {
test.equal(3, '3', '3 equals "3"');
test.done();
},
'strictEqual': function(test) {
test.strictEqual('3', 3, '3 and "3" are not strictly equal');
test.done();
},
'notEqual' : function(test) {
test.notEqual(3, 'three', '3 not equals three');
test.done();
},
'ok' : function(test) {
test.ok(false, 'not truthy ');
test.done();
}
}
};
清单 11-5 展示了利用 nodeunit 测试框架来创建异步方法测试的解决方案。
清单 11-5 。Node 单元异步测试
var squareAsync = function(a, cb) {
result = a * a;
cb(result);
};
module.exports = {
'11-4': {
'squareAsync': function(test) {
test.expect(2);
squareAsync(three, function(result) {
test.equal(result, 9, 'three squared is nine');
});
squareAsync('three', function(result) {
test.ok(isNaN(result), 'squaring a string returns NaN');
});
test.done();
}
}
};
Nodeunit 还允许同步测试(如清单 11-6 所示)。
清单 11-6 。Node 单元同步测试
var square = function(a) {
if (typeof a !== 'number') return false;
return a * a;
};
var test = require('nodeunit');
module.exports = {
'11-3': {
'squareSync': function(test) {
test.equal(square(three), 9, 'three squared is nine');
test.equal(square('three'), false, 'cannot square a non number');
test.done();
}
}
};
它是如何工作的
Nodeunit 提供了与 assert 模块相同的 API,并增加了两个方法。首先,添加。done()方法,它告诉 nodeunit 您已经完成了那个测试,是时候进行下一个测试了。nodeunit 带来的第二个附加功能是。期望(金额)法。该方法允许您指示 nodeunit 您计划实现多少测试,并确保所有测试都已执行。如果您已经指示 nodeunit 使用 expect(2)运行两个测试,并且只运行了一个,那么测试将失败,并显示消息:Error: Expected 2 assertions, 1 ran.您将在下一节中看到,能够使用 expect()和 done()方法提供了一种测试异步代码的好方法。
要用 nodeunit 实现测试,您需要从一个模块中导出测试。正如您之前看到的,您首先需要 nodeunit 模块,然后导出您的测试。这种导出的格式可以帮助您对测试进行分组。您可以在解决方案中看到,您将本部分的所有测试分组到了 11-2 标题下。然后您分别创建每个测试,调用每个测试的断言,然后调用test.done()。
要调用 nodeunit 测试,必须先用npm install –g nodeunit安装 nodeunit。然后瞄准你想测试的模块。如果您使用一个名为 nodeunit.js 的文件,您将调用$ nodeunit nodeunit.js,测试将执行并产生一个类似于您在清单 11-7 中看到的结果,带有复选标记来表示通过测试,带有“X”表示失败。
清单 11-7 。Node 单元结果
nodeunit.js
✔ 11-2 - equal
✔ 11-2 - strictEqual
✔ 11-2 - notEqual
✖ 11-2 – ok
11-4.用 Mocha 创建测试
问题
您需要利用 Mocha 为 Node.js 应用创建测试。
解决办法
用于编写 Node.js 测试的一个著名框架是 Mocha 。如何使用它的一个例子显示在清单 11-8 中。
清单 11-8 。使用 Mocha 框架
var assert = require('assert');
var three = 3;
describe('11-2', function() {
describe('#equal', function() {
it('should return true that 3 equals "3"', function() {
assert.equal(three, '3', '3 equals "3"');
})
})
describe("#strictEqual", function() {
it('"3" only strictly equals 3.toString()', function() {
assert.strictEqual('3', three.toString(), '3 and "3" are not strictly equal');
})
})
describe("#notEqual", function() {
it('should be that 3 is not equal to three', function() {
assert.notEqual(three, 'three', '3 not equals three');
})
})
describe("#ok", function() {
it('should return that false is not truthy', function() {
assert.ok(false, 'not truthy ');
})
})
describe("#ok", function() {
it('should be true that true is truthy', function() {
assert.ok(true, 'truthy');
})
})
});
Mocha 也可以测试异步代码,如清单 11-9 中的所示。
清单 11-9 。Mocha 异步测试
var squareAsync = function(a, cb) {
result = a * a;
cb(result);
};
squareAsync(three, function(result) {
result.should.equal(9, 'three squared is nine');
});
squareAsync('three', function(result) {
// NaN !== NaN
result.should.not.equal(result);
});
有时,您会想要测试同步编写的代码。Mocha 可以像这样测试代码,如清单 11-10 所示。
清单 11-10 。摩卡同步测试
var square = function(a) {
if (typeof a !== 'number') return false;
return a * a;
};
describe('11-3 sync', function() {
describe('square a number', function() {
it('should do this syncronously', function() {
square(three).should.equal(9);
});
it('should fail when the target is not a number', function() {
square('three').should.be.false;
});
});
});
它是如何工作的
Mocha 是一个测试框架,可以利用您选择的任何断言模块。在这个例子中,您利用 Node.js assert 模块来管理您的简单断言。然而,为了让这些断言工作,您必须将您的断言包装在特定于 Mocha 的描述中。这允许您命名您的测试套件和特定案例,以便从您的测试中看到想要的输出。
为测试创建描述的方法是将它们包装在函数“describe”中。这个函数将接受一个名称和一个回调函数。测试套件将单独的测试用例嵌套在其回调函数中。一般来说,这应该看起来像清单 11-11 中的例子。
清单 11-11 。用 Mocha 构建测试套件
describe(‘Test suite’, function() {
describe(‘test-case’, function() {
// tests go here
});
});
Mocha 中的测试可以包含对预期结果的描述,当您遇到断言错误时会用到它。语法读起来几乎像一个句子,正如你在清单 11-12 中看到的,在那里你描述了测试应该得到什么结果,并且测试本身包含在回调中。测试描述只有在测试失败时才会出现。
清单 11-12 。测试失败描述
it('should return true that 3 equals "3"', function() {
assert.equal(three, '3', '3 equals "3"');
});
一旦通过$ npm install –g mocha在全球范围内安装了 Mocha,有两种主要的方法可以让 Mocha 执行测试。可以直接在一个文件上调用 Mocha,通过调用$ mocha filename.js。如果您正在测试一个单独的测试文件,这是很有价值的,但是如果您想要测试多个文件,您可以简单地创建一个测试/目录,并将您的 Mocha 测试文件移动到该目录中。一旦你完成了这些,只需调用$ mocha,它就会找到那个目录并遍历它,执行遇到的测试。
11-5.用 Chai.js 创建测试
问题
您希望使用 Chai.js 测试框架创建测试。
解决办法
包含 Node.js assert 模块 API 的测试框架的另一个例子是 Chai.js 测试框架。清单 11-13 中的显示了相同的基本测试结构的一个例子。
清单 11-13 。使用 Chai.js 进行测试
/**
* chaijs
*/
var chai = require('chai');
/* 11-2 simple */
var assert = chai.assert;
var three = 3;
assert.equal(three, '3', '3 equals "3"');
assert.strictEqual('3', three.toString(), '3 and "3" are not strictly equal');
assert.notEqual(three, 'three', '3 not equals three');
assert.ok(true, 'truthy');
//assert.isFalse(true, 'true is false');
var expect = chai.expect;
expect(three).to.be.a('number');
var should = chai.should();
three.should.be.a('number');
接下来,您可以使用 Chai.js 框架创建异步测试。这些显示在清单 11-14 中。
清单 11-14 。Chai.js 异步测试
var squareAsync = function(a, cb) {
result = a * a;
cb(result);
};
squareAsync(three, function(result) {
result.should.equal(9, 'three squared is nine');
});
squareAsync('three', function(result) {
// NaN !== NaN
expect(result).to.not.eql(result);
});
清单 11-15 显示了如何使用 Chai.js 实现同步测试。
清单 11-15 。Chai.js 同步测试
var square = function(a) {
if (typeof a !== 'number') return false;
return a * a;
};
var chai = require('chai');
var should = chai.should();
square(three).should.equal(9);
square('three').should.be.false;
它是如何工作的
Chai.js 可以作为测试驱动,附带任意 Node.js 断言模块。通过安装 Chai.js 模块并将其导入本地,开始使用 Chai 进行测试。然后,您可以用 Chai.js 提供的几种风格中的任何一种来构建您的断言。
Chai.js 为 BDD 风格的断言提供了两种方法。首先是“期望”风格。expect 风格创建了一个类似句子的结构来描述你对代码的期望。您可以构建一个如下所示的语句:expect('cheese').to.be.a('string');。当您遇到像expect(3).to.be.a('string'), or expect(3).to.equal(6);这样的语句时,这种类型的测试会抛出断言错误。
BDD 风格的第二种类型是‘应该’。“Should”在许多方面类似于“expect”模块,尽管实现“should”模块需要您执行“should”函数chai.should(),而不是像在chai.expect中那样引用函数。这是因为当您调用‘expect’时,您调用的是您正在测试的对象的函数:expect(three)。在“应该”风格中,你从你正在测试的对象开始,然后描述应该发生的行为。
如表 11-2 所示,‘should’和‘expect’样式共享相同的 API。它们还提供了一组非测试性的方法,用于以可链接的方式构建类似句子的结构。这些方法是 to,be,been,is,that,and,have,with,at,of,some。
表 11-2 。“应该”和“期望”模块的 API 方法
| 方法 | 描述 |
|---|---|
| 。高于(值) | 断言目标大于值。 |
| 。争论 | 目标必须是 arguments 对象。 |
| 。a(类型) | 确定前面的对象应为“type”类型。 |
| 。低于(值) | 目标小于值。 |
| 。closeTo(预期,增量) | 目标值在给定的期望值加上或减去一个差值的范围内。 |
| 。包含(值) | 断言前面包含给定的值,即 expect(' cheese ')to . contain(' he ')。 |
| 。深的 | 这应该与下面的 equal 或 property 方法一起使用。将对断言执行深度相等检查。 |
| 。空的 | 断言目标的长度为 0,或者对象中没有可枚举的键。 |
| 。eql(值) | 目标必须深度相等。 |
| 。相等(值) | 目标必须严格相等。如果深度方法在它之前,则执行深度相等检查。 |
| 。存在 | 目标不能为空或未定义。 |
| 。错误的 | 目标必须是假的。 |
| 。包括(值) | 断言前面包含给定的值,即 expect([1,2,4]). include(2);。 |
| 。instanceof(构造函数) | 目标必须是构造函数的实例。 |
| 。它自己 | 设置供 respondTo 方法稍后使用的自身标志。 |
| 。按键(按键 1,[按键 2],…,[按键]) | 目标正好包含所提供的键。 |
| 。最少值 | 目标值必须大于或等于该值,..is.at.least(值)。 |
| 。长度(值) | 目标必须有长度值。 |
| 。匹配(正则表达式) | 目标匹配正则表达式。 |
| 。成员(集) | 目标具有集合的相同成员,或者是集合的超集。 |
| 。most(值) | 目标小于或等于该值。 |
| 。不 | 否定链中后面的任何断言。 |
| 。空 | 目标必须为空。 |
| 。好的 | 确保目标是真实的。 |
| .ownProperty(name) | 目标将有自己的属性名。 |
| 。属性(名称,[值]) | 目标将具有属性名,并且可选地,该值必须匹配。 |
| 。响应者(方法) | 目标将响应该方法。 |
| 。满足(方法) | 目标通过给定的真值测试。 |
| 。字符串(字符串) | 字符串目标必须包含字符串。 |
| 。throw(构造函数) | 函数目标应该抛出一个异常。 |
| 。真实的 | 目标必须是真的。 |
| 。不明确的 | 目标必须是未定义的。 |
| 。在(开始,完成)范围内 | 目标必须在开始和结束范围之间。 |
同样在这个例子中,Chai.js 断言模块中使用了经典的 TDD 风格。这个模块类似于 Node.js assert 模块,但是它将更多的方法合并到 assert API 中,如表 11-3 所示。
表 11-3 。Chai.js 断言
| 方法 | 描述 |
|---|---|
| 。closeTo(实际值,期望值,差值,[消息]) | 实际值将在预期值的差值范围内。 |
| 。deepEqual(实际,预期,[消息]) | 实际值必须与预期值相等。 |
| 。deepProperty(对象,属性,[消息]) | 对象必须包含属性,并且可以深度嵌套。 |
| 。deepPropertyNotVal(对象,属性,值,[消息]) | 对象必须包含深度属性,但值不是值。 |
| 。deepPropertyVal(对象,属性,值,[消息]) | 对象必须包含值为的深度属性。 |
| 。doesNotThrow(函数,[构造函数/正则表达式],[消息]) | 作为构造函数或匹配正则表达式的实例,函数不会出错。 |
| 。相等(实际,预期,[消息]) | 实际值必须不严格等于(==)预期值。 |
| 。失败(实际、预期、[消息]、[运算符]) | 抛出一个失败。 |
| 。包括(干草堆、针、[消息]) | 干草堆里有一根针。这可以用于字符串(包含)或数组。 |
| 。includeMembers(超集,子集,[消息]) | 子集必须包含在超集中。 |
| 。instanceOf(对象,构造函数,[消息]) | 对象必须是构造函数的实例。 |
| 。isArray(值,[消息]) | 该值必须是数组。 |
| 。isBoolean(值,[消息]) | 该值必须是布尔值。 |
| 。isDefined(值,[消息]) | 该值不得未定义。 |
| 。isFalse(值,[消息]) | 该值必须为假。 |
| 。isFunction(值,[消息]) | 该值必须是函数。 |
| 。isNotArray(值,[消息]) | 该值不能是数组。 |
| 。isNotBoolean(value,[message]) | 该值不能是布尔值。 |
| 。isNotFunction(值,[消息]) | 该值不能是函数。 |
| 。isNotNull(值,[消息]) | 该值不得为空。 |
| 。isNotNumber(值,[消息]) | 该值不能是数字。 |
| 。isnotobject(值,[消息]) | 该值不能是对象。 |
| 。isNotString(值,[消息]) | 该值不能是字符串。 |
| 。isNull(值,[消息]) | 该值必须为空。 |
| 。isNumber(值,[消息]) | 该值必须是一个数字。 |
| 。isObject(值,[消息]) | 该值必须是对象。 |
| 。isString(值,[消息]) | 该值必须是字符串。 |
| 。isTrue(值,[消息]) | 该值必须为真。 |
| 。I 未定义(值,[消息]) | 该值必须未定义。 |
| 。lengthOf(对象,长度,[消息]) | 对象必须具有给定的长度。 |
| 。匹配(值,正则表达式,[消息]) | 该值必须与正则表达式匹配。 |
| 。notDeepEqual(实际,预期,[消息]) | 实际值不能等于预期值。 |
| 。notDeepProperty(对象,属性,[消息]) | 对象不得包含深度属性。 |
| 。notEqual(实际,预期,[消息]) | 实际值必须是非严格的 in equal(!idspnonenote)值。=)到预期。 |
| 。未包括(干草堆、针、[消息]) | 干草堆里没有针。 |
| 。notInstanceOf(对象,构造函数,[消息]) | 对象不能是构造函数的实例。 |
| 。notMatch(value,regexp,[message]) | 该值不得与正则表达式匹配。 |
| 。notOk(对象,[消息]) | 对象必须不是真实的。 |
| 。notProperty(对象,属性,[消息]) | 对象不得包含属性。 |
| 。notStrictEqual(实际,预期,[消息]) | 实际值必须严格不相等(!idspnonenote)。==)到预期。 |
| 。notTypeOf(值,名称,[消息]) | 该值不是一种名称类型。 |
| 。确定(对象,[消息]) | 对象必须是真实的。 |
| 。操作员(val1,操作员,val2,[消息]) | val1 与 val2 的关系由运算符决定,即 operator(3,'>',0)。 |
| 。propertyNotVal(对象,属性,值,[消息]) | 对象必须包含属性,但值不是值。 |
| 。属性(对象,属性,[消息]) | 对象必须包含属性。 |
| 。propertyVal(对象,属性,值,[消息]) | 对象必须包含值为的属性。 |
| 。sameMembers(集合 1,集合 2,[消息]) | 集合 1 和集合 2 必须具有相同的成员。 |
| 。strictEqual(实际,预期,[消息]) | 实际值必须严格等于(===)预期值。 |
| 。throws(函数,[构造函数/字符串/正则表达式],[字符串/正则表达式],[消息]) | 该函数将作为构造函数的实例出错,或者它将抛出一个带有匹配 regexp/string 的错误。 |
| 。typeOf(值,名称,[消息]) | 该值必须是类型名称。 |
11-6.创建带有誓言的测试
问题
您需要使用誓言测试框架为 Node.js 应用创建测试。
解决办法
誓言是一个受 Node.js 开发人员欢迎的测试框架,因为它很好地配合了 Node.js 的异步特性。你可以在清单 11-16 中看到使用这个框架的基本解决方案。
清单 11-16 。使用 waves . js 进行测试
var vows = require('vows'),
assert = require('assert');
var three = 3;
vows.describe('vows testing').addBatch({
'11-2': {
'basic testing': {
topic: three,
' 3 equals "3"': function(topic) {
assert.equal(three, '3');
},
' 3 and "3" are not strictly equal': function(topic) {
assert.strictEqual('3', three.toString());
},
' 3 notEqual "three"' : function(topic) {
assert.notEqual(three, 'three');
},
' false is truthy? ' : function(topic) {
assert.ok(false);
},
' true is truthy? ' : function(topic) {
assert.ok(true);
}
}
}
}).export(module);
清单 11-17 。誓言. js 同步测试
var square = function(a) {
if (typeof a !== 'number') return false;
return a * a;
};
var vows = require('vows'),
assert = require('assert');
vows.describe('vows testing').addBatch({
'11-3': {
'sync': {
topic: function(a) { return 3 * 3; },
'squared': function(topic) {
assert.equal(topic, 9);
}
}
}
});
它是如何工作的
誓言可以说是与之前的测试框架风格最大的不同。誓言是为测试异步代码而构建的测试框架,因此它非常适合 Node.js。
在誓言中,以及在你使用誓言的解决方案中,你从描述一个测试套件开始。在我们的例子中,这是通过调用vows.describe(<suite name>)创建的“誓言测试”套件。然后,将一组测试分组到一个批处理中,并通过调用。addBatch()方法,它接受一个对象文字:测试批次。
然后将这些批次分组;在这种情况下,创建了 11-2 组,并用“基本测试”进一步细分。接下来,您将测试分成单独的主题。这里你处理变量“三”,它指向值 3。在主题中,您构建您的测试,首先描述您希望看到的结果,然后通过将主题传递给回调函数来执行测试。在这个回调中,您利用 Node.js assert 模块来验证您的测试。使用誓言的这个解决方案的测试输出可以通过两种方式生成,如清单 11-18 中的所示。
清单 11-18 。誓言输出选项
$ vows vows.js
···?·
11-2 basic testing
? false is truthy?
» expected expression to evaluate to true, but was false // vows.js:24
? Broken » 4 honored ∙ 1 broken
$ vows vows.js --spec
? vows testing
11-2 basic testing
√ 3 equals "3"
√ 3 and "3" are not strictly equal
√ 3 notEqual "three"
? false is truthy?
» expected expression to evaluate to true, but was false // vows.js:24
√ true is truthy?
? Broken » 4 honored ∙ 1 broken
简单地调用誓言方法将运行测试,但是通过在调用中添加- spec 参数,您能够获得更详细的结果输出。然后,它会打印出“兑现”誓言的数量:测试通过,或者失败,意味着测试失败。
在 Node.js 开发中,使用这些框架中的任何一个来设置测试都不应该是一个限制。这些框架应该成为您开发周期的一部分,并允许您构建一个更加成熟和可靠的 Node.js 应用。在接下来的部分中,您将看到如何测试同步方法的某些点,以及更常见的如何将这些测试框架融入到使用 Node.js 构建的异步应用中。
11-7.用 Should.js 创建测试
问题
您已经创建了一个 Node.js 应用,您需要使用 Should.js 框架为其创建测试。
解决办法
Should.js 是一个测试框架,它使用非常友好和可链接的 BDD 语法进行测试。使用 Should.js 的一个基本例子显示在清单 11-19 中。
清单 11-19 。使用 Should.js
var should = require('should');
var three = 3;
should.equal(three, '3', '3 equals "3"');
should.strictEqual('3', three.toString(), '3 and "3" are not strictly equal');
should.notEqual(three, 'three', '3 not equals three');
true.should.be.ok;
false.should.not.be.ok;
three.should.be.a('number');
Should.js 也可以用于异步和同步测试,如清单 11-20 和清单 11-21 所示。
清单 11-20 。Should.js 异步测试
var squareAsync = function(a, cb) {
result = a * a;
cb(result);
};
squareAsync(three, function(result) {
result.should.equal(9, 'three squared is nine');
});
squareAsync('three', function(result) {
// NaN !== NaN
result.should.not.equal(result);
});
清单 11-21 。Should.js 同步测试
var square = function(a) {
if (typeof a !== 'number') return false;
return a * a;
};
var should = require('should');
square(three).should.equal(9);
square('three').should.be.false;
它是如何工作的
当您开始使用 Should.js 进行测试时,您会注意到它是作为 Node.js assert 模块的扩展开始的。这意味着您可以像平常一样实现 assert API 的所有项目。Should.js 还扩展了这个模块,以包含一个名为。exists()与.ok()相同。
Should.js 和 Chai.js 的“Should”风格一样,允许测试链接。这不仅有助于更好地描述行为,还允许测试框架成为一段自文档化的代码。可链接语法允许链接方法,如 an、and、be、have 和 with。这些方法实际上并不操作测试,但是便于链接和可读性。Should.js 的 API 包含表 11-4 中所示的方法。
表 11-4 。Should.js API
| 方法 | 描述 |
|---|---|
| 。a | 目标应该是声明的类型。 |
| 。超过 | 目标必须有一个高于期望值的数值。 |
| 。争论 | 目标是一个类似于 arguments 数组的对象。 |
| 。在下面 | 目标值必须低于预期值。 |
| 。空的 | 目标的长度必须为 0。 |
| 。网站 | 检查是否相等。 |
| 。平等的 | 具有严格的平等性。 |
| 。错误的 | 目标必须=== false。 |
| 。标题(字段,[值]) | 目标必须是带有可选值相等检查的指定字段。 |
| 。超文本标记语言 | 目标必须是“text/html,charset=utf-8”。 |
| 。include SQL(obj) | 对象“obj”必须存在并且相等。 |
| 。包括(对象) | “obj”对象必须存在于的索引中。适用于字符串、数组和对象。 |
| .instanceOf | 目标应该是声明的的实例。 |
| 。数据 | 目标必须是“application/json,charset=utf-8”。 |
| 。键 | 目标必须具有指定的键。 |
| 。长度 | 目标必须是指定的长度。 |
| 。比赛 | 目标必须与正则表达式匹配。 |
| 。好的 | 目标必须是真实的。 |
| 。自有财产 | 目标必须指定自己的属性。 |
| 。财产 | 目标必须包含指定的属性。 |
| 。状态(代码) | 目标状态代码必须是指定的。 |
| 。扔过来。投掷误差 | 确保引发异常。如果您的 JavaScript Linter 不能友好地解析。扔命名法。 |
| 。真实的 | 目标必须=== true。 |
| 。在…之内 | 目标应该在一个数字范围内。 |
可以看到这个 API 类似于 Chai.js 框架使用的‘should’和‘expect’API。这是因为这种类型的测试的链接语法有助于 API 中的许多特性。
在清单 11-6 中,您使用 assert 模块语法构建了一些简单的测试。然后通过使用可链接的 Should.js 语法进入 BDD 模型:true.should.be.ok;,就是一个例子。
11-8.使用 Node 覆盖报告代码覆盖
问题
您已经为 Node.js 应用构建了一些测试。现在,您想分析您编写的代码是否会被 node-cover 完全覆盖。
解决办法
有几个库可用于报告 Node.js 应用中的代码覆盖率。对于这个解决方案,您将检查其中之一:Node 覆盖。这个库可以使用 npm: $ npm install cover –g进行全局安装。一旦安装了它,您就可以在 Node.js 应用上运行覆盖率测试和报告。
在本节中,您将创建一个简单的 Node.js 应用,它将有目的地测试您的代码覆盖率,并有助于突出 Node.js 覆盖率库的优势。这个示例代码如清单 11-22 所示。
清单 11-22 。代码覆盖率示例应用
/**
* coverage tests
*/
function add(a, b, callback) {
callback(a + b);
}
function subtract(a, b, callback) {
callback(a - b);
}
add(2, 3, function(result) {
console.log(result);
});
//subtract(3, 2, function(result) {
// console.log(result);
// });
您现在可以开始对这个代码块进行代码覆盖测试了。首先,安装 node-cover,然后运行覆盖率测试,如清单 11-23 所示。
清单 11-23 。安装 Node 覆盖并运行覆盖测试
$ npm install -g cover
$ cover run 11-5-1.js
现在,您已经成功地为您的代码执行了覆盖测试。使用 node-cover,您可以通过两种方式查看该覆盖率的报告。首先,清单 11-24 展示了如何在命令提示符/终端中直接查看覆盖率。
清单 11-24 。终端中的 Node 覆盖报告
> cover run 11-5-1.js
path.existsSync is now called `fs.existsSync`.
5
在终端中查看报告后,您可能会决定,为了更好地查看结果,或者将结果发布给对您的报道感兴趣的其他方,您应该以 HTML 模式查看报告。Node 覆盖使 HTML 中的报告变得简单,如清单 11-25 所示。在这里,该模块将生成一个 cover-html 目录,并允许您在浏览器中导航到/cover_html/index.html,以便查看如图图 11-1 和图 11-2 所示的报告。
清单 11-25 。报告 HTML 中的 Node 覆盖范围。
$ cover report html

图 11-1 。Node _ 覆盖 index.html

图 11-2 。Node _ 封面细节
注意由于字体渲染的问题,在 Windows 上的命令提示符、Cygwin、Git Bash 或 PowerShell 中查看报告充其量只能算是模糊的。如果你知道你在寻找什么,你可以破译的结果,但最好是查看 HTML 报告或通过'平原'选项与$ cover report plain。
它是如何工作的
测试代码覆盖率很重要,因为它可以突出应用中不必要或未充分使用的代码。在这个解决方案中,您看到了如何利用 Node 覆盖框架来查看您的代码覆盖率。
node-cover 框架是全局安装的,允许您在您的终端或 shell 中使用 cover 方法,无论您的 Node.js 应用驻留在哪里,也无论您希望在哪里运行覆盖率测试。要运行测试,使用命令$ cover run <file>。
运行之后,您现在拥有了对代码覆盖率的引用。要查看该访问,您需要通过命令$ cover results [type] [file]查看结果。类型可以是“cli”,这是默认设置,并尝试在您的终端中生成 ASCII 图形布局。这在 Mac 或 Linux 上工作得很好,但是对于 Windows 用户来说,命令行界面(CLI)的输出几乎是无法阅读的。幸运的是,还有其他三种输出选择。第一个是 plain,它只输出代码中没有覆盖的一行或多行。对于更多的图形报告,您可以使用“html”选项,这将生成一个完整的报告,可以在浏览器中查看。或者,如果您希望管理自己的实现,或者希望在自定义报告服务中查看数据,您可以为报告选择 JSON 输出类型。JSON 输出包含了报告的所有相关细节,以及执行覆盖率测试的代码的原始源代码。
11-9.伊斯坦布尔报告代码覆盖率
问题
您希望利用伊斯坦布尔代码覆盖框架来分析代码的覆盖范围。
解决办法
有几个库可用于报告 Node.js 应用中的代码覆盖率。对于此解决方案,您将研究伊斯坦布尔。
在本节中,您将创建一个简单的 Node.js 应用,它将有目的地测试您的代码覆盖率,并有助于突出 Node.js 覆盖率库的优势。这个示例代码如清单 11-26 中的所示。
清单 11-26 。代码覆盖示例应用
/**
* coverage tests
*/
function add(a, b, callback) {
callback(a + b);
}
function subtract(a, b, callback) {
callback(a - b);
}
add(2, 3, function(result) {
console.log(result);
});
//subtract(3, 2, function(result) {
// console.log(result);
// });
您将用来显示这组代码的代码覆盖率的代码覆盖率模块是伊斯坦布尔。伊斯坦布尔是全球安装使用$ npm install –g istanbul。然后你可以为你的模块运行覆盖测试,如清单 11-27 所示。
清单 11-27 。伊斯坦布尔覆盖测试
$ istanbul cover 11-5-1.js
5
=============================================================================
Writing coverage object [c:\Users\cgackenheimer\Dropbox\book\code\11\coverage\coverage.json]
Writing coverage reports at [c:\Users\cgackenheimer\Dropbox\book\code\11\coverage]
=============================================================================
=============================== Coverage summary ===============================
Statements : 83.33% ( 5/6 )
Branches : 100% ( 0/0 )
Functions : 66.67% ( 2/3 )
Lines : 83.33% ( 5/6 )
================================================================================
伊斯坦布尔将自动为您生成一个 HTML 报告。它将把它放在 coverage 目录中一个名为 lcov-report 的目录中。上述覆盖率测试的结果可以在浏览器中查看。汇总页面如图 11-3 中的所示,详细覆盖范围如图 11-4 中的所示。

图 11-3 。伊斯坦布尔封面摘要

图 11-4 。伊斯坦布尔封面细节
它是如何工作的
您利用伊斯坦布尔作为一个模块来生成代码覆盖测试。伊斯坦布尔也是全球安装的,以便在整个项目中使用命令行。要用伊斯坦布尔生成覆盖测试,只需运行$ istanbul cover <file> 。这将为您运行测试覆盖率。
当您运行伊斯坦布尔的覆盖测试时,您也可以自动得到报告。没有像 node-cover 那样的辅助步骤来生成测试结果。运行cover命令还将直接在您的 CLI 中生成一个清晰的覆盖率摘要。它还会产生一个 JSON 输出和 HTML。正如你在图 11-4 中看到的,HTML 中的详细概述将突出显示没有用红色标出的陈述。未涵盖的功能以橙色突出显示。
将这些报告添加到您的测试工作流中对于分析您的编码非常有帮助,尤其是当您向您的实现中添加大型模块的时候。如果你已经在重构的过程中,你可能会看到在你的代码覆盖中有大量的缺口。这为下一次迭代提供了一种更明智的重构代码的方式。
11-10.构建完整的测试套件
问题
您希望在您的应用中包含一组测试,以便构建一个健壮的测试套件。
解决办法
在这个解决方案中,您将使用 Mocha 测试框架构建一套要执行的测试。为此,将所有测试构建到测试目录中。对于这个例子,你可以利用前面章节的所有测试,把它们分成几个文件(见清单 11-28 和 11-29 )。
清单 11-28 。第一个测试/Mocha.js 文件
/**
* mocha
*/
var assert = require('assert');
var three = 3;
describe('11-2', function() {
describe('equal', function() {
it('should return true that 3 equals "3"', function() {
assert.equal('three', '3', '3 equals "3"');
});
});
describe("strictEqual", function() {
it('"3" only strictly equals 3.toString()', function() {
assert.strictEqual('3', three.toString(), '3 and "3" are not strictly equal');
});
});
describe("notEqual", function() {
it('should be that 3 is not equal to three', function() {
assert.notEqual(three, 'three', '3 not equals three');
});
});
describe("ok", function() {
it('should return that false is not truthy', function() {
assert.ok(false, 'not truthy ');
});
});
describe("ok", function() {
it('should be true that true is truthy', function() {
assert.ok(true, 'truthy');
});
});
});
清单 11-29 。第二次测试/Mocha.2.js
/**
* mocha
*/
var three = 3;
var should = require('should');
var square = function(a) {
if (typeof a !== 'number') return false;
return a * a;
};
describe('11-3 sync', function() {
describe('square a number', function() {
it('should do this syncronously', function() {
square(three).should.equal(9);
});
it('should fail when the target is not a number', function() {
square('three').should.be.false;
});
});
});
var squareAsync = function(a, cb) {
result = a * a;
cb(result);
};
describe('11-4 async', function() {
describe('square a number', function() {
it('should perform async', function() {
squareAsync(three, function(result) {
result.should.equal(9);
});
});
it('should fail', function() {
squareAsync('three', function(result) {
result.should.not.be.a('number');
});
});
});
});
它是如何工作的
这是一个简单的测试套件,但是它展示了像 Mocha 这样的工具的威力,它允许您利用一个命令来执行整个测试文件夹。
当您执行mocha命令时,它将查找位于测试目录中的文件,并对每个文件执行 Mocha 测试。mocha命令将只在为至少一个 Mocha 测试格式化的文件上执行。这意味着,一个包含许多只使用 Node.js assert 方法测试代码的文件的目录,仍然只对包含 Mocha 测试的文件运行测试。不要求整个文件只包含摩卡测试;事实上,为了让 Mocha 命令行找到文件并执行测试,将结果添加到聚合总数中,文件中只需要出现一个mocha describe。
11-11.在您的工作流程中实施测试
问题
在开发 Node.js 应用时,您希望将测试作为工作流的一部分来实现。
解决办法
将测试添加到您的工作流程中可以通过多种方式完成。重要的是,您能够将测试添加到您的过程中。实现测试的一种方法是,每次为 Node.js 应用创建新代码时,简单地调用您选择的测试框架。这可以是一个手动过程,或者,正如您将在下一节中看到的,是一个自动化任务。
实现测试的另一种方法是在 package.json 文件的脚本部分添加一个测试命令,如清单 11-30 所示。
清单 11-30 。npm 测试的 Package.json
{
"name": "Ch11",
"version": "0.0.0",
"description": "chapter 11",
"main": "mocha.js",
"directories": {
"test": "test"
},
"dependencies": {
"chai": "∼1.7.2",
"nodeunit": "∼0.8.1",
"should": "∼1.2.2",
"vows": "∼0.7.0"
},
"devDependencies": {
"mocha": "∼1.12.0"
},
"scripts": {
"test": "mocha"
},
"author": "cgack"
}
在这里,每当您想要运行您的测试时,您可以简单地调用$ npm test。这允许您对每个想要测试的模块进行统一的测试调用,而不管底层的测试基础设施如何。
它是如何工作的
你在本章前面已经看到了如何使用你选择的框架来执行测试,所以在你的工作流程中实现它是简单的,就像调用框架一样。
添加使用npm test测试模块的能力是一个有用的技巧。这是因为 npm 命令行能够解析您的测试目录,并使用您为描述 Node.js 模块的 npm package.json 文件提供的测试脚本。
在工作流中实现测试的方法有很多,但是,正如您将在下一节中看到的,自动化测试是确保您不会错过测试代码机会的最简单的方法。
11-12.自动化您的测试
问题
您需要自动化您的测试,这样您就可以始终确保您的 Node.js 在每次对代码进行更改时都得到测试并通过。
解决办法
有许多方法可以自动化您的测试解决方案。如果你正在使用摩卡,最简单的方法之一就是使用摩卡手表功能。为此,调用 Mocha,如清单 11-31 所示。
清单 11-31 。摩卡手表
$ mocha -w
............
11 passing (9 ms)
1 failing
1) calc tests simple maths should be easy to subtract:
AssertionError: four minus 2 is two
at Context.<anonymous> (c:\Users\cgackenheimer\Dropbox\book\code\11\test\calc_test.js:14:11)
at Test.Runnable.run (c:\Users\cgackenheimer\AppData\Roaming\npm\node_modules\mocha\lib\runnable.js:211:32)
at Runner.runTest (c:\Users\cgackenheimer\AppData\Roaming\npm\node_modules\mocha\lib\runner.js:355:10)
at c:\Users\cgackenheimer\AppData\Roaming\npm\node_modules\mocha\lib\runner.js:401:12
at next (c:\Users\cgackenheimer\AppData\Roaming\npm\node_modules\mocha\lib\runner.js:281:14)
at c:\Users\cgackenheimer\AppData\Roaming\npm\node_modules\mocha\lib\runner.js:290:7
at next (c:\Users\cgackenheimer\AppData\Roaming\npm\node_modules\mocha\lib\runner.js:234:23)
at Object._onImmediate (c:\Users\cgackenheimer\AppData\Roaming\npm\node_modules\mocha\lib\runner.js:258:5)
at processImmediate [as _immediateCallback] (timers.js:330:15)
............
12 passing (16 ms)
\ watching
另一种测试自动化方法是利用 Grunt.js 任务运行器来驱动测试。与 Grunt 更流行的集成之一是 grunt-contrib-nodeunit 模块。这允许您用一个命令将所有的 Node 单元测试分组并一起运行。
要配置 Grunt nodeunit,您需要安装带有$ npm install –g grunt-cli的 Grunt 命令行。然后,您将通过输入$ npm install grunt-contrib-nodeunit来安装grunt-contrib-nodeunit模块。接下来,您需要在 package.json 文件中添加对 Grunt 和 grunt-contrib-nodeunit 的引用。安装完这些依赖项后,将 Grunt 安装到名为$ npm install grunt –save-dev的项目中。
现在您需要创建一个 Gruntfile.js 文件,该文件将导出您希望 Grunt 执行的任务。一个简单的 Grunt 文件用于您之前创建的 nodeunit 测试,看起来类似于清单 11-32 中的例子。
清单 11-32 。Node 单元的 Gruntfile.js
module.exports = function(grunt) {
grunt.initConfig({
nodeunit: {
all: ['nodeunit.js']
}
});
grunt.loadNpmTasks('grunt-contrib-nodeunit');
};
您现在可以通过运行grunt nodeunit命令来执行任何 Node 单元测试,如清单 11-33 所示。
清单 11-33 。正在执行 grunt Node 单元
$ grunt nodeunit
Running "nodeunit:all" (nodeunit) task
Testing nodeunit.js......OK
>> 8 assertions passed (7ms)
Done, without errors.
它是如何工作的
使用 Mocha 命令行的内置文件监视器来监视文件,最初会运行一次测试,然后监视目录中的文件是否有任何更改。
在这个解决方案中,您首先创建了一个 calc.js 模块(参见清单 11-34 ),该模块错误地将一个乘法运算符放在了减法方法中减法运算符所在的位置。
清单 11-34 。错误的 calc.js 模块
/**
* calc
*/
var calc = module.exports = {
add: function(a, b) {
return a + b;
},
subtract: function(a, b) {
return a * b;
}
};
您在您编写的测试中调用了mocha –w,watcher,期望减法能够工作,但是他们在减法测试中失败了。观察目录中的文件是否有变化。然后,您需要修改文件,以从 a 中减去 b,文件监视器会发现这一变化。一旦文件发生变化,Mocha 再次运行测试——这次通过了——并继续观察文件的变化。拥有这种对测试的持续反馈是非常有益的,它可以帮助激励你首先编写测试。
接下来,您看到了如何通过将这些测试自动化到 Grunt.js 任务运行器框架来将 Node 单元测试集成到您的测试工作流中。这里的关键是您能够设计您的 gruntfile.js,以便您能够覆盖一个目录中的所有测试或者适合某个文件结构的测试。在您的案例中,您提供了一个文件;然而,nodeunit 的 Grunt 任务配置可以接受各种命名的测试,不仅仅是这里看到的“all ”,还可以将它们分解成子模块。nodeunit 配置还允许实现通配符,因此您可以使用' test/*来定位测试目录中的所有文件。js '或类似的模式。
让测试成为 Node.js 开发过程中自然和自动化的一部分是至关重要的,尤其是在项目规模增长的时候。您已经看到了自动实现测试的两种方法,随着项目规模的增长,这减轻了运行测试的负担。
十二、调试和部署应用
在这本书里,你已经看到了很多你能够用 Node.js 产生的东西,你已经创建了一个应用,或者你正在设想一个你想要发布的应用。这意味着您需要熟悉将代码部署到多个目的地的过程。除了部署您的代码,当遇到 V8 调试模块时,您将重温并扩展第七章中的一些调试技术。
作为 Node.js 开发人员,您可以使用多种方法和平台来部署代码。在本章中,您将研究各种技术,从在您自己的机器上进行自我部署,到在基于云的平台上部署服务解决方案。您还将学习如何向 npm 发布您的模块。
在撰写本文时,有大量基于云的平台作为 Node.js 应用的服务托管解决方案。本章并不是所有可能的托管解决方案的目录,而是着眼于您可以用来托管 Node.js 应用的几种常见且实用的方法。
12-1.将信息记录到控制台
问题
您需要能够通过将信息记录到控制台窗口来收集有关应用的重要信息。
解决办法
调试代码时将信息记录到命令行的最简单方法是使用 Node.js 自带的控制台模块,在这个解决方案中,你可以看到有各种方法可以用来通过使用这个模块将数据记录到控制台,如清单 12-1 所示。
清单 12-1 。记录到控制台
/**
* Logging information to your console.
*/
console.time('timer');
console.log('hey %s', 'there');
console.info('info');
console.warn('warn');
console.error('error');
console.dir(console);
console.trace(console.error('traced'));
console.assert(true !== false, 'true equals false');
console.timeEnd('timer');
它是如何工作的
控制台模块依赖 Node.js 对stdout和stderr的使用。js 源代码中的控制台对象是通过接受这两个流作为参数来创建的(参见清单 12-2 )。
清单 12-2 。实例化控制台
function Console(stdout, stderr) {
if (!(this instanceof Console)) {
return new Console(stdout, stderr);
}
if (!stdout || !util.isFunction(stdout.write)) {
throw new TypeError('Console expects a writable stream instance');
}
if (!stderr) {
stderr = stdout;
}
var prop = {
writable: true,
enumerable: false,
configurable: true
};
prop.value = stdout;
Object.defineProperty(this, '_stdout', prop);
prop.value = stderr;
Object.defineProperty(this, '_stderr', prop);
prop.value = {};
Object.defineProperty(this, '_times', prop);
// bind the prototype functions to this Console instance
Object.keys(Console.prototype).forEach(function(k) {
this[k] = this[k].bind(this);
}, this);
}
一旦对象被实例化,stderr和stdout属性通过使用Object.defineProperty()方法被转换成控制台对象本身的属性。
在您的解决方案中,您利用了控制台对象上公开的每个核心方法。首先,也是最常见的,是console.log()方法。这个方法接受任意数量的参数,使用清单 12-3 中的模块将它们格式化,并将结果写入stdout。console.info方法与console.log方法相同,并以同样的方式格式化参数。
清单 12-3 。Console.log 作为 stdout.write
Console.prototype.log = function() {
this._stdout.write(util.format.apply(this, arguments) + '\n');
};
Console.prototype.info = Console.prototype.log;
您将遇到的下一个控制台方法是console.error()方法 。这与console.warn()法法相同。这些方法类似于console.log()和console.info()方法,不同之处在于它们写入stderr而不是stdout,,如清单 12-4 所示。
清单 12-4 。控制台.错误和控制台.警告
Console.prototype.warn = function() {
this._stderr.write(util.format.apply(this, arguments) + '\n');
};
Console.prototype.error = Console.prototype.warn;
接下来你利用console.dir()方法 。这个函数使用util.inspect()方法来描述传递给它的对象(见清单 12-5 )。在这种情况下,您检查控制台对象本身。
清单 12-5 。Console.dir()
Console.prototype.dir = function(object) {
this._stdout.write(util.inspect(object, { customInspect: false }) + '\n');
};
调用console.dir(console);的输出如清单 12-6 中的所示。
清单 12-6 。Console.dir(控制台);
{ log: [Function],
info: [Function],
warn: [Function],
error: [Function],
dir: [Function],
time: [Function],
timeEnd: [Function],
trace: [Function],
assert: [Function],
Console: [Function: Console] }
接下来,运行console.trace() 将任何错误记录到stderr堆栈跟踪中。该方法的源代码如清单 12-7 所示。您可以看到,一旦更改了名称以证明它是一个“跟踪”,就会通过console.error()方法记录错误;。
清单 12-7 。Console.trace()源
Console.prototype.trace = function() {
// TODO probably can to do this better with V8's debug object once that is
// exposed.
var err = new Error;
err.name = 'Trace';
err.message = util.format.apply(this, arguments);
Error.captureStackTrace(err, arguments.callee);
this.error(err.stack);
};
您将使用的最后一个控制台方法是console.assert()方法。这个方法利用 assert 模块来记录assert.ok()方法的结果。你可以在清单 12-8 中看到它的内部工作原理。
清单 12-8 。Console.assert()
Console.prototype.assert = function(expression) {
if (!expression) {
var arr = Array.prototype.slice.call(arguments, 1);
require('assert').ok(false, util.format.apply(this, arr));
}
};
12-2.使用图形调试工具
问题
您希望使用一种工具,这种工具允许您在类似集成开发环境(IDE)的调试工具中调试 Node.js 应用。这意味着您希望对 Node.js 调试环境进行图形化访问。
解决办法
Node.js 应用最基本也是最一致的跨平台调试工具之一是名为 node-inspector 的npm包。这个包用来绑定基于 Blink 的浏览器自带的调试器,比如 Google Chrome 或者最新版本的 Opera。然后,您将可以访问这些浏览器的本机调试工具,但它将允许您在类似 IDE 的设置中导航代码和调试 Node.js 应用。
要开始使用 node-inspector,通过npm全局安装它。这使您可以访问 Node 检查器命令,这将允许您启动调试器监听您机器上的任何目录,如清单 12-9 所示。
清单 12-9 。Node 检查器的安装和实现
$ npm install -g node-inspector
$ node-inspector
Node Inspector v0.3.1
info - socket.io started
Visit http://127.0.0.1:8080/debug?port=5858 to start debugging.
检查器运行后,您可以开始 Node.js 应用的新实例。对于这个例子,假设一个简单的 HTTP 服务器用文本“hello world”进行响应。当您启动这个 Node.js 应用时,您需要通过向命令传递参数–debug 来启用调试挂钩。这看起来就像你在清单 12-10 中看到的一样。
清单 12-10 。通过创建服务器开始 Node 检查
$ node --debug 12-2-1.js
debugger listening on port 5858
它是如何工作的
现在您已经有了一个调试器监听正确的端口,并且 inspector 正在运行,您可以通过导航到http://127.0.0.1:8080/debug?port=5858来体验使用图形调试器的乐趣。这是 Node 检查器打开 Blink 开发工具的地方,允许您调试代码。这个初始界面将类似于图 12-1 中的所示。

图 12-1 。Node 检查器初始状态
现在,您可以从这里打开 Sources 面板,查看可用的源文件。这在 图 12-2 中显示;您可以选择要调试的文件。

图 12-2 。Node 检查器中的“源”面板
一旦你选择了你的文件,你现在可以在 Node.js 中看到为该文件运行的整个源代码。这一点已经打破,检查器为您提供了调用堆栈和范围变量以及其他重要信息,可以通过调试器界面访问这些信息。

图 12-3 。Node 检查器查看源文件
最后,node-inspector 还通过提供悬停信息,让您能够访问源代码中对象的更多细节。这些信息为您提供了被检测物体的更多细节(见图 12-4 )。

图 12-4 。使用 Node 检查器将鼠标悬停在对象上
12-3.在生产环境中调试应用
问题
您有一个运行在生产服务器上的 Node.js 应用,它的行为方式会提示您进入代码并查看发生了什么。为了调试你的应用,你需要在生产过程中附加一个调试器。
解决办法
连接到当前正在运行的 Node.js 进程内置于 Node.js 调试功能中。这将把 V8 调试器附加到您的应用,就像用 debug 标志启动您的应用一样。
首先需要运行 Node.js 流程。对于这个例子,您可以启动一个简单的 HTTP 服务器,它是您在 12-2 节中为调试而创建的。一旦运行了这个,就需要在运行这个 Node.js 进程的操作系统上找到进程标识符。您可以通过活动监视器、Windows 上的任务管理器或使用 grep - $ ps ax | grep node.的 shell 来实现这一点
一旦你有了进程标识符,你就可以告诉调试器连接到进程,如清单 12-11 所示。
清单 12-11 。将调试器连接到 Node.js 进程
$ node debug -p 6284
connecting... ok
debug>
现在您有了 V8 调试器提示符,它连接到您的 Node.js 进程(显示在您的原始命令行上,如清单 12-12 所示)。
清单 12-12 。调试器已连接
$ node 12-2-1.js
Starting debugger agent.
debugger listening on port 5858
一旦您连接到进程,从debug>提示符中,您可能想知道您的应用的当前状态驻留在进程的哪个点上。你可以通过使用debug> pause暂停应用,或者进入debug> s或者下一个debug> n来完成。
现在,您可以在生产环境中调试您的应用,以查明您在应用中遇到问题的地方。
它是如何工作的
使用 Node.js 调试器调试在第七章中有详细介绍,使用命令$ node debug –p <pid>绑定到 Node.js 进程会让你访问在第七章中看到的相同命令。这是运行调试终端的 V8 调试器。
当你开始调试时,如上所述,你可以通过键入debug> n看到你在代码中的位置(见清单 12-13 )。
清单 12-13 。V8 调试器的下一步
connecting... ok
debug> n
break in net.js:1153
1151
1152 function onconnection(clientHandle) {
1153 var
handle = this;
1154 var self = handle.owner;
1155
这个清单只是一个例子,说明您在代码中的位置。在您的情况下,您可能在应用或导入到应用的模块内部的任何地方。然后你可以通过键入debug> c继续执行你的程序,如清单 12-14 所示。这个例子表明,通过继续运行,您能够运行代码,直到它遇到debugger;命令。这个命令告诉 V8 调试器停止执行并等待。
清单 12-14 。继续调试
debug> c
break in c:\Users\cgackenheimer\Dropbox\book\code\12\12-2-1.js:7
5
6 var server = http.createServer(function(req, res) {
7 debugger;
8 res.end('hello world');
9 });
您现在可以调试您的应用了,并且您不需要使用传递给它的debug命令来启动 Node.js。您只需要找到正在运行的进程,并使用进程标识符将 V8 调试器绑定到应用。
12-4.永远持续运行您的服务器
问题
您希望确保 Node.js 进程能够持续运行,并且能够在生产机器出现意外故障后重新启动。
解决办法
为了保持 Node.js 进程运行,作为开发人员,您有几种选择。在此解决方案中,您将研究如何利用 forever 模块来保持您的流程的活力和持续运行。
要开始使用,您必须首先安装 forever 模块。模块源位于https://github.com/nodejitsu/forever,可以作为全局命令行模块安装,如清单 12-15 所示。
清单 12-15 。永远安装
$ npm install -g forever
从命令行永久安装后,现在就可以利用命令行界面(CLI)运行 Node.js 进程了。为了使用 forever 模块启动 Node.js 进程,必须使用forever start命令启动脚本,如清单 12-16 所示。
清单 12-16 。永远开始
$ forever start app.js
warn: --minUptime not set. Defaulting to: 1000ms
warn: --spinSleepTime not set. Your script will exit if it does not stay up for at least 1000ms
info: Forever processing file: app.js
然后,您可以通过使用forever list命令来查看任何正在运行的应用的状态(参见清单 12-17 )。
清单 12-17 。永久列表
$ forever list
info: No forever processes running
# Once a task is running.
$ forever list
info: Forever processes running
data: uid command script forever pid logfile uptime
data: [0] UJVS /usr/local/bin/node 12-2-1.js 41659 41660 /Users/gack/.forever/UJVS.log 0:0:2:46.801
data: [1] VVX7 /usr/local/bin/node app.js 41757 41758 /Users/gack/.forever/VVX7.log 0:0:0:3.444
您可能已经注意到一些警告,指出您没有指定spinSleepTime或minUptime永久选项。要设置这些标志,必须在告诉 forever 开始之前设置它们。这将类似于清单 12-18 中所示的例子,其中您指定最小正常运行时间和自旋睡眠时间各为 20 毫秒。
清单 12-18 。设置永久标志
$ forever --minUptime 10 --spinSleepTime 10 start app.js
info: Forever processing file: app.js
它是如何工作的
Forever 是一个 Node.js 模块,它确保您告诉它执行的脚本将持续运行。当您使用 forever 启动一个应用时,您实际上是在创建一个监视器实例,该实例将尝试将您的脚本作为子进程生成。这样做是为了让 forever 能够监控子进程,并在子进程被中断时重新启动进程。启动监视器的过程如清单 12-19 所示,这是监视器的永久来源。
清单 12-19 。启动永久监视器
Monitor.prototype.start = function (restart) {
var self = this,
child;
if (this.running && !restart) {
process.nextTick(function () {
self.emit('error', new Error('Cannot start process that is already running.'));
});
return this;
}
child = this.trySpawn();
if (!child) {
process.nextTick(function () {
self.emit('error', new Error('Target script does not exist: ' + self.args[0]));
});
return this;
}
this.ctime = Date.now();
this.child = child;
this.running = true;
process.nextTick(function () {
self.emit(restart ? 'restart' : 'start', self, self.data);
});
function onMessage(msg) {
self.emit('message', msg);
}
// Re-emit messages from the child process
this.child.on('message', onMessage);
child.on('exit', function (code, signal) {
var spinning = Date.now() - self.ctime < self.minUptime;
child.removeListener('message', onMessage);
self.emit('exit:code', code, signal);
function letChildDie() {
self.running = false;
self.forceStop = false;
self.emit('exit', self, spinning);
}
function restartChild() {
self.forceRestart = false;
process.nextTick(function () {
self.start(true);
});
}
self.times++;
if (self.forceStop || (self.times >= self.max && !self.forceRestart)
|| (spinning && typeof self.spinSleepTime !== 'number') && !self.forceRestart) {
letChildDie();
}
else if (spinning) {
setTimeout(restartChild, self.spinSleepTime);
}
else {
restartChild();
}
});
return this;
};
一旦子进程启动,事件侦听器就会绑定到该进程,侦听事件。当发出“退出”事件时,forever 将检查进程是否在旋转,然后重新启动进程。这是 forever 的基础:捕获退出的进程并不断地重新启动它们。
在构建您的解决方案时,您可以看到一些用于操纵 forever 执行方式的参数。可与 forever 一起使用的可用参数和选项的完整列表显示在表 12-1 中。
表 12-1 。永久模块的选项和参数
| 名字 | 类型 | 描述 |
|---|---|---|
| 清洁日志 | 行动 | 删除所有永久日志文件。 |
| 清除 | 行动 | 遥控 set 键。 |
| 列添加 | ||
| 行动 | 将指定列添加到永久列表输出中。 | |
| 列 rm | ||
| 行动 | 从永久列表输出中移除指定的列。 | |
| 列集 | ||
| 行动 | 为永久列表输出设置所有列。 | |
| 开始 | 行动 | 启动 Node.js 脚本。 |
| 配置 | 行动 | 列出所有永久配置。 |
| 目录 | 行动 | 列出所有正在运行的进程。 |
| 日志 | 行动 | 列出所有永久进程的日志文件。 |
| 日志 |


浙公网安备 33010602011771号