Introducción a la programación asíncrona con Node.js II



En el último post de la serie sobre programación asíncrona hablamos sobre Node.js y vimos como crear un sencillo servidor HTTP con tan solo cuatro archivos JavaScript.

En la introducción a la programación asíncrona con Node.js II de hoy vamos a profundizar un poco más en este estupendo framework de sockets asíncronos para JavaScript del lado del servidor. El objetivo de hoy es sentar las bases que nos permitan saltar a temáticas más complejas en próximas entregas de la serie.

Como en el anterior artículo, vamos a utilizar una estructura de código sencilla. Utilizaremos un archivo de entrada principal de la aplicación llamado app.js y nuestro código estará distribuido en módulos. Vamos a obviar estructuras de directorios más complejas como el patrón MVC para mantener el código sencillo. En próximas entregas utilizaremos el patrón MVC de forma completa cuando hablemos sobre las plantillas.

Manejar peticiones POST

En el post anterior aprendimos a manejar peticiones GET provenientes del cliente web y a crear manejadores que las despacharan a los métodos correspondientes para producir la salida y devolverla al navegador de forma no bloqueante. Hoy vamos a ver como se maneja una petición POST sencilla.

Vamos a añadir un sencillo (y estúpido) formulario que el usuario rellenará y entonces la página será actualizada con la información proporcionada por el usuario. Vamos a mostrar este formulario en la página principal que es asimismo el manejador /a_donde_vas.

NOTA: Si no sabes de que estoy hablando revisa el post anterior.

function a_donde_vas(response) {
    console.log("Handler for request 'a_donde_vas' dispatched.");
    var html = '<html>'+
        '    <head>'+
        '        <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />'+
        '    </head>'+
        '    <body>'+
        '        <form action="/enviar" method="post">'+
        '            <p><label for="nombre">Escribe tu nombre: </label></p>'+
        '            <input type="text" name="nombre" id="nombre" />'+
        '            <p><label for="bio">Escribe una pequeña Bio sobre ti: </label></p>'+
        '            <textarea name="bio" id="bio" rows="30" cols="60"></textarea>'+
        '            <p><input type="submit" value="Enviar" /></p>'+
        '        </form>'+
        '    </body>'+
        '</html>';
    response.writeHead(200, {"Content-Type": "text/html"});
    response.write(html);
    response.end();
}
function disimula_disimula(response) {
    console.log("Handler for request 'disimula_disimula' dispatched.");
    response.writeHead(200, {"Content-Type": "text/html"});
    response.write("Y la mula dijo si");
    response.end();
}
exports.a_donde_vas = a_donde_vas;
exports.disimula_disimula = disimula_disimula;

Salida HTML del código anterior

Si hacemos click en el botón Enviar recibiremos como respuesta un error 404 enviado desde la función route del archivo router.js puesto que el path /enviar al que el formulario envía la petición no existe aún.

Para que podamos manejar las peticiones POST de forma no bloqueante, es decir, asíncrona, Node.js nos hace llegar la información proveniente del cliente en pequeñas porciones usando callbacks que son invocados ante eventos específicos. Cuando una porción de información llega, se dispara el evento data, cuando toda la información ha sido cargada, se dispara el evento end, de esta manera podemos manejar la llegada de datos desde el formulario de forma asíncrona.

Lo primero que necesitamos, es decirle a Node.js que funciones debe de invocar cuando los eventos se disparen. Para hacerlo, debemos añadir listeners al objeto request en el callback que es invocado cuando el servidor recibe una petición. En nuestro caso, la función anónima definida en server.js.

var http = require("http");
var url = require("url");
function serverStart(route, handler) {
    http.createServer(function(request, response) {
        // Variable que alberga los datos del formulario
        var postData = null;
        var pathname = url.parse(request.url).pathname;
        console.log("Received petition for " + pathname);
        // Esperamos recibir los datos en utf-8
        request.setEncoding("utf8");
        // Añadimos callbacks para eventos de envios de formularios
        request.addListener("data", function(chunk) {
            postData += chunk;
            console.log("\n************************************************************\n"+
                "Received POST data chunk '\n"+chunk+"'\n"+
                "************************************************************\n");
        });
        request.addListener("end", function() {
            route(handler, pathname, response, postData);
        });
    }).listen(4444);
    console.log("Server started...");
}
exports.start = serverStart;
Hay varios cambios en este archivo. Para empezar, hemos definido la variable postData donde alojaremos los datos provenientes del formulario. Después hemos definido que esperamos recibir esos datos codificados como utf8. Hemos añadido un capturador de eventos o event listener que dispara una función anónima cuando se lanza el evento “data“ .

Dicha función anónima introduce en la variable postData los nuevos datos conforme van llegando, no tiene misterio alguno. También hemos añadido un event listener para el evento “end“ que dispara una función anónima que pasa todos los datos al enrutador, incluida la nueva variable postData.

Hemos añadido un log de cada porción de información que nos llega a través del POST a modo de depurado. Si probamos el formulario podremos comprobar como con cantidades pequeñas de texto el evento data solo es disparado una vez pero si usamos fuentes de datos más largas es disparado en múltiples ocasiones.

Para probarlo añade al textarea el Lorem Ipsum que produce esta url y comprobarás como el evento es disparado en múltiples ocasiones.

Procesando los datos enviados

De momento no tenemos un manejador para el path /enviar vamos a crearlo y vamos a hacer que utilice los datos enviados en el formulario para alguna tarea.

var querystring = require("querystring");
function a_donde_vas(response) {
    console.log("Handler for request 'a_donde_vas' dispatched.");
    var html = '<html>'+
        '    <head>'+
        '        <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />'+
        '    </head>'+
        '    <body>'+
        '        <form action="/enviar" method="post">'+
        '            <p><label for="nombre">Escribe tu nombre: </label></p>'+
        '            <input type="text" name="nombre" id="nombre" />'+
        '            <p><label for="bio">Escribe una pequeña Bio sobre ti: </label></p>'+
        '            <textarea name="bio" id="bio" rows="30" cols="60"></textarea>'+
        '            <p><input type="submit" value="Enviar" /></p>'+
        '        </form>'+
        '    </body>'+
        '</html>';
    response.writeHead(200, {"Content-Type": "text/html"});
    response.write(html);
    response.end();
}
function disimula_disimula(response) {
    console.log("Handler for request 'disimula_disimula' dispatched.");
    response.writeHead(200, {"Content-Type": "text/html"});
    response.write("Y la mula dijo si");
    response.end();
}
function enviar(response, postData) {
    postData = querystring.parse(postData);
    console.log(postData);
    console.log("Handler for request 'enviar' dispatched.");
    response.writeHead(200, {"Content-Type": "text/html"});
    response.write("<p>Hola " + postData.nombre + "</p>");
    response.write("<p>Has dicho sobre ti: <br />" + postData.bio + "</p>");
    response.end();
}
exports.a_donde_vas = a_donde_vas;
exports.disimula_disimula = disimula_disimula;
exports.enviar = enviar;
Hemos añadido el módulo querystring para procesar los campos del formulario que el usuario ha rellenado. Esto realmente debería hacerse en el archivo server.js a la hora de procesar la llegada de porciones de información desde el formulario pero lo vamos a ver así para mantener el ejemplo lo más claro posible.

Hemos añadido la función enviar que recibe dos parámetros, response que usamos para responder de vuelta al navegador y postData que contiene los datos enviados por el usuario. Utilizamos el método parse del módulo querystring para separar la cadena postData en elementos y lo utilizamos para mostrar la salida.

Ademas de estos cambios, debemos de hacer cambios en app.js y añadir la ruta a router.js:

var server = require("./server");
var router = require("./router");
var requestHandlers = require("./requestHandlers");
var handler = {}
handler["/"] = requestHandlers.a_donde_vas;
handler["/a_donde_vas"] = requestHandlers.a_donde_vas;
handler["/disimula_disimula"] = requestHandlers.disimula_disimula;
handler["/enviar"] = requestHandlers.enviar;
server.start(router.route, handler);
El cambio a app.js es minúsculo, tan solo hemos añadido una linea para mapear el path /enviar con la función requestHandlers.enviar.
function route(handler, pathname, response, postData) {
    console.log("Routing a new petition for" + pathname);
    if (typeof handler[pathname] === 'function') {
        handler[pathname](response, postData);
    } else {
        console.log("No request handler for " + pathname + " skipping");
        response.writeHead(404, {"Content-Type": "text/html"});
        response.write("404 Not Found");
        response.end();
    }
}
exports.route = route;
Los cambios en router.js también son menores, solamente hemos añadido soporte al enrutador para que pase la variable postData a los manejadores desde el servidor.

Respuesta al formulario con los datos introducidos por el usuario

Conclusión

Hoy hemos aprendido a manejar las peticiones POST provenientes del navegador, aquellos programadores que estén acostumbrados a otros frameworks de programación asíncrona como Twisted en mi caso, habrán comprobado que en Node.js hay que preocuparse de bastantes cosas que en el módulo de HTTP de Twisted (por ejemplo) quedan ocultas en la implementación del módulo.

En Node.js debemos de implementar desde más abajo el servicio web que con otras librerías, pero existen ya módulos diseñados específicamente para facilitar la tarea de crear servicios web. En próximas entregas de la serie conoceremos un poco sobre esos módulos y aprenderemos más cosas sobre esta librería.



En Genbeta Dev:

Portada de Genbeta