Node.js: Какие существуют методы для написания чистого, простого кода обратного вызова?
Код node.js известен тем, что превращается в спагетти обратного вызова.
Каковы лучшие методы для преодоления этой проблемы и написания простого, понятного и понятного кода обратного вызова в файле node.js?
5 ответов
Посмотрите на Обещания: http://promises-aplus.github.io/promises-spec/
Это открытый стандарт, предназначенный для решения этой проблемы.
Я использую модуль узла '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);
}
Затем мы можем связать обещания методом.then():
foo(<any-id>)
.then(function(result) {
// another promise
})
.then(function() {
// so on
});
Также возможно создать обещание из таких ценностей, как:
Q([]).then(function(val) { val.push('foo') });
И многое другое, см. Документы.
Смотрите также:
Несколько вещей могут быть сделаны, чтобы избежать "стиля matrioska".
Вы можете хранить обратные вызовы для переменных:
var on_read = function (foo, bar) { // some logic }, on_insert = function (err, data) { someAsyncRead(data, on_read); }; someAsyncInsert('foo', on_insert);
Вы можете использовать некоторые модули, которые помогают в этих сценариях.
// 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); });
Я бы предложил 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
были переписаны для использования / расширения хэша. (Возможно, вам придется написать дополнительные оболочки вокруг внешних функций.)
Как видите, код в этом подходе читается аккуратно и линейно. И поскольку все обратные вызовы выставлены, модульное тестирование становится намного проще.
По большей части работающий пример приложения Twitter OAuth2, использующий библиотеку обещаний Криса Q с https.request
, Nodejs Экспресс 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
Попробуйте нод-лайн
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);
});