arrow&v

7. Implementando un servidor web con node.js


En la sección 3 se analizó como el programa servidor_precio_bitcoin.js, usando tan solo el módulo “http”, creó un servidor web localmente en nuestra máquina y se pudo obtener una página Web que el generó luego de obtener precios de Bitcoin de otro sitio web usando métodos del módulo ‘https’.


En esta sección vamos a explicar con más detalles de qué forma se logró hacer eso. También se marcará la diferencia entre el mecanismo usado en el ejemplo para "devolver” el HTML de la página web a mostrar en un navegador y otro mecanismo más útil para el manejo de páginas basado en machotes (templates).


El primer comando que vemos en ese programa luego de especificar el uso de los módulos ‘http’ y ‘https’ es la creación dinámica de un web server con el método http.createServer():


var server = http.createServer(function (req, res) { ….


La variable "server” fue creada y se le asignó el objeto que devuelve http.createServer() el cual representa el servidor Web y sobre el cual se llamará el método que echa a andar el servidor más abajo:


server.listen(5000);


Luego de ejecutar esa línea el servidor web está funcionando y se puede acceder usando la dirección http://localhost:5000 en cualquier navegador en la misma computadora donde ejecuto el programa. También se puede hacer accesible desde cualquier otro computador o móvil con navegador en la red local si se configura el sistema operativo a donde corremos el programa de node.js para aceptar conexiones en red local en el puerto 5000 (configuración del firewall) usando la dirección IP local de la máquina en vez de ‘localhost’.


Para lograr una página accesible desde internet se requiere de una configuración en el router que lo permita, pero como no se está implementando https en el ejemplo de web server, no sería una conexión segura. Por lo tanto, no recomendamos ese ejercicio. Si realmente quieres hacer una prueba de acceso externo sin implementar un web server seguro y correctamente configurado en el firewall; podrías optar por usar un programa con "ngrok” (https://ngrok.com/) Este expone servidores locales de web con o sin https, implementado a través de túneles seguros atravesando tu firewall y con direcciones públicas asignadas en su momento para hacer pruebas. Es un servicio sin costo en la opción básica que es apta para el tipo de ejemplos que mostramos acá. Pero nuevamente, es una opción no recomendada por temas de seguridad.


Regresando al método createServer() donde se creó el objeto que representa el servidor web, nótese que recibe como parámetro una función la cual es declarada ahí mismo y a su vez requiere 2 parámetros los cuales representan un objeto distinto cada uno:


Req: Este es el objeto de requisición (request) tipo http.IncomingMessage el cual contiene todo lo que el servidor web recibe cuando un navegador y otro proceso le accede.


Res: Este es el objeto de respuesta (response) tipo http.ServerResponse en el cual se va a insertar la información que se devuelve al navegador que hizo la solicitud originalmente.


En el programa servidor_precio_bitcoin.js realmente nunca se utilizó el objeto req porque no interesaba realmente lo que estaba enviando el navegador; ni siquiera se revisa si estaba solicitando información por medio de un HTTP GET. Pero eso lo podemos fácilmente agregar como la primera línea del bloque de líneas que forman parte de la función que se pasa como parámetro a createServer():


var server = http.createServer(function (req, res) {

console.log('req.method: ',req.method);

console.log('req.url: ',req.url);

…...


Luego de editar el código, guardélo en un nuevo archivo llamado “servidor_precio_btc_machote.js” para no perder la versión original y pueda comparar la forma en que agrega algunos nuevos elementos.


Ahora, al correr el nuevo programa, inicialmente verá exactamente lo mismo que la versión original:


$node servidor_precio_btc_machote.js

Servidor web de Node.js corriendo en puerto 5000. Acceda con navegador: http://localhost:5000


Pero si abre el sitio http://localhost:5000 con un navegador, ahora vemos lo siguiente en la consola desde donde lanzamos el programa:


req.method: GET

req.url: /

req.method: GET

req.url: /favicon.ico


Y si utiliza http://localhost:5000/precios?moneda=USD el resultado en la consola será el siguiente:


req.method: GET

req.url: /precios?moneda=USD

req.method: GET

req.url: /favicon.ico


Nótese que la página que carga el navegador es exactamente la misma porque hasta ahora no se ha cambiado el comportamiento del código en cuanto al valor a devolver usando el objeto res. Pero ya se puede ver que dentro del código existe acceso a la URL completa que se solicitó y el método utilizado, lo cual podría usarse para tomar decisiones en cuanto a cuál información devolver dependiendo de la “ruta” solicitada (lo que va después del identificador del servidor y puerto en el URL, en este caso “localhost:5000” ) y los parámetros enviados ( "moneda=USD” ) .


Ahora, ¿Por qué el código recibió 2 objectos “req” si en el navegador solo se cargó la página una vez, y porqué lo que se pide es /favicon.ico? Este es un comportamiento normal de los navegadores modernos, siempre van a solicitar el icono a mostrar en la barra de dirección del navegador, pero no habrá ningún error si el servidor web no lo tiene disponible. Si se quiere comprobar, crea una imagen y guárdala en el archivo favicon.ico en el mismo directorio donde está guardado el programa “servidor_precio_btc_machote.js” y vuelve a correr el programa (recuerda que para terminarlo solo oprimes CTRL-C dos veces en la línea de comando donde diste el comando node)


Más detalles acerca del uso de favicon.ico: https://es.wikipedia.org/wiki/Favicon


Ahora que se sabe cómo extraer los parámetros del URL que se nos envió al servidor web que implementamos en node.js, optimicemos un poco el código para devolver solo el precio en la moneda solicitada. Para ello, vamos a usar otro módulo llamado ‘url’ que nos facilita un poco el manejo de los parámetros. Sin este módulo, sería necesario manipular lo que se obtiene de req.url buscando la ubicación del carácter de símbolo de pregunta (?) y luego buscando el símbolo de = para asignar a una variable con el nombre del parámetro lo que está a la izquierda del = (pero antes de topar con el ?) y el valor del parámetro con lo que está a la derecha del = pero antes de un símbolo & si hay más parámetros... ¿Suena complicado verdad?


Con el módulo ‘url’ es una sola línea de código. Primero hay que solicitar el módulo al inicio del programa agregando la línea correspondiente así:


const https = require('https');

const http = require('http');

const url = require('url');


Y luego llamamos al método parse() pasando el url lo cual devuelve un objeto con los parámetros convenientemente convertidos en llaves, las cuales entonces se pueden acceder como un atributo del objeto así:


url.parse(req.url,true).query.moneda;


Entonces si se le asignamos a la variable “moneda”, se puede imprimir con console.log(), modificando el segmento del código que usamos para imprimir los valores de ‘req’ de esta forma:


console.log('req.method: ',req.method);

console.log('req.url: ',req.url);

var moneda=url.parse(req.url,true).query.moneda;

console.log('parametro moneda: ',moneda);


Al correr el programa de nuevo usando de nuevo el URL http://localhost:5000/precios?moneda=USD en el navegador ahora veremos esto:


$ node servidor_precio_btc_machote.js

Servidor web de Node.js corriendo en puerto 5000. Acceda con navegador: http://localhost:5000

req.method: GET

req.url: /precios?moneda=USD

parametro moneda: USD

req.method: GET

req.url: /favicon.ico

parametro moneda: undefined


Bueno, se puede observar aún la solicitud de favicon.ico y en este caso el navegador no pasa parámetros. Entonces el parámetro moneda está indefinido, pero si se observa 3 líneas antes, si se le asignó el valor ‘USD’ a la variable ‘moneda’ con lo cual ya se tiene lo necesario para usarlo y extraer el precio de bitcoin en esa moneda.


Ya con la variable ‘moneda’ lista con el valor ‘USD’ o el valor que se haya pasado en el URL que especificamos en el navegador, nuestro código puede tratar de extraer solo el precio en esa moneda de los datos que ya estamos sacando del servicio web de precios de bitcoin.


A continuación, se asigna a la variable texto_json una cadena de caracteres (strings) que se obtiene al convertir todo el objeto ‘data_json’ con el precio de Bitcoin en todas las monedas que nos devuelve el servicio de esta forma:


texto_json=JSON.stringify(datos_json, null, 4);


Recuerde que 'datos_json’ en este momento tiene una llave para cada tipo de moneda:


{ "AUD": { "15m": 57205.41, "last": 57205.41, "buy": 57205.41, "sell": 57205.41, "symbol": "AUD" },

..

..

..

"USD": { "15m": 41616.93, "last": 41616.93, "buy": 41616.93, "sell": 41616.93, "symbol": "USD" } }


Entonces se puede extraer solo el precio en ‘USD’ antes de convertir a cadena de caracteres de esta forma:


texto_json=JSON.stringify(datos_json.USD, null, 4);


Ahora, al tener el valor ‘USD’ en la variable ‘moneda’, no es posible simplemente poner la variable como un atributo del objeto ‘datos_json’ pues sería una sintaxis incorrecta, pero hay otra forma de obtener el contenido de la llave USD y es con la notación de paréntesis cuadrados así:


texto_json=JSON.stringify(datos_json.[moneda], null, 4);


De esta forma, la página web únicamente muestra lo que se solicita con el parámetro moneda.


Pero aún se escapa algo importante, el encabezado indica que estamos viendo el precio de BitCoin en todas las monedas. Lo podemos corregir fácilmente modificando la línea de código que “escribe” en el objeto de respuesta 'res’ para que incluya la variable ‘moneda’ en el texto del encabezado asi:


res.write('<html><body><h1>Precio de Bitcoin en '+moneda+'</h1><pre>'+texto_json+'</pre></body></html>');


Este es el resultado cuando se ejecuta el código nuevamente:




Uso de templates


El “escribir” directamente en el código una cadena de caracteres con el HTML de la página que se mostrará usando el objeto ‘res’ funciona, pero se complicaría mucho el código si el HTML a usar para la página va más allá de unas cuantas líneas. Así entonces lo más práctico es usar un template (machote, modelo de referencia, patrón de archivo) guardado en un archivo con la extensión .html y hacer que el código lo lea, reemplace dentro de él solo las partes que queremos cambiar y luego ya se devuelve el archivo modificado como parte de res.write()


Para modificar el código y lograr esto, primero recopile el HTML que ya tenía a un archivo .html, incluso se cuenta con más espacio para armar una página más completa.


Copia este código HTML en un archivo llamado “precio_bitcoin.html” en la misma carpeta donde se está editando “servidor_precio_btc_machote.js”


<!doctype html>

<html lang="es">

<head>

<meta charset="utf-8">

<meta name="viewport" content="width=device-width, initial-scale=1">

<title>Precio Bitcoin</title>

<meta name="description" content="Simple pagina para desplegar el precio de Bitcoin">

</head>

<body>

<h2>Precio Bitcoin</h2>

<p style="font-size: 1.5em;">Moneda: <strong>{{moneda}}</strong></p>

<p style="font-size: 1.5em;"><br />

15m: {{15m}}<br />

Ultimo: {{ultimo}}<br />

Compra: {{compra}}<br />

Venta: {{venta}}

</p>

</body>

</html>


Como se puede apreciar arriba, se ha elegido la secuencia de caracteres “{{“ y “}}” como delimitadores de marcadores de posición para elementos variables, con lo cual este código HTML cumple la función de “template” (modelo de referencia). Se eligieron esas secuencias de caracteres porque es muy poco probable que vayan a ser utilizadas como texto válido para mostrar en la página.


Entonces es posible utilizar el texto entre lo delimitadores como nombres de variables a sustituir cuando se modifique el html, “rellenando” campos en el template con los datos correctos antes de enviar al navegador para ser desplegado.


Las variables a rellenar para este template serían entonces:


{{moneda}}

{{15m}}

{{ultimo}}

{{compra}}

{{venta}}


Las cuales corresponden a las llaves del objeto con los datos que obtenemos al leer el precio del bitcoin.


Usando los métodos de cadenas de caracteres que se analizaron anteriormente, es posible hacer una búsqueda específica de cada variable delimitada en el template y reemplazarla con el valor correspondiente proveniente del objeto “datos_json” así (Se puedes ir probando cada comando después del ">” en el REPL):


>let moneda= "USD"

undefined

>var resultado_html="Esta es la moneda a usar: {{moneda}}. Aquí va más texto";

undefined >resultado_html = resultado_html.replace("{{moneda}}", moneda);

'Esta es la moneda a usar: USD. Aquí va más texto'

>console.log(resultado_html);

Esta es la moneda a usar: USD. Aquí va más texto

undefined


Para incorporar esta funcionalidad al código que se ha desarrollado, es necesario hacer varios cambios.


Aquí está el código final de "servidor_precio_btc_machote.js”, para que pueda comparar los cambios que ha recibido. Copia el código y re-emplaza todo lo que tenías en "servidor_precio_btc_machote.js” para correrlo:


// Programa de node.js que implementa un servidor web para extraer de el sitio https://www.blockchain.com

// el precio mas reciente de el BitCoin en varias monedas y mostrarlas en una pagina web de respuesta.

// Ver instrucciones de uso del API para obtener los datos en https://www.blockchain.com/api/exchange_rates_api

const https = require('https');

const http = require('http');

const url = require('url');

const fs=require('fs');

var server = http.createServer(function (req, res) {

console.log('req.method: ',req.method);

console.log('req.url: ',req.url);

console.log('Pathname: ',url.parse(req.url,true).pathname)

if (url.parse(req.url,true).pathname=='/precios') {

var moneda=url.parse(req.url,true).query.moneda;

console.log('parametro moneda: ',moneda);

https.get('https://blockchain.info/ticker', (resp) => {

let data = '';

// Un pedazo de data ha sido recibido.

resp.on('data', (pedazo) => {

data += pedazo;

});

// Ya se recibio la respuesta completa. Procedemos a procesarlo

resp.on('end', () => {

// primero creamos el objeto datos_jason con todos los datos

// que obtuvimos de la lectura del servicio web de precios bitcoin

datos_json=JSON.parse(data);

// Ahora leamos el machote de HTML del archivo local

fs.readFile('./precio_bitcoin.html', 'utf8', function(err, data) {

// revisemos si hay error de lectura del archivo HTML

if (err) {

// Si hay error, respondamos con codigo error HTML 404

res.writeHead(404);

res.end("404 Not Found");

} else {

// Respuesta exitosa, no hubo errores!

// extraigamos los valores para llenar las variables

// que necesitamos para llenar el machote

var qmins=JSON.stringify(datos_json[moneda]['15m']);

var ultimo=JSON.stringify(datos_json[moneda]['last']);

var compra=JSON.stringify(datos_json[moneda]['buy']);

var venta=JSON.stringify(datos_json[moneda]['sell']);

// Hagamos una copia del HTML a la variable resultado_html para

// ir llenando el machote

var resultado_html=data;

// Ahora hagamos el reemplazo de cada marcador de posición con

// el correspondiente valor de la variable

resultado_html = resultado_html.replace("{{moneda}}", moneda);

resultado_html = resultado_html.replace("{{15m}}", qmins);

resultado_html = resultado_html.replace("{{ultimo}}", ultimo);

resultado_html = resultado_html.replace("{{compra}}", compra);

resultado_html = resultado_html.replace("{{venta}}", venta);

// seteamos el encabezado de la respuesta

res.writeHead(200, { 'Content-Type': 'text/html' });

// y ahora seteamos el contenido a retornar en HTML para mostrar en el navegador

res.write(resultado_html);

res.end();

}

});

});

});

};

});

server.listen(5000); // por aqui estamos escuchando cualquier solicitud al servidor web

console.log('Servidor web de Node.js corriendo en puerto 5000. Acceda con navegador: http://localhost:5000/precios?moneda=XXX');

Primero, para poder leer todo el código HTML del archivo “precio_bitcoin.html” es necesario el módulo 'fs’, por esa razón se agrega esta línea de código al principio:


const fs=require('fs');


Luego, para asegurar que no tratará de procesar la lectura de precios de bitcoin cuando el navegador está pidiendo el favicon. Se usa de nuevo el módulo ‘url’ para revisar lo que se está pidiendo dentro de la función que se pasa como argumento a http.createServer(). Observa que ahora hay una toma de decisión inmediatamente después de imprimir a consola el método HTTP solicitado y el “pathname” que indica la “ruta” de lo que se pidió en el navegador. Entonces se va a verificar que esa ruta sea ‘/precios’ y así ignorar la solicitud de favicon o cualquier otra solicitud hecha a nuestro servidor web:


if (url.parse(req.url,true).pathname=='/precios') { … }


(todo el código de leer el precio de bitcoin, leer el archivo de HTML y devolver la página ahora va a quedar dentro de los delimitadores { y } )


El siguiente cambio es después de terminar de leer los datos del servicio web de precios de bitcoin ('https://blockchain.info/ticker') abrir el archivo ‘precio_bitcoin.html’ para leer todo su contenido:


fs.readFile('./precio_bitcoin.html', 'utf8', function(err, data) {...


Nótese que el tercer argumento del método readFile() del módulo 'fs’ es una función que esta declarando ahí mismo dentro del argumento, con lo cual fue necesario mover toda la funcionalidad de devolver el resultado a el navegador haciendo la solicitud original para que esté “adentro” de esta nueva función.Este estilo de código se llama anidado o "nesting” y es muy común en JavaScript y node.js pero puede ser algo confuso para los que están acostumbrados a otros lenguajes de programación donde no se suele declarar funciones como argumentos.


Esta fuera del alcance de este curso explicar cómo escribir código que evite sobreusar el anidar, pero si es importante entender que si se coloca la instrucción que devuelve los resultados al navegador con res.write() después de cerrar el bloque de código donde se leen los datos con el método fs.readFile(), entonces va a tratar de ejecutarse ese res.write() antes de leer los datos de “precio_bitcoin.html’ ni realizado los cambios necesarios en el template de html.


Regresando a la función de lectura del archivo HTML, en el código adelante se observa que lo primero que hace la nueva función (parametro de readFile()) es revisar el resultado de abrir el archivo para leerlo. Si hay algún error, la variable ‘err’ estará definida con lo que vamos a devolver un error al navegador:


if (err) {

// Si hay error, respondamos con codigo error HTML 404

res.writeHead(404);

res.end("404 Not Found");

}


De lo contrario, se puede proceder a primero copiar en variables los datos que habíamos extraído del servicio web de precios de bitcoin usando todavía la moneda definida en el parámetro de la URL original:


var qmins=JSON.stringify(datos_json[moneda]['15m']);

var ultimo=JSON.stringify(datos_json[moneda]['last']);

var compra=JSON.stringify(datos_json[moneda]['buy']);

var venta=JSON.stringify(datos_json[moneda]['sell']);


Luego se copia todo el HTML extraído del archivo en la variable ‘data’ a la nueva variable ‘resultado_html’ para luego procesarlo:


var resultado_html=data;


De uno en uno se busca el marcador de posición de las variables de interés en el HTML y lo intercambiamos por el valor leído del web service así:


resultado_html = resultado_html.replace("{{moneda}}", moneda);

resultado_html = resultado_html.replace("{{15m}}", qmins);

resultado_html = resultado_html.replace("{{ultimo}}", ultimo);

resultado_html = resultado_html.replace("{{compra}}", compra);

resultado_html = resultado_html.replace("{{venta}}", venta);


Observe que en cada línea de código es necesario volver a asignar a ‘resultado_html’ el resultado del re-emplazo para seguir modificando el mismo HTML hasta intercambiar todas las variables. Este código solo cambia una instancia de cada variable que encuentre en el template, esto no es lo ideal, pero por simplicidad se aborda así en este ejemplo.


Finalmente, vamos se puede retornar ya la página completa que consiste en el template llenado usando el mismo método res.write() de antes, pero en vez de simplemente especificar el texto a regresar estamos regresando “resultado_html” que es a donde hemos ido acumulando los cambios:


res.write(resultado_html);


Si corremos el nuevo código todo debería funcionar como lo esperamos:


$ node servidor_precio_btc_machote.js

Servidor web de Node.js corriendo en puerto 5000. Acceda con navegador: http://localhost:5000/precios?moneda=XXX

Si vamos al navegador y usamos el URL http://localhost:5000/precios?moneda=USD deberíamos ver esto en la consola ahora:


req.method: GET

req.url: /precios?moneda=USD

Pathname: /precios

parametro moneda: USD

req.method: GET

req.url: /favicon.ico

Pathname: /favicon.ico


Y en el navegador esto:




¿Mucho más limpio que la versión anterior no?


Pero, ¿No existe un módulo que permita usar templates sin tener que hacer los reemplazos con operaciones de cadenas de caracteres como .replace() para cada marcador de valor como lo hicimos arriba? ¡Por supuesto! Hay varios módulos que nos pueden ayudar con eso. Un ejemplo es Mustache: https://github.com/janl/mustache.js


Precisamente los ejemplos arriba utilizan los mismos delimitadores para los marcadores que Mustache, pero vamos a dejar el uso de módulos de templates para el proyecto final en el próximo capítulo a donde también utilizaremos un "framework” completo