Node.JS

十个Node.js开发易犯错误之四:回调地狱 回调树形结构 嵌套回调

上页

 每次有人要抨击Node,他们总是从回调地狱Callback hell(圣诞树 嵌套回调)开始,一些人认为回调嵌套是不可避免,但是这不是真的,有许多方式可以让你的代码更好和整洁,比如:


 我们在这里创建一个案例,然后使用async重构它,这个应用功能是一个前端资源分析器,实现下面功能:

  • 检查HTML代码中许多 脚本scripts / stylesheets / images ;
  • 输出它们的总数到控制台
  • 检查每个资源的长度
  • 将资源总大小输出

 除了async模块,我们还使用下面npm模块:

  • request 用户获得页面数据(body, headers, etc).
  • cheerio 作为后端的jQuery (DOM 元素selector).
  • 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); }

  });

 

  $('img').each(function(index, el) {

    var src = $(this).attr('src');

    if (src) { resources.push(src); }

  });

 

  $('link').each(function(index, el) {

    var $el = $(this);

    var src = $el.attr('href');

    var rel = $el.attr('rel');

 

    if (src) {

      if (/icon/ig.test(rel) || rel === 'stylesheet') {

        resources.push(src);

      }

    }

  });

 

  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

        });

      }

    });

  });

});

 注意到我们在代码里面标识了进行嵌套回调的地方,这是坏的编码风格,这段代码运行如下:

$ URL=https://bbc.co.uk/ node before.js
# Sample output:
# There are ~ 24 resources with a size of 0.09 Mb.

 我们使用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); }

    });

 

    $('img').each(function(index, el) {

      var src = $(this).attr('src');

      if (src) { resources.push(src); }

    });

 

    $('link').each(function(index, el) {

      var $el = $(this);

      var src = $el.attr('href');

      var rel = $el.attr('rel');

 

      if (src) {

        if (/icon/ig.test(rel) || rel === 'stylesheet') {

          resources.push(src);

        }

      }

    });

 

    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);

});

本节源码下载:Github


下页

Node.js最佳实践