Node.JS
十个Node.js开发易犯错误之四:回调地狱 回调树形结构 嵌套回调
每次有人要抨击Node,他们总是从回调地狱Callback hell(圣诞树 嵌套回调)开始,一些人认为回调嵌套是不可避免,但是这不是真的,有许多方式可以让你的代码更好和整洁,比如:
- 使用控制流程模块(如 async);
- Promises;
- Generators.
我们在这里创建一个案例,然后使用async重构它,这个应用功能是一个前端资源分析器,实现下面功能:
- 检查HTML代码中许多 脚本scripts / stylesheets / images ;
- 输出它们的总数到控制台
- 检查每个资源的长度
- 将资源总大小输出
除了async模块,我们还使用下面npm模块:
代码如下:
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