Mi experiencia con Node.js... 1ª Parte
En el último post lo dejamos diciendo algunos comandos de npm y con Node.js instalado.
Ahora vamos a seguir explicando algunas cosas que tiene la consola de Node.js y algunas peculiaridades de JavaScript y Node.js.
Lo primero que vamos a ver es una demostración de que Node.js es asíncrono. Para ello vamos a crear un archivo ejemplo1.js en la carpeta de trabajo. Dentro del archivo vamos a escribir lo siguiente:
var x=0; setTimeout(function(){ console.log("hola"); x++;},1000); console.log("mundo!"); console.log(x);
Este código visto desde una perspectiva secuencial lo que hace es imprimir hola mundo! y el valor de la x , que debería ser 1 por el sumatorio anterior, después de un segundo de espera. Vamos a ejecutarlo y a ver que es lo que imprime. El comando que utilizaremos para ejecutar ficheros es el siguiente:
node ejemplo1.js
El resultado es muy diferente al que esperábamos.
#PERSPECTIVA SECUENCIAL Hola #Esperamos un segundo mundo ! 1 #PERSPECTIVA ASÍNCRONA mundo ! 0 #Esperamos menos de un segundo Hola
Analiza un poco el código y piensa en que está haciendo Node.js para que el resultado sea así.
Si aun no lo has entendido del todo ahora viene la explicación. La diferencia entre la ejecución síncrona o secuencial y la asíncrona o concurrente es la cantidad de hilos que se ejecutan. En el caso de un programa secuencial la cantidad de hilos que ejecutan el código es 1 mientras que en un programa concurrente puede haber miles de hilos que ejecuten es código a la vez. En este caso Node.js ejecuta el código con 2 hilos. Uno empieza a ejecutar el programa principal y cuando llega a la función de espera se lanza otro hilo que es el que espera un segundo mientras que el primer hilo continua ejecutando el programa. Esto hace que durante la espera de 1 segundo acaben ejecutándose los console.log.
Para acabar de ver lo bien vamos a ver otro ejemplo:
var x=0; setTimeout(function(){ console.log("hola"); x++;},1000); console.log("mundo!"); setTimeout(function(){ console.log(x); x++;},1100);
En este caso hacemos que la impresión del valor x se retrase 1,1 segundos. Ejecútalo a ver el resultado.
mundo! hola #Impreso después de 1 segundo desde el inicio 1 #Impreso después de 1,1 segundos desde el inicio
Ahora el valor de la x es 1. En este caso se ha creado un tercer hilo que ha ejecutado ese tiempo. Lo interesante es que ha tardado solo 0,1 segundos más que la impresión del hola. Eso significa ser asíncrono.
DATA STREAM
Los data stream se utilizan para realizar operaciones de escritura/lectura de forma asíncrona ya que a la hora de escribir y leer Node.js no lee lineas completas sino que lee chunks(trozos de datos). Los data streams estándares son:
process.stdin #Entrada estándar process.stdout #Salida estándar
Los streams se procesan mediante eventos. JavaScript es un lenguaje orientado a eventos, y por tanto éstos forman parte de la sintaxis del lenguaje: object.on(ev_name, callback). Los ficheros son divididos en chunks de octetos, a partir de los cuales se generan los eventos del data
stream . Lo mejor es que lo compruebes con el siguiente programa:
stream . Lo mejor es que lo compruebes con el siguiente programa:
process.stdin .on('data',function(chunk){ console.log(chunk); process.stdout.write(chunk.toString().toUpperCase()); }) .on('end',function(){ console.log("bye"); });
Este programa detecta lo que entra por la entrada estándar (terminal en nuestro caso) y en el caso de ser datos lo que hace es imprimir el chunk por pantalla y después imprimir el chunk en formato String y en mayúscula. En el caso de que en vez de entrar datos entra un Ctrl+d imprime un bye en terminal. Como se puede ver no se crea ningún objeto, se detectan eventos dentro de ciertas partes del código, en este caso la entrada estándar. Aquí os dejo lo que aparece tras la ejecución del código anterior
Entrada --> Hola Salida --> <Buffer 48 6f 6c 61 0d 0a> HOLA
Para procesar ficheros necesitamos el módulo predefinido 'fs' que se encarga de definir los streams de lectura/escritura necesarios para procesar ficheros del sistema operativo. Aquí podemos ver un ejemplo de este módulo en acción.
var fs = require('fs'); #Importamos el módulo var writestream = fs.createWriteStream('./tmp.txt'); #Creamos un módulo de escritura para el fichero tmp.txt writestream.on('finish',function(){ #En el caso de que el módulo de escritura detecte que se ha finalizado mandará el mensaje de finalización console.log('Guardado!'); #Vemos que la función es asíncrona ya que tiene un evento que detectar y una función que realizar cunando se detecte }); writestream.write(JSON.stringify(process.env)); #Escribimos un String de la variable de entorno(Esta función es síncrona para que no existan fallos de orden al escribir) writestream.end(); #Lanzamos un evento de finalización para el writestream var readstream = fs.createReadStream("./tmp.txt"); #Creamos un módulo de lectura de tmp.txt readstream.on('data',function(chunk){ #Si detectamos datos los pasamos por consola console.log(chunk.toString();); });
También podemos añadir líneas a un fichero de texto con el siguiente método:
fs.appendFileSync("./tmp.txt",JSON.stringify({nombre:'paco'})+"\n");
Para añadir líneas a un fichero es aconsejable utilizar la versión síncrona de appendFile para evitar problemas de colisión en la escritura del fichero desde funciones asíncronas. Otros métodos útiles de este módulo son fs.stat(path, cb) ó fs.statSync(path) para comprobar la existencia y obtener las propiedades de un fichero o directorio, y fs.unlink(fname) para borrar un fichero.
COMBINANDO STREAMS (PIPE)
Una propiedad interesante de los streams es que pueden combinarse del mismo modo que las tuberías (pipe) del sistema operativo para poder leer de un archivo y escribirlo en otro. Para hacer esto podemos utilizar el módulo split que se encarga de dividir el contenido de un fichero en lineas en vez de chunks para que sea más fácil tanto la escritura como utilizar un contador de lineas.
var fs = require('fs'); #Módulo para definir streams de lectura y escritura var split = require('split'); #Módulo para dividir un fichero en lineas en vez de chunks var readstream = fs.createReadStream(process.argv[2]); var writestream = fs.createWriteStream('./tmp.txt'); var lineas=0; readstream.pipe(split()) .on('data',function(line){ writestream.write(JSON.stringify(data)); }) .on('end',function(){ console.log("Se ha acabado de escribir") });
En este ejemplo utilizamos la línea de argumentos para saber el nombre del fichero a procesar (process.argv[2]). De este modo, al ejecutar el programa deberemos pasar el nombre del fichero como argumento:
node programa.js tmp.txt
Antes de ejecutar el programa, hay que instalar el módulo que divide el fichero en líneas (split). Para ello utilizaremos el comando npm:
npm -s install split
Con este comando se crea un nuevo directorio (./node_modules) con el nuevo módulo instalado. Inspecciona su estructura, sobre todo el fichero package.json que contiene la configuración y dependencias del módulo. Aquí se puede ver la estructura de mi proyecto.
SERVIDORES
Node.js proporciona las APIs necesarias para crear distintos tipos de servidores:
Servidor Web:
Servidor Web:
var http = require('http'); var s = http.createServer(function(req, res){ res.writeHead(200,{'content-type':'text/plain'}); res.write("Hola\n"); res.end("Mundo\n"); }); s.listen(8000);
TCP Server:
var net=require('net'); var sockserver=net.createServer(function(socket){ socket.write("hello world\n"); socket.on('data',function(data){ if (data.toString().trim()==="quit"){ socket.end("\tBYE\n"); } else { socket.write("\t"+data.toString().toUpperCase()); } }); }); sockserver.listen(8001);
Para probarlo, necesitarás dos terminales (servidor y cliente) o usar un navegador como cliente:
# PARTE SERVIDOR $ node tcp_o_http_example_server.js ...
# PARTE DEL CLIENTE SERVIDOR WEB $curl http://localhost:8000 #En este caso está puesto el 8000 porque es el puerto que hemos identificado en el ejemplo. ... $curl -i http://localhost:8000 #En este caso está puesto el 8000 porque es el puerto que hemos identificado en el ejemplo. # PARTE DEL CLIENTE TCP $ nc localhost 8001 #En este caso está puesto el 8001 porque es el puerto que hemos identificado en el ejemplo. ... $ telnet localhost 8001 #En este caso está puesto el 8001 porque es el puerto que hemos identificado en el ejemplo. ...
Podemos ahora analizar el siguiente programa para realizar un chat con sockets :
var net = require('net'); var sockets = []; var sockserver=net.createServer(function(socket){ socket.write("wellcome to the chat!\n"); sockets.push(socket); socket.on('data',function(data){ if (data.toString().trim()==="quit"){ socket.end("\tBYE\n"); } else{ for(var i=0;i<sockets.length;i++){ sockets[i].write("\t"+data.toString().toUpperCase()); } } }); }); sockserver.listen(8002);
Se puede probar con tres terminales (servidor y dos clientes).
SERVICIOS REST CON EXPRESS
El modo más sencillo de poner en marcha un servicio RESTful es mediante el módulo express de Node.js. Necesitarás instalar los siguientes módulos con npm: express y body-parser para obtener una funcionalidad mínima. También usaremos el módulo predefinido path para realizar comprobaciones sobre las rutas de ficheros en el sistema operativo.
Veamos un primer ejemplo (express_example1.js):
var express = require("express") var app = express(); app.get('/',function(req,res){res.send("Hola Mundo!");}); app.listen(8000); console.log("Servidor web escuchando en puerto 8000");
Para probarlo, lanza el servidor y abre un navegador con http://localhost:8000.
$ node express_example1.js
Para añadir funcionalidades al servidor, debemos usar el método “use” del objeto express app.
Vamos a ver un ejemplo para servir ficheros estáticos.
Vamos a ver un ejemplo para servir ficheros estáticos.
var application_root=__dirname, express = require("express"), path = require("path") var app = express(); app.use(express.static(path.join(application_root,"public"))); app.get('/hola',function(req,res){res.send("Hola mundo!");}); app.get('/index',function(req,res){ res.sendFile("public/index.html",{root:application_root}); }); app.listen(8000);
Para este ejemplo, debemos crear un directorio (public) que contendrá los contenidos estáticos de nuestro servicio. Uno de estos ficheros debe ser “index.html” que será accedido desde la ruta “/”.
Este podría ser por ejemplo:
Este podría ser por ejemplo:
<html> <body> <h1>It works!</h1> <form action="persona" method="POST"> Nombre:<br> <input type="text" name="firstname" value="Mickey"> <br> Apellidos:<br> <input type="text" name="lastname" value="Mouse"> <br><br> <input type="submit" value="Submit"> </form> </body> </html>
Vamos a ver ahora como procesaríamos en el servidor rutas y peticiones de tipo POST:
var application_root = __dirname, express = require("express"), path = require("path") var app = express(); app.use(express.static(path.join(application_root,"public"))); var bodyparser = require("body-parser"); app.use(bodyparser.urlencoded({extended:true})); app.use(bodyparser.json()); var backend = require("fs"); app.get('/hello',function(req,res){res.send("Hola Mundo!");}); app.get('/index',function(req,res){ res.sendFile("public/index.html",{root:application_root}); }); app.get('/public/:fname',function(req,res){ res.sendFile(req.params.fname,{root:application_root}); }); app.post('/persona',function(req,res){ console.log(req.body); backend.appendFileSync("./data/personas.json", JSON.stringify(req.body)+"\n"); res.send({result:"ok"}); }); app.listen(8000);
Para este ejemplo debéis tener creado el directorio ./data para almacenar los datos de las personas que se envían desde el formulario de la página web, y que se guardarán en el fichero ./data/personas.json.
MÓDULOS
Si queréis crear vuestros propios módulos y reutilizarlos en el proyecto, simplemente tenéis que poner el código en un fichero con extensión “.js”. Al final del fichero indicáis que partes del módulo hacéis visibles con “export”. A continuación se muestra un ejemplo sencillo de módulo (.js)
Si queréis crear vuestros propios módulos y reutilizarlos en el proyecto, simplemente tenéis que poner el código en un fichero con extensión “.js”. Al final del fichero indicáis que partes del módulo hacéis visibles con “export”. A continuación se muestra un ejemplo sencillo de módulo (.js)
var fs = require('fs'); var split = require('split'); function getPersonas(callback){ var readstream = fs.createReadStream("./data/personas.json"); var lista=[]; readstream.pipe(split()) .on('data',function(line){ line=line.trim(); if (line.length>0){ var data=JSON.parse(line); lista.push(data); } }) .on('end',function(){ callback({result: lista}); }); }; exports.getPersonas = getPersonas; # También lo podemos usar así exports.getPersonas(function(dato)) #Los exports se pueden utilizar al principio del documento pero no es recomendable. Así identificamos cuales son los métodos públicos y cuales los privados.
Ahora podríamos usar este módulo en el programa anterior del servicio REST, añadiendo la importación del módulo con var control = require('./modulo.js'); y usando la función con control.getPersonas((data)=>{res.send(data);}) dentro del método GET /persona.
var control = require('./modulo.js'); app.post('/personas', function(req,res)){ control.getPersonas(function(data){res.send(data)}) }
Se utilizan los mismos argumentos que en el callback.
Hasta aquí queda mi primer acercamiento hacía Node.js como podéis ver por ahora se muy poco pero durante los post iré aprendiendo como programar en este lenguaje así como la manera de explicarlo. Espero que os haya gustado y para acabar dejo algunos métodos que pueden ayudaros a la hora de utilizar Javascript y Node.js. Hasta el próximo post.
- Para ver si existe en una lista un elemento Lista.indexOf('elem') > +1
- Con Lista = Lista.concat(['1', 2, 'r']) podemos concatenar a la lista otra lista.
- Con push y pop metes y sacas elementos de una lista.
- Con splice podemos coger una parte de la lista y permite indices negativos como en python.
- Podemos utilizar el método replace con expresiones regulares para cambiar partes de la cadena. Una expresión regular interesante es replace(/r{2,}/ig, ' ') esto cambia las r cuando hay más de 2, da igual que sea minúsculas o mayúsculas y lo pone en todo el texto, es decir de forma g global sino solo lo haría una vez.
- Podemos crear un elemento json con la estructura [nombre:"algo", apellido:"cosa"] aunque se puede hacer ["nombre":"algo", "apellido":"cosa"] o incluso uno si y otro no.
- Podemos acceder al objeto con J.nombre o j["nombre"] .
- Para añadir un campo al mapa podemos hacerlo con J.propiedad = valor y se añade al json. En el caso de que no esté el elemento nos lo pone como undefined. Es decir con j.propInexistente = undefined puede marcar que el valor de esa propiedad es undefined o que la propiedad no esta definida. Para hacer comprobaciones utilizamos j.prop !== valor para comparar por valor y por tipo del valor.
- Para eliminar una clave del diccionario json utilizamos delete. En el caso de comprobar tipos y valor utilizamos === para comprobar tipo y valor.
- Para conocer el tipo utilizamos typeof(variable).
- Para iterar sobre la lista utilizamos el tipico for(i=0;i<L.length;i++) aunque también existe for(var e of lista) acaba con un undefined. También iteramos con for(var e of Object.keys(J))... Devuelve las claves del Json. Podemos ver las funciones de un objeto poniendolo en el nodejs.
Comentarios
Publicar un comentario