node基础---03node编程基础

243 阅读8分钟

node编程要面对的两个难题:

  • 如何组织代码
  • 怎么做异步编程
    • 如何响应一次性事件
    • 如何处理重复性事件
    • 如何让异步逻辑顺序执行

Node 功能的组织及重用

Node的模块系统避免了对全局作用域的污染,从而也就避免了命名冲突,并简化了代码的重 用。模块还可以发布到npm(Node包管理器)存储库中,这是一个收集了已经可用并且要跟Node 社区分享的Node模块的在线存储库,使用这些模块没必要担心某个模块会覆盖其他模块的变量和 函数。

为了帮你把逻辑组织到模块中,我们会讨论下面这些主题:

  • 如何创建模块;
  • 模块放在文件系统中的什么地方;
  • 在创建和使用模块时要意识到的东西。
  1. 创建模块

模块既可能是一个文件,也可能是包含一个或多个文件的目录,如图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 扩展名可以忽略

  1. 用module.exports微调模块的创建

Node觉得不能用任何其他对象、函数或变量给 exports 赋值

  • 把 exports 换成 module.exports ,用 module.exports 可以对外提供单个变量、函数或者对象。如果你创建了一个既有 exports 又有 module.exports 的模块,那它会返回 module.exports ,而 exports 会被忽略
  • 根据需要使用 exports 或 module.exports 可以将功能组织成模块,规避掉程序脚本一直增 长产生的弊端
  1. 用node_modules重用模块
  • 要求模块在文件系统中使用相对路径存放,对于组织程序特定的代码很有帮助,但对于想要 在程序间共享或跟其他人共享代码却用处不大。Node中有一个独特的模块引入机制,可以不必知 道模块在文件系统中的具体位置。这个机制就是使用node_modules目录。

  • 前面那个模块的例子中引入的是 ./currency 。如果省略 ./ ,只写 currency ,Node会遵照 几个规则搜寻这个模块。如下图所示:

tip:用环境变量 NODE_PATH 可以改变Node模块的默认路径。如果用了它, NODE_PATH 在Windows 中应该设置为用分号分隔的目录列表,在其他操作系统中用冒号分隔

  1. 注意事项
  • 如果模块是目录,在模块目录中定义模块的文件必须被命名为index.js,除非你在这个 目录下一个叫package.json的文件里特别指明。要指定一个取代index.js的文件,package.json文件 里必须有一个用JavaScript对象表示法(JSON)数据定义的对象,其中有一个名为 main 的键,指 明模块目录内主文件的路径。图3-6中的流程图对这些规则做了汇总

  • Node能把模块作为对象缓存起来。如果程序中的两个文件引入了相 同的模块,第一个文件会把模块返回的数据存到程序的内存中,这样第二个文件就不用再去访问 和计算模块的源文件了。实际上第二个引入有机会修改缓存的数据。这种“猴子补丁”(monkey patching)让一个模块可以改变另一个模块的行为,开发人员可以不用创建它的新版本。

异步编程技术

在Node的世界里流行两种响应逻 辑管理方式:回调和事件监听

  • 如何用回调处理一次性事件;
  • 如何用事件监听器响应重复性事件;
  • 异步编程的几个难点
  1. 用回调处理一次性事件
  • 回调是一个函数,它被当做参数传给异步函数,它描述了异步操作完成之后要做什么
  • 回调层数越多,代码看起来越乱,重构和测试起来也越困难,所以最好限 制一下回调的嵌套层级。如果把每一层回调嵌套的处理做成命名函数,虽然表示相同逻辑所用的 代码变多了,但维护、测试和重构起来会更容易
  1. 用事件发射器处理重复性事件

事件发射器会触发事件,并且在那些事件被触发时能处理它们。事件是通过监听器进行处理的。监听器是跟事件相关联的,带有一个事件出现时就会被触发的回调函数。

  • 事件发射器示例

  • 响应只应该发生一次的事件

用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();
  1. 异步开发的难题

作用域是如何导致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社区称为流程控制。这种控制分为两类:串行和并行。

  1. 什么时候使用串行流程控制
  • 可以使用回调让几个异步任务按顺序执行,但如果任务很多,必须组织一下,否则过多的回 调嵌套会把代码搞得很乱
  • 用社区贡献的工具实现串行化控制
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);
  }
]);
  1. 实现串行化流程控制
  • 串行化流程控制本质上是在需要时让回调进场,而不是简单地把它们嵌套起来。
  • 为了用串行化流程控制让几个异步任务按顺序执行,需要先把这些任务按预期的执行顺序放 到一个数组中。这个数组将起到队列的作用:完成一个任务后按顺序从数组中取 出下一个。
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();
  1. 实现并行化流程控制

为了让异步任务并行执行,仍然是要把任务放到数组中,但任务的存放顺序无关紧要。每个 任务都应该调用处理器函数增加已完成任务的计数值。当所有任务都完成后,处理器函数应该执 行后续的逻辑。

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]();
  }
});
  1. 利用社区里的工具
  • 社区中的很多附加模块都提供了方便好用的流程控制工具。其中比较流行的有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();
      }
    );
  }
]);