回调地狱或末日金字塔node.js 的构建方式强制您使用异步函数。这意味着回调,回调,甚至更多回调。您可能已经看到过,甚至自己编写过这样的代码:
app.get('/login', function (req, res) { sql.query('select 1 from users where name = ?;', [ req.param('username') ], function (error, rows) { if (error) { res.writehead(500); return res.end(); } if (rows.length < 1) { res.end('wrong username!'); } else { sql.query('select 1 from users where name = ? && password = md5(?);', [ req.param('username'), req.param('password') ], function (error, rows) { if (error) { res.writehead(500); return res.end(); } if (rows.length < 1) { res.end('wrong password!'); } else { sql.query('select * from userdata where name = ?;', [ req.param('username') ], function (error, rows) { if (error) { res.writehead(500); return res.end(); } req.session.username = req.param('username'); req.session.data = rows[0]; res.rediect('/userarea'); }); } }); } });});
这实际上是直接来自我的第一个 node.js 应用程序的一个片段。如果您在 node.js 中做过一些更高级的事情,您可能会理解所有内容,但这里的问题是,每次使用某些异步函数时,代码都会向右移动。它变得更难阅读,更难调试。幸运的是,有一些解决方案可以解决这个问题,因此您可以为您的项目选择合适的解决方案。
解决方案1:回调命名和模块化最简单的方法是命名每个回调(这将帮助您调试代码)并将所有代码拆分为模块。只需几个简单的步骤即可将上面的登录示例变成一个模块。
结构让我们从一个简单的模块结构开始。为了避免上述情况,当你只是将混乱分成更小的混乱时,让我们将其作为一个类:
var util = require('util');function login(username, password) { function _checkforerrors(error, rows, reason) { } function _checkusername(error, rows) { } function _checkpassword(error, rows) { } function _getdata(error, rows) { } function perform() { } this.perform = perform;}util.inherits(login, eventemitter);
该类由两个参数构造:用户名和密码。看示例代码,我们需要三个函数:一个检查用户名是否正确(_checkusername),另一个检查密码(_checkpassword),还有一个返回用户相关数据(_getdata)并通知应用程序登录成功。还有一个 _checkforerrors 帮助器,它将处理所有错误。最后,有一个 perform 函数,它将启动登录过程(并且是类中唯一的公共函数)。最后我们继承eventemitter来简化该类的使用。
帮助者_checkforerrors 函数将检查是否发生任何错误或 sql 查询是否未返回任何行,并发出相应的错误(以及提供的原因):
function _checkforerrors(error, rows, reason) { if (error) { this.emit('error', error); return true; } if (rows.length < 1) { this.emit('failure', reason); return true; } return false;}
它还会返回 true 或 false,具体取决于是否发生错误。
执行登录perform 函数只需执行一个操作:执行第一个 sql 查询(检查用户名是否存在)并分配适当的回调:
function perform() { sql.query('select 1 from users where name = ?;', [ username ], _checkusername);}
我假设您的 sql 连接可以在 sql 变量中全局访问(只是为了简化,讨论这是否是一个好的实践超出了本文的范围)。这就是这个函数的全部内容。
检查用户名下一步是检查用户名是否正确,如果正确,则触发第二个查询 - 检查密码:
function _checkusername(error, rows) { if (_checkforerrors(error, rows, 'username')) { return false; } else { sql.query('select 1 from users where name = ? && password = md5(?);', [ username, password ], _checkpassword); }}
与混乱示例中的代码几乎相同,但错误处理除外。
检查密码这个函数与前一个函数几乎完全相同,唯一的区别是调用的查询:
function _checkpassword(error, rows) { if (_checkforerrors(error, rows, 'password')) { return false; } else { sql.query('select * from userdata where name = ?;', [ username ], _getdata); }}
获取用户相关数据此类中的最后一个函数将获取与用户相关的数据(可选步骤)并用它触发成功事件:
function _getdata(error, rows) { if (_checkforerrors(error, rows)) { return false; } else { this.emit('success', rows[0]); }}
最后的修饰和使用最后要做的事情是导出类。在所有代码后面添加这一行:
module.exports = login;
这将使 login 类成为该模块将导出的唯一内容。稍后可以像这样使用它(假设您已将模块文件命名为 login.js 并且它与主脚本位于同一目录中):
var login = require('./login.js');...app.get('/login', function (req, res) { var login = new login(req.param('username'), req.param('password)); login.on('error', function (error) { res.writehead(500); res.end(); }); login.on('failure', function (reason) { if (reason == 'username') { res.end('wrong username!'); } else if (reason == 'password') { res.end('wrong password!'); } }); login.on('success', function (data) { req.session.username = req.param('username'); req.session.data = data; res.redirect('/userarea'); }); login.perform();});
这里又多了几行代码,但是代码的可读性增加了,非常明显。此外,该解决方案不使用任何外部库,这使得如果有新人加入您的项目,它会变得完美。
这是第一种方法,让我们继续第二种方法。
解决方案 2:promise使用 promise 是解决此问题的另一种方法。承诺(正如您可以在提供的链接中阅读的那样)“表示从操作的单个完成中返回的最终值”。实际上,这意味着您可以链接调用以压平金字塔并使代码更易于阅读。
我们将使用 npm 存储库中提供的 q 模块。
q简而言之在开始之前,我先向您介绍一下q。对于静态类(模块),我们主要使用 q.nfcall 函数。它帮助我们将遵循 node.js 回调模式(其中回调的参数是错误和结果)的每个函数转换为 promise。它的使用方式如下:
q.nfcall(http.get, options);
它非常像 object.prototype.call。您还可以使用 q.nfapply ,它类似于 object.prototype.apply:
q.nfapply(fs.readfile, [ 'filename.txt', 'utf-8' ]);
此外,当我们创建 promise 时,我们使用 then(stepcallback) 方法添加每个步骤,使用 catch(errorcallback) 捕获错误,并使用 done() 结束。在这种情况下,由于 sql 对象是一个实例,而不是静态类,所以我们必须使用 q.ninvoke 或 q.npost ,它们与上面类似。不同之处在于,我们将方法的名称作为字符串传递到第一个参数中,并将我们想要使用的类的实例作为第二个参数传递,以避免方法从实例。
准备承诺首先要做的是执行第一步,使用q.nfcall或q.nfapply(使用你更喜欢的,下面没有区别):
var q = require('q');...app.get('/login', function (req, res) { q.ninvoke('query', sql, 'select 1 from users where name = ?;', [ req.param('username') ])});
请注意该行末尾缺少分号 - 函数调用将被链接起来,因此它不能在那里。我们只是像混乱的示例中那样调用 sql.query ,但我们省略了回调参数 - 它由 promise 处理。
检查用户名现在我们可以为 sql 查询创建回调,它将与“厄运金字塔”示例中的回调几乎相同。在 q.ninvoke 调用后添加以下内容:
.then(function (rows) { if (rows.length < 1) { res.end('wrong username!'); } else { return q.ninvoke('query', sql, 'select 1 from users where name = ? && password = md5(?);', [ req.param('username'), req.param('password') ]); }})
如您所见,我们使用 then 方法附加回调(下一步)。另外,在回调中我们省略了 error 参数,因为我们稍后会捕获所有错误。我们正在手动检查查询是否返回某些内容,如果是,我们将返回下一个要执行的承诺(同样,由于链接而没有分号)。
检查密码与模块化示例一样,检查密码与检查用户名几乎相同。这应该在最后一次 then 调用之后进行:
.then(function (rows) { if (rows.length < 1) { res.end('wrong password!'); } else { return q.ninvoke('query', sql, 'select * from userdata where name = ?;', [ req.param('username') ]); }})
获取用户相关数据最后一步是将用户数据放入会话中。再一次,回调与混乱的示例没有太大区别:
.then(function (rows) { req.session.username = req.param('username'); req.session.data = rows[0]; res.rediect('/userarea');})
检查错误使用 promise 和 q 库时,所有错误均由使用 catch 方法的回调集处理。在这里,无论错误是什么,我们都只发送 http 500,如上面的示例所示:
.catch(function (error) { res.writehead(500); res.end();}).done();
之后,我们必须调用 done 方法来“确保,如果错误在结束之前没有得到处理,它将被重新抛出并报告”(来自库的 readme)。现在我们漂亮的扁平化代码应该如下所示(并且行为就像混乱的代码一样):
var q = require('q');...app.get('/login', function (req, res) { q.ninvoke('query', sql, 'select 1 from users where name = ?;', [ req.param('username') ]) .then(function (rows) { if (rows.length < 1) { res.end('wrong username!'); } else { return q.ninvoke('query', sql, 'select 1 from users where name = ? && password = md5(?);', [ req.param('username'), req.param('password') ]); } }) .then(function (rows) { if (rows.length < 1) { res.end('wrong password!'); } else { return q.ninvoke('query', sql, 'select * from userdata where name = ?;', [ req.param('username') ]); } }) .then(function (rows) { req.session.username = req.param('username'); req.session.data = rows[0]; res.rediect('/userarea'); }) .catch(function (error) { res.writehead(500); res.end(); }) .done();});
代码更加简洁,并且比模块化方法涉及的重写更少。
解决方案 3:步骤库此解决方案与上一个解决方案类似,但更简单。 q 有点重,因为它实现了整个 promise 的想法。 step 库的存在只是为了消除回调地狱。使用起来也更简单,因为您只需调用从模块导出的唯一函数,将所有回调作为参数传递,并使用 this 代替每个回调。因此,可以使用 step 模块将这个混乱的示例转换成这样:
var step = require('step');...app.get('/login', function (req, res) { step( function start() { sql.query('select 1 from users where name = ?;', [ req.param('username') ], this); }, function checkusername(error, rows) { if (error) { res.writehead(500); return res.end(); } if (rows.length < 1) { res.end('wrong username!'); } else { sql.query('select 1 from users where name = ? && password = md5(?);', [ req.param('username'), req.param('password') ], this); } }, function checkpassword(error, rows) { if (error) { res.writehead(500); return res.end(); } if (rows.length < 1) { res.end('wrong password!'); } else { sql.query('select * from userdata where name = ?;', [ req.param('username') ], this); } }, function (error, rows) { if (error) { res.writehead(500); return res.end(); } req.session.username = req.param('username'); req.session.data = rows[0]; res.rediect('/userarea'); } );});
这里的缺点是没有通用的错误处理程序。尽管在一个回调中引发的任何异常都会作为第一个参数传递给下一个回调(因此脚本不会因为未捕获的异常而停止运行),但在大多数情况下,为所有错误使用一个处理程序是很方便的。
选择哪一个?这很大程度上是个人选择,但为了帮助您选择正确的选择,以下列出了每种方法的优缺点:
模块化:优点:
没有外部库有助于提高代码的可重用性缺点:
更多代码如果您要转换现有项目,则需要进行大量重写承诺(q):优点:
更少的代码如果应用于现有项目,只需稍微重写缺点:
您必须使用外部库需要一些学习步骤库:优点:
易于使用,无需学习如果转换现有项目,则几乎可以进行复制和粘贴缺点:
没有通用的错误处理程序正确缩进 step 函数有点困难结论正如您所看到的,node.js 的异步特性是可以管理的,并且可以避免回调地狱。我个人使用模块化方法,因为我喜欢让我的代码结构良好。我希望这些技巧将帮助您编写更具可读性的代码并更轻松地调试脚本。
以上就是node.js:应对异步执行的挑战的详细内容。
