Обратный звонок в nodejs?

В приведенном ниже коде я в 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());
    }

Ответ 1

Да, вы находитесь в обратном ад. Решение, предполагающее, что вы не хотите использовать async (который, я сомневаюсь, вы можете оправдать, кроме предрассудков), состоит из:

1) Сделайте больше функций верхнего уровня. Каждая функция должна выполнять либо 1, либо 2 операции ввода-вывода, как правило.

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). Прочтите другие модули для примеров

Как вам следует использовать асинхронную библиотеку, вы можете и должны составить свое мнение об этом, но ПОСЛЕ, которое вы знаете, и хорошо знаете, каждый из этих подходов:

  • обратные вызовы и основные функциональные методы javascript
  • События
  • promises
  • Вспомогательные библиотеки (async, step, nimble и т.д.)

Любой серьезный разработчик node.js знает, как использовать и работать в ВСЕ этих парадигм. Да, у каждого свой подход и, возможно, некоторый боязнь о неприемлемых подходах, но ни один из них не является сложным, и это плохо, чтобы принять решение, не указывая на какой-то нетривиальный код, который вы написали с нуля в каждой парадигме. Кроме того, вы должны попробовать несколько вспомогательных библиотек и понять, как они работают, и почему они собираются сохранить ваш шаблон. Изучение работы Тима Касуэлла Step или Caolan McMahon async будет очень полезным. Вы видели использование everyauth исходного кода promises? Мне это не нравится лично, но я, конечно же, должен признать, что автор сжал чертовски почти каждый последний бит повторения из этой библиотеки, и способ, которым он использует promises, превратит ваш мозг в крендель. Эти люди - мастера, которые многому учат. Не издевайтесь над этими библиотеками только за хипстерские очки или что-то еще.

Также хороший внешний ресурс callbackhell.com.

Ответ 2

"Если вы попытаетесь ввести код bussiness db login, используя чистый node.js, вы сразу перейдете к обратному аду"

Недавно я создал простую абстракцию с именем WaitFor для вызова асинхронных функций в режиме синхронизации (на основе Fibers): https://github.com/luciotato/waitfor

проверьте пример базы данных:

Пример базы данных (псевдокод)

чистый 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);
    }  

Примечание. Вышеприведенный код, хотя похоже, что он поймает исключения, он не будет. Захват исключений с помощью callback ад добавляет много боли, и я не уверен, что у вас будет параметр "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);
}  

Примечание. Исключения будут проверяться как ожидалось. db методы (db.select, db.execute) будут вызваны с этим = db

Ваш код

Чтобы использовать wait.for, вам нужно СТАНДАРТИЗИРОВАТЬ ВАШИ ЗВОНКИ для функции (err, data)

Если вы СТАНДАРТИЗАЦИЯ ВАШИХ 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);

см? нет обратного ада

Ответ 3

Я добавил другое решение в мой блог. Это некрасиво, но это самая читаемая вещь, которую я мог бы сделать с чистым 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;
        }
    }
}