NodeJS-入门指南-二-

110 阅读19分钟

NodeJS 入门指南(二)

原文:Beginning Node.js

协议:CC BY-NC-SA 4.0

四、Node.js 包

任何可以用 JavaScript 编写的应用最终都会用 JavaScript 编写。

—杰夫·阿特伍德的《阿特伍德定律》

正如我们在第三章中看到的,core Node.js 并没有提供大量的库。这是有充分理由的。将某些东西作为 core Node.js 的一部分发布可能会扼杀竞争和思想的发展。因此,core Node.js 试图限制它包含的内容,并依赖开源社区来描绘完整的画面。让开源社区找到问题 X 的最佳解决方案比开出万能的解决方案要好。

JavaScript 开发人员的数量比任何其他编程语言都多。此外,越来越多的人正在编写越来越多的库来完成浏览器中的任何给定任务,所有这些工作都可以在应用中使用。

为了方便在应用中使用第三方 JavaScript 库,Node.js 自带了一个名为Node Package Manager(NPM)*的包管理系统。*在本章中,我们将讨论如何在我们的应用中使用 NPM 包。我们将讨论每个 Node.js 开发人员都应该知道的几个重要问题。我们也将把这作为一个实践机会来学习更多关于 JavaScript 如何工作的知识。

重访 Node 模块

在前一章中,我们了解到有三种 Node.js 模块:基于文件的模块、核心模块和外部 node_modules。我们讨论了基于文件的模块和核心模块,现在我们将看看 node_modules *。*为了更好地理解它们,让我们更深入地看看 Node.js require函数的文件系统扫描顺序。

  • 如果传入 require 函数的模块名带有前缀。/'或'../'或'/',那么它被认为是一个基于文件的模块,文件被加载,正如我们在第三章中看到的。一些示例调用:require('./bar)require('../bar/bar')require('/full/path/to/a/node/module/file')
  • 否则,我们寻找具有相同名称的核心模块,例如,如果调用是require('bar'),则为'bar'。如果没有找到与这个名字匹配的核心模块,我们就寻找一个叫做'bar'node_module

扫描 Node 模块

我们先来看一个例子。如果一个文件/home/ryo/project/foo.js有一个 require 调用require('bar'),Node.js 按照下面的顺序扫描文件系统中的 node_modules。返回找到的第一个bar.js

  • /home/ryo/project/ node_modules/bar.js
  • /home/ryo/ node_modules/bar.js
  • /home/ node_modules/bar.js
  • /node_modules/bar.js

换句话说,Node.js 在当前文件夹中查找'node_modules/bar.js',然后是每个父文件夹,直到它到达当前文件的文件系统树的根,或者直到找到一个bar.js。一个简单的例子是模块foo.js加载模块node_modules/bar.js,如清单 4-1 和清单 4-2 所示。

清单 4-1 。hello/foo.js

var bar = require('bar');
bar(); // hello node_modules!

清单 4-2 。hello/node_modules/bar.js

module.exports = function () {
    console.log('hello node_modules!');
}

如你所见,我们的模块bar.js看起来完全一样,如果我们只是使用基于文件的模块。这是故意的。基于文件的模块和 node_modules 之间唯一的区别是扫描文件系统以加载 JavaScript 文件的方式*。其他所有行为都是一样的。*

基于文件夹的模块

在我们讨论 node_modules 机制的所有优点之前,我们需要学习 Node.js require函数支持的最后一个代码组织技巧。几个文件为同一个目标工作的情况并不少见。将这些文件组织到单个模块中是有意义的,该模块可以通过单个 require 调用来加载。我们讨论过将这样的文件组织到一个单独的文件夹中,并在第三章中用一个index.js来代表这个文件夹。

这种情况很常见,Node.js 明确支持这种机制。也就是说,如果模块的路径解析为一个文件夹(而不是一个文件),Node.js 将在该文件夹中查找一个index.js文件,并将其作为模块文件返回。这在一个简单的例子(chapter4/folderbased/indexbased1)中得到演示,在这个例子中,我们使用一个index.js导出两个模块bar1.jsbar2.js,并在一个模块foo中加载模块栏(并隐式地加载bar/index.js),如清单 4-3 所示(运行node folderbased/indexbased1/foo.js)。

清单 4-3 。从文件夹中隐式加载 index.js 的(代码:Folder base/index based 1)

// bar/bar1.js
module.exports = function () {
    console.log('bar1 was called');
}

// bar/bar2.js
module.exports = function () {
    console.log('bar2 was called');
}

// bar/index.js
exports.bar1 = require('./bar1');
exports.bar2 = require('./bar2');

// foo.js
var bar = require('./bar');
bar.bar1();
bar.bar2();

如前所述,基于文件的模块和 node_modules 之间唯一的区别是扫描文件系统的方式。因此,对于像require('./bar')这样的调用,node_modules 的相同代码将是简单地将bar文件夹移动到node_modules/bar文件夹中,并将需求调用从require('./bar')更改为require('bar')

这个例子存在于chapter4/folderbased/indexbased2文件夹中(运行node folderbased/indexbased2/foo.js)。由于调用现在解析到node_modules/bar文件夹,Node.js 寻找node_modules/bar/index.js,既然找到了,那就是为require('bar')返回的内容。(参见清单 4-4 。)

清单 4-4 。从 node_modules/module 文件夹隐式加载 index.js(代码:Folder base/index base 2)

// node_modules/bar/bar1.js
module.exports = function () {
    console.log('bar1 was called');
}

// node_modules/bar/bar2.js
module.exports = function () {
    console.log('bar2 was called');
}

// node_modules/bar/index.js
exports.bar1 = require('./bar1');
exports.bar2 = require('./bar2');

// foo.js
var bar = require('bar'); // look for a node_modules module named bar
bar.bar1();
bar.bar2();

node_modules 的 require 调用语义看起来与核心模块完全相同(比较require('fs')require('bar')函数调用)。这是故意的。使用 node_modules 时,您会有一种扩展内置 Node.js 功能的感觉。

在使用 node_modules 时,使用基于文件夹的代码组织是一种常见的策略,也是您应该尽可能做的事情。换句话说,如果只需要一个文件,就不要在 node_modules 文件夹中创建顶级 JavaScript 文件。然后,用一个node_modules/bar/index.js文件代替node_modules/bar.js

Node 模块的优势

我们现在知道 node_modules 与基于文件的模块是一样的,只是在加载模块 JavaScript 文件时使用了不同的文件系统扫描机制。此时最明显的问题是,“优势是什么?”

简化长文件相对路径

假设您有一个模块foo/foo.js,它提供了许多实用程序,您需要在应用的不同地方使用它们。在区段bar/bar.js中,你会有一个要求呼叫require('../foo/foo.js'),在区段bas/nick/scott.js中,你会有一个要求呼叫require('../../../foo/foo.js')。此时,您应该问自己:“这个foo模块是独立的吗?”如果是这样,这是一个很好的选择,移动到项目文件夹的根目录下的node_modules/foo/index.js。这样你可以简化你的调用,在你的代码中只有require('foo')

增加可重用性

如果你想与另一个项目共享一个模块foo,你只需要复制node_modules/foo到那个项目。事实上,如果你正在处理两个相似的子项目,你可以将node_modules/foo移动到包含两个项目的文件夹中,如清单 4-5 所示。这使您更容易从一个地方维护foo

清单 4-5 。使用共享 node_modules 的子项目代码组织示例

projectroot
   |-- node_modules/foo
   |-- subproject1/project1files
   |-- subproject2/project2files

减少副作用

由于 node_modules 的扫描方式,您可以将模块的可用性限制在代码库的特定部分。这允许你安全地进行部分升级,假设你的原始代码组织如清单 4-6 所示。

清单 4-6 。使用模块 foo 的演示项目

projectroot
   |-- node_modules/foo/fooV1Files
   |-- moduleA/moduleAFiles
   |-- moduleB/moduleBFiles
   |-- moduleC/moduleCFiles

现在,当你正在处理一个新模块(比如说moduleD)需要模块foo的一个新版本(并且向后不兼容)时,你可以简单地组织你的代码,如清单 4-7 所示。

清单 4-7 。模块 foo 的部分升级

projectroot
   |-- node_modules/foo/fooV1Files
   |-- moduleA/moduleAFiles
   |-- moduleB/moduleBFiles
   |-- moduleC/moduleCFiles
   |-- moduleD
          |-- node_modules/foo/fooV2Files
          |-- moduleDFiles

这样,moduleAmoduleBmoduleC继续照常运行,你可以在moduleD中使用新版本的foo

克服模块不兼容性

Node.js 不存在许多传统系统中存在的模块依赖性/不兼容性问题。在许多传统的模块系统中,moduleX不能与moduleY一起工作,因为它们依赖于moduleZ的不同(并且不兼容)版本。在 Node.js 中,每个模块可以有自己的 node_modules 文件夹,不同版本的moduleZ可以共存。模块不需要在 Node.js 中是全局的!

模块缓存和 Node 模块

你可能还记得我们在第三章中的讨论,即require在第一次调用后缓存一个请求调用的结果。原因是您不需要加载 JavaScript 并从文件系统一次又一次地运行它,从而获得了性能提升。我们说过,每次路径将解析到同一个文件时,require都返回同一个对象。

正如我们已经展示的,node_modules 只是扫描基于文件的模块的一种不同方式。因此,它们遵循相同的模块缓存规则。如果你有两个文件夹,其中moduleAmoduleB需要模块 foo,即require('foo'),它存在于某个父文件夹中,如清单 4-8 所示,它们得到相同的对象(在给定的例子中从node_modules/foo/index.js导出)。

清单 4-8 。两个模块获得相同的 foo 模块

projectroot
    |-- node_modules/foo/index.js
    |-- moduleA/a.js
    |-- moduleB/b.js

然而,考虑一下清单 4-9 中所示的代码组织。这里moduleBrequire('foo')调用将解析到moduleB/node_modules/foo/index.js,而moduleA的 require 调用将解析到node_modules/foo/index.js,因此它们没有得到相同的对象。

清单 4-9 。模块 A 和 B 得到不同的 foo 模块

projectroot
    |-- node_modules/foo/index.js
    |-- moduleA/a.js
    |-- moduleB
         |-- node_modules/foo/index.js
         |-- b.js

这是一件好事,因为我们已经看到,它可以防止你陷入依赖问题。但这种脱节是你应该意识到的。

数据

NPM 使用 JSON 文件来配置模块。在我们深入研究 NPM 之前,让我们先来看看 JSON。

JSON 入门

JSON 是一种用于通过网络传输数据的标准格式。在大多数情况下,它可以被视为 JavaScript 对象文字的子集。它基本上限制了哪些 JavaScript 对象被认为是有效的。JSON 对象使规范更容易实现,并保护用户 免受他们需要担心的边缘情况的影响。在这一节中,我们将从实践的角度来看 JSON。

JSON 规范强制实施的限制之一是,您必须对 JavaScript 对象键使用引号。这允许您避免 JavaScript 关键字不能作为对象文字的键的情况。例如,清单 4-10 中的 JavaScript 在 ECMA 脚本 3(JavaScript 的旧版本)中是一个语法错误,因为for是一个 JavaScript 关键字。

清单 4-10 。旧浏览器(ECMAScript 5 之前)中的无效 JS

var foo = { for : 0 }

相反,与所有版本的 JavaScript 兼容的同一个对象的有效表示应该是清单 4-11 中所示的内容。

清单 4-11 。即使在旧浏览器中也有效(ECMAScript 5 之前)

var foo = { "for" : 0 }

此外,JSON 规范限制了给定键的值是 JavaScript 对象的安全子集。值只能是字符串、数字、布尔值(true或 fa lse)、数组、null或其他有效的 JSON 对象。清单 4-12 中的展示了一个 JSON 对象,展示了所有这些。

清单 4-12 。样本 JSON

{
    "firstName": "John",
    "lastName": "Smith",
    "isAlive": true,
    "age": 25,
    "height_cm": 167.64,
    "address": {
        "streetAddress": "21 2nd Street",
        "city": "New York",
        "state": "NY",
    },
    "phoneNumbers": [
        { "type": "home", "number": "212 555-1234" },
        { "type": "fax", "number": "646 555-4567" }
    ],
    "additionalInfo": null
}

firstName值是字符串,age是数字,isAlive是布尔型,phoneNumbers是有效 JSON 对象的数组,additionalInfonulladdress是另一个有效 JSON 对象。这种类型限制的原因是为了简化协议。如果需要将任意 JavaScript 对象作为 JSON 传递,可以尝试将它们序列化/反序列化为一个字符串(常见于日期)或一个数字(常见于枚举)。

另一个限制是最后一个属性不能有多余的逗号。这也是因为旧的浏览器(例如,IE8)对什么是有效的 JavaScript 文字有限制。比如在清单 4-13 中,虽然第一个例子是 Node.js 和现代浏览器中有效的 JavaScript 对象文字,但它不是有效的 JSON。

清单 4-13 。最后一个值后的尾随命令

// Invalid JSON
{
    "foo": "123",
    "bar": "123",
}
// Valid JSON
{
    "foo": "123",
    "bar": "123"
}

重申一下,JSON 基本上只是 JavaScript 对象文字,有一些合理的限制,这些限制只是为了增加实现该规范的便利性,并有助于它作为数据传输协议的普及。

正在 Node.js 中加载 JSON

由于 JSON 是 web 如此重要的一部分,Node.js 已经完全接受它作为一种数据格式,甚至在本地也是如此。可以像加载 JavaScript 模块一样从本地文件系统加载 JSON 对象。每次在模块加载序列中,如果一个file.js未找到,Node.js 寻找一个file.json。如果找到了,它将返回一个表示 JSON 对象的 JavaScript 对象。让我们来看一个简单的例子。创建一个带有单键foo和字符串值的文件config.json(如清单 4-14 所示)。

清单 4-14 。json/filebased/config.js

{
    "foo": "this is the value for foo"
}

现在,让我们将这个文件作为 JavaScript 对象加载到app.js中,并注销键foo的值(如清单 4-15 所示)。

清单 4-15 。json/filebased/app.js

var config = require('./config');
console.log(config.foo); // this is the value for foo

加载 JSON 的简单性解释了为什么 Node.js 社区中如此多的库依赖于使用 JSON 文件作为配置机制。

JSON 全局

网络上的数据传输以字节的形式进行。要将内存中的 JavaScript 对象写到网络上或者保存到文件中,您需要一种方法将该对象转换成 JSON 字符串。JavaScript 中有一个名为JSON的全局对象,它提供了一些实用函数,用于将 JSON 的字符串表示转换为 JavaScript 对象,并将 JavaScript 对象转换为 JSON 字符串,以便通过网络发送或写入文件或进行其他任何操作。Node.js 和所有现代浏览器中都有这个JSON全局变量。

要将 JavaScript 对象转换成 JSON 字符串,只需调用JSON.stringify并将 JavaScript 对象作为参数传入。这个函数返回 JavaScript 对象的 JSON 字符串表示。要将 JSON 字符串转换成 JavaScript 对象,可以使用JSON.parse函数,它只是解析 JSON 字符串并返回一个与 JSON 字符串中包含的信息相匹配的 JavaScript 对象,如清单 4-16 和清单 4-17 所示。

清单 4-16 。json/convert/app.js

var foo = {
    a: 1,
    b: 'a string',
    c: true
};

// convert a JavaScript object to a string
var json = JSON.stringify(foo);
console.log(json);
console.log(typeof json); // string

// convert a JSON string to a JavaScript object
var backToJs = JSON.parse(json);
console.log(backToJs);
console.log(backToJs.a); // 1

清单 4-17 。app.js 的输出

$ node app.js
{"a":1,"b":"a string","c":true}
string
{ a: 1, b: 'a string', c: true }
1

对 JSON 及其与 JavaScript 对象文字的关系的初步理解,将有助于您成为一名成功的 Node.js 开发人员。

新公共管理理论

现在我们知道了如何使用 node_modules 创建可重用的模块。难题的下一部分回答了这个问题,“我如何获得社区与我共享的内容?”

答案:Node 包马槽,爱称 NPM 。如果你按照第一章中的规定安装 Node.js,它不仅在命令行中增加了node,还增加了npm,这只是一个与在线 NPM 注册表(www.npmjs.org/)集成的命令行工具。NPM 截图如图图 4-1 所示。

9781484201886_Fig04-01.jpg

图 4-1 。简单来说,NPM 是一种与社区共享 Node 模块的方式

package.json

NPM 生态系统不可或缺的一部分是一个简单的 JSON 文件,名为 package.json。这个文件对 NPM 有特殊的意义。当你想与世界分享你的模块时,正确地设置它是至关重要的,但是如果你正在使用其他人的模块,它也同样有用。要在当前文件夹中创建 package.json 文件,只需在命令行上运行清单 4-18 中的代码。

清单 4-18 。初始化 package.json 文件

$ npm init

这将询问您几个问题,例如模块的名称及其版本。我倾向于一直按回车直到结束。这将在当前文件夹中创建一个样板文件 package.json,其名称设置为当前文件夹,版本设置为 0.0.0,以及其他一些合理的缺省值,如清单 4-19 所示。

清单 4-19 。默认的 package.json

{
  "name": "foo",
  "version": "0.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC"
}

安装 NPM 包

让我们安装一个模块,例如,下划线(www.npmjs.org/package/underscore)到一个文件夹。要下载最新版本的下划线,只需运行清单 4-20 中的命令。

清单 4-20 。安装 NPM 模块

$ npm install underscore

这将从 npmjs.org 下载最新版本的下划线,并将其放入当前文件夹的node_modules/underscore中。要加载这个模块,您现在需要做的就是进行一个require('underscore')调用。这在清单 4-21 中有所展示,我们加载了下划线库,并简单地将数组的最小元素输出到控制台。

清单 4-21 。使用已安装的模块

// npm/install/app.js
var _ = require('underscore');
console.log(_.min([3, 1, 2])); // 1

我们将在本章的后面看一下下划线和其他流行的 NPM 包;然而,在这一点上的重点是 NPM 命令行工具。

保存依赖关系

无论何时运行npm install,你都有一个可选的命令行标志可用(--save,它告诉 NPM 将关于你安装了什么的信息写入 package.json,如清单 4-22 所示。

清单 4-22 。安装 NPM 模块并更新 package.json

$ npm install underscore --save

如果用–-save运行 install,它不仅会将underscore下载到 node_modules 中,还会更新 package.json 中的依赖项,指向已安装的下划线版本,如清单 4-23 所示。

清单 4-23 。package.json 的更新部分

"dependencies": {
    "underscore": "¹.6.0"
  }

以这种方式跟踪依赖关系有很多好处。首先,只需查看 package.json 就可以很容易地知道您使用的是哪个特定库的发布版本。只要打开他们的 package.json,看看他们所依赖的是什么。

刷新 node_modules 文件夹

要刷新 package.json 中的 node_modules 文件夹,可以运行以下命令:

$ npm install

这只是查看您的 package.json 文件,并下载您的 package.json 中指定的依赖项的新副本。

使用 package.json 的另一个优点是,您现在可以从您的源代码控制机制中排除 node_modules,因为您总是可以通过一个简单的npm install命令从 npmjs.org 获得一个副本。

列出所有依赖项

要查看你已经安装了哪些包,你可以运行npm ls命令,如清单 4-24 所示。

清单 4-24 。清单依赖关系

listing4-24.jpg

移除依赖关系

使用npm rm删除依赖关系。例如,npm rm underscore --save在本地从node_modules中删除下划线文件夹,并修改 package.json 的 dependencies 部分。该命令有一个直观的同义词npm uninstall,因为该命令在安装时是npm install

package.json 在线依赖跟踪

使用 package.json 进行依赖项跟踪的另一个好处是,如果在以后某个时候您决定与世界其他地方共享您的模块(即在npmjs.org共享),您不需要发送依赖项,因为您的用户可以在线下载它们。

如果你的 package.json 设置正确,并且他们安装了你的模块,NPM 会自动下载并安装你的模块的依赖项。看一个简单的例子,让我们安装一个有依赖关系的包(request),如清单 4-25 所示。

清单 4-25 。安装具有大量依赖项的模块

listing4-25.jpg

你可以看到 NPM 不仅安装了request,还下载了request所依赖的许多其他软件包。反过来,这些包中的每一个都可以依赖于其他包(例如,form-data依赖于asynccombined-stream),并且它们得到它们所依赖的包的自己的本地副本(并且将被下载到它们自己的 node_modules 文件夹中,例如,node_modules/request/node_modules/form-data/node_modules/async)。如前所述,由于 Node.js 中的require函数的工作方式,您可以安全地使用依赖于同一模块的不同版本的子模块,因为当使用 NPM 设置时,它们每个都有自己的副本。

语义版本控制

好的 Node.js 包/NPM 遵循语义版本化,这是一个行业标准,应该作为一个好的软件开发实践来遵循。语义学是对意义的研究。语义版本化 意味着以一种版本号具有重要意义的方式来版本化你的软件。关于语义版本化有很多可以说的,但下面是对 Node.js 开发人员的一个简单但实用的解释:

  • 简而言之,Node.js 开发人员遵循三位数版本控制方案 X.Y.Z,其中所有 X、Y 和 Z 都是非负整数。x 是主要版本,Y 是次要版本,Z 是补丁版本。
  • 如果引入了向后兼容的补丁,补丁版本必须递增。
  • 如果引入向后兼容的新功能,次要版本必须递增。
  • 如果引入了向后不兼容的修复/特性/变化,主版本必须递增。

记住这几点,您可以看到包的 1.5.0 版应该可以被 1.6.1 版就地替换,因为新版本应该是向后兼容的(主版本 1 也是如此)。这是好的包装所追求的。

然而,现实情况是,新版本有时不可避免地会引入错误,或者代码以包的最初作者没有预料到的方式被使用。在这种情况下,一些突破性的变化可能会不知不觉地被引入。

NPM / package.json 中的语义版本化

NPM 和 package.json 对语义版本化有很大的支持。你可以告诉 NPM 你想要哪个版本的软件包。例如,下面的代码安装下划线的确切版本 1.0.3:

$ npm install underscore@1.0.3

您可以使用代字号“~”告诉 NPM 您可以接受 1.0 的所有补丁版本:

$ npm install underscore@"~1.0.0"

接下来,要告诉 NPM 您可以接受任何微小的版本更改,请使用“^":

$ npm install underscore@"¹.0.0"

支持的其他版本字符串运算符包括“> =”和“>”,具有直观的数学意义,如“> =1.4.2”。同样的还有“<=” and “每次。

您也可以在 package.json 中使用这些语义版本字符串。例如,下面的package.json告诉 NPM,你的包兼容 1.6.0 版下划线的任何小升级:

"dependencies": {
   "underscore": "¹.6.0"
 }

更新依赖关系

每当您使用--save标志时,NPM 用于更新 package.json 依赖项部分的缺省值是“^”,前面是下载的版本。原因是您应该总是尝试使用主版本号没有改变的最新版本。通过这种方式,你可以免费获得任何新特性和最新的错误修复,并且不应该有任何突破性的变化。

例如,如果运行下面的命令,你会得到 package.json dependencies 部分:

$ npm install request@1.0.0 -save

以下是添加到 package.json 的默认版本字符串:

"dependencies": {
  "request": "¹.0.0"
}

然而 1.0.0 并不是最新发布的request版本。要找到与 package.json 中指定的当前语义版本兼容的最新在线版本(在本例中为¹.0.0),可以运行npm outdated,如清单 4-26 所示。

清单 4-26 。检查软件包的最新版本

$ npm outdated
npm http GET https://registry.npmjs.org/request
npm http 304 https://registry.npmjs.org/request
Package Current Wanted Latest Location
request 1.0.0 1.9.9 2.34.0 request

您可以看到与¹.0.0 兼容的最新版本是¹.9.9,这是基于我们的 package.json 中的语义字符串的想要的版本。它还向您显示有一个更新的版本可用。

要将这些包更新到最新的兼容版本,并将结果保存到您的package.json中(要将版本号与下载的相匹配),您可以简单地运行下面的命令。你更新的 package.json 显示在清单 4-27 中。

$ npm update -save

清单 4-27 。更新的 package.json

"dependencies": {
   "request": "¹.9.9"
}

了解 package.json 和命令npm installnpm rmnpm update--save NPM 标志的基本知识,以及对语义版本化的尊重,是您在项目中管理 NPM 包所需要了解的全部内容。

Global Node.js 包

在 Node.js 中制作命令行实用程序非常简单。如今学习 Node.js 的最常见动机之一是,许多前端项目的管理实用程序都是用 Node.js 编写的。有一些项目可以测试您的 web 前端,将 CoffeeScript 和 TypeScript 等各种新的编程语言编译成 JavaScript 和 Sass、stylus,以及 CSS,缩小您的 JavaScript 和 CSS 等等。jQuery、AngularJS、Ember.js、React 等流行的前端 JavaScript 项目都依赖 Node.js 脚本来管理自己的项目。

js 包的目标是提供命令行工具,你可以从命令行使用它。我们看到的所有 NPM 命令都带有一个可选的-g标志,表示您正在使用全局模块。

记得在第三章中,我们使用了一个实用程序 Browserify 将 Node.js 代码转换成浏览器兼容代码。Browserify 是我们全局安装的一个 Node.js 包(npm install -g browserify)。这将把 browserify 放到命令行上,我们在上一章中使用过。

同样,您可以更新全局软件包(npm update -g package-name)、列出全局软件包(npm ls -g)和卸载软件包(npm rm -g package-name)。例如,要卸载 Browserify,您可以运行npm rm -g browserify

在全局安装模块时,NPM 不会修改您的系统配置。命令行实用程序突然变得可用的原因是因为全局模块被放置在一个位置(例如,Mac OSX 上的/usr/local/bin和 Windows 上的用户漫游配置文件的 NPM 文件夹),在那里它们在命令行上变得可用。

对全局模块使用 require

全局安装的模块并不意味着在您的代码中使用require函数调用,尽管许多支持全局标志的包也支持在您的项目中本地安装(node_modules 文件夹)。如果在本地安装,也就是说,没有–g标志,你可以像我们已经看到的那样,通过require函数来使用它们。一个好的简单的例子是rimraf模块(www.npmjs.org/package/rimraf)。

如果rimraf是全局安装的(npm install -g rimraf),它提供了一个命令行实用程序,你可以使用它跨平台递归地、强制地删除一个目录(实际上是 Unix 命令行行话中的rm -rf)。要在全局安装rimraf后删除一个目录foo,只需运行rimraf foo

要从 Node.js 代码中做同样的事情,在本地(npm install rimraf)安装rimraf,创建一个如清单 4-28 所示的app.js,并运行它(node app.js)。

清单 4-28 。global/rimrafdemo/app.js

var rimraf = require('rimraf');
rimraf('./foo', function (err) {
    if (err) console.log('Error occured:', err);
    else console.log('Directory foo deleted!');
})

为了完整起见,值得一提的是,如果您设置了NODE_PATH环境变量,有一种从全局位置加载模块的方法。但是在使用模块时,?? 强烈反对这样做,你应该在本地使用依赖关系(package.json 和 node_modules)。

Package.json 和 require

我们看到的大多数 package.json 都是为 NPM 设计的。它所做的只是管理我们的依赖关系,并将它们放在 node_modules 中。从这一点开始,require以我们已经展示的方式工作。它在 node_modules 中查找与我们要求require加载的内容相匹配的 JavaScript 文件/文件夹,例如require('foo')中的foo。我们已经展示过,如果它解析到一个文件夹,Node.js 会尝试从那个文件夹加载index.js作为模块加载的结果。

关于require函数还有最后一件事你需要知道。您可以使用 package.json 来重定向require以从一个文件夹加载不同的文件,而不是默认文件(它会查找 index.js)。这是通过使用 package.json 中的main属性来完成的。该属性的值是要加载的 JavaScript 文件的路径。让我们看一个例子并创建一个目录结构,如清单 4-29 所示。

清单 4-29 。演示代码第四章/mainproperty 的项目结构

|-- app.js
|-- node_modules
          |-- foo
               |-- package.json
               |-- lib
                    |-- main.js

main.js是一个简单的文件,它记录到控制台以表明它已经被加载,如清单 4-30 所示。

清单 4-30 。主属性/Node 模块/foo/lib/main.js

console.log('foo main.js was loaded');

在 package.json 中,只需将 main 指向lib文件夹中的main.js:

{
    "main" : "./lib/main.js"
}

这意味着如果有人要访问require('foo'),Node.js 会查看 package.json,看到main属性,然后运行'./lib/main.js'。所以让我们在我们的app.js中要求这个模块。如果你运行它(node app.js,你会看到 main.js 确实被加载了。

require('foo');

值得一提的是,“main”是requirenode可执行文件所关心的唯一的属性。package.json 中的所有其他属性都是针对 NPM / npm可执行的,是专门为包管理设计的。

拥有这个“主”属性的好处是,它允许库开发人员完全自由地设计他们的项目,并保持结构清晰。

通常,人们会将简单的 Node.js 包(可以放在文件中的包)放入一个与包名packageName.js匹配的文件名中,然后创建一个 package.json 来指向该文件名。例如,这就是rimraf所做的——它有一个rimraf.js,这就是 package.json 的main属性所指向的,如清单 4-31 所示。

清单 4-31 。显示主要属性的 rimraf npm 模块中的 package.json

{
  "name": "rimraf",
  "version": "2.2.7",
  "main": "rimraf.js",

... truncated...

模块概述

在这一点上,似乎require有很多事情要做。确实如此,但是在我们看来,它都非常简单,这里有一个总结来证明你已经是 Node.js 模块专家了!假设你require('something')。那么接下来就是 Node.js 遵循的逻辑了:

  • 如果something是核心模块,返回。
  • 如果something是相对路径(以'开头)。/' , '../')返回文件或文件夹。
  • 如果没有,向上寻找每一层的node_modules/filenamenode_modules/foldername,直到找到与something匹配的文件或文件夹。

匹配文件或文件夹时,请按照下列步骤操作:

  • 如果它匹配一个文件名,返回它。
  • 如果它匹配一个文件夹名,并且 package.json 包含 main,则返回该文件。
  • 如果它匹配一个文件夹名并且有一个索引文件,则返回它。

当然,该文件可以是一个file.jsfile.json,因为 JSON 是 Node.js 中的第一个类!对于 JSON,我们返回解析后的 JSON,对于 JavaScript 文件,我们只需执行文件并返回'module.exports'。

这就是全部了。有了这些知识,你就可以打开查看npmjs.org和 Github 上成千上万的开源 Node.js 包。

流行的 Node.js 包

现在我们已经知道了使用 Node.js 包的所有重要细节,让我们来看看几个最流行的包。

下划线

下划线(npm install underscore)是目前 NPM 上最流行的 JavaScript 库。它是依赖项最多的库(依赖于此包的其他包)。

它被称为下划线,因为它在浏览器中使用时会创建一个全局变量' _ '。在 node 中,您可以随意命名从require('underscore')返回的变量,但是习惯上仍然使用var _ = require('underscore')

下划线为 JavaScript 提供了很多函数式编程支持,这在 Ruby 和 Python 等其他语言中也能找到。每个优秀的 JavaScript 开发人员都应该熟悉它。注意,在新版本中,下划线的一些功能被添加到核心 JavaScript 中,但是为了在所有浏览器和 Node.js 上工作,建议您使用下划线,如果只是为了一致性和减少认知负荷的话(这样您一次记住的东西就少了)。

假设我们有一个数组,我们只需要大于 100 的数组。用普通的旧 JavaScript 做这件事看起来很乏味,如清单 4-32 所示。

清单 4-32 。popular/下划线/filter/raw.js

var foo = [1, 10, 50, 200, 900, 90, 40];

var rawResults = []
for (i = 0; i < foo.length; i++) {
    if (foo[i] > 100) {
        rawResults.push(foo[i]);
    }
}
console.log(rawResults);

下划线中的相同代码更简单、更整洁。函数_.filter获取一个数组,将数组的每个元素传递给一个函数(第二个参数),并返回一个包含所有元素的数组,其中第二个函数返回true。这在清单 4-33 中有所展示。

清单 4-33 。popular/下划线/过滤器/us.js

var foo = [1, 10, 50, 200, 900, 90, 40];

var _ = require('underscore');
var results = _.filter(foo, function (item) { return item > 100 });
console.log(results);

在我们继续之前,我们将快速介绍一下函数式编程。函数式编程中的函数有明确定义的数学行为。如果输入相同,输出也将始终相同。这是函数的数学定义,而不是我们作为开发人员通常将术语函数联系在一起的编程构造。作为数学函数的一个简单例子,想想加法。如果foobar相同,那么foo+bar将永远相同。因此+就是我们所说的函数。类似地,JavaScript 函数function add(a,b){return a+b}是一个纯函数,因为输出只有依赖于输入。

纯函数易于理解、遵循,因此易于维护。阻止代码纯粹功能化的是状态。状态是通过变异(修改)对象来维护的。这就是我们在原始示例中所做的。我们正在一个循环中改变rawResults数组。这通常被称为一种强制性的编码或思考方式。但是,在下划线示例中,filter 函数接受两个参数,如果参数相同,结果将始终相同。因此,它是功能

同样,这样做的主要动机是可维护性。如果您知道filter是做什么的,那么从这一行就可以立即看出什么被过滤了。关于函数式编程还有很多可以说的,但是这应该已经足够让你发现更多了。

现在让我们看看下划线中的其他函数。_.map函数获取一个数组,为数组中存储返回值的每个元素调用一个函数作为结果,并返回一个包含所有结果的新数组。它通过一个函数将一个输入数组映射到一个输出数组。例如,假设我们要将数组中的每个元素乘以 2。我们可以使用_.map很简单地做到这一点,如清单 4-34 所示。

清单 4-34 。popular/下划线/map/app.js

// using underscore
var foo = [1, 2, 3, 4];

var _ = require('underscore');
var results = _.map(foo, function (item) { return item * 2 });
console.log(results);

集合中常见的另一个场景是获取除了符合条件的元素之外的所有元素*。对此,我们可以使用_.reject。清单 4-35 中的显示了一个只获取数组中奇数元素的例子。*

清单 4-35 。流行/下划线/拒绝/app.js

var _ = require('underscore');
var odds = _.reject([1, 2, 3, 4, 5, 6], function(num){ return num % 2 == 0; });
console.log(odds); // [1, 3, 5]

要获得数组的最大元素,请使用_.max,要获得最小元素,请使用_.min:

var _ = require('underscore');
var numbers = [10, 5, 100, 2, 1000];
console.log(_.min(numbers)); // 2
console.log(_.max(numbers)); // 1000

这足以让你开始。要了解关于下划线提供的功能的更多信息,请查看位于http://underscorejs.org/的在线文档。

处理命令行参数

我们在第三章的中看到了process.argv。这是一个简单的数组,将所有命令行参数传递给 Node 进程。我们在前一章中承诺,一旦我们了解了 NPM ,我们将会关注一个提供更好命令行处理的库。嗯,在这里。这叫乐观主义者。由于 NPM 上发布了大量的命令行工具,这是下载量最大的软件包之一。

一如既往,使用npm install optimist进行安装。它只是导出一个对象,该对象包含解析后的命令行参数作为argv属性。所以不用process.argv,你只用require('optimist').argv

说够了。我们编码吧。创建一个 JavaScript 文件,简单地记录处理过的参数,如清单 4-36 所示。

清单 4-36 。流行/乐观/app1.js

var argv = require('optimist').argv;
console.log(argv);

如果你现在运行这个,你会注意到类似于清单 4-37 中的输出。

清单 4-37 。popular/optimist/app1.js 的简单运行

$ node app.js
{ _: [],
  '$0': 'node /path/to/your/app.js' }

Optimist 将process.argv数组的前两个成员(分别是node可执行文件和 JavaScript 文件的路径)保留为'$0'。因为我们希望在这个演示中保持我们的输出清晰,所以让我们删除这个属性,这样我们就可以将所有其他内容记录到控制台。为此,修改你的代码,如清单 4-38 所示。

清单 4-38 。popular/optimist/app.js

var argv = require('optimist').argv;
delete argv['$0'];
console.log(argv);

现在,如果您运行该应用,您将获得以下输出:

$ node app.js
{ _: [] }

啊,好多了。属性argv._是所有命令行参数的数组,这些参数不是标志。标志是以减号'-'开头的参数,例如'-f'。让我们运行app.js并传入一组参数,如清单 4-39 所示。

清单 4-39 。使用非标志参数时显示输出

$ node app.js foo bar bas
{ _: [ 'foo', 'bar', 'bas'] }

作为一个用例,考虑一个实现删除文件实用程序的简单场景。如果需要,为了支持接受多个文件进行删除,所有这些文件都将放在'argv._'属性中。

如果我们想支持强制删除(-f)这样的标志,乐观主义者完全支持。您传入的任何简单标志都将成为值设置为trueargv的属性。例如,如果你想检查标志f是否被设置,只需检查argv.f是否真实。乐观主义者甚至支持一些漂亮的快捷方式,如清单 4-40 所示。

清单 4-40 。使用标志时显示输出

$ node app.js -r -f -s
{ _: [], r: true, f: true, s: true }

$ node app.js -rfs
{ _: [], r: true, f: true, s: true }

乐观主义者也支持接受值的标志,比如说,如果你想接受一个超时标志(-t 100)。乐观主义者支持它们,就像支持简单的标志一样。匹配标志名的属性设置在argv(本例中为argv.t)上,值设置为用户传递的值(本例中为100),如清单 4-41 所示。

清单 4-41 。使用带有值的标志时显示输出

$ node app.js -t 100
{ _: [], t: 100 }

$ node app.js -t "la la la la"
{ _: [], t: 'la la la la' }

如您所见,无需任何配置,开箱即可完成大量处理工作。对于大多数需要支持简单标志的情况,这就足够了。

Optimist 还有许多其他选项,允许进行高级配置,例如强制用户传入参数,强制参数为布尔值,列出配置中支持的所有命令行参数,并提供默认参数值。不管你的命令行处理使用什么case,NPM/乐观主义者已经覆盖了你,你肯定应该进一步探索它。

使用时刻处理日期/时间

内置的 JavaScript Date类型相当有限。这对于简单的情况已经足够好了,例如,您可以通过简单的构造函数调用来创建表示当前时间的日期。还有一个构造函数,允许您以想要的分辨率创建日期,例如年、月、日、小时、分钟、秒和毫秒。关于 JavaScript 日期需要注意的一点是,月份是基于 0 索引的。所以一月是 0,二月是 1,以此类推。您可以在清单 4-42 中看到一些创建的日期。

清单 4-42 。popular/moment/rawdate.js

// Now
var now = new Date();
console.log('now is:', now);

// get sections of time
var milliseconds = now.getMilliseconds();
var seconds = now.getSeconds();
var hours = now.getHours();
var minutes = now.getMinutes();
var date = now.getDate();
var month = now.getMonth();
var year = now.getFullYear();

// detailed constructor for a date
var dateCopy = new Date(year, month, date,
                                hours, minutes, seconds, milliseconds);
console.log('copy is:', dateCopy);

// Other dates
// year, month, date
console.log('1 jan 2014:', new Date(2014, 0, 1));
// year, month, date, hour
console.log('1 jan 2014 9am', new Date(2014, 0, 1, 9));

除了 moment ( npm install moment)提供的 JavaScript 基本特性集Date之外,还有很多特性。其核心是,moment 提供了一个函数,可以用来将一个 JavaScript 日期对象包装成一个moment对象。创建力矩对象有很多种方法。最简单的方法是简单地传入一个日期对象。相反,要将 moment 对象转换成 JavaScript 日期,只需调用toDate成员函数。这在清单 4-43 中进行了演示。

清单 4-43 。popular/moment/wrapping.js

var moment = require('moment');

// From date to moment
var wrapped = moment(new Date());
console.log(wrapped);

// From moment to date
var date = wrapped.toDate();
console.log(date);

Moment 提供可靠的字符串解析。解析字符串的结果是一个包装的矩对象。这显示在清单 4-44 中。为了解开包装,我们简单地调用toDate,正如我们已经在清单 4-43 中看到的。

清单 4-44 。popular/moment/parsing.js

var moment = require('moment');

// From string to date
console.log(moment("12-25-1995", "MM-DD-YYYY").toDate());
console.log(moment("2010-10-20 4:30", "YYYY-MM-DD HH:mm").toDate());

moment 提供的另一个伟大特性是日期格式支持(即日期到字符串的转换)。清单 4-45 给出了几个例子。

清单 4-45 。popular/moment/formating . js

var moment = require('moment');

var date = new Date(2010, 1, 14, 15, 25, 50);
var wrapped = moment(date);

// "Sunday, February 14th 2010, 3:25:50 pm"
console.log(wrapped.format('"dddd, MMMM Do YYYY, h:mm:ss a"'));

// "Sun, 3PM"
console.log(wrapped.format("ddd, hA"));

在格式化方面,moment.js提供了很多功能。您甚至可以获得友好的值,如“6 小时后”、“明天上午 9:40”和“上周日晚上 9:40”,如清单 4-46 所示。

清单 4-46 。popular/moment/timeago.js

var moment = require('moment');

var a = moment([2007, 0, 15]); // 15 Jan 2007
var b = moment([2007, 0, 16]); // 16 Jan 2007
var c = moment([2007, 1, 15]); // 15 Feb 2007
var d = moment([2008, 0, 15]); // 15 Jan 2008

console.log(a.from(b)); // "a day ago"
console.log(a.from(c)); // "a month ago"
console.log(a.from(d)); // "a year ago"

console.log(b.from(a)); // "in a day"
console.log(c.from(a)); // "in a month"
console.log(d.from(a)); // "in a year"

moment 提供了许多额外的好东西,希望你现在看到了探索更多的动机,并理解了如何使用它们。

序列化日期

因为我们正在讨论日期,所以让我们讨论一个在序列化日期以保存到 JSON 文件或通过网络发送 JSON 时可以遵循的良好实践。当我们在前面讨论 JSON 时,您可能已经注意到Date不被支持为有效的 JSON 值类型。通过网络传递数据有多种方式,但最简单的是以字符串形式发送。

特定日期字符串就其实际日期值而言的含义因本地区域性而异(例如,月前日期或月前日期),因此最好遵循全球标准。ISO8601 标准特别涉及如何将特定日期表示为字符串。

ISO8601 支持各种格式,但是 JavaScript 本身支持的格式类似于2014-05-08T17:35:16Z,其中日期和时间用相对于 UTC 的同一个字符串表示。因为它总是相对于 UTC,所以与用户时区无关。这是一件好事,因为用户可能与服务器不在同一个时区,而 UTC 是全球时间参考。

如果我们在 JavaScript date 上调用toJSON方法,我们得到的是 ISO8601 格式的字符串。类似地,将这个字符串传递给 JavaScript 日期构造函数会给我们一个新的 JavaScript 日期对象,如清单 4-47 所示。

清单 4-47 。流行/时刻/json.js

var date = new Date(Date.UTC(2007, 0, 1));

console.log('Original', date);

// To JSON
var jsonString = date.toJSON();
console.log(jsonString); // 2007-01-01T00:00:00.000Z

// From JSON
console.log('Round Tripped',new Date(jsonString));

这种支持也在瞬间延续。如果您在一个包装的 moment 对象上调用.toJSON,您会得到与在原始 date 对象上相同的结果。这允许您安全地序列化将日期或时刻对象作为值的对象。

最后值得一提的是,如果任何对象(不仅仅是日期)有一个toJSON方法,那么当JSON.stringify试图将它序列化为 JSON 时,它将被调用。因此,如果我们愿意的话,我们可以用它来定制任何JavaScript 对象的序列化。这在清单 4-48 中的一个简单例子中显示。

清单 4-48 。popular/moment/tojson.js

var foo = {};
var bar = { 'foo': foo };

// Uncustomized serialization
console.log(JSON.stringify(bar)); // {"foo":{}}

// Customize serialization
foo.toJSON = function () { return "custom" };
console.log(JSON.stringify(bar)); // {"foo":"custom"}

自定义控制台颜色

在处理大型 Node.js 项目时,出于监控目的,控制台上会记录相当多的信息,这种情况并不少见。随着时间的推移,这个简单的输出开始变得乏味,这是另一个你需要管理复杂性的地方。语法突出显示有助于管理代码复杂性。颜色包 ( npm install colors)给你的控制台输出带来了类似的好处,使它更容易跟踪正在发生的事情。它也是使用最多的 NPM 软件包之一(每天近 50,000 次下载)。

colors 提供的 API 极其简单。它将函数添加到本地 JavaScript 字符串中,以便您可以执行诸如"some string".red之类的操作,如果您打印这个字符串,它将在控制台上显示为红色。清单 4-49 中显示了所使用的各种选项和输出的一个小样本。

清单 4-49 。popular/colors/1basic.js

// Loading this module modifies String for the entire process
require('colors');

console.log('hello'.green); // outputs green text
console.log('world!'.red); // outputs red text
console.log('Feeling yellow'.yellow); // outputs yellow text
console.log('But you look blue'.blue); // outputs yellow text
console.log('This should cheer you up!'.rainbow); // rainbow

9781484201886_unFig04-01.jpg

用法真的很简单。除了将这种能力带到你的指尖的明显优势之外,我们向你展示这个包的原因是为了进一步定制 JavaScript 内部。让我们看看这个包实际上是如何实现的。在这个过程中,我们将重温原型(我们在第二章中讨论过的一个主题)并了解 JavaScript 属性 getters 和 setters。

它是如何工作的?

这个 API 有两个方面:

  • 如何在控制台上打印颜色
  • 如何修改 JavaScript 字符串并向其添加函数

大多数使用 ANSI 转义码 的控制台(windows 和 UNIX)都支持以特定颜色打印字符串。如果您打印这些代码中的一个,控制台的行为就会改变。创建一个简单的 JavaScript 文件,打印由一些代码包围的 JavaScript 字符串,如清单 4-50 所示。如果您运行它,您将看到控制台记录了一个红色字符串。

清单 4-50 。popular/colors/2raw.js

function getRed(str) {
    // Changes the console foreground to red
    var redCode = '\x1b31m';

    // Resets the console foreground
    var clearCode = '\x1b[39m';

    return redCode + str + clearCode;
}

console.log(getRed('Hello World!'));

这是对我们如何修改控制台行为的充分理解。阅读终端文档并找到匹配的颜色代码是一件简单的事情。作为 JavaScript 开发人员,我们更感兴趣的问题是,“我怎样才能给所有字符串添加成员函数?”

在第二章的[中,我们讨论了当你用 new 操作符创建一个对象时,函数的prototype如何被复制到创建的实例的__proto__成员中。因为它是一个引用,如果你给原始函数prototype添加一个属性,所有使用这个函数创建的对象实例都将获得新的属性。

幸运的是,JavaScript 中的所有本机类型(日期、字符串、数组、数字等等)都是由与类型名称匹配的函数创建的。因此,如果我们向这些函数的原型添加一个成员,我们就可以成功地扩展这些类型的所有实例。清单 4-51 提供了一个简单的例子来演示这个原则,我们给所有的ArraysNumbersStrings添加了一个属性foo

清单 4-51 。popular/colors/3 prototypeintrop . js

Array.prototype.foo = 123;
Number.prototype.foo = 123;
String.prototype.foo = 123;

var arr = [];
var str = '';
var num = 1;

console.log(arr.foo); // 123
console.log(str.foo); // 123
console.log(num.foo); // 123

要给字符串添加一个函数,添加到String.prototyp,如清单 4-52 所示。

清单 4-52 。popular/colors/4addFunction.js

String.prototype.red = function (str) {
    // Changes the console foreground to red
    var redCode = '\x1b31m';

    // Resets the console foreground
    var clearCode = '\x1b[39m';

    return redCode + this + clearCode;
}

console.log('Hello World!'.red());

但是,请注意,在这个例子中,我们在字符串上调用了一个函数,即'Hello World!'.red(),而当我们使用颜色时,我们只是简单地调用了'Hello World!'.red。也就是说,有了颜色,我们就不需要“call()”这个成员了。这是因为颜色将red定义为属性获取器而不是函数

属性 getter/setter 只是一种插入 JavaScript 的 getter/read 值(例如,foo.bar)和 setter/set 值(例如,foo.bar = 123)语义的方法。添加 getter/setter 的一个简单方法是使用所有 JavaScript 对象上都有的__defineGetter__ / __defineSetter__成员函数 。清单 4-53 给出了一个简单的例子来演示这种用法。

[清单 4-53 。popular/colors/5 property intro . js

var foo = {};

foo.__defineGetter__('bar', function () {
    console.log('get bar was called!');
});

foo.__defineSetter__('bar', function (val) {
    console.log('set bar was called with value:',val);
});

// get
foo.bar;
// set
foo.bar = 'something';

所以,最后要在所有字符串上添加'.red'属性,我们只需要将它添加到String.prototype 中,如清单 4-54 所示。

清单 4-54 。popular/colors/6addProperty.js

String.prototype.__defineGetter__('red', function (str) {
    // Changes the console foreground to red
    var redCode = '\x1b[31m';

    // Resets the console foreground
    var clearCode = '\x1b[39m';

    return redCode + this + clearCode;
});

console.log('Hello World!'.red);

至少,您现在对 JavaScript 语言有了更深的理解,并能更好地理解它的成功。在向你们展示了所有这些力量之后,我们给出一个必须的警告。正如我们以前说过的,全球状态是糟糕和不直观的。因此,如果您开始以不受控制的方式(在各种不同的文件中)向这些本机类型(字符串、数字、数组等)添加成员,下一个人将很难理解这种功能来自哪里。将这种能力保留给专门为扩展内置类型而设计的模块,并确保记录下来!还要注意不要覆盖任何现有的或本地的 JavaScript 行为,因为其他库可能依赖于它!

额外资源

NPM 在线注册:http://npmjs.org/

语义版本化官方指南:http://semver.org/

NPM 语义版本解析器:https://github.com/isaacs/node-semver

摘要

在本章中,我们讨论了 Node.js 模块系统剩余的复杂性。在这个过程中,我们展示了为什么模块系统需要以这种方式工作的优势。我们认为最大的优势是没有困扰许多其他环境的依赖地狱问题,在这种情况下,模块不兼容会阻止您使用依赖于第三个模块的不同版本的两个模块。

我们展示了 NPM 是如何工作的。它只是一种管理 Node.js 社区共享的基于 node_modules 的模块的方法。我们浏览了 NPM 提供的重要命令行选项来管理您使用的社区包。

您还了解了 JSON 和语义版本。这两条信息对于所有开发人员(不仅仅是 Node.js 开发人员)都是至关重要的信息。

最后,我们展示了一些重要的 Node.js 包,以及您可以从中吸取的经验教训。这些应该有助于让你成为世界级的 Node.js 和 JavaScript 开发人员,你应该不怕打开 node_modules 文件夹,看看是什么让你喜欢的库打勾

五、事件和流

在我们研究 Node.js 开发的具体领域之前,我们需要解决一些关于 JavaScript 的核心概念,特别是 Node.js。Node.js 致力于成为创建服务器应用的最佳、最简单的方式。事件和流在实现这个目标的过程中扮演着重要的角色。

Node.js 是单线程;我们已经讨论了这个事实的优点。由于 Node.js 的事件性质,它对事件订阅/取消订阅模式提供了一流的支持。这种模式非常类似于您在浏览器中使用 JavaScript 处理事件的方式。

流数据是非常适合 Node.js 的领域之一。流对于改善用户体验和降低服务器资源利用率非常有用。

为了理解我们如何创建自己的事件发射器和流,我们首先需要理解 JavaScript 继承。

JavaScript 中的经典继承

我们在第二章中看到了原型是如何工作的。JavaScript 支持原型继承。在 JavaScript 中,在当前项上查找一个成员(比如item.foo),然后是它的原型(item.__proto__.foo),接着是它的原型的原型(item.__proto__.__proto__.foo),依此类推,直到原型本身(比如item.__proto__.__proto__.__proto__)是null。我们已经看到了如何用 JavaScript 来模拟一个经典的面向对象的结构。现在让我们看看如何用它来实现传统的面向对象的继承。

达成继承模式

让我们创建一个动物类。它有一个简单的成员函数,名为walk。我们已经讨论过,当使用new操作符(例如new Animal)调用函数时,函数中的``this'指的是新创建的对象。我们还讨论了由于使用了new操作符,构造函数的原型成员(Animal.prototype)被对象原型(animal.proto`)引用。(参见清单 5-1 )。

清单 5-1 。oo/1animal.js

function Animal(name) {
    this.name = name;
}
Animal.prototype.walk = function (destination) {
    console.log(this.name, 'is walking to', destination);
};

var animal = new Animal('elephant');
animal.walk('melbourne'); // elephant is walking to melbourne

为了更好地理解“??”上的查找是如何进行的,请看一下图 5-1 中的图表。

9781484201886_Fig05-01.jpg

图 5-1 。从原型中查找成员的示例

现在让我们在一个新的类中继承所有的Animal类功能 ,例如Bird。为此,我们需要做两件事:

  • Bird构造函数中调用Animal构造函数。这确保了为Bird对象(我们示例中的Animal.name)正确设置属性。
  • 想办法让所有父(Animal)原型成员(例如,__proto__.walk)成为子(Bird)实例原型的原型的成员。这将允许Bird实例(例如,bird)在它们自己的原型(bird.__proto__.fly)上拥有它们自己的函数,在它们原型的原型(bird.__proto__.__proto__.walk)上拥有它们的父成员。这叫做建立一个原型链

我们将从充实Bird类开始。基于算法,它将看起来像清单 5-2 中的代码。

清单 5-2 。为继承做准备

function Bird(name){
    // Call the Animal constructor
}
// Setup the prototype chain between Bird and Animal

// Finally create child instance
var bird = new Bird('sparrow');

调用父构造函数

我们不能简单地从Bird调用父Animal构造函数。这是因为如果我们这样做,那么Animal中的“??”将不会引用新创建的Bird对象(从new Bird创建)。因此,我们需要将Animal函数中this的含义指向Bird函数中this的值。幸运的是,我们可以通过使用所有 JavaScript 函数上可用的'.call'成员函数来强制解释含义(它来自Function.prototype)。清单 5-3 展示了call成员。像往常一样,评论解释了正在发生的事情。

清单 5-3 。oo/2call.js

var foo = {};
var bar = {};

// A function that uses `this`
function func(val) {
    this.val = val;
}

// Force this in func to be foo
func.call(foo, 123);

// Force this in func to be bar
func.call(bar, 456);

// Verify:
console.log(foo.val); // 123
console.log(bar.val); // 456

你可以看到我们将func`'函数中的this'强制为foo,然后是bar。太好了。现在我们知道了如何强制this`,让我们用它来调用父 Node,如清单 5-4 所示。

清单 5-4 。调用父构造函数

function Bird(name){
    Animal.call(this,name);

    // Any additional initialization code you want
}
// Copy all Animal prototype members to Bird

每次需要调用父构造函数时,都要使用这种模式(Parent.call(this, /* additional args */))。现在你对为什么会这样有了一个明确的功能理解。

设置原型链

我们需要一种机制,这样当我们创建一个新的Bird(比如,bird = new Bird)时,它的原型链就包含了所有的父原型函数(比如,bird.__proto__.__proto__.walk)。如果我们做Bird.prototype.__proto__ = Animal.prototype,这可以很简单地完成。

这个过程之所以有效,是因为当我们执行bird = new Bird时,我们将有效地获得bird.__proto__.__proto__ = Animal.prototype,这将使父原型成员(例如,Animal.prototype.walk)在子原型(bird.__proto__.__proto__.walk))上可用,这是我们想要的结果。清单 5-5 显示了一个简单的代码样本。

清单 5-5 。oo/3 协议类型. js

// Animal Base class
function Animal(name) {
    this.name = name;
}
Animal.prototype.walk = function (destination) {
    console.log(this.name, 'is walking to', destination);
};

var animal = new Animal('elephant');
animal.walk('melbourne'); // elephant is walking to melbourne

// Bird Child class
function Bird(name) {
    Animal.call(this, name);
}
Bird.prototype.__proto__ = Animal.prototype;
Bird.prototype.fly = function (destination) {
    console.log(this.name, 'is flying to', destination);
}

var bird = new Bird('sparrow');
bird.walk('sydney'); // sparrow is walking to sydney
bird.fly('melbourne'); // sparrow is flying to melbourne

为了理解继承成员(在我们的例子中是bird.walk)的查找是如何执行的,请看一下图 5-2 。

9781484201886_Fig05-02.jpg

图 5-2 。从原型链中查找成员的示例

注意手动修改__proto__属性是不推荐的*,因为它不是 ECMAScript 标准的一部分。我们稍后将讨论设置原型的更标准的方法,但是这里展示的原理会让您成为 JavaScript 原型继承的专家。*

*构造函数属性

默认情况下,每个函数都有一个名为“prototype”的成员,我们已经看到了。默认情况下,这个成员有一个指向函数本身的constructor属性。清单 5-6 演示了这一点。

清单 5-6 。oo/4 构造器/1basic.js

function Foo() { }
console.log(Foo.prototype.constructor === Foo); // true

有这个属性有什么好处?在使用一个函数(例如,instance = new Foo)创建了一个实例之后,您可以使用一个简单的查找instance.constructor(实际上是查看instance.__proto__.constructor)来访问构造函数。清单 5-7 在一个例子中展示了这一点,在这个例子中,我们使用命名函数的属性name(function Foo)来记录是什么创建了这个对象。

清单 5-7 。oo/4constructor/2new.js

function Foo() { }

var foo = new Foo();
console.log(foo.constructor.name); // Foo
console.log(foo.constructor === Foo); // true

了解构造函数属性使您能够在需要时对实例进行运行时反射。

正确的 Node.js 方式

我们在第三章中讨论的util核心模块 ( require('utils'))提供了一个可爱的小函数来为我们创建原型链,这样你就不需要自己处理__proto__(非标准属性)了。该函数名为 ],接受一个子类,后跟一个父类,如清单 5-8 中的示例所示。Bird类扩展了我们前面看到的Animal`类。

清单 5-8 。oo/5nodejs/util.js

// util function
var inherits = require('util').inherits;

// Bird Child class
function Bird(name) {
    // Call parent constructor
    Animal.call(this, name);

    // Additional construction code
}
inherits(Bird, Animal);

// Additional member functions
Bird.prototype.fly = function (destination) {
    console.log(this.name, 'is flying to', destination);
}

var bird = new Bird('sparrow');
bird.walk('sydney'); // sparrow is walking to sydney
bird.fly('melbourne'); // sparrow is flying to melbourne

有两件事值得注意:

  • 调用父构造函数:Animal.call(this, /* any original arguments */)
  • 设置原型链:inherits(Bird, Animal);

简单到成为第二天性,这就是你继承类所需要做的一切!

覆盖子类中的函数

要覆盖父函数但仍利用一些原始功能,只需执行以下操作:

  • 在子 Nodeprototype上创建一个同名的函数。
  • 调用父函数的方式类似于我们调用父构造函数的方式,基本上是使用Parent.prototype.memberfunction.call(this, /*any original args*/)语法。

清单 5-9 展示了这一点。

清单 5-9 。oo/6override.js

// util function
var inherits = require('util').inherits;

// Base
function Base() { this.message = "message"; };
Base.prototype.foo = function () { return this.message + " base foo" };

// Child
function Child() { Base.call(this); };
inherits(Child, Base);

// Overide parent behaviour in child
Child.prototype.foo = function () {
    // Call base implementation + customize
    return Base.prototype.foo.call(this) + " child foo";
}

// Test:
var child = new Child();
console.log(child.foo()); // message base foo child foo

我们简单地创建了子函数Child.prototype.foo并在Base.prototype.foo.call(this).中调用父函数

检查继承链

正如我们所看到的,建立一个原型链(__proto__.__proto__)有一个额外的好处,它允许你检查一个特定的对象实例是否属于一个特定的类,或者它的父类,或者它的父类,等等。这是使用instanceof操作符完成的。

在伪代码中当你做someObj instanceof Func时你使用这个算法:

  • 检查someObj.__proto__ == Func.prototype,如果是,返回true
  • 如果不是,检查someObj.__proto__.__proto__ == Func.prototype,如果是,返回true
  • 重复向上移动原型链。
  • 如果__proto__null并且我们还没有找到匹配,返回false

从伪代码中,您可以看到它非常类似于如何执行属性查找。我们沿着原型链向上走,直到找到一个等于 ?? 的 ??。当new操作符将prototype复制到__proto__时,找到匹配表示new操作符正在指定的Func上使用。使用instanceof的快速演示如清单 5-10 所示。

清单 5-10 。oo/7instanceof.js

var inherits = require('util').inherits;

function A() { }
function B() { }; inherits(B, A);
function C() { }

var b = new B();
console.log(b instanceof B); // true because b.__proto__ == B.prototype
console.log(b instanceof A); // true because b.__proto__.__proto__ == A.prototype
console.log(b instanceof C); // false

对 util.inherits 内部的更深入的理解

你不需要通过来了解这一部分,但这是值得的,因为你可以坐在酷孩子的桌子旁。我们说过不推荐手动设置__proto__,因为它不是标准化 JavaScript 的一部分。

幸运的是,JavaScript 中有一个函数可以创建一个已经设置了指定的__proto__的空白对象。该功能被称为Object. create ,其工作方式如清单 5-11 所示。

清单 5-11 。oo/8internals/1check.js

var foo = {};
var bar = Object.create(foo);
console.log(bar.__proto__ === foo); // true

在这个例子中,我们简单地验证了新创建的对象(即,bar)的__proto__成员被设置为我们传递给Object.create的成员(换句话说,foo)。它可以用于继承,如清单 5-12 所示。

清单 5-12 。oo/8internals/2inherit.js

// Animal Base class
function Animal() {
}
Animal.prototype.walk = function () {
    console.log('walking');
};

// Bird Child class
function Bird() {
}
Bird.prototype = Object.create(Animal.prototype);

var bird = new Bird();
bird.walk();

与我们之前展示的原始非标准__proto__机制相比,这里我们简单地将Bird.prototype.__proto__ = Animal.prototype替换为有效的Bird.prototype = { __proto__ : Animal.prototype }

这种机制正确地继承了父类的成员,但是它产生了一个小问题。当我们重新分配Bird.prototype时,Bird.prototype.constructor中的constructor信息丢失了,因为我们将Bird.prototype重新分配给了一个全新的对象。要恢复constructor属性,一个简单的解决方案是向Object. create传递第二个参数,该参数指定要添加到要创建的对象的附加属性。在清单 5-13 中,我们指定constructor是一个指向函数本身的属性,这就是Bird.prototype.constructor最初的样子(记住Bird.prototype.constructor === Bird)。

清单 5-13 。oo/8 internals/3 inherit better . js

// Animal Base class
function Animal() {
}
Animal.prototype.walk = function () {
    console.log('walking');
};

// Bird Child class
function Bird() {
}
Bird.prototype = Object.create(Animal.prototype, {
    constructor: {
        value: Bird,
        enumerable: false,
        writable: true,
        configurable: true
    }
});

var bird = new Bird();
bird.walk();
console.log(bird.constructor === Bird); // true

这正是 Node.js util 模块中的实现(用 JavaScript 编写)。清单 5-14 中的显示了直接来自源代码的实现。

清单 5-14 。从 Node.js 源 util.js 检索的代码

exports.inherits = function(ctor, superCtor) {
  ctor.super_ = superCtor;
  ctor.prototype = Object.create(superCtor.prototype, {
    constructor: {
      value: ctor,
      enumerable: false,
      writable: true,
      configurable: true
    }
  });
};

inherits函数做的另一件事是向子类添加一个属性super_,该属性指向父类。这只是约定俗成,这样你就知道在调试或编写基于反射的代码时,这个子函数原型已经从这个super_类接收了成员。

掌握继承非常复杂,因为 JavaScript 是用简单的原型继承设计的。我们只是利用它提供的能力来模仿传统的 OO 层次结构。

Node.js 事件

我们已经有了一种使用回调基于某些事件执行某些代码的方法。处理重要事件的更一般的概念是事件。事件就像广播,而回调就像握手。引发事件的组件对其客户端一无所知,而使用回调的组件却知道很多。这使得事件非常适合于事件的重要性由客户端决定的场景。也许客户想知道,也许不想。注册多个客户端也更加简单,正如我们将在本节中看到的那样。

Node.js 内置了对核心events模块中事件的支持。像往常一样,使用require('events')加载模块。事件模块有一个简单的类“EventEmitter”,我们接下来会介绍它。

EventEmitter 类

EventEmitter是一个被设计用来使发出事件(这并不奇怪)和订阅引发的事件变得容易的类。清单 5-15 提供了一个小代码示例,我们订阅一个事件,然后引发它。

清单 5-15 。events/1basic.js

var EventEmitter = require('events').EventEmitter;

var emitter = new EventEmitter();

// Subscribe
emitter.on('foo', function (arg1, arg2) {
    console.log('Foo raised, Args:', arg1, arg2);
});

// Emit
emitter.emit('foo', { a: 123 }, { b: 456 });

如示例所示,您可以通过一个简单的new EventEmitter`'调用来创建一个新实例。要订阅事件,可以使用on'函数传入事件名称(总是一个字符串),后跟事件处理函数(也称为*监听器*)。最后,我们使用emit`函数引发一个事件,该函数传入事件名,后跟我们希望传入监听器的任意数量的参数(在清单 5-15 中,我们使用了两个参数进行演示)。

多个用户

正如我们之前提到的,使用事件的优势之一是为多个订阅者提供内置的支持。清单 5-16 是一个简单的例子,一个事件有多个订阅者。

清单 5-16 。events/2multiple.js

var EventEmitter = require('events').EventEmitter;
var emitter = new EventEmitter();

emitter.on('foo', function () {
    console.log('subscriber 1');
});

emitter.on('foo', function () {
    console.log('subscriber 2');
});

// Emit
emitter.emit('foo');

在这个例子中需要注意的另一件事是,侦听器是按照它们为事件注册的顺序被调用的。这是 Node.js 单线程特性的一个很好的结果,它使您更容易对代码进行推理。此外,为事件传递的任何参数都在不同的订阅者之间共享,如清单 5-17 所示。

清单 5-17 。事件/3shared.js

var EventEmitter = require('events').EventEmitter;
var emitter = new EventEmitter();

emitter.on('foo', function (ev) {
    console.log('subscriber 1:', ev);
    ev.handled = true;
});

emitter.on('foo', function (ev) {
    if (ev.handled) {
        console.log('event already handled');
    }
});

// Emit
emitter.emit('foo', { handled: false });

在这个示例中,第一个侦听器修改了传递的事件参数,第二个侦听器获得了修改后的对象。你可以潜在地利用这个事实让你摆脱一个棘手的局面,但我要高度警惕这一点。显示事件参数共享的原因是为了警告您在侦听器中直接修改事件对象的危险。

注销

下一个要问的问题是我们如何退订一个事件。EventEmitter有一个removeListener函数,它接受一个事件名,后面跟着一个函数对象,以便从监听队列中删除。需要注意的一点是,您必须有一个对要从监听队列中移除的函数的引用,因此,不应该使用匿名(内联)函数。这是因为如果 JavaScript 中的两个函数体相同,它们就不相等,如下面的清单 5-18 所示,因为这是两个不同的函数对象。

清单 5-18 。演示函数不等式的示例

$ node -e "console.log(function(){} == function(){})"
false

清单 5-19 展示了如何取消订阅一个监听器。

清单 5-19 。events/4unsubscribe.js

var EventEmitter = require('events').EventEmitter;
var emitter = new EventEmitter();

var fooHandler = function () {
    console.log('handler called');

    // Unsubscribe
    emitter.removeListener('foo',fooHandler);
};

emitter.on('foo', fooHandler);

// Emit twice
emitter.emit('foo');
emitter.emit('foo');

在此示例中,我们在事件引发一次后取消订阅该事件。结果,第二个事件被忽略了。

是否引发过此事件?

这是一个常见的用例,您并不关心事件是否每次都被引发——只关心它被引发一次。为此,EventEmitter提供了一个函数“once”,它只调用注册的监听器一次。清单 5-20 演示了它的用法。

清单 5-20 。events/5once.js

var EventEmitter = require('events').EventEmitter;
var emitter = new EventEmitter();

emitter.once('foo', function () {
    console.log('foo has been raised');
});

// Emit twice
emitter.emit('foo');
emitter.emit('foo');

foo的事件监听器只会被调用一次。

听众管理

作为 Node.js 事件处理专家,您需要了解在EventEmitter上提供的一些额外的实用函数。

EventEmitter有一个成员函数listeners,它接受一个事件名并返回订阅该事件的所有侦听器。这在调试事件侦听器时非常有用。清单 5-21 演示了它的用法。

清单 5-21 。events/6listeners.js

var EventEmitter = require('events').EventEmitter;
var emitter = new EventEmitter();

emitter.on('foo', function a() { });
emitter.on('foo', function b() { });

console.log(emitter.listeners('foo')); // [ [Function: a], [Function: b]]

EventEmitter实例还会在添加新的侦听器时引发“newListener”事件,在删除侦听器时引发“removeListener”事件,这在一些棘手的情况下会有所帮助,比如当您想要跟踪事件侦听器注册/取消注册的时刻。当添加或删除监听器时,它对您想要做的任何管理都很有用,如清单 5-22 中的所示。

清单 5-22 。events/7listenerevents.js

var EventEmitter = require('events').EventEmitter;
var emitter = new EventEmitter();

// Listener addition / removal notifications
emitter.on('removeListener', function (eventName, listenerFunction) {
    console.log(eventName, 'listener removed', listenerFunction.name);
});
emitter.on('newListener', function (eventName, listenerFunction) {
    console.log(eventName, 'listener added', listenerFunction.name);
});

function a() { }
function b() { }

// Add
emitter.on('foo', a);
emitter.on('foo', b);

// Remove
emitter.removeListener('foo', a);
emitter.removeListener('foo', b);

请注意,如果您在添加了“newListener”的处理程序之后添加了“removeListener”,那么您也会得到关于添加了“removeListener”的通知,这就是为什么我们习惯上像在本示例中那样首先添加removeListener事件处理程序。

EventEmitter 内存泄漏

处理事件时,内存泄漏的一个常见来源是在回调中订阅事件,但在结束时忘记取消订阅。默认情况下,EventEmitter将允许每种事件类型的有 10 个监听器,并且它将向控制台输出一个警告。此警告是专门为您提供帮助的。您的所有代码都将继续运行。换句话说,将会在没有警告的情况下添加更多的侦听器,并且当一个事件被引发时,所有的侦听器都会被调用,如清单 5-23 所示。

清单 5-23 。events/8maxEventListeners.js

var EventEmitter = require('events').EventEmitter;
var emitter = new EventEmitter();

var listenersCalled = 0;

function someCallback() {
    // Add a listener
    emitter.on('foo', function () { listenersCalled++ });

    // return from callback
}

for (var i = 0; i < 20; i++) {
    someCallback();
}
emitter.emit('foo');
console.log('listeners called:', listenersCalled); // 20

应用的输出显示在清单 5-24 中。您可以看到,尽管有警告,但当我们发出事件时,所有 20 个侦听器都被调用了。

清单 5-24 。运行最大事件监听器演示

$ node 8maxEventListeners.js
(node) warning: possible EventEmitter memory leak detected. 11 listeners added.
Use emitter.setMaxListeners() to increase limit.
Trace
    at EventEmitter.addListener (events.js:160:15)
    at someCallback (/path/to/8maxEventListeners.js:8:13)
    at Object.<anonymous> (/path/to/8maxEventListeners.js:14:5)
    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:902:3
listeners called: 20

这种内存泄漏的一个常见原因是在回调出错时忘记取消订阅事件。一个简单的解决方案是在回调中创建一个新的事件发射器。这样,事件发射器就不会被共享,当回调终止时,它会和它的所有订阅者一起被释放。

最后,在有些情况下,拥有 10 个以上的侦听器是一个有效的场景。在这种情况下,您可以使用setMaxListeners成员函数增加这个警告的限制,如清单 5-25 所示。

清单 5-25 。events/9setMaxListeners.js

var EventEmitter = require('events').EventEmitter;
var emitter = new EventEmitter();

// increase limit to 30
emitter.setMaxListeners(30);

// subscribe 20 times
// No warning will be printed
for (var i = 0; i < 20; i++) {
    emitter.on('foo', function () { });
}
console.log('done');

请注意,这增加了该事件发射器上所有事件类型的限制。此外,您可以传入 0,以允许在没有警告的情况下订阅无限数量的事件侦听器。

Node.js 默认情况下尽量做到安全;在服务器环境中工作时,内存泄漏会造成很大的影响,这也是此警告消息存在的原因。

错误事件

一个'error'事件在 Node.js 中被视为一个特殊的异常案例,如果它没有没有监听器,那么默认的动作是打印一个堆栈跟踪并退出程序。清单 5-26 给出了一个简单的例子来说明这一点。

清单 5-26 。事件/10errorEvent.js

var EventEmitter = require('events').EventEmitter;
var emitter = new EventEmitter();

// Emit an error event
// Since there is no listener for this event the process terminates
emitter.emit('error', new Error('Something horrible happened'));

console.log('this line never executes');

如果您运行这段代码,您将得到一个输出,如清单 5-27 中的所示。如果你需要引发一个error事件,你应该使用一个Error对象,就像我们在这个例子中所做的那样。您还可以从示例中看到,当流程终止时,包含console.log的最后一行永远不会执行。

清单 5-27 。错误事件示例的运行示例

$ node 10errorEvent.js

events.js:72
        throw er; // Unhandled 'error' event
              ^
Error: Something horrible happened
    at Object.<anonymous> (/path/to/10errorEvent.js:6:23)
    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:902:3

因此,教训是:只有在异常的情况下必须处理,才引发错误event

创建您自己的事件发射器

既然您已经是 Node.js 中处理和引发事件的专家,那么大量的开源外围应用就向您敞开了大门。许多库导出继承自EventEmitter的类,因此遵循相同的事件处理机制。在这个阶段,了解如何扩展EventEmitter并创建一个内置了EventEmitter所有功能的公共类是很有用的。

创建自己的EventEmitter所需要做的就是从类的构造函数中调用EventEmitter构造函数,并使用util.inherits函数来建立原型链。考虑到我们在本章开始时对这个问题的讨论,这应该是你的第二天性。清单 5-28 是演示这一点的一个简单例子。

清单 5-28 。事件/11custom.js

var EventEmitter = require('events').EventEmitter;
var inherits = require('util').inherits;

// Custom class
function Foo() {
    EventEmitter.call(this);
}
inherits(Foo, EventEmitter);

// Sample member function that raises an event
Foo.prototype.connect = function () {
    this.emit('connected');
}

// Usage
var foo = new Foo();
foo.on('connected', function () {
    console.log('connected raised!');
});
foo.connect();

你可以看到你的类的用法就像它是一个EventEmitter一样。有了这两行简单的代码,您就有了一个全功能的自定义事件发射器。

处理事件

core Node.js 中的许多类都继承自EventEmitter。全局process对象也是EventEmitter的一个实例,正如你在清单 5-29 中看到的。

清单 5-29 。演示进程是 EventEmitter 的示例

$ node -e "console.log(process instanceof require('events').EventEmitter)"
true

全局异常处理程序

任何全局未处理的异常都可以通过侦听进程上的“uncaughtException”事件来截获。您不应该在此事件处理程序之外继续执行,因为这只会在应用处于不稳定状态时发生。最好的策略是为了方便起见记录错误,用错误代码退出进程,如清单 5-30 所示。

清单 5-30 。process/1 un catch . js

process.on('uncaughtException', function (err) {
    console.log('Caught exception: ', err);
    console.log('Stack:', err.stack);
    process.exit(1);
});

// Intentionally cause an exception, but don't try/catch it.
nonexistentFunc();

console.log('This line will not run.');

如果你运行清单 5-30 中的代码,你会得到一个不错的错误日志,如清单 5-31 所示。

清单 5-31 。未捕获异常的运行示例

$ node 1uncaught.js
Caught exception: [ReferenceError: nonexistentFunc is not defined]
Stack: ReferenceError: nonexistentFunc is not defined
    at Object.<anonymous> (E:\DRIVE\Google Drive\BEGINNING NODEJS\code\chapter5\
process\1uncaught.js:8:1)
    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:902:3

如果任何事件发射器引发“错误”事件,并且没有侦听器订阅该事件的事件发射器,则“uncaughtError”事件也会在流程上引发。

出口

当进程将要退出时,发出exit事件。此时无法中止退出。事件循环已经在拆卸中,所以此时你不能做任何异步操作。(参见清单 5-32 。)

清单 5-32 。process/2exit.js

process.on('exit', function (code) {
    console.log('Exiting with code:', code);
});

process.exit(1);

请注意,事件回调是在进程退出时使用的退出代码中传递的。此事件主要用于调试和日志记录。

信号

Node.js process对象还支持 UNIX 的信号概念,这是一种进程间通信的形式。它还模拟了 Windows 系统上最重要的程序。Windows 和 UNIX 都支持的一个常见场景是,用户试图在终端中使用 Ctrl+C 组合键来中断进程。默认情况下,Node.js 将退出该进程。但是,如果您有一个监听器订阅了SIGINT(信号中断)事件,监听器将被调用,您可以选择是否要退出进程(process.exit)或继续执行。清单 5-33 提供了一个小例子,我们选择继续运行并在五秒钟后退出。

清单 5-33 。process/3 signal . js

setTimeout(function () {
    console.log('5 seconds passed. Exiting');
}, 5000);
console.log('Started. Will exit in 5 seconds');

process.on('SIGINT', function () {
    console.log('Got SIGINT. Ignoring.');
});

如果您执行这个示例并按 Ctrl+C,您将得到一条消息,表明我们选择忽略它。最后,一旦我们没有任何未完成的任务,进程将在五秒钟后自然退出(如清单 5-34 所示)。

清单 5-34 。忽略 Ctrl+C 消息的示例运行演示

$ node 3signals.js
Started. Will exit in 5 seconds
Got SIGINT. Ignoring.
Got SIGINT. Ignoring.
5 seconds passed. Exiting

一滴一滴地是装满水的罐子。

—佛陀

流在创建高性能的 web 应用 中扮演着重要的角色。为了理解流带来了什么,考虑一个简单的例子,从 web 服务器提供一个大文件(1GB)。在没有溪流的情况下,它看起来像图 5-3 。用户将不得不等待很长时间才能得到他们所请求的文件的任何迹象。这叫做缓冲,我们应该尽可能的限制它。除了用户体验明显不好,还浪费资源。在我们开始将文件发送给用户之前,需要加载完整的文件并保存在内存中。

9781484201886_Fig05-03.jpg

图 5-3 。缓冲网络响应

当我们使用流式传输时,同样的场景看起来要好得多。我们开始读取文件,每当我们有一个新的数据块时,我们将它发送到客户端,直到我们到达末尾,如图图 5-4 所示。

9781484201886_Fig05-04.jpg

图 5-4 。流媒体网络响应

用户体验的改善和服务器资源的更好利用是 steams 背后的主要动机。

最重要的概念是Readable流、Writable流、Duplex流、**、**和Transform流。一个可读流是一个你可以从中读取数据但不能写入的流。一个很好的例子是process.stdin,它可以用来从标准输入流数据。可写流是可以写入但不能读取的流。一个很好的例子是process.stdout,它可以用来将数据流输出到标准输出。一个双工流是一个你可以读写的流。网络套接字就是一个很好的例子。您可以向网络套接字写入数据,也可以从中读取数据。转换流是双工流的一种特殊情况,流的输出以某种方式从输入中计算出来。这些溪流也被称为溪流。一个很好的例子就是加密和压缩流。

流的所有基本构建块都存在于使用require('stream')加载的 Node.js 核心流模块中。这个模块中有实现流的基类,恰当地称为ReadableWritableDuplexTransform

Node.js 中的流是基于事件的,这就是为什么在我们深入流之前对事件有一个牢固的理解是很重要的。所有这些流类都继承自一个基本抽象Stream类(抽象是因为你不应该直接使用它),它又继承自EventEmitter(我们前面已经看到了)。这个层次结构在清单 5-35 中演示。

清单 5-35 。streams/1 concepts/event based . js

var stream = require('stream');
var EventEmitter = require('events').EventEmitter;

console.log(new stream.Stream() instanceof EventEmitter); // true

console.log(new stream.Readable({}) instanceof stream.Stream); // true
console.log(new stream.Writable({}) instanceof stream.Stream); // true
console.log(new stream.Duplex({}) instanceof stream.Stream); // true
console.log(new stream.Transform({}) instanceof stream.Stream); // true

在我们了解如何创建自己的流之前,让我们看看如何使用 Node.js 库中现有的流。

管道

所有的流都支持管道操作,可以使用pipe成员函数来完成。这是 Node.js 中的流如此出色的原因之一。考虑我们简单的初始场景,从文件系统加载一个文件并将其传输到客户机。这可以像代码段fileSystemStream.pipe(userSocket)一样简单。

您可以通过管道从可读取的流(可读/双工/转换)传输到可写入的流(可写/双工/转换)。这个函数被称为管道,因为它模仿了命令行管道操作符的行为,例如cat file.txt | grep lol

核心模块fs提供了从文件创建可读或可写流的实用函数。清单 5-36 是一个将文件从文件系统传输到用户控制台的例子。

清单 5-36 。streams/2pipe/1basic.js

var fs = require('fs');

// Create readable stream
var readableStream = fs.createReadStream('./cool.txt');

// Pipe it to stdout
readableStream.pipe(process.stdout);

您还可以使用pipe链接多个流。例如,清单 5-37 中的代码从一个文件中创建一个读取流,通过一个 zip 转换流,然后通过管道将其传输到一个可写文件流。这将在文件系统上创建一个 zip 文件。

清单 5-37 。streams/2pipe/2chain.js

var fs = require('fs');
var gzip = require('zlib').createGzip();

var inp = fs.createReadStream('cool.txt');
var out = fs.createWriteStream('cool.txt.gz');

// Pipe chain
inp.pipe(gzip).pipe(out);

Node.js 中的流是基于事件的。pipe操作所做的只是订阅源上的相关事件,并调用目的地上的相关函数。对于大多数目的来说,pipe是作为 API 消费者需要了解的全部内容,但是当您想要更深入地研究流时,了解更多的细节是值得的。

消费可读流

我们已经说过很多次,流是基于事件工作的。可读流最重要的事件是'readable'。每当有新数据要从流中读取时,都会引发此事件。一旦进入事件处理程序,就可以调用流中的read函数从流中读取数据。如果这是流的结尾,read 函数返回null,如清单 5-38 所示。

清单 5-38 。streams/3readable/basic.js

process.stdin.on('readable', function () {
    var buf = process.stdin.read();
    if (buf != null) {
        console.log('Got:');
        process.stdout.write(buf.toString());
    }
    else {
        console.log('Read complete!');
    }
});

清单 5-39 中显示了该代码的一个运行示例,其中我们从命令行将数据传输到process.stdin

清单 5-39 。streams/3readable/basic.js 的运行示例

$ echo 'foo bar bas' | node basic.js
Got:
'foo bar bas'
Read complete!

写入可写流

要写一个流,只需调用write来写一些数据。当你写完(流结束)时,你只需调用end。如果你愿意,你也可以使用end成员函数写一些数据,如清单 5-40 所示。

清单 5-40 。streams/4writable/basic.js

var fs = require('fs');
var ws = fs.createWriteStream('message.txt');

ws.write('foo bar ');
ws.end('bas');

在这个例子中,我们简单地将foo bar bas写到一个可写的文件流中。

创建您自己的流

创建自己的流与创建自己的EventEmitter非常相似。对于从相关基类继承的流,流类和实现一些基方法。这在表 5-1 中有详细说明。

继承机制和我们之前看到的一样。也就是你从你的类构造函数中调用基构造函数,在声明你的类之后调用utils.inherits

表 5-1 。创建您自己的自定义流

|

使用案例

|

班级

|

要实现的方法

| | --- | --- | --- | | 只读 | 易读的 | _ 阅读 | | 只写 | 可写的 | _ 写入 | | 阅读和写作 | 双层公寓 | _ 读,_ 写 | | 对读取的数据进行操作并写入结果 | 改变 | _ 转换,_ 刷新 |

创建可读的流

如上所述,您只是从Readable类继承。您在自己的类中实现了_read成员,当有人请求读取数据时,stream API 会在内部调用这个成员。如果您有想要传递(推送)的数据,您可以调用继承的成员函数push来传递数据。如果您调用push(null),这表示读取流结束。

清单 5-41 是一个返回 1-1000 的可读流的简单例子。如果你运行这个,你将会看到所有这些数字被打印出来(当我们用管道连接到stdout)。

清单 5-41 。streams/5 create readable/counter . js

var Readable = require('stream').Readable;
var util = require('util');

function Counter() {
    Readable.call(this);
    this._max = 1000;
    this._index = 1;
}
util.inherits(Counter, Readable);

Counter.prototype._read = function () {
    var i = this._index++;
    if (i > this._max)
        this.push(null);
    else {
        var str = ' ' + i;
        this.push(str);
    }
};

// Usage, same as any other readable stream
var counter = new Counter();
counter.pipe(process.stdout);

如您所见,底层的可读类为您提供了大部分流逻辑。

创建可写流

创建自己的可写流类类似于创建可读流。您从Writable类继承并实现了_write方法。_write方法以需要处理的块作为第一个参数传递。

清单 5-42 是一个简单的可写流,它将所有传入的数据记录到控制台。在这个例子中,我们简单地从可读文件流传输到这个可写流(Logger)。

清单 5-42 。streams/6 create writable/logger . js

var Writable = require('stream').Writable;
var util = require('util');

function Logger() {
    Writable.call(this);
}
util.inherits(Logger, Writable);

Logger.prototype._write = function (chunk) {
    console.log(chunk.toString());
};

// Usage, same as any other Writable stream
var logger = new Logger();

var readStream = require('fs').createReadStream('message.txt');
readStream.pipe(logger);

同样,在大多数情况下,大部分功能由Writable基类在内部处理。

摘要

希望这一章已经让你对 JavaScript 这种语言有了更好的理解。有几个简单的想法提供了很大的表现力。本章开始时,我们提供了 JavaScript 原型继承的速成课程,并解释了在 Node.js 中实现这一点是多么简单。然后,我们展示了 Node.js 如何内置对常见事件处理范例的支持。我们还演示了如何通过简单的继承创建自己的事件发射器。最后,我们看了 streams,以及为什么您想要将它们添加到您的武器库中。您看到了在 Node.js 中消费和写入流是多么容易。Node.js 几乎就像是为它们而设计的一样!在本章的最后,我们讨论了如何利用 Node.js 核心基类提供的内置功能创建自己的定制流。*