arrow&v

9. Nueva función para leer y procesar templates: leer_machote()


Analice ahora los cambios principales en el proyecto final comparado con los ejercicios que veníamos desarrollando anteriormente:


Como se observa en el directorio con los archivos que acaban de crear, tenemos ahora 3 archivos que contienen la palabra “machote” en su nombre y si los revisamos veremos que hay varios marcadores de posición delimitados con {{ y }} . Entonces, en vez de escribir un bloque de código para el manejo de cada una hemos creado la función leer_machote() definida así:


function leer_machote( archivo, cambios) {….}


Los parámetros que espera recibir son: el nombre del archivo conteniendo el template a llenar, un objeto de JavaScript indicando los marcadores a buscar y el valor con el cual reemplazarlos. La función devuelve entonces todo el texto contenido en el archivo especificado, pero ya con los marcadores reemplazados con el valor respectivo.


Observe entonces como se procesa la primera página a mostrar usando el template en “btc_analisis_compra_venta_machote.html”:


var cambios={

'datos_semana' : datos_semana,

'datos_mes' : datos_mes

}

resultado_html=await leer_machote('./btc_analisis_compra_venta_machote.html',cambios);


Note como se ha agregado un nuevo operador justo antes de recibir la respuesta de la función: “await” Este operador y las funciones declaradas como “async” son amplias y no se detallarán en este curso, pero es importante entender que permiten “esperar” el resultado de la función ya que, dentro de “leer_machote()”, se hace lectura de un archivo, una de las funciones regresa inmediatamente y es necesario pasarle una función que va a procesar los resultados de la lectura. Puedes encontrar mas detalles de “await”, “async” y promesas de JavaScript acá: https://developer.mozilla.org/es/docs/Web/JavaScript/Reference/Operators/await


Además de pasar toda la funcionalidad de manejo de templates a una sola función, se ha cambiado la forma de buscar los marcadores para no solo cambiar el primero que encuentra, sino cambiar todos los que estén contenidos en el template.


El cambio se refleja en la siguiente línea de "servidor_mercado_btc.js”:


texto_modificado.replace("{{"+key+"}}", value);


Al uso de una expresión regular así:


texto_modificado.replace( new RegExp("{{"+key+"}}", 'g') , value);


Nueva función para leer los datos estadísticos de los precios de bitcoin: datos_btc_hileras()


En los ejercicios anteriores se usó el servicio web en 'https://blockchain.info/ticker' para extraer el precio de bitcoin en diferentes monedas y con los precios de compra/venta al momento en que se accedía al servicio. Pero para este proyecto lo que interesa es el precio de Bitcoin solo en moneda USD (por simplicidad) de los últimos 7 días (semana anterior) y los últimos 30 días (mes anterior) para mostrarle al usuario de forma gráfica el comportamiento del precio.


Para lograrlo, se implementa la función datos_btc_hileras() dentro del programa 'servidor_mercado_btc.js’ y se le pasa como parámetro el rango de tiempo o "timeframe” de los datos que se van a enviar al servicio Web en https://api.blockchain.info/charts/market-price

Para que devuelva en formato JSON los datos que necesitamos.


Observe ese proceso de lectura entre las siguientes líneas:


var texto_datos_precios="["

https.get('https://api.blockchain.info/charts/market-price?timespan='+timeframe+'&format=json', (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', async () => {

// 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);

...


En las líneas anteriores podemos también apreciar que esta funcionalidad está dentro de una función declarada en línea, pero que es el único parámetro de la creación de un objeto tipo “Promise”. Lo cual también es necesario para que cuando se llama a datos_btc_hileras() desde el bloque principal de código que define el servidor web en http.createServer(), se espere a terminar de leer los datos antes de devolverlos para su despliegue.


Al igual que para el operador “await” no se va a explicar en detalle el uso de promesas en este curso, pero aquí puede ir aprendiendo más sobre del tema: https://developer.mozilla.org/es/docs/Web/JavaScript/Reference/Global_Objects/Promise


Luego de leer todos los datos para una semana o un mes y convertirlos a formato jason dentro de la variable datos_jason, ahora es necesario procesarlos para poder tener una cadena de caracteres en este formato para reemplazar el marcador de puesto correspondiente en el template de la página donde vamos a mostrar las gráficas.


El código JavaScript empotrado en “btc_analisis_compra_venta_machote.html” necesita los datos en este formato para poder pasárselo a la librería de gráficas y mostrarlas en la página definida ahí:


[

[new Date(1646352000000), 42464.4],

[new Date(1646438400000), 39141.25],

[new Date(1646524800000), 39399.58],

[new Date(1646611200000), 38407.69],

[new Date(1646697600000), 38032.5],

[new Date(1646784000000), 38753.1],

[new Date(1646870400000), 41956.53]

]


En las líneas del template HTML/Javascript donde se especifica {{datos_semana}} y {{datos_mes}} ahí se coloca esta estructura como único parámetro del método addRows() del objeto google.visualization.DataTable() que vamos a describir más adelante.


Como es necesario devolver esa estructura arriba como una cadena de caracteres (string) al punto en el código que llama a datos_btc_hileras(), lo que tiene más sentido es construirla a medida que se leen los datos provenientes de datos_json .


Para esto usamos un bucle tipo "for”:



for (let i = 0; i < datos_json.values.length; i++) {

texto_datos_precios=texto_datos_precios+

'[new Date('+(datos_json.values[i].x*1000).toString()+

'), '+

datos_json.values[i].y.toString()+

']'

if (i != datos_json.values.length-1) {

texto_datos_precios=texto_datos_precios+','

}

}

texto_datos_precios=texto_datos_precios+']'


El valor de la fecha/hora de cada lectura de precio usa el valor correcto basado en tiempo desde Epoch, pero el servicio web lo devuelve en segundos. En este caso, es necesario el valor en milisegundos por lo que lo multiplicamos por 1000 antes de convertirlo a una cadena de caracteres, esta cadená será el parámetro a pasar al crear un nuevo objeto tipo Date() en el template. El precio o tipo de cambio no necesita ajustes, por lo tanto solo se convierte también a cadena de caracteres.


Luego del bucle, tenemos que agregar un ‘]’ para cerrar la estructura y esté en el formato correcto antes de devolverlo.



Uso de una librería de graficas de JavaScript de Google en la página

A pesar de que el foco de este curso es en node.js, para mostrar la gráfica de los precios de bitcoin es mucho más sencillo pasar los datos a la página web y usar ahí un poco de código JavaScript para que “dibuje” la gráfica. Para ello se utiliza la librería Google Charts.


Para poder usarla, en el archivo “btc_analisis_compra_venta_machote.html” debemos referenciarla como lo pueden ver en la linea 1 del archivo:


<script type="text/javascript" src="https://www.gstatic.com/charts/loader.js"></script>


Luego, en las siguientes líneas se puede ver el uso de etiquetas "div” con los nombres "chart1_div” y “chart2_div” que tienen como propósito indicar donde van a ir las gráficas que van a ser dibujadas dentro de la función drawBackgroundColor() que está definida más adelante.


<h2>Semana</h2>

<div id="chart1_div"></div>

<h2>Mes</h2>

<div id="chart2_div"></div>


Finalmente, se observa como se inicializa la librería de gráficas de Google y como se asocia a la función drawBackgroundColor(). Esas líneas se ejecutan luego de cargar el formulario definido en HTML, entonces las gráficas con el precio de bitcoin de la semana anterior y el mes anterior se van a mostrar prácticamente al mismo tiempo que el resto del formulario.


google.charts.load('current', {packages: ['corechart', 'line']});

google.charts.setOnLoadCallback(drawBackgroundColor);


Mas detalles de cómo usar la librería Google Charts se encuentran acá: https://developers.google.com/chart/interactive/docs



Uso de formularios de HTML para obtener y procesar datos provistos por usuarios


Para este ejercicio, se mantiene el uso del verbo GET de HTTP, pero implementando formularios en HTML para recopilar datos de los usuarios, ya sean digitados en el navegador, por una escogencia de opción o fecha.


Una forma popular de manejar formularios es usar el verbo GET de HTTP para que el navegador reciba el formulario inicial y luego especificar que el navegador envíe los datos con el verbo POST. Con esto se evita tener que pasarlos como parámetros en la dirección y así evitar mostrarlos y satura la barra de dirección. Pero para este proyecto son pocos los parámetros necesarios, por eso se prefiere el uso de HTTP GET para simplificar el código de node.js del servidor.


Observe entonces como en la línea de btc_analisis_compra_venta_machote.html que inicia <form, se define el formulario con la acción "/aceptar” y sin especificar el método, lo cual va a usar por defecto el verbo HTTP GET.


<form class="form-inline" action="/aceptar">



Hay muchas formas de especificar el método, incluyendo el llamar una función. Pero si simplemente especificamos una “ruta” como “/aceptar”, entonces el navegador sabe que al oprimir el botón de someter o "submit” lo que tiene que hacer es navegar al mismo servidor web que le entregó la página donde está el formulario, pero usando la ruta “/aceptar”.


Más adelante se analizará cómo se implementan las rutas en general, pero por ahora se continúa evaluando el mecanismo usado por el formulario. En las siguientes líneas se definen los elementos del formulario, como el campo de “input” de texto llamado "cantidad”, y la opción con botones radiales llamada “transaccion”:

<label for="cantidad">Cantidad:</label>

<input type="text" id="cantidad" placeholder="0" name="cantidad">

<br>

<br>

<input type="radio" name="transaccion" value="compra"> Compra

<input type="radio" name="transaccion" value="venta" checked> Venta


Ahora, para poder enviar los datos de vuelta al servidor web, necesitamos un botón tipo “submit” al cual no hay que asignarle nombre realmente:


<button type="submit">Aceptar</button>


Esta es la parte de la página que comprende el formulario que acabamos de definir:



Cuando el usuario vea la página y escriba algo en el campo de texto “cantidad”, elija la compra o venta y oprima el botón de aceptar, el navegador va a extraer esos datos y generar esta dirección para hacer otro GET a nuestro servidor web:



Note que esta es precisamente la dirección que vemos al desplegarse la siguiente página que se desea mostrar en el programa; la de confirmación de la transacción y ofrecimiento de recordatorio que se mencionó al principio de este capítulo.


Más detalles y un tutorial completo acerca de formularios en HTML se pueden encontrar acá:

https://developer.mozilla.org/es/docs/Learn/Forms/Your_first_form


Entonces, para analizar la parte del código dentro de “servidor_mercado_btc.js” que va a recibir esos datos y procesarlos, solo tenemos que dirigirnos al case:


case '/aceptar' :

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

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

console.log('parametro cantidad: ',cantidad, ' transaccion: ',transaccion);


Observe el uso ya familiar de el módulo ‘url’ para extraer los parámetros y crear la siguiente página que se desea mostrar al usuario, que en este caso estaría basada en el machote definido en “btc_trans_resultado_machote.html”.


Es así como en las siguientes lineas se carga la fecha y hora en una variable para reemplazar en el marcador correspondiente del template, se envía la cantidad y tipo de transacción que recibimos en la ruta “/aceptar” y se llama a leer_machote() para tener listo el código HTML para retornar al navegador para mostrar la página completa con el resultado:


var ahora=new Date();

var fecha_hora_inicial=ahora.getFullYear().toString()+"-"+String(ahora.getMonth()+1).padStart(2, '0')+"-"+

String(ahora.getDate()).padStart(2,'0')+"T"+

String(ahora.getHours()).padStart(2,'0')+":"+String(ahora.getMinutes()).padStart(2,'0')

var cambios={

'cantidad' : cantidad,

'transaccion' : transaccion,

'fecha_hora_inicial' : fecha_hora_inicial

}

resultado_html=await leer_machote('./btc_trans_resultado_machote.html',cambios)


Probablemente se pregunten, ¿Por qué hubo que hacer tantas operaciones sobre la variable “ahora” para obtener la cadena de caracteres que almacenamos en “fecha_hora_inicial"?


La razón es porque se necesita que ‘fecha_hora_inicial’ esté en el formato correcto para colocarlo en esta parte del formulario dentro de “btc_trans_resultado_machote.html”:


<input type="datetime-local" id="recordatorio" name="recordatorio" value="{{fecha_hora_inicial}}" min="{{fecha_hora_inicial}}">


Ese campo de tipo “datetime-local” requiere un formato “AAAA-MM-DDTHH:MM” (e.j. “2022-03-15T13:00”) y no hay un método del objecto Date() que nos entregue una cadena de caracteres formateado exactamente así.


Entonces es necesario usar una combinación de los métodos getFullYear(), getMonth(), getDate(), getHours() y getMinutes() más la concatenación de cadenas de caracteres usando el operador ‘+’ además del método String() para convertir de número a cadena y, finalmente, el uso del método padStart() para asegurar que las cifras a usar para todo menos el año tengan exactamente 2 caracteres.


Ya con “fecha_hora_inicial” listo, lo agregamos al objeto "cambios” para pasárselo a leer_machote() como se mencionó arriba.


En la próxima línea se retorna la página lista al navegador y le corresponde al navegador procesar ese nuevo formulario:


res.write(resultado_html);


El formulario especificado en “btc_analisis_compra_venta_machote.html” tiene un formato parecido, pero una gran diferencia en el uso de campos de entrada escondidos. Observe las siguientes líneas de ese archivo:


<input type="hidden" value={{transaccion}} name="transaccion">

<input type="hidden" value={{cantidad}} name="cantidad">


La decisión de enviarle esos valores a ese formulario a pesar de que no es necesario que se modifiquen en este segundo formulario es porque resulta útil tenerlos a mano para el procesamiento final de la orden de compra o venta. Entonces, es muy conveniente agregarlos como parte del formulario, asignarles el valor que venía del formulario anterior y con esto cuando se oprima el botón tipo “submit” en este formulario, pasarán a la ruta “/recordar” como parámetros otra vez ya que esa es la ruta especificada para este formulario como se puede ver en esta línea:


<form class="form-inline" action="/recordar">


Ahora, en este momento se podría pensar que otra forma de mantener esos valores en el programa de node.js sin pasarlos a la página solo para ser pasados de vuelta es declarar unas variables globales en “servidor_mercado_btc.js”, asignarles valores dentro del código que procesa la ruta "/aceptar” y luego accederlo desde el código para el manejo de la ruta “/recordar”, ¿no?


Eso no funcionaría por el simple hecho de que nuestro programa de node.js, al ser un servidor web, tiene que tener la capacidad de manejar múltiples “sesiones” con diferentes usuarios con lo cual un usuario podría estar solicitando la página entregada por la ruta “/recordar” mientras otro acaba de usar la ruta “/aceptar” donde eligió valores distintos para la cantidad de BTC para la transacción y una compra en vez de venta. Entonces al acceder la variable global para la cantidad de BTC a nombre del primer usuario recibiría el valor elegido por el segundo.


Sí existen formas de separar valores de variables para distintas “sesiones” de diferentes usuarios para que cada uno tenga variables que pueden pasarse del manejo de una página a otra, pero típicamente se requiere el uso de “frameworks”. Decidimos mejor usar valores escondidos en formularios por ahora.


Finalmente, note el uso del campo de entrada tipo “datetime-local”:


<input type="datetime-local" id="recordatorio" name="recordatorio" value="{{fecha_hora_inicial}}" min="{{fecha_hora_inicial}}">


Esto permite, con una sola línea de HTML, hacer que el navegador muestre los componentes gráficos necesarios para que un usuario elija una fecha y hora así:



¡Un gran ahorro de código HTML/JavaScript! Como se puede apreciar, al especificar la opción “min=” podemos evitar que el usuario elija una fecha/hora antes de un mínimo especificado, que es justo la fecha/hora en que le hemos indicado al formulario que muestre el estado inicial del calendario para mayor comodidad.



Uso de un machote de archivo en formato iCal para el recordatorio


Cuando se oprime el botón de "recordar” en el formulario definido en “btc_trans_resultado_machote.html “; la ruta "/recordar” de “servidor_mercado_btc.js” empieza ejecutarse, con lo cual se almacenan en variables la fecha de recordatorio elegida, tipo de transacción y cantidad de BTC escogidas desde el primer formulario y que pasamos de nuevo al servidor web como parámetros de un HTTP GET.


Aquí entonces se inicia la tarea de modificar un template de un archivo tipo .ics que contiene un recordatorio en formato iCal el cual es muy conveniente de usar pues prácticamente todos los sistemas operativos y programas de manejo de calendario saben cómo abrirlo y ofrecerle al usuario crear una nueva cita o recordatorio con los datos contenida en ella.


El machote que usamos para ello está contenido en el archivo “BTC_transaccion_machote.ics” y, una vez más, es necesario hacer un cambio de formato de fecha/hora pues, en el caso del formato iCal, requiere que sea de esta forma: “AAAAMMDDTHHMMSS” (ej.. “20220315T130000”)


Esta vez la conversión es más sencilla porque el campo tipo “datetime-local” de HTML nos devuelve un valor en el mismo formato que tuvimos que usar para pasar el valor inicial: “AAAA-MM-DDTHH:MM” (e.j. “2022-03-15T13:00”)


Entonces solo hay que eliminar los caracteres '-’ y ‘:” y agregar dos ceros al final (no se incluyen segundos específicos en el recordatorio )


Esto se logra con el código en las líneas de “servidor_mercado_btc.js” como se puede ver aca:


var fecha_hora=recordatorio;

fecha_hora = fecha_hora.replace(/-/g, '');

fecha_hora = fecha_hora.replace(/:/g, '');

fecha_hora = fecha_hora+'00'


También el formato iCal necesita que se especifique la banda horaria a usar en el recordatorio. Afortunadamente uno de los módulos innatos de node.js brinda eso en el formato exacto requerido por iCal de esta forma:


var banda_horaria=Intl.DateTimeFormat().resolvedOptions().timeZone;


Ya con eso solo falta "compilar” o armar el nombre del evento basado en las variables que ya se tienen de esta forma:


var nombre_evento=transaccion+" "+cantidad+" BTC"


Luego de llamar de nuevo a la función leer_machote() pero esta vez con el nombre de archivo del machote para el recordatorio (“BTC_transaccion_machote.ics”) y el objeto con los cambios apropiados tenemos listo los contenidos del archivo .ics. Pero antes de crearlo, es necesario elegir donde lo vamos a colocar y como se va a llamar lo cual hacemos a continuación.


var nombre_archivo="BTC_Transaccion_"+fecha_hora+".ics"


Aquí vemos como se agrega el valor de la variable “fecha_hora” que ya se había adaptado arriba a una base de nombre “BTC_Transaccion” para que cada recordatorio tenga un archivo independiente.


Al usar el comando para crear el archivo con el texto contenido en la variable “resultado_ics” luego de procesar el machote, se agrega el nombre de archivo calculado al nombre del directorio que se especificó en el código como la constante “dir_recordatorios”.


Así se ve entonces la creación del archivo .ics:


fs.writeFile(dir_recordatorios+nombre_archivo, resultado_ics, 'utf8', (err) => {…}



Manejo de múltiples rutas en un mismo programa de node.js


Luego de revisar el código que maneja alguna de las tres "rutas” que estamos manejando en este proyecto quizás ya se tenga una buena idea de como se logra dentro de la función que se le pasa a http.createServer() y sin mucha confusión.


Se hace una sola evaluación de la ruta que el navegador está solicitando, con una declaración para toma de decisiones 'switch’:



switch (url.parse(req.url,true).pathname) {


Entonces cada bloque de código correspondiente a cada ruta se inicia con una instancia case como se puede ver y se termina con la interrupción break.


case '/' :

break;


Para todas las rutas es necesario dar un valor de retorno al navegador con el método res.end(), incluyendo el caso de una ruta no definida en nuestro código:


default :

res.end("Operacion no soportada");

break;


Técnicamente se debió retornar un código HTTP 404 para indicar que esa página no existe; pero al menos existe un mensaje con el retorno de ese texto para ser visualizado por el usuario en su navegador si se equivoca de ruta.