NodeJS-基础知识-全-

NodeJS 基础知识(全)

原文:zh.annas-archive.org/md5/41C152E6702013095E0E6744245B8C51

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

Node.js 只是一个让您在服务器端使用 JavaScript 的工具。但是,它实际上做的远不止这些——通过扩展 JavaScript,它允许更加集成和高效的开发方式。毫不奇怪,它是全栈 JavaScript 开发人员的基本工具。无论您是在后端还是前端工作,使用 Node.js 都可以采用更加协作和敏捷的工作方式,这样您和您的团队就可以专注于交付高质量的最终产品。这将确保您准备好迎接任何新的挑战。

本书将快节奏地介绍依赖管理、运行自己的 HTTP 服务器、实时通信以及一切必要的内容,让您快速上手 Node.js。

本书涵盖内容

第一章,“入门”,介绍了 Node.js 的设置。您还将学习如何利用和管理依赖项。

第二章,“简单 HTTP”,介绍了如何运行一个简单的 HTTP 服务器,并帮助您理解路由和中间件的使用。

第三章,“认证”,介绍了使用中间件和 JSON Web Token 对用户进行认证。

第四章,“调试”,介绍了在开发任务中集成事后调试技术以及如何调试您的 Node.js 程序。

第五章,“配置”,介绍了使用集中式配置选项、参数和环境变量配置和维护软件。

第六章,“LevelDB 和 NoSQL”,介绍了 NoSQL 数据库的概念,如 LevelDB 和 MongoDB。还介绍了简单键/值存储和更完整的文档数据库的使用。

第七章,“Socket.IO”,探讨了客户端、服务器之间的实时通信,以及它如何对用户进行身份验证和通知。

第八章,“创建和部署包”,侧重于共享模块并为生态系统做出贡献

第九章,“单元测试”,使用 Mocha、Sinon 和 Chance 测试您的代码,并介绍如何使用模拟函数和生成随机值来测试您的代码

第十章,“使用不止 JavaScript”,解释了在 Node.js 中使用 CoffeeScript 来扩展语言功能。

您需要什么

需要一台运行 Unix(Macintosh)、Linux 或 Windows 的计算机,以及您喜欢的集成开发环境。如果您没有集成开发环境,那么您有几个选择,例如:

这本书适合谁

这本书对任何想了解 Node.js 的人都有帮助(Node.js 是什么,如何使用它,它在哪里有用以及何时使用它)。熟悉服务器端和 Node.js 是先决条件。

约定

在本书中,您将找到许多文本样式,用于区分不同类型的信息。以下是一些这些样式的示例,以及它们的含义解释。

文本中的代码字词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 用户名显示如下:“我们可以通过使用include指令包含其他上下文。”

代码块设置如下:

<script type='application/javascript' src='script_a.js'></script>
<script type='application/javascript' src='script_b.js'></script>

任何命令行输入或输出都以以下形式编写:

[~]$ npm install -g n

新术语重要单词以粗体显示。您在屏幕上看到的单词,比如菜单或对话框中的单词,会在文本中以这种方式出现:“如果用户没有同时输入用户名和密码,服务器将返回500 Bad Request”。

注意

警告或重要提示会以这样的方式出现在框中。

提示

技巧和窍门会以这种方式出现。

第一章:入门

每个 Web 开发人员都必须偶尔遇到它,即使他们只是涉足简单的网页。每当您想要使您的网页更加交互式时,您会使用您值得信赖的朋友,比如 JavaScript 和 jQuery,并一起开发一些新的东西。您可能已经使用 AngularJS 或 Backbone 开发了一些令人兴奋的前端应用程序,并且想了解您可以用 JavaScript 做些什么。

在多个浏览器上测试您的网站时,您可能会偶尔遇到谷歌浏览器,并且您可能已经注意到它是 JavaScript 应用程序的一个很好的平台。

谷歌浏览器和 Node.js 有一个非常大的共同点:它们都在谷歌的高性能 V8 JavaScript 引擎上运行,这使我们在浏览器中使用的引擎与后端使用的引擎相同,非常酷,对吧?

设置

为了开始使用 Node.js,我们需要下载并安装 Node.js。最好的安装方式是前往nodejs.org/并下载安装程序。

在撰写本文时,当前版本的 Node.js 是 4.2.1。

为了确保一致性,我们将使用npm包来安装正确版本的 Node.JS,为此,我们将使用www.npmjs.com/package/n中描述的n包。

目前,这个包只支持*nix机器。对于 Windows,请参见 nvm-windows 或从nodejs.org/dist/v4.2.1/下载二进制文件。

一旦你安装了 Node.js,打开终端并运行:

[~]$ npm install -g n

-g参数将全局安装包,这样我们就可以在任何地方使用这个包。

Linux 用户可能需要运行安装全局包的命令作为sudo

使用最近安装的包,运行:

[~]$ n

这将显示一个包含以下包的屏幕:

 node/0.10.38
 node/0.11.16
 node/0.12.0
 node/0.12.7
 node/4.2.1

如果node/4.2.1没有标记,我们可以简单地运行以下包;这将确保安装node/4.2.1

[~]$ sudo n 4.2.1

为了确保node运行正常,让我们创建并运行一个简单的hello world示例:

[~/src/examples/example-1]$ touch example.js
[~/src/examples/example-1]$ echo "console.log(\"Hello world\")" > example.js
[~/src/examples/example-1]$ node example.js
Hello World

很好,它起作用了;现在让我们开始做正事。

Hello require

在前面的示例中,我们只是记录了一个简单的消息,没有什么有趣的,所以让我们在这一部分深入一点。

在浏览器中使用多个脚本时,我们通常只需要包含另一个脚本标签,如:

<script type='application/javascript' src='script_a.js'></script>
<script type='application/javascript' src='script_b.js'></script>

这两个脚本共享相同的全局范围,这通常会导致一些不寻常的冲突,当人们想要给变量赋予相同的名称时。

//script_a.js
function run( ) {
    console.log( "I'm running from script_a.js!" );
}
$( run );

//script_b.js
function run( ) {
    console.log( "I'm running from script_b.js!" );
}
$( run );

这可能会导致混乱,当许多文件被压缩并挤在一起时会导致问题;script_a声明了一个全局变量,然后在script_b中再次声明,运行代码时,我们在控制台上看到以下内容:

> I'm running from script_b.js!
> I'm running from script_b.js!

解决这个问题并限制全局范围的污染最常见的方法是将我们的文件包装在一个匿名函数中,如下所示:

//script_a.js
(function( $, undefined ) {
    function run( ) {
        console.log( "I'm running from script_a.js!" );
    }
    $( run );
})( jQuery );

//script_b.js
(function( $, undefined ) {
    function run( ) {
        console.log( "I'm running from script_b.js!" );
    }
    $( run );
})( jQuery );

现在当我们运行这个时,它按预期工作:

> I'm running from script_a.js!
> I'm running from script_b.js!

这对于不依赖外部的代码来说是很好的,但是对于依赖外部代码的代码该怎么办呢?我们只需要导出它,对吧?

类似以下代码将会起作用:

(function( undefined ) {
    function Logger(){  
    }
    Logger.prototype.log = function( message /*...*/ ){
        console.log.apply( console, arguments );
    }
    this.Logger = Logger; 
})( )

现在,当我们运行这个脚本时,我们可以从全局范围访问 Logger:

var logger = new Logger( );
logger.log( "This", "is", "pretty", "cool" )
> This is pretty cool

所以现在我们可以分享我们的库,一切都很好;但是如果其他人已经有一个暴露相同Logger类的库呢。

node是如何解决这个问题的呢?Hello require!

Node.js 有一种简单的方式来从外部来源引入脚本和模块,类似于 PHP 中的 require。

让我们在这个结构中创建一些文件:

/example-2
    /util
        index.js
        logger.js
    main.js

/* util/index.js */
var logger = new Logger( )
var util = {
    logger: logger
};

/* util/logger.js */

function Logger(){
}
Logger.prototype.log = function( message /*...*/ ){
    console.log.apply( console, arguments );
};

/* main.js */
util.logger.log( "This is pretty cool" );

我们可以看到main.js依赖于util/index.js,而util/index.js又依赖于util/logger.js

这应该可以正常工作吧?也许不是。让我们运行命令:

[~/src/examples/example-2]$ node main.js
ReferenceError: logger is not defined
 at Object.<anonymous> (/Users/fabian/examples/example-2/main.js:1:63)
 /* Removed for simplicity */
 at Node.js:814:3

那么为什么会这样呢?它们不应该共享相同的全局范围吗?嗯,在 Node.js 中,情况有些不同。还记得我们之前包装文件的那些匿名函数吗?Node.js 会自动将我们的脚本包装在其中,这就是 Require 适用的地方。

让我们修复我们的文件,如下所示:

/* util/index.js */
Logger = require( "./logger" )

/* main.js */
util = require( "./util" );  

如果您注意到,我在需要util/index.js时没有使用index.js;原因是当您需要一个文件夹而不是一个文件时,您可以指定一个代表该文件夹代码的索引文件。这对于像模型文件夹这样的东西非常方便,您可以在一个 require 中公开所有模型,而不是为每个模型单独 require。

现在,我们已经需要了我们的文件。但是我们得到了什么?

[~/src/examples/example-2]$ node
> var util = require( "./util" );
> console.log( util );
{} 

但是,还没有日志记录器。我们错过了一个重要的步骤;我们没有告诉 Node.js 我们想要在我们的文件中公开什么。

要在 Node.js 中公开某些内容,我们使用一个名为module.exports的对象。有一个简写引用,就是exports。当我们的文件包装在一个匿名函数中时,moduleexports都作为参数传递,如下例所示:

function Module( ) {
    this.exports = { };
}

function require( file ) {
    // .....
    returns module.exports;
} 

var module = new Module( );
var exports = module.exports;

(function( exports, require, module ) {
    exports = "Value a"
    module.exports = "Value b"
})( exports, require, module );
console.log( module.exports );
// Value b

提示

下载示例代码

您可以从您在www.packtpub.com购买的所有 Packt 图书中下载示例代码文件。如果您在其他地方购买了本书,您可以访问www.packtpub.com/support并注册,以便直接通过电子邮件接收文件。

示例显示exports最初只是对module.exports的引用。这意味着,如果您使用exports = { },则您设置的值在函数范围之外将无法访问。但是,当您向exports对象添加属性时,实际上是向module.exports对象添加属性,因为它们都是相同的值。将值分配给module.exports将导出该值,因为它可以通过模块在函数范围之外访问。

有了这个知识,我们最终可以以以下方式运行我们的脚本:

/* util/index.js */
Logger = require( "./logger.js" );
exports.logger = new Logger( );

/* util/logger.js */
function Logger( ){
} 
Logger.prototype.log = ( message /*...*/ ) {
    console.log.apply( console, arguments );
};
module.exports = Logger;

/* main.js */
util = require( "./utils" );
util.logger.log( "This is pretty cool" );

运行main.js

[~/src/examples/example-2]$ node main.js
This is pretty cool

还可以使用 Require 在我们的代码中包含模块。在需要模块时,我们不需要使用文件路径,只需要使用我们想要的node模块的名称。

Node.js 包括许多预构建的核心模块,其中之一是util模块。您可以在nodejs.org/api/util.html找到util模块的详细信息。

让我们看看util模块命令:

[~]$ node
> var util = require( "util" )
> util.log( 'This is pretty cool as well' )
01 Jan 00:00:00 - This is pretty cool as well 

你好 npm

除了内部模块之外,还有一个完整的包生态系统;Node.js 最常见的包管理器是npm。截至目前,共有 192,875 个可用的包。

我们可以使用npm来访问为我们执行许多操作的包,从路由 HTTP 请求到构建我们的项目。您还可以浏览www.npmjs.com/上提供的包。

使用包管理器,您可以引入其他模块,这很好,因为您可以花更多时间在业务逻辑上,而不是重新发明轮子。

让我们下载以下包,使我们的日志消息变得丰富多彩:

[~/src/examples/example-3]$ npm install chalk

现在,要使用它,创建一个文件并需要它:

[~/src/examples/example-3]$ touch index.js
/* index.js */
var chalk = require( "chalk" );
console.log( "I am just normal text" )
console.log( chalk.blue( "I am blue text!" ) )

运行此代码时,您将看到默认颜色的第一条消息和蓝色的第二条消息。让我们看看这个命令:

[~/src/examples/example-3]$ node index.js
I am just normal text
I am blue text!

当您需要某个其他人已经实现的东西时,下载现有包的能力非常方便。正如我们之前所说,有很多可供选择的包。

我们需要跟踪这些依赖关系,有一个简单的解决方案:package.json

使用package.json,我们可以定义诸如项目名称、主要脚本是什么、如何运行测试、我们的依赖关系等内容。您可以在docs.npmjs.com/files/package.json找到属性的完整列表。

npm提供了一个方便的命令来创建这些文件,并且会询问您创建package.json文件所需的相关问题:

[~/src/examples/example-3]$ npm init

上述实用程序将引导您完成创建package.json文件的过程。

它只涵盖了最常见的项目,并尝试猜测有效的默认值。

运行npm help json命令以获取有关这些字段的最终文档,并了解它们的确切作用。

之后,使用npm和安装<pkg> --save来安装一个包并将其保存为package.json文件中的依赖项。

^C随时退出:

name: (example-3)
version: (1.0.0) 
description: 
entry point: (main.js)
test command: 
git repository: 
keywords:
license: (ISC) 
About to write to /examples/example-3/package.json:
{
  "name": "example-3",
  "version": "1.0.0",
  "description": "",
  "main": "main.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "....",
  "license": "ISC"
}
Is this ok? (yes) 

该实用程序将为您提供默认值,因此最好只需使用Enter键跳过它们。

现在,在安装我们的包时,我们可以使用--save选项将chalk保存为依赖项,如下所示:

[~/src/examples/example-3]$ npm install --save chalk

我们可以看到 chalk 已经被添加了:

[~/examples/example-3]$ cat package.json
{
  "name": "example-3",
  "version": "1.0.0",
  "description": "",
  "main": "main.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "...",
  "license": "ISC",
  "dependencies": {
    "chalk": "¹.0.0"
  }
}

我们可以通过修改package.json文件手动添加这些依赖项;这是保存依赖项的最常见方法。

您可以在此处阅读有关包文件的更多信息:docs.npmjs.com/files/package.json

如果您正在创建服务器或应用程序而不是模块,您很可能希望找到一种方法,以便无需始终提供主文件的路径来启动您的进程;这就是package.json文件中的脚本对象发挥作用的地方。

要设置启动脚本,您只需在scripts对象中设置start属性,如下所示:

"scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "node server.js"
}

现在,我们需要做的就是运行 npm start,然后 npm 将运行我们已经指定的启动脚本。

我们可以定义更多的脚本,例如,如果我们想要一个用于开发环境的启动脚本,我们也可以定义一个开发属性;但是,对于非标准的脚本名称,我们需要使用npm run <script>而不是只使用npm <script>。例如,如果我们想要运行我们的新开发脚本,我们将不得不使用npm run development

npm具有在不同时间触发的脚本。我们可以定义一个postinstall脚本,该脚本在运行npm install后运行;如果我们想要触发包管理器来安装模块(例如,bower),我们可以使用这个。

您可以在此处阅读有关脚本对象的更多信息:docs.npmjs.com/misc/scripts

如果您正在团队开发中工作,需要定义一个包,其中项目将安装在不同的机器上。如果您使用诸如git之类的源代码控制工具,建议您将node_modules目录添加到您的忽略文件中,如下所示:

[~/examples/example-3]$ echo "node_modules" > .gitignore
[~/examples/example-3]$ cat .gitignore
node_modules

总结

这很快,不是吗?我们已经涵盖了我们继续旅程所需的 Node.js 的基础知识。

我们已经介绍了相对于常规 JavaScript 代码在浏览器中,如何轻松地暴露和保护公共和私有代码,全局范围可能会受到严重污染。

我们还知道如何从外部源包括包和代码,以及如何确保所包含的包是一致的。

正如您所看到的,在许多包管理器中有一个庞大的包生态系统,例如npm,正等待我们使用和消耗。

在下一章中,我们将专注于创建一个简单的服务器来路由、认证和消耗请求。

为 Bentham Chang 准备,Safari ID 为 bentham@gmail.com 用户编号:2843974 © 2015 Safari Books Online,LLC。此下载文件仅供个人使用,并受到服务条款的约束。任何其他用途均需版权所有者的事先书面同意。未经授权的使用、复制和/或分发严格禁止,并违反适用法律。保留所有权利。

第二章:简单的 HTTP

现在我们已经了解了基础知识,我们可以继续学习一些更有用的东西。在本章中,我们将学习如何创建一个 HTTP 服务器和路由请求。在使用 Node.js 时,你会经常遇到 HTTP,因为服务器端脚本是 Node.js 的常见用途之一。

Node.js 自带一个内置的 HTTP 服务器;你所需要做的就是要求包含的http包并创建一个服务器。你可以在nodejs.org/api/http.html上阅读更多关于该包的信息。

var Http = require( 'http' );

var server = Http.createServer( );

这将创建一个属于你自己的 HTTP 服务器,准备就绪。然而,在这种状态下,它不会监听任何请求。我们可以在任何可用的端口或套接字上开始监听,如下所示:

var Http = require( 'http' );

var server = Http.createServer( );
server.listen( 8080, function( ) {
    console.log( 'Listening on port 8080' ); 
});

让我们把前面的代码保存为server.js并运行它:

[~/examples/example-4]$ node server.js
Listening on port 8080

通过在浏览器中导航到http://localhost:8080/,你会看到请求已被接受,但服务器没有响应;这是因为我们还没有处理这些请求,我们只是在监听它们。

当我们创建服务器时,我们可以传递一个回调函数,每次有请求时都会调用它。传递的参数将是:requestresponse

function requestHandler( request, response ) {
}
var server = Http.createServer( requestHandler );

现在每次收到请求时,我们都可以做一些事情:

var count = 0;
function requestHandler( request, response ) {
    var message;
    count += 1;
    response.writeHead( 201, {
        'Content-Type': 'text/plain'
    });

    message = 'Visitor count: ' + count;
    console.log( message );
    response.end( message );
}

让我们运行脚本并从浏览器请求页面;你应该看到访客计数:1返回到浏览器:

[~/examples/example-4]$ node server.js
Listening on port 8080
Visitor count: 1
Visitor count: 2

然而,出现了一些奇怪的事情:多生成了一个请求。谁是访客 2?

http.IncomingMessage(参数request暴露了一些属性,可以用来弄清楚这一点。我们现在最感兴趣的属性是url。我们期望只有/被请求,所以让我们把这个添加到我们的消息中:

message = 'Visitor count: ' + count + ', path: ' + request.url;

现在你可以运行代码,看看发生了什么。你会注意到/favicon.ico也被请求了。如果你没有看到这个,那么你一定在想我在说什么,或者你的浏览器最近是否访问过http://localhost:8080并且已经缓存了图标。如果是这种情况,你可以手动请求图标,例如从http://localhost:8080/favicon.ico

[~/examples/example-4]$ node server.js
Listening on port 8080
Visitor count: 1, path: /
Visitor count: 2, path: /favicon.ico

我们还可以看到,如果我们请求任何其他页面,我们将得到正确的路径,如下所示:

[~/examples/example-4]$ node server.js
Listening on port 8080
Visitor count: 1, path: /
Visitor count: 2, path: /favicon.ico
Visitor count: 3, path: /test
Visitor count: 4, path: /favicon.ico
Visitor count: 5, path: /foo
Visitor count: 6, path: /favicon.ico
Visitor count: 7, path: /bar
Visitor count: 8, path: /favicon.ico
Visitor count: 9, path: /foo/bar/baz/qux/norf
Visitor count: 10, path: /favicon.ico

然而,这并不是我们想要的结果,除了少数路由之外,我们希望返回404: Not Found

介绍路由

路由对于几乎所有的 Node.js 服务器都是必不可少的。首先,我们将实现我们自己的简单版本,然后再转向更复杂的路由。

我们可以使用switch语句来实现我们自己的简单路由器,例如:

function requestHandler( request, response ) {
    var message,
        status = 200;

    count += 1;

    switch( request.url ) {
        case '/count':
            message = count.toString( );
            break;
        case '/hello':
            message = 'World';
            break;
        default: 
            status = 404;
            message = 'Not Found';
            break;
    }

    response.writeHead( 201, {
        'Content-Type': 'text/plain'
    });
    console.log( request.url, status, message );
    response.end( message ); 
}

让我们运行以下示例:

[~/examples/example-4]$ node server.js
Listening on port 8080
/foo 404 Not Found
/bar 404 Not Found
/world 404 Not Found
/count 200 4
/hello 200 World
/count 200 6

你可以看到每次请求时计数都在增加;然而,它并不是每次都返回。如果我们没有为该路由定义一个特定的情况,我们将返回404: Not Found

对于实现 RESTful 接口的服务,我们希望能够根据 HTTP 方法路由请求。请求对象使用method属性来暴露这一点。

将这个添加到日志中,我们可以看到这个:

console.log( request.method, request.url, status, message );

运行示例并执行你的请求,你可以使用一个 REST 客户端来调用一个 POST 请求:

[~/examples/example-4]$ node server.js
Listening on port 8080
GET /count 200 1
POST /count 200 2
PUT /count 200 3
DELETE /count 200 4

我们可以实现一个路由器来根据方法路由,但是已经有一些包可以为我们做到这一点。现在我们将使用一个叫做router的简单包:

[~/examples/example-5]$ npm install router

现在,我们可以对我们的请求进行一些更复杂的路由:

让我们创建一个简单的 RESTful 接口。

首先,我们需要创建服务器,如下所示:

/* server.js */
var Http = require( 'http' ),
    Router = require( 'router' ), 
    server,
    router; 

router = new Router( );

server = Http.createServer( function( request, response ) {
    router( request, response, function( error ) {
        if( !error ) {
            response.writeHead( 404 );
        } else {
            //Handle errors
            console.log( error.message, error.stack );
            response.writeHead( 400 );
        }       
        response.end( '\n' );
    });
});

server.listen( 8080, function( ) {
    console.log( 'Listening on port 8080' );
});

运行服务器应该显示服务器正在监听。

[~/examples/example-5]$ node server.js
Listening on port 8080

我们想要定义一个简单的接口来读取、保存和删除消息。我们可能还想要读取单个消息以及消息列表;这本质上定义了一组 RESTful 端点。

REST 代表Representational State Transfer;这是许多 HTTP 编程接口使用的一种非常简单和常见的风格。

我们想要定义的端点是:

HTTP 方法 端点 用途
POST /message 创建消息
GET /message/:id 读取消息
DELETE /message/:id 删除消息
GET /message 读取多条消息

对于每种 HTTP 方法,路由器都有一种用于映射路由的方法。这个接口的形式是:

router.<HTTP method>( <path>, [ ... <handler> ] )

我们可以为每个路由定义多个处理程序,但我们稍后会回到这一点。

我们将逐个路由进行实现,并将代码追加到server.js的末尾。

我们想要把我们的消息存储在某个地方,在现实世界中我们会把它们存储在数据库中;然而,为了简单起见,我们将使用一个带有简单计数器的数组,如下所示:

var counter = 0,
    messages = { };

我们的第一个路由将用于创建消息:

function createMessage( request, response ) {
    var id = counter += 1;
    console.log( 'Create message', id );
    response.writeHead( 201, {
        'Content-Type': 'text/plain'
    });
    response.end( 'Message ' + id );
}
router.post( '/message', createMessage );

我们可以通过运行服务器并向http://localhost:8000/message发送 POST 请求来确保这个路由工作。

[~/examples/example-5]$ node server.js
Listening on port 8080
Create message 1
Create message 2
Create message 3

我们还可以确认计数器正在递增,因为每次我们发出请求时 id 都会增加。我们将这样做来跟踪消息的数量并为每条消息赋予一个唯一的 id。

现在这个工作了,我们需要能够读取消息文本,为此我们需要能够读取客户端发送的请求正文。这就是多个处理程序发挥作用的地方。我们可以以两种不同的方式来解决这个问题,如果我们只在一个路由中读取正文,或者如果我们正在执行与路由特定的其他操作,例如授权,我们将在路由中添加一个额外的处理程序,例如:

router.post( '/message', parseBody, createMessage ) 

我们可以通过为所有方法和路由添加一个处理程序来完成另一种方式;这将在路由处理程序之前首先执行,这些通常被称为中间件。您可以将处理程序视为一系列函数,其中每个函数都在完成其任务后调用下一个函数。有了这个想法,您应该注意添加处理程序的顺序,无论是中间件还是路由,都将决定操作的顺序。这意味着,如果我们注册一个对所有方法执行的处理程序,我们必须首先执行这个处理程序。

路由器公开了一个函数来添加以下处理程序:

router.use( function( request, response, next ) {
    console.log( 'middleware executed' );
    // Null as there were no errors
    // If there was an error then we could call `next( error );`
    next( null );
});

您可以将此代码添加到createMessage的实现之前:

完成后,运行服务器并进行以下请求:

[~/examples/example-5]$ node server.js
Listening on port 8080
middleware executed
Create message 1

中间件在路由处理程序之前执行。

现在我们知道了中间件的工作原理,我们可以按照以下方式使用它们:

[~/examples/example-5]$ npm install body-parser

用以下内容替换我们的自定义中间件:

var BodyParser = require( 'body-parser' );
router.use( BodyParser.text( ) );

在这个阶段,我们只想将所有请求读取为纯文本。

现在我们可以在createMessage中检索消息。

function createMessage( request, response ) {
    var id = counter += 1,
        message = request.body;

    console.log( 'Create message', id, message );
    messages[ id ] = message;
    response.writeHead( 201, {
        'Content-Type': 'text/plain',
        'Location': '/message/' + id 
    });
    response.end( message );
}

运行server.js并向http://localhost:8080/message发送POST请求;你会看到类似以下消息:

[~/examples/example-5]$ node server.js
Listening on port 8080
Create message 1 Hello foo
Create message 2 Hello bar

如果你注意到,你会发现一个标题返回了消息的新位置和它的 id,如果我们请求http://localhost:8080/message/1,应该返回第一条消息的内容。

然而,这个路由有一些不同之处;每次创建消息时都会生成一个密钥。我们不想为每条新消息设置一个新的路由,因为这样效率非常低。相反,我们创建一个与模式匹配的路由,比如/message/:id。这是在 Node.js 中定义动态路由的常见方式。

路由的id部分称为参数。我们可以在我们的路由中定义任意数量的这些参数,并使用请求引用它们;例如,我们可以有一个类似于/user/:id/profile/:attribute的路由。

有了这个想法,我们可以创建我们的readMessage处理程序,如下所示:

function readMessage( request, response ) {
    var id = request.params.id,
        message = messages[ id ];
    console.log( 'Read message', id, message );

    response.writeHead( 200, {
        'Content-Type': 'text/plain'
    });
    response.end( message );
}
router.get( '/message/:id', readMessage );

现在让我们把前面的代码保存在server.js文件中并运行服务器:

[~/examples/example-5]$ node server.js
Listening on port 8080
Create message 1 Hello foo
Read message 1 Hello foo
Create message 2 Hello bar
Read message 2 Hello bar
Read message 1 Hello foo

通过向服务器发送一些请求,我们可以看到它正在工作。

删除消息几乎与读取消息相同;但我们不返回任何内容并将原始消息值设置为 null:

function deleteMessage( request, response ) {
    var id = request.params.id;

    console.log( 'Delete message', id );

    messages[ id ] = undefined;

    response.writeHead( 204, { } );

    response.end( '' );
}

router.delete( '/message/:id', deleteMessage )

首先运行服务器,然后按照以下方式创建、读取和删除消息:

[~/examples/example-5]$ node server.js
Listening on port 8080
Delete message 1
Create message 1 Hello
Read message 1 Hello
Delete message 1
Read message 1 undefined

看起来不错;然而,我们遇到了一个问题。我们不应该在删除消息后再次读取消息;如果我们找不到消息,我们将在读取和删除处理程序中返回404。我们可以通过向我们的读取和删除处理程序添加以下代码来实现这一点:

    var id = request.params.id,
        message = messages[ id ];

    if( typeof message !== 'string' ) {
        console.log( 'Message not found', id );

        response.writeHead( 404 );
        response.end( '\n' );
        return;
    } 

现在让我们把前面的代码保存在server.js文件中并运行服务器:

[~/examples/example-5]$ node server.js
Listening on port 8080
Message not found 1
Create message 1 Hello
Read message 1 Hello

最后,我们希望能够阅读所有消息并返回所有消息值的列表:

function readMessages( request, response ) {
    var id,
        message,
        messageList = [ ],
        messageString;

    for( id in messages ) {
        if( !messages.hasOwnProperty( id ) ) {
            continue;
        }
        message = messages[ id ];
        // Handle deleted messages
        if( typeof message !== 'string' ) {
            continue;
        }
        messageList.push( message );
    }

    console.log( 'Read messages', JSON.stringify( 
        messageList, 
        null, 
        '  ' 
    ));

    messageString = messageList.join( '\n' );

    response.writeHead( 200, {
        'Content-Type': 'text/plain'
    });

    response.end( messageString );
}
router.get( '/message', readMessages );

现在让我们把前面的代码保存在server.js文件中并运行服务器:

[~/examples/example-5]$ node server.js
Listening on port 8080
Create message 1 Hello 1
Create message 2 Hello 2
Create message 3 Hello 3
Create message 4 Hello 4
Create message 5 Hello 5
Read messages [
 "Hello 1",
 "Hello 2",
 "Hello 3",
 "Hello 4",
 "Hello 5"
]

太棒了;现在我们有了一个完整的 RESTful 接口来读写消息。但是,我们不希望每个人都能读取我们的消息;它们应该是安全的,我们还想知道谁创建了这些消息,我们将在下一章中介绍这个问题。

总结

现在我们拥有了制作一些非常酷的服务所需的一切。我们现在可以从头开始创建一个 HTTP,路由我们的请求,并创建一个 RESTful 接口。

这将帮助您创建完整的 Node.JS 服务。在下一章中,我们将介绍身份验证。

为 Bentham Chang 准备,Safari ID 为 bentham@gmail.com 用户编号:2843974 © 2015 Safari Books Online,LLC。此下载文件仅供个人使用,并受到服务条款的约束。任何其他使用均需版权所有者的事先书面同意。未经授权的使用、复制和/或分发严格禁止并违反适用法律。保留所有权利。

第三章:认证

我们现在可以创建 RESTful API,但我们不希望每个人都能访问我们暴露的所有内容。我们希望路由是安全的,并且能够跟踪谁在做什么。

Passport 是一个很棒的模块,另一个中间件,帮助我们验证请求。

Passport 公开了一个简单的 API,供提供者扩展并创建策略来验证用户。在撰写本文时,有 307 个官方支持的策略;但是,您完全可以编写自己的策略并发布供他人使用。

基本身份验证

passport 最简单的策略是接受用户名和密码的本地策略。

我们将为这些示例引入 express 框架,现在您已经了解了它在底层的基本工作原理,我们可以将它们整合在一起。

您可以安装expressbody-parserpassportpassport-local。Express 是一个内置电池的 Node.js Web 框架,包括路由和使用中间件的能力:

[~/examples/example-19]$ npm install express body-parser passport passport-local

目前,我们可以将我们的用户存储在一个简单的对象中,以便以后引用,如下所示:

var users = {
    foo: {
        username: 'foo',
        password: 'bar',
        id: 1
    },
    bar: {
        username: 'bar',
        password: 'foo',
        id: 2
    }
}

一旦我们有了一些用户,我们就需要设置 passport。当我们创建本地策略的实例时,我们需要提供一个verify回调,其中我们检查用户名和密码,同时返回一个用户:

var Passport = require( 'passport' ),
    LocalStrategy = require( 'passport-local' ).Strategy;

var localStrategy = new LocalStrategy({
    usernameField: 'username',
    passwordField: 'password'
  },
  function(username, password, done) {
    user = users[ username ];

    if ( user == null ) {
        return done( null, false, { message: 'Invalid user' } );
    }

    if ( user.password !== password ) {
        return done( null, false, { message: 'Invalid password' } );    
    }

    done( null, user );
  }
)

在这种情况下,verify回调期望使用done调用用户。它还允许我们在用户无效或密码错误时提供信息。

现在,我们有了一个策略,我们可以将其传递给 passport,这允许我们以后引用它并用它来验证我们的请求,如下所示:

Passport.use( 'local', localStrategy );

您可以在每个应用程序中使用多种策略,并通过您传递的名称引用每个策略,在这种情况下是'local'

现在,让我们创建我们的服务器,如下所示:

var Express = require( 'express' );

var app = Express( );

我们将不得不使用body-parser中间件。这将确保当我们发布到我们的登录路由时,我们可以读取我们的主体;我们还需要初始化 passport:

var BodyParser = require( 'body-parser' );
app.use( BodyParser.urlencoded( { extended: false } ) );
app.use( BodyParser.json( ) );
app.use( Passport.initialize( ) );

要登录到我们的应用程序,我们需要创建一个使用身份验证的post路由作为处理程序之一。其代码如下:

app.post(
    '/login',
    Passport.authenticate( 'local', { session: false } ),
    function ( request, response ) {

    }
);

现在,当我们向/login发送POST请求时,服务器将验证我们的请求。

经过身份验证后,user属性将填充在请求对象上,如下所示:

app.post(
    '/login',
    Passport.authenticate( 'local', { session: false } ),
    function ( request, response ) {
        response.send( 'User Id ' + request.user.id );
    }
);

最后,我们需要监听请求,就像所有其他服务器一样:

app.listen( 8080, function( ) {
    console.log( 'Listening on port 8080' );
});

让我们运行示例:

[~/examples/example-19]$ node server.js
Listening on port 8080

现在,当我们向服务器发送POST请求时,我们可以验证用户。如果用户没有同时传递用户名和密码,服务器将返回400 Bad Request

提示

如果您不熟悉curl,您可以使用诸如 Advanced REST Client 之类的工具:

chromerestclient.appspot.com/

在接下来的示例中,我将使用命令行界面curl

我们可以通过执行POST/login命令来执行登录请求:

[~]$ curl -X POST http://localhost:8080/login -v
< HTTP/1.1 400 Bad Request

如果用户提供了错误的详细信息,那么将返回401 Unauthorized

[~]$ curl -X POST http://localhost:8080/login \
 -H 'Content-Type: application/json' \
 -d '{"username":"foo","password":"foo"}' \
 -v
< HTTP/1.1 401 Unauthorized

如果我们提供了正确的详细信息,那么我们可以看到我们的处理程序被调用,并且正确的数据被返回:

[~]$ curl -X POST http://localhost:8080/login \
 -H 'Content-Type: application/json' \
 -d '{"username":"foo","password":"bar"}'
User Id 1
[~]$ curl -X POST http://localhost:8080/login \
 -H 'Content-Type: application/json' \
 -d '{"username":"bar","password":"foo"}'
User Id 2

Bearer 令牌

现在我们有了一个经过身份验证的用户,我们可以生成一个令牌,以便在将来的请求中使用,而不是在任何地方都传递我们的用户名和密码。这通常被称为 Bearer 令牌,方便的是,passport 有一个策略可以实现这一点。

对于我们的令牌,我们将使用一种称为JSON Web TokenJWT)的东西。JWT 允许我们从 JSON 对象中编码令牌,然后解码和验证它们。存储在其中的数据是开放和简单的,因此不应该在其中存储密码;但是,它使验证用户变得非常简单。我们还可以为这些令牌提供到期日期,这有助于限制令牌被暴露的严重性。

您可以在jwt.io/上阅读有关 JWT 的更多信息。

您可以使用以下命令安装 JWT:

[~/examples/example-19]$ npm install jsonwebtoken

一旦用户经过身份验证,我们就可以安全地为他们提供一个令牌,以便在将来的请求中使用:

var JSONWebToken = require( 'jsonwebtoken' ),
    Crypto = require( 'crypto' );

var generateToken = function ( request, response ) {

    // The payload just contains the id of the user
    // and their username, we can verify whether the claim
    // is correct using JSONWebToken.verify     
    var payload = {
        id: user.id,
        username: user.username
    };
    // Generate a random string
    // Usually this would be an app wide constant
    // But can be done both ways
    var secret = Crypto.randomBytes( 128 )
                       .toString( 'base64' );
    // Create the token with a payload and secret
    var token = JSONWebToken.sign( payload, secret );

    // The user is still referencing the same object
    // in users, so no need to set it again
    // If we were using a database, we would save
    // it here
    request.user.secret = secret

    return token;
}

var generateTokenHandler = function ( request, response  ) {
    var user = request.user;    
    // Generate our token
    var token = generateToken( user );
    // Return the user a token to use
    response.send( token );
};

app.post(
    '/login',
    Passport.authenticate( 'local', { session: false } ),
    generateTokenHandler
);

现在,当用户登录时,他们将收到一个我们可以验证的令牌。

让我们运行我们的 Node.js 服务器:

[~/examples/example-19]$ node server.js
Listening on port 8080

现在我们登录时会收到一个令牌:

[~]$ curl -X POST http://localhost:8080/login \
 -H 'Content-Type: application/json' \
 -d '{"username":"foo","password":"bar"}'
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZC
I6MSwidXNlcm5hbWUiOiJmb28iLCJpYXQiOjE0MzcyO
TQ3OTV9.iOZO7oCIceZl6YvZqVP9WZLRx-XVvJFMF1p
pPCEsGGs

我们可以将此输入调试器中的jwt.io/并查看内容,如下所示:

{
  "id": 1,
  "username": "foo",
  "iat": 1437294795
}

如果我们有密钥,我们可以验证令牌是否正确。签名每次请求令牌时都会更改:

[~]$ curl -X POST http://localhost:8080/login \
 -H 'Content-Type: application/json' \
 -d '{"username":"foo","password":"bar"}'
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZC
I6MSwidXNlcm5hbWUiOiJmb28iLCJpYXQiOjE0MzcyO
TQ5OTl9.n1eRQVOM9qORTIMUpslH-ycTNEYdLDKa9lU
pmhf44s0

我们可以使用passport-bearer对用户进行身份验证;它的设置方式与passport-local非常相似。但是,与其从主体接受用户名和密码不同,我们接受一个持票人令牌;这可以通过查询字符串、主体或Authorization标头传递:

首先,我们必须安装passport-http-bearer

[~/examples/example-19]$ npm install passport-http-bearer

然后让我们创建我们的验证器。有两个步骤:第一步是确保解码的信息与我们的用户匹配,这通常是我们检索用户的地方;然后,一旦我们有一个用户并且它是有效的,我们可以根据用户的密钥检查令牌是否有效:

var BearerStrategy = require( 'passport-http-bearer' ).Strategy;

var verifyToken = function( token, done ) {
    var payload = JSONWebToken.decode( token );
    var user = users[ payload.username ];
    // If we can't find a user, or the information
    // doesn't match then return false
    if ( user == null ||
         user.id !== payload.id ||
         user.username !== payload.username ) {
        return done( null, false );
    }
    // Ensure the token is valid now we have a user
    JSONWebToken.verify( token, user.secret, function ( error, decoded ) {
        if ( error || decoded == null ) {
            return done( error, false );
        }
        return done( null, user );
    });
}   
var bearerStrategy = new BearerStrategy(
    verifyToken
)

我们可以将此策略注册为持票人,以便以后使用:

Passport.use( 'bearer', bearerStrategy );

我们可以创建一个简单的路由,用于检索经过身份验证的用户的用户详细信息:

app.get(
    '/userinfo',
    Passport.authenticate( 'bearer', { session: false } ),
    function ( request, response ) {
        var user = request.user;
        response.send( {
            id: user.id,
            username: user.username
        });
    }
);

让我们运行 Node.js 服务器:

[~/examples/example-19]$ node server.js
Listening on port 8080

一旦我们收到一个令牌:

[~]$ curl -X POST http://localhost:8080/login \
 -H 'Content-Type: application/json' \
 -d '{"username":"foo","password":"bar"}'

我们可以在我们的请求中使用结果:

[~]$ curl -X GET http://localhost:8080/userinfo \
 -H 'Authorization: Bearer <token>'
{"id":1,"username":"foo"}

OAuth

OAuth 提供了许多优势;例如,它不需要处理用户的实际识别。我们可以让用户使用他们信任的服务登录,例如 Google、Facebook 或 Auth0。

在接下来的示例中,我将使用Auth0。他们提供了一个免费帐户供您使用:auth0.com/

您需要注册并创建一个api(选择AngularJS + Node.js),然后转到设置并记下域、客户端 ID 和客户端密钥。您需要这些来设置OAuth

我们可以使用passport-oauth2使用 OAuth 进行身份验证:

[~/examples/example-19]$ npm install --save passport-oauth2

与我们的持票人令牌一样,我们希望验证服务器返回的内容,这将是一个具有 ID 的用户对象。我们将与我们的数据中的用户匹配或创建一个新用户:

var validateOAuth = function ( accessToken, refreshToken, profile, done ) {

    var keys = Object.keys( users ), user = null;

    for( var iKey = 0; iKey < keys.length; i += 1 ) {
        user = users[ key ];
        if ( user.thirdPartyId !== profile.user_id ) { continue; }
        return done( null, user );
    }

    users[ profile.name ] = user = {
        username: profile.name,
        id: keys.length,
        thirdPartyId: profile.user_id
    }
    done( null, user );

};

一旦我们有一个验证用户的函数,我们就可以为我们的 OAuth 策略组合选项:

var oAuthOptions = {
    authorizationURL: 'https://<domain>.auth0.com/authorize',
    tokenURL: 'https://<domain>.auth0.com/oauth/token',
    clientID: '<client id>',
    clientSecret: '<client secret>',
    callbackURL: "http://localhost:8080/oauth/callback"
}

然后我们创建我们的策略,如下所示:

var OAuth2Strategy = require( 'passport-oauth2' ).Strategy;
oAuthStrategy = new OAuth2Strategy( oAuthOptions, validateOAuth );

在使用我们的策略之前,我们需要使用我们自己的策略userProfile方法进行鸭子类型处理,这样我们就可以请求用户对象在validateOAuth中使用:

var parseUserProfile = function ( done, error, body ) {
    if ( error ) {
        return done( new Error( 'Failed to fetch user profile' ) )
    }

    var json;
    try {
        json = JSON.parse( body );
    } catch ( error ) {
        return done( error );
    }
    done( null, json );
}

var getUserProfile = function( accessToken, done ) {
    oAuthStrategy._oauth2.get(
        "https://<domain>.auth0.com/userinfo",
        accessToken,
        parseUserProfile.bind( null, done )
    )
}
oAuthStrategy.userProfile = getUserProfile

我们可以将此策略注册为oauth,以便以后使用:

Passport.use( 'oauth', oAuthStrategy );

我们需要创建两个路由来处理我们的 OAuth 身份验证:一个路由用于启动流程,另一个用于识别服务器返回:

app.get( '/oauth', Passport.authenticate( 'oauth', { session: false } ) );

我们可以在这里使用我们的generateTokenHandler,因为我们的请求上会有一个用户。

app.get( '/oauth/callback',
  Passport.authenticate( 'oauth', { session: false } ),
  generateTokenHandler
);

我们现在可以启动我们的服务器并请求http://localhost:8080/oauth;服务器将重定向您到Auth0。登录后,您将收到一个令牌,您可以在/userinfo中使用。

如果您使用会话,您可以将用户保存到会话中,并将其重定向回您的首页(或为已登录用户设置的默认页面)。对于单页应用程序,例如使用 Angular 时,您可能希望将用户重定向到 URL 中带有令牌,以便客户端框架抓取并保存。

总结

我们现在可以对用户进行身份验证;这很棒,因为我们现在可以弄清楚这些人是谁,然后限制用户访问某些资源。

在下一章中,我们将介绍调试,如果我们的用户没有被验证,我们可能需要使用它。

为 Bentham Chang 准备,Safari ID bentham@gmail.com 用户编号:2843974 © 2015 Safari Books Online,LLC。此下载文件仅供个人使用,并受到服务条款的约束。任何其他用途均需著作权所有者的事先书面同意。未经授权的使用、复制和/或分发严格禁止并违反适用法律。保留所有权利。

第四章:调试

在使用 Node.js 的过程中,不可避免地会遇到一些棘手的错误。因此,让我们预先期望它们并为那一天做好准备。

日志

我们可以使用一些方法来调试我们的软件;我们要看的第一种方法是日志记录。记录消息的最简单方法是使用console。在大多数先前的示例中,console已被用来描述正在发生的事情,而无需查看整个 HTTP 请求和响应,从而使事情变得更加可读和简单。

一个例子是:

var Http = require( 'http' );

Http.createServer( function( request, response ) {
    console.log( 
        'Received request', 
        request.method,
        request.url 
    )

    console.log( 'Returning 200' );

    response.writeHead( 200, { 'Content-Type': 'text/plain' } );
    response.end( 'Hello World\n' );

}).listen( 8000 );

console.log( 'Server running on port 8000' );

运行此示例将在控制台上记录请求和响应:

[~/examples/example-6]$ node server.js
Server running on port 8000
Received request GET /
Returning 200
Received request GET /favicon.ico
Returning 200
Received request GET /test
Returning 200
Received request GET /favicon.ico
Returning 200

如果我们使用接受中间件的框架,比如 express,我们可以使用一个简单的npm包叫做morgan;您可以在www.npmjs.com/package/morgan找到该包:

[~/examples/example-7]$ npm install morgan
[~/examples/example-7]$ npm install router

我们可以通过使用require将其引入我们的代码并将其添加为中间件来使用它:

var Morgan = require( 'morgan' ),
    Router = require( 'router' ),
    Http = require( 'http' );

router = new Router( );

router.use( Morgan( 'tiny' ) ); 

/* Simple server */
Http.createServer( function( request, response ) {
    router( request, response, function( error ) {
        if( !error ) {
            response.writeHead( 404 );  
        } else {
            //Handle errors
            console.log( error.message, error.stack );
            response.writeHead( 400 );
        }
        response.end( '\n' );

    });
}).listen( 8000 );

console.log( 'Server running on port 8000' );

function getInfo ( request, response ) {
    var info = process.versions;

    info = JSON.stringify( info );
    response.writeHead( 200, { 'Content-Type': 'application/json' } );
    response.end( info );
}
router.get( '/info', getInfo );

服务器运行时,我们可以在不必为每个处理程序添加日志的情况下查看每个请求和响应:

[~/examples/example-7]$ node server.js
Server running on port 8000
GET /test 404 - - 4.492 ms
GET /favicon.ico 404 - - 2.281 ms
GET /info 200 - - 1.120 ms
GET /info 200 - - 1.120 ms
GET /test 404 - - 0.199 ms
GET /info 200 - - 0.494 ms
GET /test 404 - - 0.162 ms

这种类型的日志记录是查看服务器上正在使用的内容以及每个请求花费多长时间的简单方法。在这里,您可以看到第一个请求花费的时间最长,然后它们变得快得多。差异仅为 3 毫秒;如果时间更长,可能会成为一个大问题。

我们可以通过更改我们传递给 morgan 的格式来增加记录的信息,如下所示:

router.use( Morgan( 'combined' ) );

通过运行服务器,您将看到更多信息,例如远程用户、请求的日期和时间、返回的内容量以及他们正在使用的客户端。

[~/examples/example-7]$ node server.js 
Server running on port 8000
::1 - - [07/Jun/2015:11:09:03 +0000] "GET /info HTTP/1.1" 200 - "-" "--REMOVED---"

时间绝对是一个重要因素,因为在筛选您将获得的大量日志时,它可能会有所帮助。有些错误就像一个定时炸弹,等待在周六晚上 3 点爆炸。如果进程已经死亡并且日志已经消失,所有这些日志对我们来说都毫无意义。还有另一个流行且有用的包叫做bunyan,它将许多日志记录方法包装成一个。

Bunyan 带来了可写流的优势,可以将日志写入磁盘上的文件或stdout。这使我们能够保存日志以进行事后调试。您可以在www.npmjs.com/package/bunyan找到有关bunyan的更多详细信息。

现在,让我们安装该软件包。我们希望它在本地和全局都安装,以便我们还可以将其用作命令行工具:

 [~/examples/example-8]$ npm install –g bunyan
 [~/examples/example-8]$ npm install bunyan 

现在,让我们做一些日志记录:

var Bunyan = require( 'bunyan' ),
    logger;

logger = Bunyan.createLogger( {
    name: 'example-8'
});
logger.info( 'Hello logging' );

运行我们的示例:

[~/examples/example-8]$ node index.js
{"name":"example-8","hostname":"macbook.local","pid":2483,"level":30,"msg":"Hello logging","time":"2015-06-07T11:35:13.973Z","v":0}

这看起来不太好看,对吧?Bunyan 使用简单的结构化 JSON 字符串保存消息;这使得它易于解析、扩展和阅读。Bunyan 配备了一个 CLI 实用程序,使一切变得美观。

如果我们使用实用程序运行示例,那么我们将看到输出格式很好:

[~/examples/example-8]$ node index.js | bunyan
[2015-06-07T11:38:59.698Z]  INFO: example-8/2494 on macbook.local: Hello logging

如果我们添加了更多级别,您将在控制台上看到每个级别的颜色不同,以帮助我们识别它们:

var Bunyan = require( 'bunyan' ),
    logger;
logger = Bunyan.createLogger( {
    name: 'example-8'
});
logger.trace( 'Trace' );
logger.debug( 'Debug' );
logger.info( 'Info' );
logger.warn( 'Warn' );
logger.error( 'Error' );
logger.fatal( 'Fatal' );

logger.fatal( 'We got a fatal, lets exit' );
process.exit( 1 );

让我们运行示例:

[~/examples/example-8]$ node index.js | bunyan
[2015-06-07T11:39:55.801Z]  INFO: example-8/2512 on macbook.local: Info
[2015-06-07T11:39:55.811Z]  WARN: example-8/2512 on macbook.local: Warn
[2015-06-07T11:39:55.814Z] ERROR: example-8/2512 on macbook.local: Error
[2015-06-07T11:39:55.814Z] FATAL: example-8/2512 on macbook.local: Fatal
[2015-06-07T11:39:55.814Z] FATAL: example-8/2512 on macbook.local: We got a fatal, lets exit

如果注意到,跟踪和调试没有在控制台上输出。这是因为它们用于跟踪程序的流程而不是关键信息,通常非常嘈杂。

我们可以通过在创建记录器时将其作为选项传递来更改我们想要查看的日志级别:

logger = Bunyan.createLogger( {
    name: 'example-8',
    level: Bunyan.TRACE 
});

现在,当我们运行示例时:

[~/examples/example-8]$ node index.js | bunyan
[2015-06-07T11:55:40.175Z] TRACE: example-8/2621 on macbook.local: Trace
[2015-06-07T11:55:40.177Z] DEBUG: example-8/2621 on macbook.local: Debug
[2015-06-07T11:55:40.178Z]  INFO: example-8/2621 on macbook.local: Info
[2015-06-07T11:55:40.178Z]  WARN: example-8/2621 on macbook.local: Warn
[2015-06-07T11:55:40.178Z] ERROR: example-8/2621 on macbook.local: Error
[2015-06-07T11:55:40.178Z] FATAL: example-8/2621 on macbook.local: Fatal
[2015-06-07T11:55:40.178Z] FATAL: example-8/2621 on macbook.local: We got a fatal, lets exit

通常我们不希望看到低于信息级别的日志,因为任何有用于事后调试的信息都应该使用信息级别或更高级别进行记录。

Bunyan 的 API 非常适用于记录错误和对象的功能。它在其 JSON 输出中保存了正确的结构,可以直接显示:

try {
    ref.go( );
} catch ( error ) {
    logger.error( error );
}

让我们运行示例:

[~/examples/example-9]$ node index.js | bunyan
[2015-06-07T12:00:38.700Z] ERROR: example-9/2635 on macbook.local: ref is not defined
 ReferenceError: ref is not defined
 at Object.<anonymous> (~/examples/example-8/index.js:9:2)
 at Module._compile (module.js:460:26)
 at Object.Module._extensions..js (module.js:478:10)
 at Module.load (module.js:355:32)
 at Function.Module._load (module.js:310:12)
 at Function.Module.runMain (module.js:501:10)
 at startup (node.js:129:16)
 at node.js:814:3

如果我们查看示例并进行漂亮打印,我们将看到它们将其保存为错误:

[~/examples/example-9]$ npm install -g prettyjson
[~/examples/example-9]$ node index.js | prettyjson
name:     example-9
hostname: macbook.local
pid:      2650
level:    50
err: 
 message: ref is not defined
 name:    ReferenceError
 stack: 
 """
 ReferenceError: ref is not defined
 at Object.<anonymous> (~/examples/example-8/index.js:9:2)
 at Module._compile (module.js:460:26)
 at Object.Module._extensions..js (module.js:478:10)
 at Module.load (module.js:355:32)
 at Function.Module._load (module.js:310:12)
 at Function.Module.runMain (module.js:501:10)
 at startup (node.js:129:16)
 at node.js:814:3
 """
msg:      ref is not defined
time:     2015-06-07T12:02:33.875Z
v:        0

这很有用,因为如果您只记录错误,如果您使用了JSON.stringify,则会得到一个空对象,或者如果您使用了toString,则只会得到消息:

try {
    ref.go( );
} catch ( error ) {
    console.log( JSON.stringify( error ) );
    console.log( error );
    console.log( {
        message: error.message
        name: error.name
        stack: error.stack
    });
}

让我们运行示例:

[~/examples/example-10]$ node index.js
{}
[ReferenceError: ref is not defined]
{ message: 'ref is not defined',
 name: 'ReferenceError',
 stack: '--REMOVED--' }

使用logger.error( error )logger.error( { message: error.message /*, ... */ } );更简单和更清晰。

如前所述,bunyan使用流的概念,这意味着我们可以写入文件、stdout或任何其他我们希望扩展到的服务。

要写入文件,我们只需要将其添加到设置时传递给bunyan的选项中:

var Bunyan = require( 'bunyan' ),
    logger;

logger = Bunyan.createLogger( {
    name: 'example-11',
    streams: [
        {
            level: Bunyan.INFO,
            path: './log.log'   
        }
    ]
});
logger.info( process.versions );
logger.info( 'Application started' );

通过运行示例,您将看不到任何日志输出到控制台,而是会写入文件:

 [~/examples/example-11]$ node index.js

如果您列出目录中的内容,您会看到已创建了一个新文件:

[~/examples/example-11]$ ls 
index.js     log.log      node_modules

如果您读取文件中的内容,您会看到日志已经被写入:

[~/examples/example-11]$ cat log.log
{"name":"example-11","hostname":"macbook.local","pid":3614,"level":30,"http_parser":"2.3","node":"0.12.2","v8":"3.28.73","uv":"1.4.2-node1","zlib":"1.2.8","modules":"14","openssl":"1.0.1m","msg":"","time":"2015-06-07T12:29:46.606Z","v":0}
{"name":"example-11","hostname":"macbook.local","pid":3614,"level":30,"msg":"Application started","time":"2015-06-07T12:29:46.608Z","v":0}

我们可以通过bunyan运行它,以便将其打印出来:

[~/examples/example-11]$ cat log.log | bunyan
[~/examples/example-11]$ cat log.log | bunyan
[2015-06-07T12:29:46.606Z]  INFO: example-11/3614 on macbook.local:  (http_parser=2.3, node=0.12.2, v8=3.28.73, uv=1.4.2-node1, zlib=1.2.8, modules=14, openssl=1.0.1m)
[2015-06-07T12:29:46.608Z]  INFO: example-11/3614 on macbook.local: Application started

现在我们可以记录到文件中,我们还希望能够在消息显示时看到它们。如果我们只是记录到文件中,我们可以使用:

[~/examples/example-11]$ tail -f log.log | bunyan

这将记录到正在写入的文件stdout;或者我们可以向bunyan添加另一个流:

logger = Bunyan.createLogger( {
    name: 'example-11',
    streams: [
        {
            level: Bunyan.INFO,
            path: './log.log'   
        },
        {
            level: Bunyan.INFO,
            stream: process.stdout
        }
    ]
});

运行示例将在控制台上显示日志:

[~/examples/example-11]$ node index.js | bunyan
 [2015-06-07T12:37:19.857Z] INFO: example-11/3695 on macbook.local: (http_parser=2.3, node=0.12.2, v8=3.28.73, uv=1.4.2-node1, zlib=1.2.8, modules=14, openssl=1.0.1m) [2015-06-07T12:37:19.860Z] INFO: example-11/3695 on macbook.local: Application started

我们还可以看到日志已经附加到文件中:

[~/examples/example-11]$ cat log.log | bunyan
 [2015-06-07T12:29:46.606Z]  INFO: example-11/3614 on macbook.local:  (http_parser=2.3, node=0.12.2, v8=3.28.73, uv=1.4.2-node1, zlib=1.2.8, modules=14, openssl=1.0.1m)
[2015-06-07T12:29:46.608Z]  INFO: example-11/3614 on macbook.local: Application started
[2015-06-07T12:37:19.857Z]  INFO: example-11/3695 on macbook.local:  (http_parser=2.3, node=0.12.2, v8=3.28.73, uv=1.4.2-node1, zlib=1.2.8, modules=14, openssl=1.0.1m)
[2015-06-07T12:37:19.860Z]  INFO: example-11/3695 on macbook.local: Application started

很好,现在我们已经记录下来了,我们应该怎么处理呢?

好吧,知道错误发生的地方是有帮助的,当您周围有很多匿名函数时,情况就会变得非常混乱。如果您注意到覆盖 HTTP 服务器的示例中,大多数函数都是命名的。当涉及到回调时,这对于跟踪错误非常有帮助。

让我们看看这个例子:

try {
    a = function( callback ) {
        return function( ) {
            callback( );
        };
    };
    b = function( callback ) {
        return function( ) {
            callback( );
        }
    };
    c = function( callback ) {
        return function( ) {
            throw new Error( "I'm just messing with you" ); 
        };
    };
    a( b( c( ) ) )( );
} catch ( error ) {
    logger.error( error );
}

它可能看起来有点混乱,因为它确实如此。让我们运行以下示例:

[~/examples/example-12]$ node index.js | bunyan
 [2015-06-07T12:51:11.665Z] ERROR: example-12/4158 on macbook.local: I'm just messing with you
 Error: I'm just messing with you
 at /Users/fabian/examples/example-12/index.js:19:10
 at /Users/fabian/examples/example-12/index.js:14:4
 at /Users/fabian/examples/example-12/index.js:9:4
 at Object.<anonymous> (/Users/fabian/examples/example-12/index.js:22:16)
 at Module._compile (module.js:460:26)
 at Object.Module._extensions..js (module.js:478:10)
 at Module.load (module.js:355:32)
 at Function.Module._load (module.js:310:12)
 at Function.Module.runMain (module.js:501:10)
 at startup (node.js:129:16)

您可以看到我们的代码中没有函数名称,堆栈跟踪也没有命名,这与前几个函数不同。在 Node.js 中,函数的命名将来自变量名或实际函数名。例如,如果您使用Cls.prototype.func,那么名称将是Cls.func,但如果您使用函数func,那么名称将是func

您可以看到这里有一点好处,但是一旦您开始使用涉及async回调的模式,这将变得非常有用:

[~/examples/example-13]$ npm install q

让我们在回调中抛出一个错误:

var Q = require( 'q' );

Q( )
.then( function() {
    // Promised returned from another function
    return Q( )
    .then( function( ) {
        throw new Error( 'Hello errors' ); 
    });
})
.fail( function( error ) {
    logger.error( error );
});

运行我们的示例给我们:

[~/examples/example-13]$ node index.js | bunyan
 [2015-06-07T13:03:57.047Z] ERROR: example-13/4598 on macbook.local: Hello errors
 Error: Hello errors
 at /Users/fabian/examples/example-13/index.js:12:9
 at _fulfilled (/Users/fabian/examples/example-13/node_modules/q/q.js:834:54)

这是开始变得难以阅读的地方;为我们的函数分配简单的名称可以帮助我们找到错误的来源:

return Q( )
    .then( function resultFromOtherFunction( ) {
        throw new Error( 'Hello errors' ); 
    });

运行示例:

[~/examples/example-13]$ node index.js | bunyan
 [2015-06-07T13:04:45.598Z] ERROR: example-13/4614 on macbook.local: Hello errors
 Error: Hello errors
 at resultFromOtherFunction (/Users/fabian/examples/example-13/index.js:12:9)
 at _fulfilled (/Users/fabian/examples/example-13/node_modules/q/q.js:834:54)

错误处理

调试的另一个方面是处理和预期错误。我们可以以三种方式处理我们的错误:

  • 一个简单的try/catch

  • 在进程级别捕获它们

  • 在域级别捕获错误

如果我们期望发生错误并且我们能够在不知道正在执行的结果的情况下继续,那么try/catch函数就足够了,或者我们可以处理并返回错误,如下所示:

function parseJSONAndUse( input ) {
    var json = null;
    try {
        json = JSON.parse( input );
    } catch ( error ) {
        return Q.reject( new Error( "Couldn't parse JSON" ) );
    }
    return Q( use( json ) );
}

另一种捕获错误的简单方法是向您的进程添加错误处理程序;在这个级别捕获的任何错误通常是致命的,应该视为这样处理。进程的退出应该跟随,您应该使用一个包,比如foreverpm2

process.on( 'uncaughtException', function errorProcessHandler( error ) {
    logger.fatal( error );
    logger.fatal( 'Fatal error encountered, exiting now' );
    process.exit( 1 );
});

在捕获到未捕获的错误后,您应该始终退出进程。未捕获的事实意味着您的应用程序处于未知状态,任何事情都可能发生。例如,您的 HTTP 路由器可能出现错误,无法将更多请求路由到正确的处理程序。您可以在nodejs.org/api/process.html#process_event_uncaughtexception上阅读更多相关信息。

在全局级别处理错误的更好方法是使用domain。使用域,您几乎可以沙箱一组异步代码在一起。

让我们在请求服务器的情境下思考。我们发出请求,从数据库中读取数据,调用外部服务,写回数据库,进行一些日志记录,执行一些业务逻辑,并且我们期望来自代码周围所有外部来源的数据都是完美的。然而,在现实世界中并非总是如此,我们无法处理可能发生的每一个错误;此外,我们也不希望因为一个非常特定的请求出现错误而导致整个服务器崩溃。这就是我们需要域的地方。

让我们看下面的例子:

var Domain = require( 'domain' ),
    domain;

domain = Domain.create( );

domain.on( 'error', function( error ) {
    console.log( 'Domain error', error.message );
});

domain.run( function( ) {
    // Run code inside domain
    console.log( process.domain === domain );
    throw new Error( 'Error happened' ); 
});

让我们运行这段代码:

[~/examples/example-14]$ node index.js
true
Domain error Error happened

这段代码存在问题;然而,由于我们是同步运行的,我们仍然将进程置于一个破碎的状态。这是因为错误冒泡到了节点本身,然后传递给了活动域。

当我们在异步回调中创建域时,我们可以确保进程可以继续。我们可以通过使用process.nextTick来模拟这一点:

process.nextTick( function( ) {
    domain.run( function( ) {
        throw new Error( 'Error happened' );
    });
    console.log( "I won't execute" );
}); 

process.nextTick( function( ) {
    console.log( 'Next tick happend!' );
});

console.log( 'I happened before everything else' );

运行示例应该显示正确的日志:

[~/examples/example-15]$ node index.js
I happened before everything else
Domain error Error happened
Next tick happend!

摘要

在本章中,我们介绍了一些事后调试方法,帮助我们发现错误,包括日志记录、命名惯例和充分的错误处理。

在下一章中,我们将介绍如何配置我们的应用程序。

为 Bentham Chang 准备,Safari ID 为 bentham@gmail.com 用户编号:2843974 © 2015 Safari Books Online,LLC。此下载文件仅供个人使用,并受到服务条款的约束。任何其他使用都需要版权所有者的事先书面同意。未经授权的使用、复制和/或分发严格禁止并违反适用法律。保留所有权利。

第五章:配置

随着我们的应用程序变得越来越大,我们开始失去对配置做什么的视野;我们可能也会陷入这样一种情况:我们的代码在 12 个不同的地方运行,每个地方都需要一些代码来做一些其他事情,例如连接到不同的数据库。然后,对于这 12 个环境,我们有三个版本:生产、暂存和开发。突然间,情况变得非常复杂。这就是为什么我们需要能够从更高层次配置我们的代码,这样我们就不会在这个过程中破坏任何东西。

JSON 文件

有几种方法可以配置我们的应用程序。我们将首先看一种简单的 JSON 文件。

如果我们查看默认支持的扩展名,我们可以看到我们可以将 JSON 直接导入到我们的代码中,如下所示:

[~/examples/example-16]$ node
> require.extensions
{ '.js': [Function],
'.json': [Function],
'.node': [Function: dlopen] }

让我们创建一个简单的服务器,使用配置文件而不是硬编码文件:

首先,我们必须创建配置文件:

{
    "host": "localhost",
    "port": 8000
}

有了这个,我们现在可以创建我们的服务器了:

var Config = require('./config.json'),
    Http = require('http');
Http.createServer(function(request, response) {

}).listen(Config.port, Config.host, function() {
    console.log('Listening on port', Config.port, 'and host', Config.host);
});

现在,我们只需要更改config文件,而不是更改代码来更改服务器运行的端口。

但是我们的config文件有点太通用了;我们不知道主机或端口是什么,以及它们与什么相关。

在配置时,键需要更具体,这样我们才知道它们被用于什么,除非应用程序直接给出了上下文。例如,如果应用程序只提供纯静态内容,那么使用更通用的键可能是可以接受的。

为了使这些配置键更具体,我们可以将它们全部包装在一个服务器对象中:

{
    "server": {
        "host": "localhost",
        "port": 8000
    }
}

现在,为了了解服务器的端口,我们需要使用以下代码:

Config.server.port

一个可能有用的例子是连接到数据库的服务器,因为它们可以接受端口和主机作为参数:

{
    "server": {
        "host": "localhost",
        "port": 8000
    },
    "database": {
        "host": "db1.example.com",
        "port": 27017
    }
}

环境变量

我们可以通过使用环境变量来配置我们的应用程序的另一种方式。

这些可以由你运行应用程序的环境或使用的命令来定义。

在 Node.js 中,你可以使用process.env来访问环境变量。使用env时,你不希望过多地污染这个空间,所以最好是给键加上与你自己相关的前缀——你的程序或公司。例如,Config.server.host变成了process.env.NAME_SERVER_HOST;原因是我们可以清楚地看到与你的程序相关的内容和不相关的内容。

使用环境变量来配置我们的服务器,我们的代码将如下所示:

var Http = require('http'),
    server_port,
    server_host;

server_port = parseInt(process.env.FOO_SERVER_PORT, 10);
server_host = process.env.FOO_SERVER_HOST;

Http.createServer(function(request, response) {

}).listen(server_port, server_host, function() {
    console.log('Listening on port', server_port, 'and host', server_host);
});

为了使用我们的变量运行这段代码,我们将使用:

[~/examples/example-17]$ FOO_SERVER_PORT=8001 \
FOO_SERVER_HOST=localhost node server.js
Listening on port 8001 and host localhost

你可能注意到我不得不对FOO_SERVER_PORT使用parseInt;这是因为以这种方式传递的所有变量本质上都是字符串。我们可以通过执行typeof process.env.FOO_ENV来看到这一点:

[~/examples/example-17]$ FOO_ENV=1234 node
> typeof process.env.FOO_ENV
'string'
> typeof parseInt( process.env.FOO_ENV, 10 )
'number'

尽管这种配置非常简单易于创建和使用,但可能不是最佳方法,因为如果变量很多,很难跟踪它们,并且它们很容易被遗漏。

参数

配置可以通过作为进程启动时传递给 Node.js 的参数来完成,你可以使用process.argv来访问这些参数,argv代表参数向量。

process.argv返回的数组始终会在索引0处有一个node。例如,如果你运行node server.js,那么process.argv的值将是[ 'node', '/example/server.js' ]

如果你向 Node.js 传递一个参数,它将被添加到process.argv的末尾。

如果你运行node server.js --port=8001process.argv将包含[ 'node', '/example/server.js', '--port=8001' ],非常简单,对吧?

尽管我们可以有所有这些配置,但我们应该始终记住,配置可以被简单地排除,即使这种情况发生,我们仍希望我们的应用程序能够运行。通常情况下,当你有配置选项时,你应该提供默认的硬编码值作为备份。

密码和私钥等参数永远不应该有默认值,但通常标准的链接和选项应该有默认值。在 Node.js 中很容易给出默认值,你只需要使用 OR 运算符。

value = value || 'default';

基本上,这样做的作用是检查值是否为falsy;如果是,则使用默认值。你需要注意那些你知道可能是falsy的值,布尔值和数字肯定属于这个范畴。

在这些情况下,你可以使用一个检查 null 值的 if 语句,如下所示:

if ( value == null ) value = 1

总结

配置就介绍到这里。在本章中,你学会了三种创建动态应用程序的方法。我们学到了应该以一种可以识别值的变化和它们对应用程序的影响的方式命名配置键。我们还学会了如何使用环境变量和 argv 将简单参数传递给我们的应用程序。

有了这些信息,我们可以继续在下一章中连接和利用数据库。

为 Bentham Chang 准备,Safari ID bentham@gmail.com 用户编号:2843974 © 2015 Safari Books Online,LLC。此下载文件仅供个人使用,并受到服务条款的约束。任何其他使用都需要版权所有者的事先书面同意。未经授权的使用、复制和/或分发严格禁止并违反适用法律。保留所有权利。

第六章:Level DB 和 NoSQL

在本章中,我们将介绍两种可以与 Node.js 一起使用的数据库变体;一种提供了非常轻量级和简单的功能集,而另一种则为我们提供了更灵活和通用的功能集。在本章中,我们将介绍 LevelDB 和 MongoDB

Level DB

Node.js 的一个很棒的地方是我们在前端和后端都使用相同的语言,NoSQL 数据库也是如此。它们中的大多数从一开始就支持 JSON;这对于使用 Node.js 的任何人来说都很棒,因为不需要花时间制作关系模型,将其转换为类似 JSON 的结构,将其传递到浏览器,对其进行操作,然后再反转这个过程。

使用原生支持 JSON 的数据库,您可以立即开始工作并投入使用。

Google 为我们提供了一个简单的入口到一个可以安装并准备使用的 NoSQL 数据库,只需一个命令即可:

[~/examples/example-18]$ npm install level

您将看到这将安装LevelDOWNLevelUP

LevelDOWNLevelDB的低级绑定,LevelUP是对其的简单封装。

LevelDB在设置方面非常简单。一旦安装完成,我们只需创建一个LevelUP实例,并将其传递到我们希望存储数据库的位置:

var LevelUP = require( 'level' ),
    db = new LevelUP( './example-db');

现在我们有了一种快速简单的存储数据的方法。

由于LevelDB只是一个简单的键/值存储,它默认使用字符串键和字符串值。如果这是您希望存储的所有信息,这是很有用的。您还可以将其用作简单的缓存存储。它有一个非常简单的 API,此阶段我们只关注四种方法:putgetdelcreateReadStream;大多数方法的作用都很明显:

方法 用途 参数
put 插入键值对 键,值,回调函数(错误)
get 获取键值对 键,回调函数(错误,值)
del 删除键值对 键,回调函数(错误)
createReadStream 获取多个键值对

一旦我们创建了数据库,要插入数据,我们只需要做以下操作:

db.put( 'key', 'value', function( error ) {
    if ( error ) return console.log( 'Error!', error )

    db.get( 'key', function( error, value ) {
        if ( error ) return console.log( 'Error!', error )

        console.log( "key =", value )
    });
});

如果运行代码,我们将看到我们插入并检索到了我们的值:

[~/examples/example-18]$ node index.js
key = value

这不是我们简单的 JSON 结构;但是,它只是一个字符串。要使我们的存储保存 JSON,我们只需要将值编码作为选项传递给数据库,如下所示:

var LevelUP = require( 'level' ),
    db = new LevelUP( './example-db', {
        valueEncoding: 'json'
    });

现在我们可以存储 JSON 数据:

db.put( 'jsonKey', { inner: 'value' }, function ( error ) {
    if ( error ) return console.log( 'Error!', error )

    db.get( 'jsonKey', function( error, value ) {
        if ( error ) return console.log( 'Error!', error )

        console.log( "jsonKey =", value )
    });
});

然而,字符串可以存储为 JSON,我们仍然可以将字符串作为值传递,并且也可以检索它。

运行此示例将显示以下内容:

[~/examples/example-18]$ node index.js
key = value
jsonKey = { inner: 'value' }

现在,我们已经掌握了简单的方法,现在我们可以继续使用createReadStream

此函数返回一个对象,可以与 Node.js 内置的ReadableStream进行比较。对于数据库中的每个键/值对,它将发出一个data事件;它还会发出其他事件,如errorend。如果error没有事件监听器,那么它将传播,从而终止整个进程(或域),如下所示:

db.put( 'key1', { inner: 'value' }, function( error ) {
    if ( error ) return console.log( 'Error!', error )

    var stream = db.createReadStream( );

    stream
    .on( 'data', function( pair ) {
        console.log( pair.key, "=", pair.value );
    })
    .on( 'error', function( error ) {
        console.log( error );
    })
    .on( 'end', function( ) {
        console.log( 'end' );
    });
});

运行此示例:

[~/examples/example-20]$ node index.js
key1 = { inner: 'value' }
end

如果我们在数据库中放入更多数据,将会发出多个data事件:

[~/examples/example-20]$ node index.js
key1 = { inner: 'value' }
key2 = { inner: 'value' }
end

MongoDB

正如您所看到的,使用 Node.js 的数据库可以非常简单。如果我们想要更完整的东西,我们可以使用另一个名为MongoDB的 NoSQL 数据库——另一个非常受欢迎的基于文档的数据库。

对于这组示例,您可以使用托管数据库,使用提供者如 MongoLab(他们提供免费的开发层级),或者您可以按照docs.mongodb.org/manual/installation上的说明在本地设置数据库。

一旦您有一个要连接的数据库,我们就可以继续。

MongoDB 有几个可以与 Node.js 一起使用的模块,最受欢迎的是 Mongoose;但是,我们将使用核心的 MongoDB 模块:

[~/examples/example-21]$ npm install mongodb

要使用我们的数据库,我们首先需要连接到它。我们需要为客户端提供一个连接字符串,一个带有mongodb协议的通用 URI。

如果您有一个本地的 mongo 数据库在没有凭据的情况下运行,您将使用:

mongodb://localhost:27017/database

默认端口是27017,所以你不需要指定它;但是为了完整起见,它已经包含在内。

如果你正在使用 MongoLab,他们会提供给你一个连接字符串;它应该是这种格式:

mongodb://<dbuser>:<dbpassword>@<ds>.mongolab.com:<port>/<db>

连接到我们的数据库实际上非常简单。我们只需要提供驱动程序一个连接字符串,然后我们就可以得到一个数据库:

var MongoDB = require('mongodb'),
    MongoClient = MongoDB.MongoClient;

connection = "mongodb://localhost:27017/database"

MongoClient.connect( connection, function( error, db ) {
    if( error ) return console.log( error );

    console.log( 'We have a connection!' );
});

MongoDB 中的每组数据都存储在一个集合中。一旦我们有了数据库,我们就可以获取一个集合来运行操作:

var collection = db.collection( 'collection_name' );

在一个集合中,我们有一些简单的方法,拥有很大的力量,为我们提供了一个完整的 CRUD“API”。

MongoDB 中的每个文档都有一个 ID,它是ObjectId的一个实例。他们用于此 ID 的属性是_id

要保存一个文档,我们只需要调用save,它接受一个对象或对象数组。集合中的单个对象称为文档:

var doc = {
    key: 'value_1'  
};
collection.save( doc, { w: 1 }, function( ) {
    console.log( 'Document saved' )
});

如果我们使用带有 ID 的文档调用save函数,那么该文档将被更新而不是插入:

var ObjectId = MongoDB.ObjectId
// This document already exists in my database
var doc_id = {
    _id: new ObjectId( "55b4b1ffa31f48c6fa33a62a" ),
    key: 'value_2'
};
collection.save( doc_id, { w: 1 }, function( ) {
    console.log( 'Document with ID saved' );
});

现在我们在数据库中有了文档,我们可以查询它们,如下所示:

collection.find( ).toArray( function( error, result ) {
    console.log( result.length + " documents in our database!" )
});

如果find没有提供回调函数,它将返回一个游标;这使我们能够使用limitsorttoArray等方法。

你可以向find传递一个查询来限制返回的内容。为了通过其 ID 查找对象,我们需要使用类似于以下的东西:

collection.find(
    { _id: new ObjectId( "55b4b1ffa31f48c6fa33a62a" ) },
    function( error, documents ) {
        console.log( 'Found document', documents[ 0 ] );
    }
);

我们还可以通过任何其他可能使用的属性进行过滤:

collection.find(
    { key: 'value' },
    function( error, documents ) {
        console.log( 'Found', documents.length, 'documents' );  
    }
);

如果你以前使用过 SQL,你一定会注意到缺少操作符,比如ORANDNOT。但是,你不需要担心,因为 mongo 提供了许多等价物。

你可以在这里看到完整的列表:docs.mongodb.org/manual/reference/operator/query/

所有操作符都以美元符号开头,例如$and$or$gt$lt

你可以查看文档以查看使用这些的具体语法。

要使用$or条件,你需要将其包含在其中,就好像它是一个属性一样:

collection.find(
    {
        $or: [
            { key: 'value' },
            { key: 'value_2' }
        ]
    },
    function( error, documents ) {
        console.log( 'Found', documents.length, 'documents' );  
    }
);

使用诸如 MongoDB 这样的数据库使我们能够更有力地检索数据并创建更具功能的软件。

摘要

现在我们有可以存储数据的地方。一方面,我们有一个简单的键/值存储,为我们提供了一种非常方便的存储数据的方式;另一方面,我们有一个功能丰富的数据库,为我们提供了一整套查询操作符。

这两个数据库将在接下来的章节中帮助我们,因为我们将更接近创建我们的全栈应用程序。

在下一章中,我们将介绍Socket.IO,这是一个建立在 WebSockets 之上的实时通信框架。

为 Bentham Chang 准备,Safari ID 为 bentham@gmail.com 用户编号:2843974 © 2015 Safari Books Online,LLC。此下载文件仅供个人使用,并受到服务条款的约束。任何其他使用都需要版权所有者的事先书面同意。未经授权的使用、复制和/或分发严格禁止并违反适用法律。保留所有权利。

第七章:Socket.IO

简单的 HTTP 非常适合不需要实时数据的情况,但是当我们需要在事件发生时得知情况时怎么办。例如,如果我们正在创建一个具有聊天界面或类似功能的网站呢?

这就是 Web sockets 发挥作用的时候。Web sockets 通常被称为 WebSockets,是全双工或双向低延迟通信通道。它们通常被用于消息应用程序和游戏,其中需要在服务器和客户端之间中继消息。有一个非常方便的npm模块叫做socket.io,它可以为任何 Node.js 应用程序添加 Web sockets。

要安装它,我们只需要运行:

[~/examples/example-27] npm install socket.io

Socket.IO 可以非常简单地设置以监听连接。首先,我们希望能够提供一个静态的 html 页面来运行客户端代码:

var Http = require( 'http' ),
    FS = require( 'fs' );

var server = Http.createServer( handler );

server.listen( 8080 );

function handler( request, response ) {
    var index = FS.readFileSync( 'index.html' );
    index = index.toString( );

    response.writeHead(200, {
        'Content-Type': 'text/html',
        'Content-Length': Buffer.byteLength( index )
    });
    response.end( index );
}

现在,让我们在同一目录中创建一个名为index.html的 HTML 文件:

<html>
    <head>
        <title>WS Example</title>
    </head>
    <body>
        <h2>WS Example</h2>
        <p id="output"></p>
        <!-- SocketIO Client library -->
        <script src="img/socket.io.js"></script>
        <script type="application/javascript">
            /* Our client side code will go here */
        </script>
    </body>
</html>

让我们运行我们的示例,并确保我们得到我们的页面,我们应该能够在屏幕上看到WS Example。现在,要为我们的应用程序添加 socket 支持,我们只需要要求socket.io并指定要使用IOServer进行监听的http服务器:

var IOServer = require( 'socket.io' );
var io = new IOServer( server );

现在,每当有一个新的 socket 连接在8080上,我们将在io上收到一个connection事件:

io.on( 'connection', function( socket ) {
    console.log( 'New Connection' );
});

让我们向客户端添加一些代码。Socket.IO 为我们提供了一个客户端库,并通过端点/socket.io/socket.io.js公开了这一点。这已经包含在前面的index.html文件中。

提示

所有客户端代码都包含在index.html文件的第二个script标签中。

要与服务器建立连接,我们只需要调用io.connect并传递位置。这将为我们返回一个 socket,我们可以用它与服务器通信。

我们在这里使用了 Socket.IO 提供的客户端,因为它会检测 WebSockets 是否可用,如果可能的话会使用它们。否则,它将利用其他方法,如轮询,以确保它可以在任何地方工作,而不仅仅是在现代浏览器上:

var socket = io.connect( 'http://localhost:8080' );

我们将使用一个p元素来将消息记录到屏幕上。我们可以使用这段代码来做到这一点,然后我们只需要调用logScreen

var output = document.getElementById( 'output' );

function logScreen( text ) {
    var date = new Date( ).toISOString( );
    line = date + " " + text + "<br/>";
    output.innerHTML =  line + output.innerHTML
}

一旦建立连接,就像在服务器端一样,会发出一个connection事件,我们可以使用on来监听这个事件:

socket.on( 'connection', function( ){
    logScreen( 'Connection!' );
});

现在,一旦我们导航到http://localhost:8080,我们就可以运行我们的服务器。您应该能够看到Connection!显示出来:

Socket.IO

要在服务器端接收消息,我们只需要监听message事件。现在,我们将简单地将消息回显:

socket.on( 'connection', function( ){
    socket.on( 'message', function ( message ) {
        socket.send( message );
    });
});

在客户端,我们只需要调用send来发送消息,我们希望在连接事件中执行此操作。双方的api非常相似,正如你所看到的:

socket.send( 'Hello' );

在客户端,我们还希望监听消息并将其记录到屏幕上:

socket.on( 'message', logScreen );

一旦我们重新启动服务器并刷新页面,我们应该能够看到屏幕上出现一个额外的Hello消息。

[~/examples/example-27]$ node index.js
Hello

这是因为服务器现在可以向客户端发送数据包。这也意味着我们可以随时更新客户端。例如,我们可以每秒向客户端发送一个更新:

socket.on( 'connection', function( ){
    function onTimeout( ) {
        socket.send( 'Update' );
    }
    setInterval( onTimeout, 1000 );
});

现在,当我们重新启动服务器时,我们应该能够每秒看到一个更新消息。

您可能已经注意到,您无需刷新网页即可重新打开连接。这是因为socket.io会透明地保持我们的连接“活动”,并在需要时重新连接。这消除了使用 sockets 的所有痛苦,因为我们没有这些麻烦。

房间

Socket.IO 还有房间的概念,多个客户端可以被分组到不同的房间中。要模拟这一点,您只需要在多个选项卡中导航到http://localhost:8080

一旦客户端连接,我们需要调用join方法告诉 socket 要加入哪个房间。如果我们希望做一些特定用户的群聊之类的事情,我们需要在数据库中有一个房间标识符或创建一个。现在我们只是让每个人加入同一个房间:

socket.on( 'connection', function( ){
    console.log( 'New Connection' );
    var room = 'our room';
    socket.join( room, function( error ) {
        if ( error ) return console.log( error );

        console.log( 'Joined room!' );
    });
});

每次我们打开一个标签页,我们都应该看到一个消息,告诉我们已经加入了一个房间:

[~/examples/example-27]$ node index.js
New Connection
Joined room!
New Connection
Joined room!
New Connection
Joined room

有了这个,我们可以向整个房间广播消息。每次有人加入时让我们这样做。在加入回调中:

socket
    .to( room )
    .emit(
        'message',
        socket.id + ' joined the room!'
    );

如果你在浏览器中查看,每次连接时其他客户端都会收到通知,有人加入了:

x3OwYOkOCSsa6Qt5AAAF joined the room!
mlx-Cy1k3szq8W8tAAAE joined the room!
Connection!
Connecting

这很棒,我们现在几乎可以直接在浏览器之间通信了!

如果我们想离开一个房间,我们只需要调用leave,在调用该函数之前我们将进行广播:

socket
    .to( room )
    .emit(
        'message',
        socket.id + ' is leaving the room'
    );
socket.leave( room );

在运行时,您不会看到来自另一个客户端的任何消息,因为您立即离开了:但是,如果您对此进行延迟,您可能会看到另一个客户端进入和离开:

leave = function( ) {
    socket
        .to( room )
        .emit(
            'message',
            socket.id + ' is leaving the room'
        );
    socket.leave( room );
};

setTimeout( leave, 2000 );

认证

对于认证,我们可以使用与 HTTP 服务器相同的方法,并且我们可以接受 JSON Web Token

在这些示例中,为了简单起见,我们将只有一个单一的 HTTP 路由来登录。我们将签署一个 JWT,稍后我们将通过检查签名来进行身份验证

我们需要安装一些额外的npm模块;我们将包括chance,以便我们可以生成一些随机数据。

[~/examples/example-27] npm install socketio-jwt jsonwebtoken chance

首先,我们需要一个到login的路由。我们将修改我们的处理程序以监视/login的 URL:

if ( request.url === '/login' ) {
    return generateToken( response )
}

我们的新函数generateToken将使用chance创建一个 JSON Web Token,并且我们还需要一个令牌的密钥:

var JWT = require( 'jsonwebtoken' ),
    Chance = require( 'chance' ).Chance( );

var jwtSecret = 'Our secret';

function generateToken( response ) {

    var payload = {
        email: Chance.email( ),
        name: Chance.first( ) + ' ' + Chance.last( )
    }

    var token = JWT.sign( payload, jwtSecret );

    response.writeHead(200, {
        'Content-Type': 'text/plain',
        'Content-Length': Buffer.byteLength( token )
    })
    response.end(token);
}

现在,每当我们请求http://localhost:8080/login时,我们将收到一个可以使用的令牌:

[~]$ curl -X GET http://localhost:8080/login
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJlbW
joiR2VuZSBGbGVtaW5nIiwiaWF0IjoxNDQxMjcyMjM0
e1Y

我们可以将其输入到jwt.io/的调试器中并查看内容:

{
  "email": "jefoconeh@ewojid.io",
  "name": "Gene Fleming",
  "iat": 1441272234
}

太棒了,我们有一个令牌和一个为我们生成的随机用户。现在,我们可以用这个来验证我们的用户。Socket.IO 在服务器上有一个方法来做到这一点,我们只需要向其传递一个处理程序类型函数。这就是socketio-jwt的作用,我们向其传递我们的密钥,它将确保它是一个真实的令牌,非常简单:

var SocketIOJWT = require( 'socketio-jwt' );

io.use( SocketIOJWT.authorize({
    secret: jwtSecret,
    handshake: true }));

现在,当我们尝试从客户端连接到我们的服务器时,它永远不会发出connect事件,因为我们的客户端没有经过身份验证。这正是我们想要的。

我们首先想要包装我们的 Socket.IO 代码(稍后我们将调用它);我们还想给它一个token参数:

function socketIO ( token ) {

    var socket = io.connect( 'http://localhost:8080' );

    var output = document.getElementById( 'output' );

    function logScreen( text ) {
        var date = new Date( ).toISOString( );
        line = date + " " + text + "<br/>";
        output.innerHTML =  line + output.innerHTML
    }

    logScreen( 'Connecting' );

    socket.on( 'connect', function( ){
        logScreen( 'Connection!' );
        socket.send( 'Hello' );

    });
    socket.on( 'message', logScreen );

}

接下来,我们将创建一个login函数,这将请求登录 URL,然后将响应传递给socketIO函数,如下所示:

function login( ) {
{
   var request = new XMLHttpRequest();
    request.onreadystatechange = function() {

            if (
            request.readyState !== 4 ||
            request.status !== 200
            ) return

           socketIO( request.responseText );
    }
    request.open( "GET", "/login", true );
    request.send( null );
}

然后我们想调用登录函数:

login( );

我们可以通过更改connect调用以传递查询字符串来将令牌传递给服务器:

var socket = io.connect( 'http://localhost:8080', {
    query: 'token=' + token
});

现在,当我们运行服务器并导航到我们的客户端时,我们应该能够连接 - 太棒了!由于我们已经经过身份验证,我们还可以针对每个用户响应个性化消息,在我们的服务器端connection事件处理程序内,我们将向客户端发出消息。

我们的 socket 将有一个名为decoded_token的新属性;使用这个属性,我们将能够查看我们令牌的内容:

var payload = socket.decoded_token;
var name = payload.name;

socket.emit( 'message', 'Hello ' + name + '!' );

一旦我们加入房间,我们可以告诉其他也加入的客户端:

socket
    .to( room )
    .emit(
        'message',
        name + ' joined the room!'
    );

总结

Socket.IO 为我们的应用程序带来了惊人的功能。我们现在可以立即与其他人通信,无论是个别通信还是在房间中广播。通过识别用户的能力,我们可以记录消息或该用户的历史,准备通过 RESTful API 提供。

我们现在已经准备好构建实时应用程序了!

为 Bentham Chang 准备,Safari ID bentham@gmail.com 用户编号:2843974 © 2015 Safari Books Online,LLC。此下载文件仅供个人使用,并受到服务条款的约束。任何其他使用都需要版权所有者的事先书面同意。未经授权的使用、复制和/或分发严格禁止并违反适用法律。保留所有权利。

第八章:创建和部署包

现在我们已经拥有了创建 Node.js 应用程序和服务器所需的所有组件,我们现在将更多地关注分享我们的模块并为生态系统做出贡献。

所有 npm 上的包都是由社区中的某个人上传、维护和贡献的,所以让我们看看我们如何自己做同样的事情。

创建 npm 包

我们可以从以下步骤开始:

首先我们需要创建一个用户:

[~]$ npm add user 
Username: <username>
Password:
Email: (this IS public) <email>

一旦我们有了一个用户,我们就为 npm 打开了大门。

现在,让我们创建一个包:

[~/examples/example-22]$ npm init
{
 "name": "njs-e-example-package",
 "version": "1.0.0",
 "description": "",
 "main": "index.js",
 "scripts": {
 "test": "echo \"Error: no test specified\" && exit 1"
 },
 "author": "",
 "license": "ISC"
}

要发布这个包,我们只需要运行npm publish

[~/examples/example-22]$ npm publish
+ njs-e-example-package@1.0.0

您可以看到我们已经成功发布了我们的包,您可以查看我发布的包:

www.npmjs.com/package/njs-e-example-package

为了发布它,您将不得不给您的包取一个别的名字;否则,我们将会有冲突。

现在我们可以运行以下命令:

[~/examples/example-21]$ npm install njs-e-example-package
njs-e-example-package@1.0.0 node_modules/njs-e-example-package

然后我们就会有这个包!这不是很酷吗?

如果我们再次尝试发布,将会出现错误,因为版本1.0.2已经发布,如下截图所示:

创建 npm 包

要增加我们的包版本,我们只需要执行:

[~/examples/example-22]$ npm version patch
v1.0.1

现在我们可以再次发布:

[~/examples/example-22]$ npm publish
+ njs-e-example-package@1.0.1

您可以转到 npm 上的包页面,您会看到版本号和发布计数已经更新。

Node.js 中的版本控制遵循semver模式,由主要、次要和补丁版本组成。当增加补丁版本时,这意味着 API 保持不变,但在幕后修复了一些东西。如果增加了次要版本,这意味着发生了不破坏 API 的更改,例如添加了一个方法。如果更新了主要版本,这意味着发生了破坏 API 的更改;例如删除了一个方法或方法签名发生了变化。

有时,项目中有一些你不希望被其他人推出去的东西。这可能是原始源代码、一些证书,或者一些开发密钥。就像使用git一样,我们有一个叫做.npmignore的忽略文件。

默认情况下,如果没有.npmignore但有.gitignore,npm 将忽略.gitignore文件匹配的内容。如果您不喜欢这种行为,那么您可以创建一个空的.npmignore文件。

.npmignore文件遵循与.gitignore相同的规则,规则如下:

  • 空行或以#开头的行将被忽略

  • 标准的 glob 模式有效

  • 您可以用斜杠/结束模式以指定目录

  • 您可以通过在模式前加上感叹号!来否定一个模式

例如,如果我们有一个包含密钥的证书目录:

[~/examples/example-22]$ mkdir certificates
[~/examples/example-22]$ touch certifticates/key.key

我们可能不希望这被发布,所以在我们的忽略文件中我们将有:

certificates/

我们也不希望有任何我们搁置的key文件,所以我们也添加了这个:

*.key

现在,让我们发布:

[~/examples/example-22]$ npm version patch
v1.0.2
[~/examples/example-22]$ npm publish
+ njs-e-example-package@1.0.2

现在,让我们安装我们的包:

[~/examples/example-23]$ npm install njs-e-example-package@1.0.2

现在,当我们列出目录中的内容时,我们不会看到所有的证书都被传播出去:

[~/examples/example-23]$ ls node_modules/njs-e-example-package
package.json

这很好,但是如果我们想保护整个包而不仅仅是一些证书呢?

我们只需要在package.json文件中将private设置为true,这样当我们运行npm publish时,它将阻止 npm 发布模块:

我们的package.json应该看起来像这样:

{
  "name": "example-23",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "UNLICENSED",
  "dependencies": {
    "njs-e-example-package": "¹.0.2"
  },
  "private": true
}

现在,当我们运行npm publish时:

[~/examples/example-23]$ npm publish
npm ERR! This package has been marked as private

太棒了,这正是我们想要看到的。

总结

看起来我们离准备好所有关于 Node.js 的事情都越来越近了。我们现在知道如何设置、调试、开发和分发我们的软件。

在下一章中,我们将介绍我们需要了解的另一个概念:单元测试。

为 Bentham Chang 准备,Safari ID 为 bentham@gmail.com 用户编号:2843974 © 2015 Safari Books Online,LLC。此下载文件仅供个人使用,并受到服务条款的约束。任何其他用途均需版权所有者的事先书面同意。未经授权的使用、复制和/或分发严格禁止并违反适用法律。保留所有权利。

第九章:单元测试

我们已经走了这么远,但还没有做任何测试!这不太好,是吗?通常,如果不是总是,测试是软件开发中的一个主要关注点。在本章中,我们将介绍 Node 的单元测试概念。

Node.js 有许多测试框架,在本章中我们将介绍 Mocha。

安装 mocha

为了确保mocha在所有地方都安装了,我们需要全局安装它。这可以使用npm install-g标志来完成:

[~/examples/example-24]$ npm install -g mocha

现在,我们可以通过终端控制台使用 Mocha。

通常,我们将所有测试代码放在项目的test子目录中。我们只需要运行mocha,假设我们首先编写了一些测试,就可以运行我们的代码。

与许多(如果不是所有)单元测试框架一样,Mocha 使用断言来确保测试正确运行。如果抛出错误并且没有处理,那么测试被认为是失败的。断言库的作用是在传递意外值时抛出错误,因此这很有效。

Node.js 提供了一个简单的断言模块,让我们来看一下:

[~/examples/example-24]$ node
> assert = require( 'assert' )
> expected = 1
> actual = 1
> assert.equal( actual, expected )
> actual = 1
> assert.equal( actual, expected )
AssertionError: 2 == 1

正如我们所看到的,如果断言不通过,就会抛出错误。但是,提供的错误消息并不是很方便;为了解决这个问题,我们也可以传递错误消息:

> assert.equal( actual, expected, 'Expected 1' )
AssertionError: Expected 1

有了这个,我们就可以创建一个测试。

Mocha 提供了许多创建测试的方法,这些方法称为接口,默认的称为 BDD。

您可以在mochajs.org/#interfaces上查看所有接口。

BDD(行为驱动开发)接口可以与 Gherkin 进行比较,其中我们指定一个功能和一组场景。它提供了帮助定义这些集合的方法,describecontext用于定义一个功能,itspecify函数用于定义一个场景。

例如,如果我们有一个函数,用于连接某人的名和姓,测试可能看起来像下面这样:

var GetFullName = require( '../lib/get-full-name' ),
    assert = require( 'assert' );

describe( 'Fetch full name', function( ) {

    it( 'should return both a first and last name', function( ) {
        var result = GetFullName( { first: 'Node', last: 'JS' } )
        assert.equal( result, 'Node JS' );
    })
})

我们还可以为此添加一些其他测试;例如,如果没有传递对象,则会引发错误:

it( 'should throw an error when an object was not passed', function( ) {
    assert.throws(
        function( ) {
            GetFullName( null );
        },
        /Object expected/
    )
})

您可以在mochajs.org/上探索更多 mocha 特定的功能。

Chai

除了许多测试框架之外,还有许多断言框架,其中之一称为Chai。完整的文档可以在chaijs.com/找到。

不要使用 Node.js 提供的内置断言模块,我们可能想要使用 Chai 等模块来扩展我们的可能性。

Chai 有三组接口,should,expect 和 assert。在本章中,我们将介绍 expect。

使用 expect 时,您使用自然语言描述您想要的内容;例如,如果您想要某物存在,可以说expect( x ).to.exist而不是assert( !!x )

var Expect = require( 'chai' ).expect
var Assert = require( 'assert' )

var value = 1

Expect( value ).to.exist
assert( !!value )

使用自然语言使得阅读您的测试变得更加清晰。

这种语言可以链接在一起;我们有tobebeenisthatwhichandhashavewithatofsame,这些可以帮助我们构建句子,比如:

Expect( value ).to.be.ok.and.to.equal( 1 )

但是,这些词只是用于可靠性,它们不会修改结果。还有很多其他词可以用来断言事物,比如notexistsok等等。您可以在chaijs.com/api/bdd/上查看它们。

chai 的一些用法示例包括:

Expect( true ).to.be.ok
Expect( false ).to.not.be.ok
Expect( 1 ).to.exists
Expect( [ ] ).to.be.empty
Expect( 'hi' ).to.equal( 'hi' )
Expect( 4 ).to.be.below( 5 )
Expect( 5 ).to.be.above( 4 )
Expect( function() {} ).to.be.instanceOf( Function )

存根方法

如果它看起来像一只鸭子,游泳像一只鸭子,嘎嘎叫像一只鸭子,那么它可能就是一只鸭子

在编写测试时,您只想测试代码的“单元”。通常这将是一个方法,为其提供一些输入,并期望得到某种输出,或者如果它是一个void函数,则期望不返回任何内容。

有了这个想法,你必须把你的应用程序看作处于沙盒状态,不能与外部世界交流。例如,它可能无法与数据库通信或进行任何外部请求。如果你要(通常应该)实现持续集成和部署,这种假设是很好的。这也意味着在测试的机器上除了 Node.js 和测试框架之外,没有外部要求,这些可能只是你的软件包的一部分。

除非你要测试的方法非常简单,没有任何外部依赖,否则你可能会想要mock你知道它将执行的方法。一个很好的模块就是 Sinon.js;它允许你创建stubsspies,以确保正确的数据从其他方法返回,并确保它们首先被调用。

sinon提供了许多辅助功能,如前所述,其中之一就是spy。spy 主要用于包装一个函数,以查看其输入和输出。一旦 spy 被应用到一个函数上,对外界来说,它的行为完全相同。

var Sinon = require( 'sinon' );

var returnOriginal = function( value ) {
    return value;
}

var spy = Sinon.spy( returnOriginal );

result = spy( 1 );
console.log( result ); // Logs 1

我们可以使用 spy 来检查函数是否被调用:

assert( spy.called )

或者每次调用时传递了什么参数:

assert.equal( spy.args[ 0 ][ 0 ], 1 )

如果我们用一个对象和一个要替换的方法提供了spy,那么在完成后我们可以恢复原始的方法。我们通常会在测试的tear down中这样做:

var object = {
    spyOnMe: function( value ) {
        return value;
    }
}
Sinon.spy( object, 'spyOnMe' )

var result = object.spyOnMe( 1 )
assert( result.called )
assert.equal( result.args[ 0 ][ 0 ], 1 )

object.spyOnMe.restore( )

我们还有一个stub函数,它继承了spy的所有功能,但是完全替换了原始函数,而不是调用它。

这样我们就可以定义行为,例如,它返回什么:

var stub = Sinon.stub( ).returns( 42 )
console.log( stub( ) ) // logs 42

我们还可以为一组传递的参数定义返回值:

var stub = Sinon.stub( )
stub.withArgs( 1, 2, 3 ).returns( 42 )
stub.withArgs( 3, 4, 5 ).returns( 43 )

console.log( stub( 1, 2, 3 ) ) // logs 42
console.log( stub( 3, 4, 5 ) ) // logs 43

假设我们有这组方法:

function Users( ) {

}
Users.prototype.getUser = function( id ) {
    return Database.findUser( id );
}
Users.prototype.getNameForUser = function( id ) {
    var user = this.getUser( id );
    return user.name;
}
module.exports = Users

现在,我们只关心用户被返回的情况,因为如果找不到用户,getUser函数将抛出错误。知道这一点,我们只想测试当找到用户时它返回他们的名字。

这是一个完美的例子,我们想要stub一个方法的时候:

var Sinon = require( 'sinon' );
var Users = require( '../lib/users' );
var Assert = require( 'assert' );

it( 'should return a users name', function( ) {

    var name = 'NodeJS';
    var user = { name: name };

    var stub = Sinon.stub( ).returns( user );

    var users = new Users( );
    users.getUser = stub;

    var result = users.getNameForUser( 1 );

    assert.equal( result, name, 'Name not returned' );
});

我们可以通过作用域传递函数,而不是替换函数,用传递的对象替换 this;两种方式都可以。

var result = users.getNameForUser.call(
    {
        getUser: stub
    },
    1
);

摘要

我们现在可以轻松创建一个 Node.js 应用所需的一切。测试只是其中一个对于任何成功的软件都是必不可少的事情。我们介绍了使用 mocha 作为测试框架和 chai 作为断言框架。

在下一章中,我们将介绍如何在 Node.js 中使用另一种语言,CoffeeScript!

为 Bentham Chang 准备,Safari ID bentham@gmail.com 用户编号:2843974 © 2015 Safari Books Online,LLC。此下载文件仅供个人使用,并受到服务条款的约束。任何其他用途均需版权所有者的事先书面同意。未经授权的使用、复制和/或分发严格禁止并违反适用法律。保留所有权利。

第十章:使用不仅仅是 JavaScript

在整本书中,我们只使用了 JavaScript。嗯,它不就是叫 Node.js 吗?

但这并不意味着我们不能使用其他语言。只要它编译成 JavaScript,我们就可以使用,只要它编译成 JavaScript,我们就可以使用。

这里有一个常见语言的大列表可用:github.com/jashkenas/coffeescript/wiki/list-of-languages-that-compile-to-JS

如果您错过了强类型语言,或者只是想要稍微不同的语法,那么肯定会有一个选项适合您。

一些常见的语言包括CoffeeScriptTypeScript,它们与 Node.js 一起工作得很好,因为它们都编译成 JavaScript。在本章中,我们将介绍CoffeeScript的用法。TypeScript的用法类似;然而,语法遵循与 C#和 Java 类似的路径。

CoffeeScript

安装和开始使用其他语言非常简单。让我们来看看 CoffeeScript:

我们需要全局安装 CoffeeScript,这样我们就可以使用类似node的命令:

[~] npm install -g coffee-script

现在我们可以运行coffee

[~] coffee
>

语法与 JavaScript 非常相似:

[~] coffee
> 1 + 1
2
> console.log( 'Hello' )
Hello

我们使用.coffee而不是.js扩展名。

首先,我们将创建一个 CoffeeScript 文件:

/* index.coffee */
console.log( 'Hello CoffeeScript!' )

然后要运行它,我们只需要使用coffee命令,类似于node命令:

[~/examples/example-25] coffee index.coffee
Hello CoffeScript!

要将我们的.coffee文件编译成.js,我们可以使用-c。编译后,我们可以直接在 Node.js 中运行它们:

[~/examples/example-25] coffee -c index.coffee
[~/examples/example-25] node index.js
Hello CoffeeScript!

如果我们有一堆 CoffeeScript 想要一次性编译成 JavaScript,我们可以使用coffee -c -o ./lib ./src。这将获取./src中的所有.coffee文件,将它们编译成.js,然后输出到./lib

您需要为其他用户编译所有文件,以便他们可以在他们的 JavaScript 代码旁边使用我们的 CoffeeScript 代码。另一种选择是将 CoffeeScript 作为依赖项并将注册文件require到您的应用程序中,如下所示:

/* index.js */
require( 'coffee-script/register' );
require( './other.coffee' );

如果您不希望编译您的 CoffeeScript,或者您正在使用需要 JavaScript 文件的工具,如 Gulp 或 Grunt,您可能需要这样做。

提示

要查看 JavaScript 和 CoffeeScript 之间的等价物,您可以使用该网站js2.coffee/,它提供了一种简单的比较两者的方法。

CoffeeScript 基本上就是 JavaScript;然而,它的目标是可读性和简单性。简单性也意味着它试图限制 JavaScript 的不好的部分,并暴露出好的部分。

对于初学者(和专家)来说,使用 CoffeeScript 通常是很好的,因为它使用英语而不是计算机语言。例如,我们可以使用英语单词is而不是===(三个等号)来检查两个值是否相等。因此,x === y变成了x is y,这意味着在阅读时不需要翻译。

除了is之外,还有其他关键字,如isntnotorandyesno

使用这些关键字而不是符号操作符可以为读者和程序员提供清晰度。CoffeeScript 的格式与 Python 类似,函数和代码块的声明方式;缩进表示块的结束和开始。

代码块和函数

在 JavaScript 中,您通常会使用大括号将块组合在一起,如下例所示:

if ( true ) 
{
  console.log( 'It was true!' ) 
}

在 CoffeeScript 中,您将省略所有大括号,实际上所有括号都被省略了:

if true 
  console.log( 'It was true!' )

在声明函数时也是如此,注意我们使用的是箭头而不是关键字function。参数列表只在需要命名参数时才需要:

func = ->
  console.log( 'I executed' )

CoffeeScript 尝试尽可能多地假设,同时仍然给程序员足够的控制。

您可能还注意到,当声明函数时,我没有使用var关键字。这是因为它是隐式声明的,您可以通过将上述代码编译成 JavaScript 来看到。

var func;
func = function()
{
  return console.log('I executed');
};

你可以看到在这个编译后的代码中,函数中的最后一个语句是返回值,这意味着我们不需要声明返回值,只需假设最后一个值被返回。这使得创建单行函数非常简单,例如:

add = ( a, b ) -> a + b 

与 JavaScript 不同,你可以为函数提供默认参数,这可以与 C#进行比较;然而,它不仅限于常量,因为它本质上执行函数内的语句:

keys = { }
func = ( key, date = new Date ) ->
  keys[ key ] = date

你可以通过编译上面的函数来看到这一点:

var func, keys;
keys = {};
func = function(key, date) 
{
  if (date == null)
  {
    date = new Date();
  }
  return keys[key] = date;
};

基本上,CoffeeScript 所做的就是检查值是否为nullundefined

存在运算符

你可以使用存在运算符来检查一个值是否为nullundefined,该运算符用于检查值是否存在。通过在变量后使用问号符号来表示;如果值存在则语句为真,否则为假。

在表达式中使用这个:

date = null 
if not date?
  date = new Date( )
console.log( date )

你也可以将其作为简写运算符使用,例如:

date ?= new Date( )
console.log( date ) 

上面两个代码示例的行为完全相同,实际上编译后会得到相同的代码:

var date;
date = null;
if (date == null) 
{
  date = new Date();
}

你也可以使用存在运算符来确保在访问其属性之前存在一个值。例如,如果你想从日期中获取时间,或者如果日期不存在则获取-1

getTime = ( date = null ) -> date?.getTime( ) ? -1 

date赋予null值表明我们不介意是否传递了值:

当一个对象不存在且使用了运算符时,返回的值是undefined,这意味着我们可以再次使用相同的运算符来返回一个默认值。

对象和数组

除了 CoffeeScript 试图做出的所有假设,它确实试图消除 JavaScript 中所有不必要的语法。另一个例子是在定义数组和对象时,使用新行声明一个新项。例如,通常你会这样定义一个数组:

array = [
  1,
  2,
  3
]

这仍然有效;然而,在 CoffeeScript 中你可以省略分隔每个项的逗号:

array = [
  1
  2
  3
]

你也可以将这两种风格混合在一起:

array = [
  'a', 'b', 'c'
  1, 2, 3
  true, false
]

你也可以对对象做同样的操作,比如:

object = {
  foo: 1
  bar: 2
}

对于对象,你甚至可以省略花括号,使用缩进来显示对象中的差异:

object = 
  foo: 1
  bar: 2
  foobar: 
    another: 3
    key: 4

在 CoffeeScript 中循环数组,你只需要使用for…in循环,例如:

for value, index in array
  console.log( value, index ) 
  continue if typeof value is 'string'
  console.log( 'Value was not a string' )

如果你不想使用项目的索引,你可以简单地不要求它:

for value in array
  console.log( value )

与 JavaScript 循环一样,你可以使用breakcontinue来控制流程。

在 CoffeeScript 中循环对象可以使用for…of循环,这与 JavaScript 提供的for…of循环有些不同:

for key, value of object 
  console.log( key, value ) 

for…in循环一样,如果你不想要值,可以排除它:

for key of object 
  console.log( key )

对于两种类型的循环,命名是无关紧要的:

for key, value of object 
    # Note that this will let dates and arrays through ( etc )
    continue unless value instanceof Object 
    for nestedKey, nestedValue of value
      console.log(nestedKey, nestedValue )

与 JavaScript 不同,CoffeeScript 提供了一种自然的方式来声明类和继承。

要在 JavaScript 中定义一个类,你需要先声明一个函数:

function User( username ) {
  this.username = username;
}

然后你会声明prototype方法:

User.prototype.getUsername = function( ) {
  return this.username;
}

如果你有一个static方法,你可以在函数上定义它,而不是在原型上:

User.createUser = function( username ) {
  return new User( username );
}

在 CoffeeScript 中,你可以使用class关键字并给类命名。然后你可以声明构造函数、静态方法和实例(原型)方法:

class User
  @createUser: ( username ) ->
    return new User( username )

  constructor: ( username ) ->
    this.username = username
  getUsername: ->
    return this.username

通常,你会将所有的static方法放在构造函数上面,这样它们就与实例方法分开了。这避免了混淆,你可能已经注意到我用@前缀声明了静态方法createUser,这是在 CoffeeScript 中定义静态方法的方式。然而,你也可以使用传统的 JavaScript 方法User.createUser = ->,两种方式都可以在这里工作。

当实例被创建或构造时运行的代码被称为构造函数。这与许多其他语言使用的术语相同,所以应该很熟悉。构造函数本质上就是一个函数。

所有实例方法的声明方式与对象的属性类似。

随着类的出现,还有另一个符号,即@符号。当在实例上使用时,您可以使用它来引用this关键字。例如,getUsername方法可以编写为:

getUsername: ->
  return @username

或者,如果我们想要删除返回语句并将其变成一行:

getUsername: -> @username 

@符号也可以在参数列表中使用,以声明我们希望将实例属性设置为传递的值。例如,如果我们有一个setUsername方法,我们可以这样做:

setUsername: ( username ) ->
  @username = username

或者我们可以这样做:

setUsername: ( @username ) ->

这两种方法将编译为相同的 JavaScript 代码。

考虑到我们可以在参数列表中使用@符号,我们可以重构我们的构造函数为:

constructor: ( @username ) ->

使用 CoffeeScript 类的另一个优势是我们可以定义继承。要做到这一点,我们所需要做的就是使用extends关键字,这与其他语言类似。

在这些示例中,我们希望有两个PersonRobot,它们扩展了基本的User类。

对于我们的人,我们希望能够为他们提供一个名字和年龄,以及User类所需的用户名。

首先,我们需要声明我们的类:

class Person extends User

然后声明我们的constructor。在我们的constructor中,我们将调用super函数,这将执行父类User的构造函数,并且我们希望将用户名传递给它,如下所示:

  constructor: ( username, @name, @age ) ->
    super( username )

然后我们添加两个方法,getNamegetAge

  getName: -> @name
  getAge: -> @age

接下来,我们将对Robot做同样的事情,只是这次我们只需要一个username@usage

class Robot extends User
  constructor: ( username, @usage ) –>
    super( username )
  getUsage: -> @usage 

现在我们可以创建我们的类的实例并进行比较,如下所示:

类

总结

CoffeeScript 试图对您的代码进行良好的假设。这有助于消除 JavaScript 开发人员遇到的一些问题。例如,=====之间的区别。

您可以在coffeescript.org/了解有关 CoffeeScript 特定语法的更多信息。

在本章中,我们已经介绍了利用另一种语言。这可以帮助初学者减轻对 JavaScript 风格或语法的困扰。对于习惯于更多语言特性的人来说,这是一个很大的优势,因为它有助于消除人们通常遇到的陷阱。

为 Bentham Chang 准备,Safari ID 为 bentham@gmail.com 用户编号:2843974 © 2015 Safari Books Online,LLC。此下载文件仅供个人使用,并受到服务条款的约束。任何其他用途均需版权所有者的事先书面同意。未经授权的使用、复制和/或分发严格禁止并违反适用法律。保留所有权利。

posted @ 2024-05-23 15:58  绝不原创的飞龙  阅读(8)  评论(0编辑  收藏  举报