javascript - servidor - Node.js: ¿Qué técnicas existen para escribir un código de devolución de llamada simple y limpio?
nodebeginner español (5)
Eche un vistazo a Promesas: http://promises-aplus.github.io/promises-spec/
Es un estándar abierto que pretende resolver este problema.
Estoy usando el módulo de nodo ''q'', que implementa este estándar: https://github.com/kriskowal/q
Caso de uso simple:
var Q = require(''q'');
Por ejemplo tenemos métodos como:
var foo = function(id) {
var qdef = Q.defer();
Model.find(id).success(function(result) {
qdef.resolve(result);
});
return (qdef.promise);
}
Entonces podemos encadenar promesas por el método .then ():
foo(<any-id>)
.then(function(result) {
// another promise
})
.then(function() {
// so on
});
También es posible crear promesas a partir de valores como:
Q([]).then(function(val) { val.push(''foo'') });
Y mucho más, ver docs.
Ver también:
El código node.js es conocido por convertirse en espagueti de devolución de llamada.
¿Cuáles son las mejores técnicas para superar este problema y escribir un código de devolución de llamada limpio, sin complejos y fácil de entender en node.js?
En su mayor parte, ejemplo de solo trabajo de Twitter OAuth2, utilizando la biblioteca de promesa de Kris con https.request
, Nodejs Express api route. Primer intento del usuario en la línea de tiempo GET. Si el 401 responde, actualice el token del portador y vuelva a intentar la línea de tiempo del usuario. Tuve que usar Q.when
para manejar una promesa que devuelve otra promesa (encadenamiento) o un valor.
/**
* 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
Probar línea de nodo
https://github.com/kevin0571/node-line
Uso:
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);
});
Se pueden hacer varias cosas para evitar el ''estilo matrioska''.
Puede almacenar devoluciones de llamada a variables:
var on_read = function (foo, bar) { // some logic }, on_insert = function (err, data) { someAsyncRead(data, on_read); }; someAsyncInsert(''foo'', on_insert);
Puede utilizar algunos modules que ayudan en esos escenarios.
// 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); });
Yo sugeriría 1) usar CoffeeScript y 2) usar devoluciones de llamada con nombre y pasar el estado entre ellas en un hash, en lugar de anidar devoluciones de llamada o permitir que las listas de argumentos sean muy largas. Así que en lugar de
var callback1 = function(foo) {
var callback2 = function(bar) {
var callback3 = function(baz) {
doLastThing(foo, bar, baz);
}
doSomethingElse(bar, callback3);
}
doSomething(foo, callback2);
}
someAsync(callback1);
en su lugar puedes simplemente escribir
callback1 = (state) -> doSomething state.foo, callback2
callback2 = (state) -> doSomethingElse state.bar, callback3
callback3 = (state) -> doLastThing state
someAsync callback1
una vez que tu doSomething
, doLastThing
y doLastThing
hayan sido reescritos para usar / extender un hash. (Es posible que deba escribir envoltorios adicionales alrededor de funciones externas).
Como puede ver, el código en este enfoque se lee de forma clara y lineal. Y como todas las devoluciones de llamada están expuestas, las pruebas unitarias se vuelven mucho más fáciles.