Node.JS开发者常犯的10个错误(一)


发布者 ourjs  发布时间 1420773582237
关键字 编程技巧  分享 
Node.JS在过去几年有着长足的发展。越来越多的人采用基于Node的NPM来发布他们的模块,并且远远超过了其它语言 。然而当你从其它语言转向Node时,需要一些时间才能适合它的哲学。

这篇文章我们将讨论一下Node开发者常见的一些错误,以及避免方法。

1. 不使用开发工具


不像PHP、Ruby等其它语言,node改完代码之后每次都需要重启,在浏览器上表现就是需要刷新页面。你可以手动做这些事情,但这会降低你的开发速度。

1.1 自动重启


大多数人会在编辑器中保存文件,单击[CTRL+C],然后手动重启服务,但是你可以使用工具来自动完成这一步。


这些模块会监测文件的改变,然后自动重启你的服务器。首先安装一个

npm i nodemon -g

然后用nodemon代替node运行你的程序

$ nodemon server.js
14 Nov 21:23:23 - [nodemon] v1.2.1
14 Nov 21:23:23 - [nodemon] to restart at any time, enter `rs`
14 Nov 21:23:23 - [nodemon] watching: *.*
14 Nov 21:23:23 - [nodemon] starting `node server.js`
14 Nov 21:24:14 - [nodemon] restarting due to changes...
14 Nov 21:24:14 - [nodemon] starting `node server.js`

很简单吧

2. 阻塞循环


因为Node.js是在一个进程中运行的,每个阻塞事件循环(Event Loop)的操作就会阻塞一切。当有数千个并连连接时,你阻塞其中一个,其它的都得等待。 注* 参见  Node.js安全教程:防止阻塞Event Loop的潜在攻击 

下面是一些可能发生的情况

  • 解析一个超大的JSON文件
  • 尝试为一个超大的源代码文件添加代码高亮(比如使用 Ace  或 highlight.js
  • 分析一个超大的命令行输出。(比如在子进程child process中运行git log)

这些攻击可能是在你无意之间就会发生的,因为解析一个15Mb大的文件并不是经常碰到的,但是在DDOS攻击时这一个漏洞就足够了。

幸运的是你可以通过一些属性或第三方模块检测到事件循环的阻塞或延时,像StrongOpsblocked

这些工具的基本原理就是计算一个interval定时器两次间隔响应的时间,如果显著变长,就证明当前的EventLoop被阻塞或是被攻击了。下面是一段示例代码。


  var getHrDiffTime = function(time) {
    // ts = [seconds, nanoseconds]
    var ts = process.hrtime(time);
    // 将秒转换成豪秒
    return (ts[0] * 1000) + (ts[1] / 1000000);
  };

  var outputDelay = function(interval, maxDelay) {
    maxDelay = maxDelay || 100;

    var before = process.hrtime();

    setTimeout(function() {
      var delay = getHrDiffTime(before) - interval;

      if (delay < maxDelay) {
        console.log('delay is %s', chalk.green(delay));
      } else {
        console.log('delay is %s', chalk.red(delay));
      }

      outputDelay(interval, maxDelay);
    }, interval);
  };

  outputDelay(300);

  // 这里模拟了一个密集CPU运算,每2秒运行一次
  setInterval(function compute() {
    var sum = 0;

    for (var i = 0; i <= 999999999; i++) {
      sum += i * 2 - (i + 1);
    }
  }, 2000);


3 同一个回调被运行很多次


有多少次你保存完文件之后,看到服务很快地挂掉了?最常见的情况就是你运行了两次回调。或者说在第一次时你忘记了返回。下面是一个代理服务器的例子:


var request = require('request');
var http = require('http');
var url = require('url');
var PORT = process.env.PORT || 1337;

var expression = /[-a-zA-Z0-9@:%_\+.~#?&//=]{2,256}\.[a-z]{2,4}\b(\/[-a-zA-Z0-9@:%_\+.~#?&//=]*)?/gi;
var isUrl = new RegExp(expression);

var respond = function(err, params) {
  var res = params.res;
  var body = params.body;
  var proxyUrl = params.proxyUrl;

  res.setHeader('Content-type', 'text/html; charset=utf-8');

  if (err) {
    console.error(err);
    res.end('An error occured. Please make sure the domain exists.');
  } else {
    res.end(body);
  }
};

http.createServer(function(req, res) {
  var queryParams = url.parse(req.url, true).query;
  var proxyUrl = queryParams.url;

  if (!proxyUrl || (!isUrl.test(proxyUrl))) {
    res.writeHead(200, { 'Content-Type': 'text/html' });
    res.write("Please provide a correct URL param. For ex: ");
    res.end("<a href='http://localhost:1337/?url=http://www.google.com/'>http://localhost:1337/?url=http://www.google.com/</a>");
  } else {
    // ------------------------
    // Proxying happens here
    // TO BE CONTINUED
    // ------------------------
  }
}).listen(PORT);


request(proxyUrl, function(err, res, body) {
  if (err) {
    respond(err, {
        res: res,
        proxyUrl: proxyUrl
    });

    //此时response.end() 己被调用,但忘记了返回
  }

  respond(null, {
    res: res,
    body: body,
    proxyUrl: proxyUrl
  });
});



注* 即在一个MiddleWare或Filter中将响应关闭后 response.end() ,又在另外一个回调中运行了一个response.end() 此时会发生异常。


4 回调地狱(Callback Hell)


Callback Hell是node程序经常被抨击的一点,在NodeJS中回调嵌套是无法避免的,但是你可以使用一些工具保持你代码的优美和整洁:

  • 使用流程控制模块 (像 async
  • Promises模式

除了async模块,下面的例子还用到了其它几个

  • request 获取页面数据 (body, headers, 等等)
  • cheerio 后端的jQuery(DOM 选择器)
  • once 确保回调只被运行一次

  var URL = process.env.URL;
  var assert = require('assert');
  var url = require('url');
  var request = require('request');
  var cheerio = require('cheerio');
  var once = require('once');
  var isUrl = new RegExp(/[-a-zA-Z0-9@:%_\+.~#?&//=]{2,256}\.[a-z]{2,4}\b(\/[-a-zA-Z0-9@:%_\+.~#?&//=]*)?/gi);

  assert(isUrl.test(URL), 'must provide a correct URL env variable');

  request({ url: URL, gzip: true }, function(err, res, body) {
    if (err) { throw err; }

    if (res.statusCode !== 200) {
      return console.error('Bad server response', res.statusCode);
    }

    var $ = cheerio.load(body);
    var resources = [];

    $('script').each(function(index, el) {
      var src = $(this).attr('src');
      if (src) { resources.push(src); }
    });

    // .....
    // similar code for stylesheets and images
    // checkout the github repo for the full version

    var counter = resources.length;
    var next = once(function(err, result) {
      if (err) { throw err; }

      var size = (result.size / 1024 / 1024).toFixed(2);

      console.log('There are ~ %s resources with a size of %s Mb.', result.length, size);
    });

    var totalSize = 0;

    resources.forEach(function(relative) {
      var resourceUrl = url.resolve(URL, relative);

      request({ url: resourceUrl, gzip: true }, function(err, res, body) {
        if (err) { return next(err); }

        if (res.statusCode !== 200) {
          return next(new Error(resourceUrl + ' responded with a bad code ' + res.statusCode));
        }

        if (res.headers['content-length']) {
          totalSize += parseInt(res.headers['content-length'], 10);
        } else {
          totalSize += Buffer.byteLength(body, 'utf8');
        }

        if (!--counter) {
          next(null, {
            length: resources.length,
            size: totalSize
          });
        }
      });
    });
  });

使用async重构以后,就能像下面这样:


  var async = require('async');

  var rootHtml = '';
  var resources = [];
  var totalSize = 0;

  var handleBadResponse = function(err, url, statusCode, cb) {
    if (!err && (statusCode !== 200)) {
      err = new Error(URL + ' responded with a bad code ' + res.statusCode);
    }

    if (err) {
      cb(err);
      return true;
    }

    return false;
  };

  async.series([
    function getRootHtml(cb) {
      request({ url: URL, gzip: true }, function(err, res, body) {
        if (handleBadResponse(err, URL, res.statusCode, cb)) { return; }

        rootHtml = body;

        cb();
      });
    },
    function aggregateResources(cb) {
      var $ = cheerio.load(rootHtml);

      $('script').each(function(index, el) {
        var src = $(this).attr('src');
        if (src) { resources.push(src); }
      });

      // similar code for stylesheets && images; check the full source for more

      setImmediate(cb);
    },
    function calculateSize(cb) {
      async.each(resources, function(relativeUrl, next) {
        var resourceUrl = url.resolve(URL, relativeUrl);

        request({ url: resourceUrl, gzip: true }, function(err, res, body) {
          if (handleBadResponse(err, resourceUrl, res.statusCode, cb)) { return; }

          if (res.headers['content-length']) {
            totalSize += parseInt(res.headers['content-length'], 10);
          } else {
            totalSize += Buffer.byteLength(body, 'utf8');
          }

          next();
        });
      }, cb);
    }
  ], function(err) {
    if (err) { throw err; }

    var size = (totalSize / 1024 / 1024).toFixed(2);
    console.log('There are ~ %s resources with a size of %s Mb.', resources.length, size);
  });