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 。npmjs.org/[上的 npm 搜索](npmjs.org/)
或者,您可以利用 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 安装软件包时,也可以设置一个全局标志。通过将软件包设置为全局安装,该用户的任何应用都可以访问它,因为它安装在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 所示。您指定的版本可以是显式编写的,也可以是将安装最新版本的缩写版本。
清单 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 所示。
清单 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( )参数为整个套接字进行配置。在上面的示例中,它被设置为 utf-8 字符串的默认值,但是可以被更改为任何有效的编码类型。将该设置更改为每个有效类型会导致不同的输出,如清单 2-33 所示。
清单 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 对象。``