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

Node.js es una librería y entorno de ejecución de E/S dirigida por eventos y por lo tanto asíncrona que se ejecuta sobre el intérprete de JavaScript creado por Google V8. Lo cierto es que está muy de moda aunque no es algo nuevo puesto que existen librerías como Twisted que hacen exactamente lo mismo pero si es cierto que es la primera basada en JavaScript y parece que tiene un gran rendimiento o eso dicen los benchmarks.

No es la primera vez ni mucho menos que hablamos sobre programación dirigida por eventos y sobre frameworks de programación asíncrona y espero que tampoco sea la última. Lo que si es cierto es que es la primera vez que hablamos de este tema utilizando para ello un lenguaje que siempre ha estado enfocado al desarrollo del frontend y no del backend como ocurre en Node.js

En esta introducción a la programación asíncrona con Node.js, vamos a crear un servidor HTTPS básico que responda a algunas de nuestras peticiones de manera predefinida.

Estructuración del código

Node.js te permite implementar al patrón de diseño MVC a través de varios archivos o módulos donde alojar el código de nuestras aplicaciones y de esta forma no tener que escribir todo el código en un solo archivo JavaScript. Para nuestros ejemplos vamos a escribir un archivo principal llamado app.js que usaremos para iniciar nuestra aplicación y la lógica la programaremos en los módulos.

El servicio Web

Node.js integra un completo servidor (y cliente) web en el módulo http. Para hacer uso de un módulo en Node.js hemos de utilizar el comando require con un único parámetro, el nombre del módulo que queremos cargar:

/* Iniciamos el módulo http /
var http = require("http");
/ Creamos el objeto del servidor /
http.createServer(function(request, response) {
    / Preparamos las cabeceras de respuesta */
    response.writeHead(200, {"Content-Type": "text/html"});
    response.write("¿Que pasa tronco?");
    response.end();
}).listen(4444);
Ahora podemos ejecutarlo escribiendo en la consola node server.js. Si apuntamos nuestro navegador a http://localhost:4444 nos encontraremos con nuestro mensaje.

En el ejemplo anterior hemos creado un servidor con el mítico ejemplo Hello World! adaptado al Genbetiano. Vamos a ver como funciona.

El método createServer del objeto http devuelve un objeto de tipo servidor al que le concatenamos directamente la llamada a su método listen y lo ponemos a escuchar en el puerto 4444 porque somos unos cuatreros.

Al método se le pasa un único parámetro que es una función anónima (que también podríamos haber definido previamente y pasarla como argumento, el resultado sería el mismo). Cada vez que el servidor recibe una nueva petición, Node.js llama a la función que le hemos pasado como parámetro, a este tipo de funciones se les llama callbacks.

A dicho callback se le pasan dos parámetros, request y response. Sus nombres son autodescriptivos, el primero de ellos es un objeto que contiene todos los datos relacionados con la petición y el segundo objeto es utilizado para responder al navegador. Cuando hemos acabado de responder al navegador tenemos que ejecutar response.end().

Ahora que hemos visto como crear un sencillo servicio web, vamos a refactorizar el código de server.js para convertirlo en un módulo de Node.js. No voy a hablar ni a poner ejemplos sobre TDD pero te recomiendo que le eches un vistazo a los módulos de terceros para TDD/BDD compatibles con Node.js

>var http = require("http");
function serverStart() {
    http.createServer(function(request, response) {
        response.writeHead(200, {"Content-Type": "text/html"});
        response.write("¿Que pasa tronco?");
        response.end();
    }).listen(4444);
    console.log("Server started...");
}
exports.start = serverStart;
Para convertir nuestro código a un módulo solo tenemos que encerrarlo dentro de una función y exportarla. Ahora vamos a crear nuestro archivo principal App.js:
var server = require("./server");
server.start();
Si ejecutamos node App,js, veremos el mensaje Server started... en la consola y si recargamos el navegador veremos que nuestra aplicación web cuatrera sigue funcionando.

Routing

Ahora debemos atender a diferentes peticiones, para hacerlo, haremos uso del routing o enrutado de peticiones. Los que tengáis experiencia con otros frameworks como Twisted, Django o Rails (por ejemplo) os resultará muy familiar. Toda la información requerida está contenida en el objeto request que se le pasa a la función anónima al crear nuestro servidor.

Para poder procesar esa información con el objetivo de crear rutas para las peticiones HTTP hemos de hacer uso del módulo url. Vamos a crear un nuevo módulo llamado router.js donde escribiremos el código necesario para empezar a enrutar y a modificar el código de nuestro servidor.

var http = require("http");
var url = require("url");
function serverStart(route) {
    http.createServer(function(request, response) {
        var pathname = url.parse(request.url).pathname;
        console.log("Received petition for " + pathname);
        route(pathname);
        response.writeHead(200, {"Content-Type": "text/html"});
        response.write("¿Que pasa tronco?");
        response.end();
    }).listen(4444);
    console.log("Server started...");
}
exports.start = serverStart;
Los cambios introducidos han sido mínimos como puedes comprobar, ahora nuestro servidor web es capaz de distinguir entre peticiones basándose en su URL. Esto nos permite mapear peticiones hacia nuestros manejadores.
function route(pathname) {
    console.log("Routing a new petition for " + pathname);
}
exports.route = route;
Este código aún no rutea nada por que no hemos creado ningún manejador aún. De momento sirve para nuestro propósito didáctico. Vamos a modificar la entrada principal de la aplicación, el archivo App.js:
var server = require("./server");
var router = require("./router");
server.start(router.route);
Si ejecutamos nuestro servidor y recargamos la página veremos el siguiente log en la consola:
$ node App.js 
Server started...
Received petition for /
Routing a new petition for /
Received petition for /favicon.ico
Routing a new petition for /favicon.ico
La petición a favicon.ico es normal al usar navegadores web modernos, ignórala de momento.

Manejadores

Como ya hemos hablado en anteriores oportunidades, cada petición debe tener asociado un manejador de eventos que se haga cargo de procesar la petición. Así es como podemos modelar la lógica de negocio de nuestra aplicación. Vamos a crear un nuevo módulo al que llamaremos requestHandlers.js y que va a contener el código a ejecutar por cada evento. Esta forma de estructurar el código de la aplicación no es el más adecuado, pero lo hago así para mantenerlo simple para nuestros ejemplos. Asegúrate de usar siempre un patrón MVC para ordenar tu código.

function a_donde_vas(response) {
    console.log("Handler for request 'a_donde_vas' dispatched.");
    response.writeHead(200, {"Content-Type": "text/html"});
    response.write("Patatas traigo");
    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;
Aunque no estoy escribiendo ejemplos de TDD supongo que veis tan claramente como lo veo yo que a este último ejemplo le hace falta una refactorización en condiciones, pero vamos a vencer el impulso y vamos a dejarlo así para mantener todo sencillo. Se usa el objeto request para conseguir que nuestras respuestas sean asíncronas.

Ahora necesitamos refactorizar nuestro código en server.js y router.js para adaptarlo a nuestros manejadores.

var http = require("http");
var url = require("url");
function serverStart(route, handler) {
    http.createServer(function(request, response) {
        var pathname = url.parse(request.url).pathname;
        console.log("Received petition for " + pathname);
        route(handler, pathname, response);
        response.writeHead(200, {"Content-Type": "text/html"});
        response.write("¿Que pasa tronco?");
        response.end();
    }).listen(4444);
    console.log("Server started...");
}
exports.start = serverStart;
Los cambios son mínimos, hemos añadido dos parámetros a la llamada de la función route con la finalidad de añadir nuestro manejador y el objeto response. Sigamos con el archivo router.js:
function route(handler, pathname, response) {
    console.log("Routing a new petition for" + pathname);
    if (typeof handler[pathname] === 'function') {
        handler[pathname](response);
    } 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;
Aquí si hay cambios importantes, ahora la función obtiene el manejador a ejecutar y lo ejecuta si es una función pasándole como parámetros el objeto response y delegamos la respuesta directamente al manejador. Si no existe un manejador al que pasarle la petición, devuelve una página de error 404.

Tan solo nos queda hacer unas modificaciones en App.js para despachar las peticiones. También añadimos el parámetro handler a la función start

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;
server.start(router.route, handler);

Conclusión

Hemos hecho una breve introducción a la programación asíncrona con Node.js para ir abriendo boca, en la segunda parte de este artículo hablare sobre como manejar peticiones POST y como hacer algo realmente útil.


Más en Genbeta Dev | Introducción a la programación dirigida por eventos, Introducción a la programación asíncrona con Twisted

Portada de Genbeta