Node.js, la pirámide de la fatalidad (incluso con async), ¿puedes escribirla mejor?

Me considero un desarrollador de node.js muy experimentado.

Sin embargo, todavía me pregunto si hay una mejor manera de escribir el siguiente código, así que no obtengo la pirámide de la fatalidad … Ahora fui fácil contigo, tengo un código que mi pirámide alcanza hasta 20 pisos, no broma y eso es CON usar async.js !!!

El problema es que realmente tengo muchas dependencias en las variables de vista previa, por lo que todo debe estar nested. El tipo que escribió el libro “Async Javascript, construye aplicaciones más sensibles con menos código” explica que pondría las funciones en el scope de la raíz, lo cual seguro que se desharía de la pirámide, pero ahora tendrías un montón de altos. variables de scope (posiblemente incluso globales, dependiendo del scope en el que las declare) y esta contaminación puede dar como resultado algunos errores bastante desagradables (esto podría causar varios conflictos con otras secuencias de comandos si se configura en el espacio global). yachhh … o incluso peor, ya que estamos tratando con async, anulaciones variables …). De hecho, la belleza del cierre es bastante abultada en la puerta.

Lo que él recomienda es hacer algo como:

function checkPassword(username, passwordGuess, callback) { var passwordHash; var queryStr = 'SELECT * FROM user WHERE username = ?'; db.query(selectUser, username, queryCallback); function queryCallback(err, result) { if (err) throw err; passwordHash = result['password_hash']; hash(passwordGuess, hashCallback); } function hashCallback(passwordGuessHash) { callback(passwordHash === passwordGuessHash); } } 

Una vez más, no es un enfoque limpio en mi humilde opinión.

Entonces, si miras mi código (de nuevo, esto es solo un fragmento, tengo nidos mucho más grandes en otros lugares) a menudo verás que mi código se aleja cada vez más de la izquierda; y eso es con el uso de cosas como cascada y asíncrono para cada …

Aquí hay un pequeño ejemplo:

 ms.async.eachSeries(arrWords, function (key, asyncCallback) { pg.connect(pgconn.dbserver('galaxy'), function (err, pgClient, pgCB) { statement = "SELECT * FROM localization_strings WHERE local_id = 10 AND string_key = '" + key[0] + "'"; pgClient.query(statement, function (err, result) { if (pgconn.handleError(err, pgCB, pgClient)) return; // if key doesn't exist go ahead and insert it if (result.rows.length == 0) { statement = "SELECT nextval('last_resource_bundle_string_id')"; pgClient.query(statement, function (err, result) { if (pgconn.handleError(err, pgCB, pgClient)) return; var insertIdOffset = parseInt(result.rows[0].nextval); statement = "INSERT INTO localization_strings (resource_bundle_string_id, string_key, string_revision, string_text,modified_date,local_id, bundle_id) VALUES "; statement += " (" + insertIdOffset + ",'" + key[0] + "'," + 0 + ",'" + englishDictionary[key[0]] + "'," + 0 + ",10,20)"; ms.log(statement); pgClient.query(statement, function (err, result) { if (pgconn.handleError(err, pgCB, pgClient)) return; pgCB(); asyncCallback(); }); }); } pgCB(); asyncCallback(); }); }); }); 

En mis scripts profundos, conté más de 25 paréntesis de cierre, CRAZY, y todo mientras recordaba dónde llamar a mi último callback para que Async continúe con la siguiente iteración …

¿Hay una solución a este problema? ¿O es sólo la naturaleza de la bestia?

Como dijo Mithon en su respuesta, las promesas pueden hacer este código mucho más claro y ayudar a reducir la duplicación. Digamos que crea dos funciones de envoltura que devuelven promesas, correspondientes a las dos operaciones de base de datos que está realizando, connectToDb y queryDb . Entonces tu código puede escribirse como algo así como:

 ms.async.eachSeries(arrWords, function (key, asyncCallback) { var stepState = {}; connectToDb('galaxy').then(function(connection) { // Store the connection objects in stepState stepState.pgClient = connection.pgClient; stepState.pgCB = connection.pgCB; // Send our first query across the connection var statement = "SELECT * FROM localization_strings WHERE local_id = 10 AND string_key = '" + key[0] + "'"; return queryDb(stepState.pgClient, statement); }).then(function (result) { // If the result is empty, we need to send another 2-query sequence if (result.rows.length == 0) { var statement = "SELECT nextval('last_resource_bundle_string_id')"; return queryDb(stepState.pgClient, statement).then(function(result) { var insertIdOffset = parseInt(result.rows[0].nextval); var statement = "INSERT INTO localization_strings (resource_bundle_string_id, string_key, string_revision, string_text,modified_date,local_id, bundle_id) VALUES "; statement += " (" + insertIdOffset + ",'" + key[0] + "'," + 0 + ",'" + englishDictionary[key[0]] + "'," + 0 + ",10,20)"; ms.log(statement); return queryDb(stepState.pgClient, statement); }); } }).then(function (result) { // Continue to the next step stepState.pgCB(); asyncCallback(); }).fail(function (error) { // Handle a database error from any operation in this step... }); }); 

Todavía es complejo, pero la complejidad es más manejable. Agregar una nueva operación de base de datos a cada “paso” ya no requiere un nuevo nivel de sangría. También tenga en cuenta que todo el manejo de errores se realiza en un solo lugar, en lugar de tener que agregar una línea if (pgconn.handleError(...)) cada vez que realice una operación de base de datos.

Actualización : tal como se solicitó, así es como podría definir las dos funciones de envoltura. Asumiré que estás usando kriskowal / q como tu biblioteca de promesas:

 function connectToDb(dbName) { var deferred = Q.defer(); pg.connect(pgconn.dbserver(dbName), function (err, pgClient, pgCB) { if (err) { deferred.reject(err) } else { deferred.resolve({pgClient: pgClient, pgCB: pgCB}) } }); return deferred.promise; } 

Puede usar este patrón para crear una envoltura alrededor de cualquier función que tome una callback de un solo uso.

El queryDb es aún más sencillo porque su callback le proporciona un solo valor de error o un solo valor de resultado, lo que significa que puede usar el método de utilidad makeNodeResolver incorporado de q para resolver o rechazar el aplazamiento:

 function queryDb(pgClient, statement) { var deferred = Q.defer(); pgClient.query(statement, deferred.makeNodeResolver()); return deferred.promise; } 

Para obtener más información sobre promesas, echa un vistazo a mi libro: Async JavaScript , publicado por PragProg.

El problema para este tipo de cosas son las promesas. Si no has oído hablar de ellos, te sugiero que leas la q de kriskowal .

Ahora, no sé si el db.query devuelve una promesa o no. Si no es así, puede encontrar un contenedor de db que lo haga o una biblioteca de db diferente. Si esa no es una opción, puede “promisificar” la biblioteca db que está usando. Vea Cómo usar promesas con Nodo , y especialmente la sección “Ajustar una función que toma una callback de estilo Nodo”.

¡La mejor de las suertes! 🙂

La forma más sencilla de combatir la pirámide asincrónica del infierno es segregar sus devoluciones de llamadas asíncronas en funciones más pequeñas que puede colocar fuera de su bucle principal. Lo más probable es que al menos pueda dividir algunas de sus devoluciones de llamada en funciones más fáciles de mantener que puedan usarse en cualquier otro lugar de su base de código, pero la pregunta que está haciendo es un poco vaga y puede resolverse de muchas maneras.

Además, debe considerar lo que Stuart mencionó en su respuesta y tratar de combinar algunas de sus consultas. Me preocupa más que tengas más de 20 llamadas anidadas, lo que indicaría algo seriamente erróneo en tu estructura de callback, así que antes de nada vería tu código.

Considere volver a escribir su código para tener menos de ida y vuelta con la base de datos. La regla de oro que utilizo para estimar el rendimiento de una aplicación bajo una carga pesada es que cada llamada asíncrona agregará dos segundos a la respuesta (uno para la solicitud y otro para la respuesta).

Por ejemplo, ¿hay alguna manera de descargar esta lógica en la base de datos? O una forma de "SELECT nextval('last_resource_bundle_string_id')" al mismo tiempo que usted "SELECT * FROM localization_strings WHERE local_id = 10 AND string_key = '" + key[0] + "'" (quizás un procedimiento almacenado)?

Rompo cada nivel de la pirámide de la fatalidad en una función y los encadeno uno a otro. Creo que es mucho más fácil de seguir. En el ejemplo anterior lo haría de la siguiente manera.

 ms.async.eachSeries(arrWords, function (key, asyncCallback) { var pgCB; var pgClient; var connect = function () { pg.connect(pgconn.dbserver('galaxy'), function (err, _pgClient, _pgCB) { pgClient = _pgClient; pgCB = _pgCB; findKey(); }); }; var findKey = function () { statement = "SELECT * FROM localization_strings WHERE local_id = 10 AND string_key = '" + key[0] + "'"; pgClient.query(statement, function (err, result) { if (pgconn.handleError(err, pgCB, pgClient)) return; // if key doesn't exist go ahead and insert it if (result.rows.length == 0) { getId(); return; } pgCB(); asyncCallback(); }); }; var getId = function () { statement = "SELECT nextval('last_resource_bundle_string_id')"; pgClient.query(statement, function (err, result) { if (pgconn.handleError(err, pgCB, pgClient)) return; insertKey(); }); }; var insertKey = function () { var insertIdOffset = parseInt(result.rows[0].nextval); statement = "INSERT INTO localization_strings (resource_bundle_string_id, string_key, string_revision, string_text,modified_date,local_id, bundle_id) VALUES "; statement += " (" + insertIdOffset + ",'" + key[0] + "'," + 0 + ",'" + englishDictionary[key[0]] + "'," + 0 + ",10,20)"; ms.log(statement); pgClient.query(statement, function (err, result) { if (pgconn.handleError(err, pgCB, pgClient)) return; pgCB(); asyncCallback(); }); }; connect(); });