nodejs中的回调地狱?

时间:2013-08-07 05:12:36

标签: node.js callback

在下面的代码中,我在callbackhell中吗?如何在纯javascript中不使用任何异步模块的情况下克服这种情况?

emailCallBack(e_data, email);
if (email_list.length) {
  checkEmail(email_list.pop());
} else {
  completionCallback();
}

将上述代码复制到多个位置,以使代码按预期工作。

function processInviteEmails(email_list, user_id, emailCallBack, completionCallback){
      function checkEmail(email){
        try {
          check(email).isEmail();
          //is valid email
          checkConnected(email, user_id, function(connect_status, user_row, user_meta_row, connect_row){
            var e_data;
            //insert to connect and send msg to queue
            if(connect_status === 'not connected'){
              var cur_date = moment().format('YYYY-MM-DD');
              var dbData = {
                "first_name": '',
                "last_name": '',
                "email": email,
                "user_id": user_id,
                "status": "invited",
                "unsubscribe_token": crypto.randomBytes(6).toString('base64'),
                "created": cur_date,
                "modified": cur_date
              };
              ConnectModel.insert(dbData, function(result){
                if (result.insertId > 0) {
                  //send to email queue
                  //Queue Email
                  MailTemplateModel.getTemplateData('invitation', function(res_data){
                    if(res_data.status === 'success'){
                      var unsubscribe_hash = crypto.createHash("md5")
                        .update(dbData.unsubscribe_token + email)
                        .digest('hex');
                      var unsubscribe_link = app.locals.SITE_URL+'/unsubscribe/' + result.insertId + '/' + unsubscribe_hash;
                      var template_row = res_data.template_row;
                      var user_full_name = user_row.user_firstname+' '+ user_row.user_lastname;
                      var invitation_link = 'http://'+user_row.url_alias+'.'+ app.locals.SITE_DOMAIN;
                      var mailOptions = {
                        "type": 'invitation',
                        "to": dbData.email,
                        "from_name" : user_full_name,
                        "subject": template_row.message_subject
                          .replace('[[USER]]',  user_full_name),
                        "text": template_row.message_text_body
                          .replace('[[USER]]', user_full_name)
                          .replace('[[INVITATION_LINK]]', invitation_link)
                          .replace('[[UNSUBSCRIBE_LINK]]', unsubscribe_link),
                        "html": template_row.message_body
                          .replace('[[USER]]', user_full_name)
                          .replace('[[INVITATION_LINK]]', invitation_link)
                          .replace('[[UNSUBSCRIBE_LINK]]', unsubscribe_link)
                      };
                      mailOptions = JSON.stringify(mailOptions);
                      //send email to queue
                      sqsHelper.addToQueue(cfg.sqs_invitation_url, mailOptions, function(data){
                        if(data){
                          e_data = null;
                        }
                        else{
                          e_data = new Error('Unable to Queue ');
                        }
                        emailCallBack(e_data, email);
                        if (email_list.length) {
                          checkEmail(email_list.pop());
                        } else {
                          completionCallback();
                        }
                      });
                    }
                    else{
                      e_data = new Error('Unable to get email template');
                      emailCallBack(e_data, email);
                      if (email_list.length) {
                        checkEmail(email_list.pop());
                      } else {
                        completionCallback();
                      }
                    }
                  });
                }
                else{
                  e_data = new Error('Unable to Insert connect');
                  emailCallBack(e_data, email);
                  if (email_list.length) {
                    checkEmail(email_list.pop());
                  } else {
                    completionCallback();
                  }
                }
              });
            }
            else{
              e_data = new Error('Already connected');
              emailCallBack(e_data, email);
              if (email_list.length) {
                checkEmail(email_list.pop());
              } else {
                completionCallback();
              }
            }
          });
        } catch (e) {
          //invalid email
          emailCallBack(e, email);
          if (email_list.length) {
            checkEmail(email_list.pop());
          } else {
            completionCallback();
          }
        }
      }
      checkEmail(email_list.pop());
    }

3 个答案:

答案 0 :(得分:31)

是的,你在回调地狱。假设您不想使用异步的解决方案(我怀疑您可以证明除偏见以外的其他理由)包括:

1 )制作更多顶级功能。根据经验,每个函数应执行1或2个IO操作。

2 )调用这些函数,使代码遵循由一小组控制流“粘合”函数组成的业务逻辑组成的一长串短核函数模式。

而不是:

saveDb1 //lots of code
  saveDb2 //lots of code
    sendEmail //lots of code

目的:

function saveDb1(arg1, arg2, callback) {//top-level code}
function saveDb2(arg1, arg2, callback) {//top-level code}
function sendEmail(arg1, arg2, callback) {//top-level code}
function businessLogic(){//uses the above to get the work done}

3 )使用更多的函数参数,而不是依赖于闭包

4 )发送事件并解除您的代码!了解如何嵌套代码将数据写入数据库,然后构建电子邮件并将其添加到队列中?难道你不明白这两者是不是一个接一个地存在的?电子邮件非常适合发送事件的核心业务逻辑和收听这些事件并对邮件进行排队的电子邮件模块。

5 )从特定事务业务逻辑中分离应用程序级服务连接代码。处理与网络服务的连接应该更广泛地处理,而不是嵌入一组特定的业务逻辑。

6 )阅读其他模块的例子

至于你是否应该使用异步库,你可以而且应该对此有所了解,但你知道 AFTER ,并且非常了解这些方法中的每一种:

  1. 回调和基本功能javascript技术
  2. 事件
  3. 帮助程序库(异步,步进,灵活等)
  4. 任何认真的node.js开发人员都知道如何在这些范例的 ALL 中使用和工作。是的,每个人都有他们喜欢的方法,也许有一些书呆子愤怒的关于非偏爱的方法,但这些都不困难,如果不能指出你从头开始编写的一些非平凡的代码,那么你的决定就不好了。每个范例。此外,您应该尝试几个帮助程序库,并了解它们的工作原理以及它们为什么要为您节省样板。研究Tim Caswell的Step或Caolan McMahon async的工作将会非常具有启发性。您是否看到everyauth源代码使用了承诺?我个人不喜欢它,但我肯定不得不承认作者已经在该库的每一个重复点附近挤压了,并且他使用promises的方式将把你的大脑变成椒盐卷饼。这些人都是巫师,教授很多。不要仅仅针对时髦点或其他任何内容来嘲笑这些库。

    另外一个好的外部资源是callbackhell.com

答案 1 :(得分:8)

“如果你尝试使用纯node.js编写业务db登录,你就直接回调地狱”

我最近创建了一个名为WaitFor的简单抽象,以同步模式调用异步函数(基于Fibers):https://github.com/luciotato/waitfor

检查数据库示例:

数据库示例(伪代码)

pure node.js(温和的回调地狱):

var db = require("some-db-abstraction");

function handleWithdrawal(req,res){  
    try {
        var amount=req.param("amount");
        db.select("* from sessions where session_id=?",req.param("session_id"),function(err,sessiondata) {
            if (err) throw err;
            db.select("* from accounts where user_id=?",sessiondata.user_ID),function(err,accountdata) {
                if (err) throw err;
                    if (accountdata.balance < amount) throw new Error('insufficient funds');
                    db.execute("withdrawal(?,?),accountdata.ID,req.param("amount"), function(err,data) {
                        if (err) throw err;
                        res.write("withdrawal OK, amount: "+ req.param("amount"));
                        db.select("balance from accounts where account_id=?", accountdata.ID,function(err,balance) {
                            if (err) throw err;
                            res.end("your current balance is "  + balance.amount);
                        });
                    });
                });
            });
        }
        catch(err) {
            res.end("Withdrawal error: "  + err.message);
    }  

注意:上面的代码,虽然它看起来会捕获异常,它不会。 使用回调地狱捕捉异常会增加很多痛苦,而且我不确定你是否会有'res'参数 回应用户。如果有人喜欢修复这个例子......请成为我的客人。

使用 wait.for

var db = require("some-db-abstraction"), wait=require('wait.for');

function handleWithdrawal(req,res){  
    try {
        var amount=req.param("amount");
        sessiondata = wait.forMethod(db,"select","* from session where session_id=?",req.param("session_id"));
        accountdata= wait.forMethod(db,"select","* from accounts where user_id=?",sessiondata.user_ID);
        if (accountdata.balance < amount) throw new Error('insufficient funds');
        wait.forMethod(db,"execute","withdrawal(?,?)",accountdata.ID,req.param("amount"));
        res.write("withdrawal OK, amount: "+ req.param("amount"));
        balance=wait.forMethod(db,"select","balance from accounts where account_id=?", accountdata.ID);
        res.end("your current balance is "  + balance.amount);
        }
    catch(err) {
        res.end("Withdrawal error: "  + err.message);
}  

注意:将按预期捕获异常。 将使用this = db

调用db方法(db.select,db.execute)

您的代码

为了使用wait.for,你必须标准化你的CALLBACKS才能运行(错误,数据)

如果您标准化您的回复,您的代码可能如下所示:

//run in a Fiber
function processInviteEmails(email_list, user_id, emailCallBack, completionCallback){

    while (email_list.length) {

      var email = email_list.pop();

      try {

          check(email).isEmail(); //is valid email or throw

          var connected_data = wait.for(checkConnected,email,user_id);
          if(connected_data.connect_status !== 'not connected') throw new Error('Already connected');

          //insert to connect and send msg to queue
          var cur_date = moment().format('YYYY-MM-DD');
          var dbData = {
            "first_name": '',
            "last_name": '',
            "email": email,
            "user_id": user_id,
            "status": "invited",
            "unsubscribe_token": crypto.randomBytes(6).toString('base64'),
            "created": cur_date,
            "modified": cur_date
          };

          result = wait.forMethod(ConnectModel,'insert',dbData);
          // ConnectModel.insert shuold have a fn(err,data) as callback, and return something in err if (data.insertId <= 0) 

          //send to email queue
          //Queue Email
          res_data = wait.forMethod(MailTemplateModel,'getTemplateData','invitation');
          // MailTemplateModel.getTemplateData shuold have a fn(err,data) as callback
          // inside getTemplateData, callback with err=new Error('Unable to get email template') if (data.status !== 'success') 

          var unsubscribe_hash = crypto.createHash("md5")
            .update(dbData.unsubscribe_token + email)
            .digest('hex');
          var unsubscribe_link = app.locals.SITE_URL+'/unsubscribe/' + result.insertId + '/' + unsubscribe_hash;
          var template_row = res_data.template_row;
          var user_full_name = user_row.user_firstname+' '+ user_row.user_lastname;
          var invitation_link = 'http://'+user_row.url_alias+'.'+ app.locals.SITE_DOMAIN;
          var mailOptions = {
            "type": 'invitation',
            "to": dbData.email,
            "from_name" : user_full_name,
            "subject": template_row.message_subject
              .replace('[[USER]]',  user_full_name),
            "text": template_row.message_text_body
              .replace('[[USER]]', user_full_name)
              .replace('[[INVITATION_LINK]]', invitation_link)
              .replace('[[UNSUBSCRIBE_LINK]]', unsubscribe_link),
            "html": template_row.message_body
              .replace('[[USER]]', user_full_name)
              .replace('[[INVITATION_LINK]]', invitation_link)
              .replace('[[UNSUBSCRIBE_LINK]]', unsubscribe_link)
          };
          mailOptions = JSON.stringify(mailOptions);
          //send email to queue ... callback(err,data)
          wait.forMethod(sqsHelper,'addToQueue',cfg.sqs_invitation_url, mailOptions); 

      } catch (e) {
          // one of the callback returned err!==null 
          emailCallBack(e, email);
      }

    } // loop while length>0

    completionCallback();

  }

  // run the loop in a Fiber (keep node spinning)
  wait.launchFiber(processInviteEmails,email_list, user_id, emailCallBack, completionCallback);

见?没有回调地狱

答案 2 :(得分:1)

我在my blog中提出了另一个解决方案。它很丑,但它是我用纯javascript做的最可读的东西。

var flow1 = new Flow1(
    {
        execute_next_step: function(err) {
            if (err) {
                console.log(err);
            };
        }
    }
);

flow1.execute_next_step();

function Flow1(parent_flow) {
    this.execute_next_step = function(err) {
        if (err) return parent_flow.execute_next_step(err);
        if (!this.next_step) this.next_step = 'START';
        console.log('Flow1:', this.next_step);
        switch (this.next_step) {
            case 'START':
                this.next_step = 'FIRST_ASYNC_TASK_FINISHED';
                firstAsyncTask(this.execute_next_step.bind(this));
                break;
            case 'FIRST_ASYNC_TASK_FINISHED':
                this.firstAsyncTaskReturn = arguments[1];
                this.next_step = 'ANOTHER_FLOW_FINISHED';
                this.another_flow = new AnotherFlow(this);
                this.another_flow.execute_next_step();
                break;
            case 'ANOTHER_FLOW_FINISHED':
                this.another_flow_return = arguments[1];
                this.next_step = 'FINISH';
                this.execute_next_step();
                break;
            case 'FINISH':
                parent_flow.execute_next_step();
                break;
        }
    }
}

function AnotherFlow(parent_flow) {
    this.execute_next_step = function(err) {
        if (err) return parent_flow.execute_next_step(err);
        if (!this.next_step) this.next_step = 'START';
        console.log('AnotherFlow:', this.next_step);
        switch (this.next_step) {
            case 'START':
                console.log('I dont want to do anything!. Calling parent');
                parent_flow.execute_next_step();
                break;
        }
    }
}
相关问题