Node.js: Какие существуют методы для написания чистого, простого кода обратного вызова?

node.js, как известно, превращается в спагетты обратного вызова.

Каковы наилучшие методы для преодоления этой проблемы и написания чистого, некомплексного, легко понятного кода обратного вызова в node.js?

Ответ 1

Взгляните на Promises: http://promises-aplus.github.io/promises-spec/

Это открытый стандарт, предназначенный для решения этой проблемы.

Я использую node модуль 'q', который реализует этот стандарт: https://github.com/kriskowal/q

Простой вариант:

var Q = require('q');

Например, у нас есть метод вроде:

var foo = function(id) {
  var qdef = Q.defer();

  Model.find(id).success(function(result) {
    qdef.resolve(result);
  });

  return (qdef.promise);
}

Тогда мы можем цепью promises методом .then():

foo(<any-id>)
.then(function(result) {
  // another promise
})
.then(function() {
  // so on
});

Также можно создать обещание от таких значений, как:

Q([]).then(function(val) { val.push('foo') });

И многое другое, см. документы.

См. также:

Ответ 2

Можно сделать несколько вещей, чтобы избежать "стиля матриоски".

  • Вы можете сохранять обратные вызовы для переменных:

    var on_read = function (foo, bar) {
          // some logic 
        },
    
        on_insert = function (err, data) {
          someAsyncRead(data, on_read);
        };
    
    someAsyncInsert('foo', on_insert);
    
  • Вы можете использовать modules, который поможет в этих сценариях.

    // Example using funk
    var funk = require('funk');
    for(var i = 0; i < 10; i++) {
      asyncFunction(i, funk.add(function (data) {
        this[i] = data;
      }));
    }
    
    funk.parallel(function () {
      console.log(this);
    });
    

Ответ 3

Я бы предложил 1) используя CoffeeScript и 2) используя именованные обратные вызовы и состояние передачи между ними в хэше, а не вложенные обратные вызовы или предоставление списков аргументов очень долго. Поэтому вместо

var callback1 = function(foo) {
  var callback2 = function(bar) {
    var callback3 = function(baz) {
      doLastThing(foo, bar, baz);
    }
    doSomethingElse(bar, callback3);
  }
  doSomething(foo, callback2);
}
someAsync(callback1);

вместо этого вы можете просто написать

callback1 = (state) -> doSomething state.foo, callback2
callback2 = (state) -> doSomethingElse state.bar, callback3
callback3 = (state) -> doLastThing state
someAsync callback1

как только ваши doSomething, doSomethingElse и doLastThing были переписаны для использования/расширения хэша. (Возможно, вам придется писать дополнительные обертки вокруг внешних функций.)

Как вы можете видеть, код в этом подходе читается аккуратно и линейно. И поскольку все обратные вызовы отображаются, модульное тестирование становится намного проще.

Ответ 4

Попробуйте node -line

https://github.com/kevin0571/node-line

Использование:

var line = require("line");
line(function(next) {
    obj.action1(param1, function(err, rs) {
        next({
            err: err,
            rs: rs
        });
    });
}, function(next, data) {
    if (data.err) {
        console.error(err);
        return;
    }
    obj.action2(param2, function(err, rs) {
        if (err) {
            console.error(err);
            return;
        }
        next(rs);
   });
}, function(rs) {
   obj.finish(rs);
});

Ответ 5

По большей части, работая в приложении только для приложений Twitter OAuth2, используя библиотеку обещаний Криса Q с https.request, маршрут Nodejs Express api. Первая попытка пользовательской временной шкалы GET. Если ответ 401, обновляющий токен-носитель, повторите попытку пользователя. Мне пришлось использовать Q.when для выполнения обещания, которое возвращает другое обещание (цепочка) или значение.

 /**
 * Using Rails-like standard naming convention for endpoints.
 * GET     /things              ->  index
 * POST    /things              ->  create
 * GET     /things/:id          ->  show
 * PUT     /things/:id          ->  update
 * DELETE  /things/:id          ->  destroy
 */

'use strict';

// var _ = require('lodash');
var http = require('http');
var https = require('https');
var querystring = require('querystring');
var Q = require('q')

// Get list of twtimelines
exports.index = function(req, res) {
    var tid = req.query.tid
    if (tid) {
        Q.when(reqTimeline(tid, true, res), function(value) {
            // > value
            // 404
            // > body1
            // '{"errors":[{"code":34,"message":"Sorry, that page does not exist."}]}'
        })
    } else {
        res.json({
            errors: [{
                message: 'no tid specified in query'
            }]
        });
    }
};


function reqPromise(options, postData) {
    var deferred = Q.defer()

    var req = https.request(options, function(res) {
        // console.log("statusCode: ", res.statusCode);
        // console.log("headers: ", res.headers);
        var statusCode = res.statusCode
        deferred.notify(res)

        res.on('data', function(d) {
            //process.stdout.write(d);
            deferred.notify(d)
        }).on('end', function() {
            deferred.resolve(statusCode)
        });
    });

    req.on('error', function(e) {
        console.error(e);
        deferred.reject(e)
    });

    req.write(postData);
    req.end();
    return deferred.promise
} // deferRequest

function isIncomingMessage(ot) {
    return ot instanceof http.IncomingMessage
}

function isBuffer(ot) {
    return ot instanceof Buffer
}

function reqTimeline(screen_name, reqBearerTokenOn401, res) {
    var optionsUserTimeline = {
        hostname: 'api.twitter.com',
        path: '/1.1/statuses/user_timeline.json?' + querystring.stringify({
            count: '3',
            screen_name: screen_name
        }),
        method: 'GET',
        headers: {
            //'Authorization': 'Bearer ' + JSON.parse(body1).access_token
            'Authorization': 'Bearer ' + process.env.BEARER_TOKEN
        } // headers
    };
    console.log("optionsUserTimeline", optionsUserTimeline)

    var statusCode;
    var body1 = new Buffer(''); // default utf8 string buffer ?
    return reqPromise(optionsUserTimeline, '')
        .then(function(value) { // done
                if (reqBearerTokenOn401 && value === 401) {
                    console.log("reqTimeline - requesting bearer token")
                    return reqBearerToken(screen_name, res)
                }
                console.log("reqTimeline - done done:", value)
                res.end()
                return value
            },
            function(reason) { // error
                console.log("reqTimeline - error:", body1)
            },
            function(progress) {
                console.log("reqTimeline - progress:", body1)
                if (isIncomingMessage(progress)) {
                    body1 = body1.slice(0, 0) // re-set buffer
                    statusCode = progress.statusCode;
                    if (reqBearerTokenOn401 && statusCode === 401) {
                        // readyn for retry
                    } else {
                        res.writeHead(statusCode)
                    }
                } else if (isBuffer(progress)) {
                    if (reqBearerTokenOn401 && statusCode === 401) {
                        body1 += progress
                    } else {
                        res.write(progress)
                    }
                } else {
                    throw "reqTimeline - unexpected progress"
                }
            });
} // reqTimeline

function reqBearerToken(screen_name, res) {
    var postData = querystring.stringify({
        'grant_type': 'client_credentials'
    })
    var optionsBearerToken = {
            hostname: 'api.twitter.com',
            path: '/oauth2/token',
            method: 'POST',
            headers: {
                'Authorization': 'Basic ' + new Buffer(
                    process.env.CONSUMER_KEY + ":" + process.env.CONSUMER_SECRET
                ).toString('base64'),
                'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8',
                'Content-Length': postData.length
            } // headers
        }
        // console.log("key", process.env.CONSUMER_KEY)
        // console.log("secret", process.env.CONSUMER_SECRET)
        // console.log("buf", new Buffer(
        //  process.env.CONSUMER_KEY + ":" + process.env.CONSUMER_SECRET
        // ).toString())
        console.log("optionsBearerToken", optionsBearerToken)

    var body2 = new Buffer(''); // default utf8 string buffer ?
    return reqPromise(optionsBearerToken, postData)
        .then(function(value) { // done
            console.log("reqBearerToken - done:", body2)
            if (value === 200) {
                console.log("reqBearerToken - done done")
                process.env.BEARER_TOKEN = JSON.parse(body2).access_token;
                return reqTimeline(screen_name, false, res)
            }
            return value
        }, function(reason) {
            throw "reqBearerToken - " + reason
        }, function(progress) {
            if (isIncomingMessage(progress)) {
                body2 = body2.slice(0, 0) // reset buffer
            } else if (isBuffer) {
                body2 += progress
            } else {
                throw "reqBearerToken - unexpected progress"
            }
        });
} // reqBearerToken