node编程要面对的两个难题:
- 如何组织代码
- 怎么做异步编程
- 如何响应一次性事件
- 如何处理重复性事件
- 如何让异步逻辑顺序执行
Node 功能的组织及重用
Node的模块系统避免了对全局作用域的污染,从而也就避免了命名冲突,并简化了代码的重 用。模块还可以发布到npm(Node包管理器)存储库中,这是一个收集了已经可用并且要跟Node 社区分享的Node模块的在线存储库,使用这些模块没必要担心某个模块会覆盖其他模块的变量和 函数。
为了帮你把逻辑组织到模块中,我们会讨论下面这些主题:
- 如何创建模块;
- 模块放在文件系统中的什么地方;
- 在创建和使用模块时要意识到的东西。
- 创建模块
模块既可能是一个文件,也可能是包含一个或多个文件的目录,如图3-3所示。如果模块是 个目录,Node通常会在这个目录下找一个叫index.js的文件作为模块的入口(这个默认设置可以 重写)
- 定义一个Node模块
var canadianDollar = 0.91;
function roundTwoDecimals(amount) {
return Math.round(amount * 100) / 100;
}
exports.canadianToUS = function(canadian) {
return roundTwoDecimals(canadian * canadianDollar);
}
exports.USToCanadian = function(us) {
return roundTwoDecimals(us / canadianDollar);
}
- 引入一个模块
var currency = require('./currency');
console.log('50 Canadian dollars equals this amount of US dollars:');
console.log(currency.canadianToUS(50));
console.log('30 US dollars equals this amount of Canadian dollars:');
console.log(currency.USToCanadian(30));
tip:在引入时, .js 扩展名可以忽略
- 用module.exports微调模块的创建
Node觉得不能用任何其他对象、函数或变量给 exports 赋值
- 把 exports 换成 module.exports ,用 module.exports 可以对外提供单个变量、函数或者对象。如果你创建了一个既有 exports 又有 module.exports 的模块,那它会返回 module.exports ,而 exports 会被忽略
- 根据需要使用 exports 或 module.exports 可以将功能组织成模块,规避掉程序脚本一直增 长产生的弊端
- 用node_modules重用模块
-
要求模块在文件系统中使用相对路径存放,对于组织程序特定的代码很有帮助,但对于想要 在程序间共享或跟其他人共享代码却用处不大。Node中有一个独特的模块引入机制,可以不必知 道模块在文件系统中的具体位置。这个机制就是使用node_modules目录。
-
前面那个模块的例子中引入的是 ./currency 。如果省略 ./ ,只写 currency ,Node会遵照 几个规则搜寻这个模块。如下图所示:
- 注意事项
- 如果模块是目录,在模块目录中定义模块的文件必须被命名为index.js,除非你在这个 目录下一个叫package.json的文件里特别指明。要指定一个取代index.js的文件,package.json文件 里必须有一个用JavaScript对象表示法(JSON)数据定义的对象,其中有一个名为 main 的键,指 明模块目录内主文件的路径。图3-6中的流程图对这些规则做了汇总
- Node能把模块作为对象缓存起来。如果程序中的两个文件引入了相 同的模块,第一个文件会把模块返回的数据存到程序的内存中,这样第二个文件就不用再去访问 和计算模块的源文件了。实际上第二个引入有机会修改缓存的数据。这种“猴子补丁”(monkey patching)让一个模块可以改变另一个模块的行为,开发人员可以不用创建它的新版本。
异步编程技术
在Node的世界里流行两种响应逻 辑管理方式:回调和事件监听
- 如何用回调处理一次性事件;
- 如何用事件监听器响应重复性事件;
- 异步编程的几个难点
- 用回调处理一次性事件
- 回调是一个函数,它被当做参数传给异步函数,它描述了异步操作完成之后要做什么
- 回调层数越多,代码看起来越乱,重构和测试起来也越困难,所以最好限 制一下回调的嵌套层级。如果把每一层回调嵌套的处理做成命名函数,虽然表示相同逻辑所用的 代码变多了,但维护、测试和重构起来会更容易
- 用事件发射器处理重复性事件
事件发射器会触发事件,并且在那些事件被触发时能处理它们。事件是通过监听器进行处理的。监听器是跟事件相关联的,带有一个事件出现时就会被触发的回调函数。
- 事件发射器示例
- 响应只应该发生一次的事件
用once方法相应单次事件
- 创建事件发射器:一个PUB/SUB的例子
用Node内置的事件模块创建自己的事件发射器。
下面的代码定义了一个 channel 事件发射器,带有一个监听器,可以向加入频道的人做出响 应。注意这里用 on (或者用比较长的 addListener )方法给事件发射器添加了监听器:
然而这个 join 回调永远都不会被调用,因为你还没发射任何事件。所以还要在上面的代码 中加上一行,用 emit 函数发射这个事件:
为了增加能够附加到事件发射器上的监听器数量,不让Node在监听器数量超过10个时向你发 出警告,可以用 setMaxListeners 方法。以频道事件发射器为例,可以用下面的代码增加监听 器的数量:
channel.setMaxListeners(50);
- 扩展事件监听器:文件监视器
function Watcher(watchDir, processedDir) {
this.watchDir = watchDir;
this.processedDir = processedDir;
}
var events = require('events')
, util = require('util');
util.inherits(Watcher, events.EventEmitter);
<!--上面那段代码中的 inherits 语句等同于下面的JavaScript:-->
<!--Watcher.prototype=new events.EventEmitter();-->
var fs = require('fs')
, watchDir = './watch'
, processedDir = './done';
Watcher.prototype.watch = function() {
var watcher = this;
fs.readdir(this.watchDir, function(err, files) {
if (err) throw err;
for(index in files) {
watcher.emit('process', files[index]);
}
})
}
Watcher.prototype.start = function() {
var watcher = this;
fs.watchFile(watchDir, function() {
watcher.watch();
});
}
var watcher = new Watcher(watchDir, processedDir);
watcher.on('process', function process(file) {
var watchFile = this.watchDir + '/' + file;
var processedFile = this.processedDir + '/' + file.toLowerCase();
fs.rename(watchFile, processedFile, function(err) {
if (err) throw err;
});
});
watcher.start();
- 异步开发的难题
作用域是如何导致bug出现的:
function asyncFunction(callback) {
setTimeout(function() {
callback()
}, 200);
}
var color = 'blue';
asyncFunction(function() {
console.log('The color is ' + color);
});
color = 'green';
用匿名函数保留全局变量的值:
function asyncFunction(callback) {
setTimeout(function() {
callback()
}, 200);
}
var color = 'blue';
(function(color) {
asyncFunction(function() {
console.log('The color is ' + color);
})
})(color);
color = 'green';
用JavaScript闭包可以“冻结” color 的值。在Node开发中你要用到很多JavaScript编程技巧,这只是其中之一。
异步逻辑的顺序化
让一组异步任务顺序执行的概念被Node社区称为流程控制。这种控制分为两类:串行和并行。
- 什么时候使用串行流程控制
- 可以使用回调让几个异步任务按顺序执行,但如果任务很多,必须组织一下,否则过多的回 调嵌套会把代码搞得很乱
- 用社区贡献的工具实现串行化控制
var flow = require('nimble');
flow.series([
function (callback) {
setTimeout(function() {
console.log('I execute first.');
callback();
}, 1000);
},
function (callback) {
setTimeout(function() {
console.log('I execute next.');
callback();
}, 500);
},
function (callback) {
setTimeout(function() {
console.log('I execute last.');
callback();
}, 100);
}
]);
- 实现串行化流程控制
- 串行化流程控制本质上是在需要时让回调进场,而不是简单地把它们嵌套起来。
- 为了用串行化流程控制让几个异步任务按顺序执行,需要先把这些任务按预期的执行顺序放 到一个数组中。这个数组将起到队列的作用:完成一个任务后按顺序从数组中取 出下一个。
var fs = require('fs');
var request = require('request');
var htmlparser = require('htmlparser');
var configFilename = './rss_feeds.txt';
function checkForRSSFile () {
fs.exists(configFilename, function(exists) {
if (!exists)
return next(new Error('Missing RSS file: ' + configFilename));
next(null, configFilename);
});
}
function readRSSFile (configFilename) {
fs.readFile(configFilename, function(err, feedList) {
if (err) return next(err);
feedList = feedList
.toString()
.replace(/^\s+|\s+$/g, '')
.split("\n");
var random = Math.floor(Math.random()*feedList.length);
next(null, feedList[random]);
});
}
function downloadRSSFeed (feedUrl) {
request({uri: feedUrl}, function(err, res, body) {
if (err) return next(err);
if (res.statusCode != 200)
return next(new Error('Abnormal response status code'))
next(null, body);
});
}
function parseRSSFeed (rss) {
var handler = new htmlparser.RssHandler();
var parser = new htmlparser.Parser(handler);
parser.parseComplete(rss);
if (!handler.dom.items.length)
return next(new Error('No RSS items found'));
var item = handler.dom.items.shift();
console.log(item.title);
console.log(item.link);
}
var tasks = [ checkForRSSFile,
readRSSFile,
downloadRSSFeed,
parseRSSFeed ];
function next(err, result) {
if (err) throw err;
var currentTask = tasks.shift();
if (currentTask) {
currentTask(result);
}
}
next();
- 实现并行化流程控制
为了让异步任务并行执行,仍然是要把任务放到数组中,但任务的存放顺序无关紧要。每个 任务都应该调用处理器函数增加已完成任务的计数值。当所有任务都完成后,处理器函数应该执 行后续的逻辑。
var fs = require('fs');
var completedTasks = 0;
var tasks = [];
var wordCounts = {};
var filesDir = './text';
function checkIfComplete() {
completedTasks++;
if (completedTasks == tasks.length) {
for(var index in wordCounts) {
console.log(index +': ' + wordCounts[index]);
}
}
}
function countWordsInText(text) {
var words = text
.toString()
.toLowerCase()
.split(/\W+/)
.sort();
for(var index in words) {
var word = words[index];
if (word) {
wordCounts[word] = (wordCounts[word]) ? wordCounts[word] + 1 : 1;
}
}
}
fs.readdir(filesDir, function(err, files) {
if (err) throw err;
for(var index in files) {
var task = (function(file) {
return function() {
fs.readFile(file, function(err, text) {
if (err) throw err;
countWordsInText(text);
checkIfComplete();
});
}
})(filesDir + '/' + files[index]);
tasks.push(task);
}
for(var task in tasks) {
tasks[task]();
}
});
- 利用社区里的工具
-
社区中的很多附加模块都提供了方便好用的流程控制工具。其中比较流行的有Nimble、Step和Seq三个。
-
下面这个例子在微软的Windows中无法使用 因为Windows中没有 tar 和 curl这两个命令,所以下面这个例子在Windows中无法使用
在简单的程序中使用社区附加模块中的流程控制工具
var flow = require('nimble');
var exec = require('child_process').exec;
function downloadNodeVersion(version, destination, callback) {
var url = 'http://nodejs.org/dist/node-v' + version + '.tar.gz';
var filepath = destination + '/' + version + '.tgz';
exec('curl ' + url + ' >' + filepath, callback);
}
flow.series([
function (callback) {
flow.parallel([
function (callback) {
console.log('Downloading Node v0.4.6...');
downloadNodeVersion('0.4.6', '/tmp', callback);
},
function (callback) {
console.log('Downloading Node v0.4.7...');
downloadNodeVersion('0.4.7', '/tmp', callback);
}
], callback);
},
function(callback) {
console.log('Creating archive of downloaded files...');
exec(
'tar cvf node_distros.tar /tmp/0.4.6.tgz /tmp/0.4.7.tgz',
function(error, stdout, stderr) {
console.log('All done!');
callback();
}
);
}
]);