介绍

随着Node.js的发展,Javascript应用在这几年突飞猛进的流行。如果你去modulecounts.com上看看,你会发现Node packages已经超过了Ruby。另外Node packages的发展速度已经超过了Ruby、Python和Java总和。
modulecounts
在这篇文章里将介绍Node最重要的几个方面带领你走上正轨,开始构建自己的应用。

是什么让Node比Rails及其他语言更加流行?

Node自身构建在Chrome最强的的Javascript引擎,作为异步式、事件驱动的框架,专为建设可伸缩网络应用而设计。基本上是Javascript加一堆C/C++的互相作用的文件系统,来启动HTTP或TCP等服务。

Node是单线程的,使用基于event loop的并发模型。它是非阻塞的,所以它不会使程序等待,而是注册一个回调函数使程序继续运行。这意味着我们不使用多线程也可以操控异步操作。

在一些顺序执行的语言中如PHP,为了获取HTML的内容,你会这么做:

1
2
$response = file_get_contents("http://example.com");
print_r($response);

在Node你用回调函数可以这么做:

1
2
3
4
5
6
7
var http = require('http');
http.request({ hostname: 'example.com' }, function(res) {
res.setEncoding('utf8');
res.on('data', function(chunk) {
console.log(chunk);
});
}).end();

在实现方面有两大不同:

  • Node允许在等待响应的时候执行其他任务。
  • Node应用程序不将缓存文件放到内存中,但它会将文件一部分一部分的输出。

Node和其他event loop驱动的系统(如Ruby的EventMachine library和Python的Twisted)有很大的不同。

Node所有的类库完全是为非阻塞而设计的,其他的就不能这么说了。

使用场景

Node是I/O限制应用(或等待用户事件应用)的理想开发工具,可是并不适用于大量使用CPU的应用,但在数据密集型实时应用(DIRT)、单页应用、JSON API服务和数据流应用中表现出色。

npm,官方Node package包管理工具

Node的成功很大部分归功于npm,npm是随Node一起安装的package包管理工具。npm有很多优点:

  • 将应用依赖类库安装在项目下,而非全局安装。
  • 能在同一时间管理不同版本的同一模块。
  • 你能指定压缩包或git仓库来安装。
  • 你能很容易的发布自己的模块到npm上。
  • 在创建CLI(其他人可以通过npm安装并使用)应用很有用。

资源

想了解更多关于为什么用Node,可以访问这篇文章

安装Node.js和NPM

在Windows和OS X下可以通过专门的安装包安装,然而有时候你想在不同版本的Node上测试自己的代码,可以使用NVM(Node Version manager)。

通过NVM你可以安装不同版本的Node在你的操作系统里,实现不同版本间的切换。下面介绍如何在Ubuntu系统下安装NVM。

首先我们要确保我们系统安装了C++编译器:

1
2
$ sudo apt-get update
$ sudo apt-get install build-essential libssl-dev

接着拷贝下面这行命令到控制台:

1
$ curl https://raw.githubusercontent.com/creationix/nvm/v0.13.1/install.sh | bash

在此NVM应该就被正确安装了,我们确认一下:

1
$ nvm

如果当输入nvm时没有出现错误,表示安装成功。现在我们可以安装Node和npm了:

1
$ nvm install v0.10.31

输出应该是这样的:

1
2
3
$ nvm install v0.10.31
################################################################## 100.0%
Now using node v0.10.31

这样node和npm都可以在控制台使用了:

1
2
3
$ node -v && npm -v
v0.10.31
1.4.23

还有最后一件事我们需要做,需要设置设置默认的Node版本当我们下次登录:

1
$ nvm alias default 0.10.31

我们可以安装其他版本的Node就像我们之前那样安装,可以使用nvm use进行版本切换:

1
2
$ nvm install v0.8.10
$ nvm use v0.8.10

如果不知道你在系统了装了什么版本的Node,你可以输入nvm list,将以列表形式显示版本信息,包括现在版本和默认版本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$ nvm list
v0.6.3
v0.6.12
v0.6.14
v0.6.19
v0.7.7
v0.7.8
v0.7.9
v0.8.6
v0.8.11
v0.10.3
v0.10.12
v0.10.15
v0.10.21
v0.10.24
v0.11.9
current: v0.10.24
default -> v0.10.24

资源

想了解更多关于如何用NVM安装Node,请访问这篇文章

Node基础

我们将看下Node.js主要概念:

  • 如何引入外部模块
  • 回调函数的作用
  • 事件驱动(EventEmitter)模式
  • 数据流

模块

Java和Python用import去加载其他模块,PHP和Ruby用require加载。Node在模块上实现了CommonJS接口,通过require关键字去加载依赖模块。

例如我们可以这样加载系统模块:

1
2
var http = require('http');
var dns = require('dns');

我们还能加载本地文件:

1
var myFile = require('./myFile'); // loads myFile.js

用那npm安装模块,可在这个网站或Github上搜索。下面介绍如何下载模块到本地:

1
2
# where express === module name
$ npm install express

你能在代码里引用npm安装的模块,和引用系统模块一样,你不用考虑绝对和相对路径。

有一点非常好的是Node模块不会自动污染全局作用域,相反你需要为模块分配一个变量名,这意味着你不用担心两个或多个模块里的函数名称冲突了。

当创建你自己的模块,在exports的时候需要注意(无论是函数、对象、数字或是其它)。你让exports等于一个对象:

1
2
var person = { name: 'John', age: 20 };
module.exports = person;

也可以直接在exports对象上添加相应的属性或函数:

1
2
exports.name = 'John';
exports.age = 20;

不同的模块之间不共享作用域,所以如果你想要在不同的模块之间共享变量,你必须引入这个模块。另一个需要注意的是模块只加载一次,之后将缓存到Node。

跟浏览器不同,Node没有window全局变量,但是有两个全局变量globalsprocess,你必须避免在这两个全局变量添加属性或函数。

回调函数

在异步编程里,如果函数没有执行完成是不会返回值的,不过我们可以采用continuation-passing style(CPS)解决,想了解更多关于CPS,点击这里

通过这种方式,一个异步的函数可以调用一个回调函数(一般是作为最后一个参数传入),这样如果异步函数执行完后就会执行相应的回调函数。

下面是一个查看谷歌IPv4地址的例子:

1
2
3
4
5
var dns = require('dns');
dns.resolve4('www.google.com', function (err, addresses) {
if (err) throw err;
console.log('addresses: ' + JSON.stringify(addresses));
});

我们写了个回调函数(那个匿名函数)作为dns.resolve4这个异步函数的第二个参数传入。一旦异步函数dns.resolve4执行完成了就会调用回调函数,以此来继续程序。

事件

这个标准的回调函数范式对于在异步函数执行完成后需要提醒情况非常适用。然而,有一些情况是对于不同事件需要非同时的提醒:

让我们看一个调用IRC客户端的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var irc = require('irc');
var client = new irc.Client('irc.freenode.net', 'myIrcBot', {
channels: ['#sample-channel']
});
client.on('error', function(message) {
console.error('error: ', message);
});
client.on('connect', function() {
console.log('connected to the irc server');
});
client.on('message', function (from, to, message) {
console.log(from + ' => ' + to + ': ' + message);
});
client.on('pm', function (from, message) {
console.log(from + ' => ME: ' + message);
});

在上面的例子,我们监听不同的事件:

  • connect事件,当客户端和IRC服务端连接成功时候触发。
  • error事件,当发生错误时触发。
  • 当信息来临时触发messagepm事件。

上面提到的事件让使用EventEmitter模式更加理想了。

EventEmitter模式实现了用户可以订阅他们喜欢的事件。这种模式可能你在浏览器那已经很熟悉了,经常用于绑定DOM事件。

Node的核心里有一个EventEmitter类,让我们创造自己的EventEmitter对象。现在我们创建一个继承EventEmitterMemoryWatcher类,再绑定两个事件:

  • 定期的data事件,表示内存使用量
  • error事件,防止内存使用超出限制

MemoryWatcher类如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var EventEmitter = require('events').EventEmitter;
var util = require('util');
function MemoryWatcher(opts) {
if (!(this instanceof MemoryWatcher)) {
return new MemoryWatcher();
}
opts = opts || {
frequency: 30000 // 30 seconds
};
EventEmitter.call(this);
var that = this;
setInterval(function() {
var bytes = process.memoryUsage().rss;
if (opts.maxBytes && bytes > opts.maxBytes) {
that.emit('error', new Error('Memory exceeded ' + opts.maxBytes + ' bytes'));
}else {
that.emit('data', bytes);
}
}, opts.frequency);
}
util.inherits(MemoryWatcher, EventEmitter);

使用非常简单:

1
2
3
4
5
6
7
8
9
10
var mem = new MemoryWatcher({
maxBytes: 12455936,
frequency: 5000
});
mem.on('data', function(bytes) {
console.log(bytes);
});
mem.on('error', function(err) {
throw err;
});

一个更简单的创造EventEmitter对象方式是创建一个EventEmitter的新对象:

1
2
3
4
5
var EventEmitter = require('events').EventEmitter;
var emitter = new EventEmitter();
setInterval(function() {
console.log(process.memoryUsage().rss);
}, 30000);

数据流

流是异步地操纵数据流的一个抽象的接口,它们和Unix的管道命令很相似,可以分为五类:可读、可写、转换、双向和经典。

和Unix管道命令一样,Node数据流也实现了.pipe()接口。数据流最棒的地方是你不需要将所有数据缓存到内存中,它们就可以很好的结合在一起。

为了更好的理解数据流是如何工作的,我们将创建一个可以读取数据的应用,用AES-256算法加密数据,用gzip压缩数据。所有都是使用数据流的,这意味着每读一块数据我们都将它加密和压缩:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var crypto = require('crypto');
var fs = require('fs');
var zlib = require('zlib');
var password = new Buffer(process.env.PASS || 'password');
var encryptStream = crypto.createCipher('aes-256-cbc', password);
var gzip = zlib.createGzip();
var readStream = fs.createReadStream(filename); // current file
var writeStream = fs.createWriteStream(dirname + '/out.gz');
readStream // reads current file
.pipe(encryptStream) // encrypts
.pipe(gzip) // compresses
.pipe(writeStream) // writes to out file
.on('finish', function () {
// all done
console.log('done');
});

在这里我们使用可读流,将它导入到加密流,再导入到gzip压缩流,最后导入到输出流(将内容显示到屏幕上)。加密流和压缩流属于转换流,它表示将输入数据以某种方式计算处理后输出数据。

应用运行后会生成一个out.gz的文件。现在我们来解密这个文件,并将内容输出到控制台:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var crypto = require('crypto');
var fs = require('fs');
var zlib = require('zlib');
var password = new Buffer(process.env.PASS || 'password');
var decryptStream = crypto.createDecipher('aes-256-cbc', password);
var gzip = zlib.createGunzip();
var readStream = fs.createReadStream(__dirname + '/out.gz');
readStream // reads current file
.pipe(gzip) // uncompresses
.pipe(decryptStream) // decrypts
.pipe(process.stdout) // writes to terminal
.on('finish', function () {
// finished
console.log('done');
});

资源

想了解更多关于Node基础,可以访问这里。更深入的了解数据流,请点击数据流手册

错误处理

错误处理是Node最重要的主题之一。如果忽略或者处理不当,你的整个应用将崩溃或处于不一致的状态。

Error-first回调函数

error-first回调函数是Node回调函数的一个标准协议,它起源于Node核心,后来被用户接受同时成为今天的标准。这是个很简单的规定,只有一条规则:回调函数的第一个参数必须是error对象。

这就意味着可能出现两种场景:

  • 如果error参数是空,则操作成功。
  • 如果error参数不为空,则报错并需要你的处理

让我们看一下我们是如何用Node读取文件内容的:

1
2
3
fs.readFile('/foo.txt', function (err, data) {
// ...
});

fs.readFile的回调函数有两个参数:error对象和文件内容。

现在我们来实现一个相似的函数,使得可以读取多个文件的数据,通过一个数组传递参数。特征和之前的一样,只不过把单一文件路径参数改成一个数组:

1
readFiles(filesArray, callback);

我们尊重error-first模式不会在readFiles函数里面处理错误,不过会将这任务转交给回调函数。readFiles函数会遍历文件路径数组,并读取数据。如果遇到错误,它将调用回调函数,有且只调用一次。当完成读取数组里最后一个文件的内容时,将调用回调函数并将null作为第一个参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
var fs = require('fs');
function readFiles(files, callback) {
var filesLeft = files.length;
var contents = {};
var error = null;
var processContent = function(filePath) {
return function(err, data) {
// an error was previously encountered and the callback was invoked
if (error !== null) { return; }
// an error happen while trying to read the file, so invoke the callback
if (err) {
error = err;
return callback(err);
}
contents[filePath] = data;
// after the last file read was executed, invoke the callback
if (!--filesLeft) {
callback(null, contents);
}
};
};
files.forEach(function(filePath) {
fs.readFile(filePath, processContent(filePath));
});
}

EventEmitter错误

我们要小心处理EventEmitter(数据流也一样),因为如果有未处理的错误事件将导致我们的应用崩溃。这是最简单的例子,由我们自己触发错误:

1
2
3
var EventEmitter = require('events').EventEmitter;
var emitter = new EventEmitter();
emitter.emit('error', new Error('something bad happened'));

由你的应用决定,可以是个严重的错误(难以发现)或不会使你的应用崩溃的错误(如失败时发送一个信息)。无论如何都要绑定一个error事件:

1
2
3
emitter.on('error', function(err) {
console.error('something went wrong with the ee:' + err.message);
});

用verror模块传递更具描述性的错误

有很多种情况是需要我们将错误指派给回调函数。实际上,我们上面写readFiles函数的时候做过这样的事。可是在读取文件的时候发现错误我们只要指派给回调函数吗?

让我们试着读取一个不存在的文件,看看会发生什么:

1
2
3
4
readFiles(['non-existing-file'], function(err, contents) {
if (err) { throw err; }
console.log(contents);
});

上面的例子输出如下:

1
2
3
4
5
$ node readFiles.js
/Users/alexandruvladutu/www/airpair-article/examples/readFiles.js:34
if (err) { throw err; }
^
Error: ENOENT, open '/Users/alexandruvladutu/www/airpair-article/examples/non-existing-file'

这错误提示并不是那么理想,因为在现实当中的情况可能是一个函数调用另一个函数再调用原来的函数。例如,你或许有个函数叫readMarkdownFiles,它会调用readFiles去读取markdown文件。

上面输出的错误追踪并不是那么的有用,所以你还要深度的挖掘错误的来源。幸运的是我们可以通过整合verror模块到应用中来解决这样的问题。

使用方法就是引入verror模块,在调用回调函数时用verror对象包住我们的error对象并提供更多的错误信息:

1
2
3
4
5
6
var verror = require('verror');
function readFiles(files, callback) {
...
return callback(new VError(err, 'failed to read file %s', filePath));
...
}

让我们看看输出结果:

1
2
3
4
5
6
7
8
$ node readFiles-verror.js
/Users/alexandruvladutu/www/airpair-article/examples/readFiles-verror.js:35
if (err) { throw err; }
^
VError: failed to read file /Users/alexandruvladutu/www/airpair-article/examples/non-existing-file: ENOENT, open '/Users/alexandruvladutu/www/airpair-article/examples/non-existing-file'
at /Users/alexandruvladutu/www/airpair-article/examples/readFiles-verror.js:17:25
at fs.js:207:20
at Object.oncomplete (fs.js:107:15)

现在我们知道在读取文件上出现错误,并且知道错误来自readFiles函数。

这是一个简单的例子,但展示了verror的强大。在我们的产品中,这个模块是非常有用的,因为代码库可能非常大,错误源可能比我们的例子藏得更深。

资源

想了解更多,可以访问这篇文章

使用node-inspector调试Node应用

很多小的错误我们可以使用console.log去追踪,但是更复杂的错误我们可以使用node-inspector,它有很多很吸引人的特性,但最重要的还是:

  • 它是基于Blink开发者工具的,所以它用起来像前端开发工具。
  • 它可以设置断点。
  • 我们能step over、step in、step out和resume(继续)。
  • 我们可以检测作用域、变量、对象属性。
  • 除了检测,我们还能修改变量、对象属性。

通过npm安装:

1
$ npm install -g node-inspector

让我们来写个最简单的例子:

1
2
3
4
5
6
7
var http = require('http');
var port = process.env.PORT || 1337;
http.createServer(function(req, res) {
res.writeHead(200, { 'Content-Type': 'text/html' });
res.end(new Date() + '\n');
}).listen(port);
console.log('Server running on port %s', port);

为了调试我们的例子,我们要输入下面这行命令:

1
2
# basically `node-debug` instead of `node`
$ node-debug example.js

这样不仅让我们的应用跑起来,而且还会在Chrome里面打开node-inspector面板。让我们在请求上设置一个断点(通过点击边缘上的数字)。现在让我们打开另一个窗口,访问http://localhost:1337。浏览器会出现加载的状态,但打开node-inspector面板看看:

node-inspector

如果你打开console选卡,你能检测requestresponse对象,还能修改他们等等。这只是node-inspector入门的一个很简单的例子,但在实际运用当中通过这样的调试方式可以解决更复杂的错误问题。

资源

想了解更多关于调试的东西,可以访问这篇文章

用Express和Socket.IO创建一个实时应用

Express是Node最流行的框架,且Socket.IO是客户端和服务端双向实时通信的框架。所以我们要用上面两个框架创建一个简单的像素跟踪应用,具有显示板并且可以实时显示访问者。

除了ExpressSocket.IO模块,我们还需要安装emptygif模块。当用户访问http://localhost:1337/tpx.gif,正在访问zh首页的用户会收到一条信息,这条信息与客户端相关,包括IP地址和浏览器引擎。

下面是server.js文件的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var emptygif = require('emptygif');
var express = require('express');
var app = express();
var server = require('http').createServer(app);
var io = require('socket.io')(server);
app.get('/tpx.gif', function(req, res, next) {
io.emit('visit', {
ip: req.ip,
ua: req.headers['user-agent']
});
emptygif.sendEmptyGif(req, res, {
'Content-Type': 'image/gif',
'Content-Length': emptygif.emptyGifBufferLength,
'Cache-Control': 'public, max-age=0' // or specify expiry to make sure it will call everytime
});
});
app.use(express.static(__dirname + '/public'));
server.listen(1337);

在前端我们只要监听由服务端发出的visit事件,修改相应的UI:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
<!DOCTYPE HTML>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Realtime pixel tracking dashboard</title>
<style type="text/css">
.visit {
margin: 5px 0;
border-bottom: 1px dotted #CCC;
padding: 5px 0;
}
.ip {
margin: 0 10px;
border-left: 1px dotted #CCC;
border-right: 1px dotted #CCC;
padding: 0 5px;
}
</style>
</head>
<body>
<h1>Realtime pixel tracking dashboard</h1>
<div class="visits"></div>
<script src="https://code.jquery.com/jquery-1.10.2.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/moment.js/2.8.1/moment.min.js"></script>
<script src="/socket.io/socket.io.js"></script>
<script>
$(function() {
var socket = io();
var containerEl = $('.visits');
socket.on('visit', function(visit) {
var newItem = '<div class="visit">';
newItem += '<span class="date">' + moment().format('MMMM Do YYYY, HH:mm:ss') + '</span>';
newItem += '<span class="ip">' + visit.ip + '</span>';
newItem += '<span class="ua">' + visit.ua + '</span></div>';
containerEl.append(newItem);
});
});
</script>
</body>
</html>

现在让我们启动应用程序,打开显示板,用不同的浏览器打开我们的应用,显示如下:

dashboard

资源

想了解更多关于Express和Socket.IO,可以点击这个教程

总结

Node并不是万能的,但是希望你能在正确的情况下使用。简而言之,对于I/O等待和高并发的应用,Node是一个好的选择。

npm注册量每天都在增长,这说明将会有越来越多的Node模块可以使用。我们不仅学习了如何安装Node,也了解了Node的核心,例如回调函数、事件和数据流。在文章的末尾我们解决了一些实际的问题,例如错误处理、调试和创建了一个实例。

如果你还怀疑Node是否足够成熟,你要知道一些大公司像雅虎、沃尔玛或PayPal正在用它。如果你还有什么问题可以提出来。

原文链接:

文章目录
  1. 1. 介绍
    1. 1.1. 是什么让Node比Rails及其他语言更加流行?
    2. 1.2. 使用场景
    3. 1.3. npm,官方Node package包管理工具
    4. 1.4. 资源
  2. 2. 安装Node.js和NPM
    1. 2.1. 资源
  3. 3. Node基础
    1. 3.1. 模块
    2. 3.2. 回调函数
    3. 3.3. 事件
    4. 3.4. 数据流
    5. 3.5. 资源
  4. 4. 错误处理
    1. 4.1. Error-first回调函数
    2. 4.2. EventEmitter错误
    3. 4.3. 用verror模块传递更具描述性的错误
    4. 4.4. 资源
  5. 5. 使用node-inspector调试Node应用
    1. 5.1. 资源
  6. 6. 用Express和Socket.IO创建一个实时应用
    1. 6.1. 资源
  7. 7. 总结