Curso node.js

Preview:

DESCRIPTION

Curso de Node.js http://redradix.com/courses

Citation preview

Pero, ¿Qué es Node.js?

¿Qué es Node.js?

Lo que todos sabemos

• Hay Javascript por alguna parte

• Backend

• ¿Algo que ver con NoSQL?

• Sirve para hacer chats

¿Qué es Node.js?

Lo que no está tan claro

• ¿Es un framework para Javascript?

• ¿Es una librería?

• ¿Qué tiene que ver v8 con Node.js?

• ¿Para qué sirve (además de los chats)?

¿Qué es Node.js?

Node.js es:

• “Una plataforma de software usada para construir aplicaciones de red escalables (especialmente servidores). Node.js utiliza JavaScript como lenguaje, y alcanza alto rendimiento utilizando E/S no bloqueante y un bucle de eventos de una sola hebra”.

¿Qué es Node.js?

Node.js es:

• “Una plataforma de software usada para construir aplicaciones de red escalables (especialmente servidores). Node.js utiliza JavaScript como lenguaje, y alcanza alto rendimiento utilizando E/S no bloqueante y un bucle de eventos de una sola hebra”.

• Wat?

¿Qué es Node.js?

Por ejemplo, si...

• ...para ruby tenemos Rails...

• ...para python tenemos Django...

• ...para php tenemos Symphony...

¿Qué es Node.js?

Por ejemplo, si...

• ...para ruby tenemos Rails...

• ...para python tenemos Django...

• ...para php tenemos Symphony...

• ¿Podríamos decir que Node.js es el equivalente para JavaScript?

¿Qué es Node.js?

¡¡NO!!

¿Qué es Node.js?

¿Qué es un “lenguaje de programación”?

¿Qué es Node.js?

¿Qué es un “lenguaje de programación”?

• Una gramática que define la sintaxis del lenguaje

• Un intérprete/compilador que lo sabe interpretar y ejecutar

¿Qué es Node.js?

¿Qué es un “lenguaje de programación”?

• Una gramática que define la sintaxis del lenguaje

• Un intérprete/compilador que lo sabe interpretar y ejecutar

• Mecanismos para interactuar con el mundo exterior (llamadas al sistema)

¿Qué es Node.js?

¿Qué es un “lenguaje de programación”?

• Una gramática que define la sintaxis del lenguaje

• Un intérprete/compilador que lo sabe interpretar y ejecutar

• Mecanismos para interactuar con el mundo exterior (llamadas al sistema)

• Librería estándar (consola, ficheros, red, etc,...)

¿Qué es Node.js?

¿Qué es un “lenguaje de programación”?

• Una gramática que define la sintaxis del lenguaje

• Un intérprete/compilador que lo sabe interpretar y ejecutar

• Mecanismos para interactuar con el mundo exterior (llamadas al sistema)

• Librería estándar (consola, ficheros, red, etc,...)

• Utilidades (intérprete interactivo, depurador, paquetes)

¿Qué es Node.js?

• Una gramática que define la sintaxis del lenguaje

• Un intérprete/compilador que lo sabe interpretar y ejecutar

• Mecanismos para interactuar con el mundo exterior (llamadas al sistema)

• Librería estándar (consola, ficheros, red, etc,...)

• Utilidades (intérprete interactivo, depurador, paquetes)

v8 (JavaScript)

Node.js

¿Qué es Node.js?

Node.js Ruby Python Java

Lenguaje

Motor

Entorno

Framework

JavaScript Ruby Python Java

v8 YARV cPython JavaVM

Node.js Ruby Standard

Library

Python Standard

Library

Java SE

??? Rails Django Spring

¿Qué es Node.js?

Node.js es algo más:

• Una filosofía sobre cómo hacer las cosas

• Un modelo de ejecución singular

• Muy enfocado hacia aplicaciones de red

¿Qué es Node.js?

Node.js es algo más:

• Una filosofía sobre cómo hacer las cosas

• Un modelo de ejecución singular

• Muy enfocado hacia aplicaciones de red

Una filosofía

Node.js se crea con un objetivo en mente:

• Escribir aplicaciones muy eficientes (E/S) con el lenguaje dinámico más rápido (v8) para soportar miles de conexiones simultáneas

• Sin complicarse la vida innecesariamente

- Sin paralelismo

- Lenguaje sencillo y muy extendido

- API muy pequeña y muy consistente

- Apoyándose en Eventos y Callbacks

Una filosofía

Una filosofía

• No es la mejor opción para todos los casos

- Si puedes hacerlo con Rails/Django/Spring, hazlo

• Evita las “soluciones totales”

- Una necesidad, una herramienta

- Combina diferentes herramientas simples

• Entiende lo que estás haciendo

• Flexibilidad > magia

- Tu código es tu responsabilidad

- Cada aplicación es un mundo

¿Qué es Node.js?

Node.js es algo más:

• Una filosofía sobre cómo hacer las cosas

• Un modelo de ejecución singular

• Muy enfocado hacia aplicaciones de red

Un modelo de ejecución

Para entender Node.js, tenemos que entender dos ideas fundamentales:

• Concurrencia vs. paralelismo (asincronía)

• Eventos

Un modelo de ejecución

Concurrencia vs. Paralelismo

• ¿Qué significa que dos cosas suceden “a la vez”?

Un modelo de ejecución

¿Qué significa que dos cosas suceden “a la vez”?

Un modelo de ejecución

¿Qué significa que dos cosas suceden “a la vez”?

Un modelo de ejecución

Concurrencia vs. Paralelismo

• Paralelismo: varios actores realizando una acción cada uno simultáneamente

• Concurrencia: un solo actor, con varias tareas “activas” entre las que va alternando

Un modelo de ejecución

Paralelismo

Concurrencia

Un modelo de ejecución

Un modelo de ejecución

La potencia de Node.js es (curiosamente):

• Un modelo de ejecución concurrente

- Muchos clientes o tareas activas

• Pero NO paralelo

- Una única hebra

Un modelo de ejecución

• ¿Qué ventajas tiene evitar el paralelismo?

• ¿Qué desventajas?

• Y por tanto, ¿Cuándo es útil el modelo de Node.js?

Un modelo de ejecución

Patrón Reactor

• Un patrón de diseño para manejar eventos donde peticiones de servicio se transladan concurrentemente a un manejador central que se encarga de desmultiplexar las peticiones y despacharlas síncronamente mediante sus manejadores particulares asociados.

Un modelo de ejecución

Patrón Reactor

• Un patrón de diseño para manejar eventos donde peticiones de servicio se transladan concurrentemente a un manejador central que se encarga de desmultiplexar las peticiones y despacharlas síncronamente mediante sus manejadores particulares asociados.

• WAT??

Un modelo de ejecución

Patrón Reactor

Bucle Principal

Un modelo de ejecución

Patrón Reactor

Bucle Principal

Mundo Exterior

Un modelo de ejecución

Patrón Reactor

Bucle Principal

Mundo Exterior

Suceso

Patrón Reactor

Un modelo de ejecución

Bucle Principal

Mundo Exterior

Suceso

Patrón Reactor

Un modelo de ejecución

Bucle Principal

Mundo Exterior

Suceso

Suceso 2

Patrón Reactor

Un modelo de ejecución

Bucle Principal

Mundo Exterior

Suceso

Suceso 2

Patrón Reactor

Un modelo de ejecución

Bucle Principal

Mundo Exterior

Suceso

Suceso 2

Tick!

Patrón Reactor

Un modelo de ejecución

Bucle Principal

Mundo Exterior

Suceso

Suceso 2

Tick!Evento:

“Suceso”

Patrón Reactor

Un modelo de ejecución

Bucle Principal

Mundo Exterior

Suceso

Suceso 2

Tick!Evento:

“Suceso”

Manejadores

Patrón Reactor

Un modelo de ejecución

Bucle Principal

Mundo Exterior

Suceso

Suceso 2

Tick!

Manejadores

Patrón Reactor

Un modelo de ejecución

Bucle Principal

Mundo Exterior

Suceso

Suceso 2

Tick!

Manejadores

Suceso 3

Patrón Reactor

Un modelo de ejecución

Bucle Principal

Mundo Exterior

Suceso

Suceso 2

Tick!

ManejadoresSuceso 3

Patrón Reactor

Un modelo de ejecución

Bucle Principal

Mundo Exterior

Suceso

Suceso 2

Tick!

ManejadoresSuceso 3

Listo!

Patrón Reactor

Un modelo de ejecución

Bucle Principal

Mundo Exterior

Suceso

Suceso 2

Tick!

ManejadoresSuceso 3

Patrón Reactor

Un modelo de ejecución

Bucle Principal

Mundo Exterior

Suceso

Suceso 2

Tick!

ManejadoresSuceso 3

Evento: “Suceso 2”

???

Patrón Reactor

Un modelo de ejecución

Bucle Principal

Mundo Exterior

Suceso

Suceso 2

ManejadoresSuceso 3

Patrón Reactor

• Programación contra eventos

• Una vez puestos los manejadores, se pueden ejecutar en cualquier orden

- El orden lo determina el orden en que aparezcan sucesos

• La ejecución de los manejadores bloquea la hebra

• Nunca hay dos manejadores ejecutándose al mismo tiempo

• Muy eficiente... cuando E/S >> ejecuión del manejador

Un modelo de ejecución

¿Qué es Node.js?

Node.js es algo más:

• Una filosofía sobre cómo hacer las cosas

• Un modelo de ejecución singular

• Muy enfocado hacia aplicaciones de red

¿Qué es Node.js?

Muy enfocado hacia aplicaciones de red

• ¿Por qué?

¿Qué es Node.js?

Muy enfocado hacia aplicaciones de red

• Mucha E/S

- Por tanto, mucho tiempo con la CPU inactiva

• Para aprovechar ese tiempo, necesitas otros clientes que lo puedan aprovechar

• Similar a un camarero en un bar

- Es un “Patron Reactor” del mundo real

- Para aprovecharlo, tiene que haber varios clientes!

- Un cliente no termina más deprisa por dedicarle un camarero sólo a él

Toma de contacto

Ahora en serio: ¿Qué es Node.js?

Necesitas:

• Un ordenador

• Un editor de texto

• Saber abrir una consola/terminal

• Tener node instalado (mejor si es v. 0.10)

- Asegúrate de tener también npm

- Como alternativa: http://c9.io

• Saber manejarte con JavaScript

Ahora en serio: ¿Qué es Node.js?

console.log("Hola, Mundo!");

Ahora en serio: ¿Qué es Node.js?

$ node hola.js

Ahora en serio: ¿Qué es Node.js?

Ahora en serio: ¿Qué es Node.js?

Pero...

• ¿Y ese rollo del patrón Reactor?

• ¿Por qué termina el programa en vez de quedarse escuchando sucesos en el bucle principal?

Ahora en serio: ¿Qué es Node.js?

Lo que pasa en realidad:

• Node.js ejecuta todo tu código del tirón

• Coloca los manejadores que hayas definido

• Si no hay ningún manejador que se pueda ejecutar en el futuro, el programa termina!

Ahora en serio: ¿Qué es Node.js?

setTimeout(function() { console.log("Hola, Mundo del futuro!");}, 1000);

setInterval(function() { console.log("Hola otra vez, Mundo del futuro!");}, 1000);

Ahora en serio: ¿Qué es Node.js?

Ahora en serio: ¿Qué es Node.js?

setTimeout(function() { while (true);}, 100);

setInterval(function() { console.log("Hola otra vez, Mundo del futuro!");}, 1000);

Ahora en serio: ¿Qué es Node.js?

var start = Date.now();

setTimeout(function() { for (var i=Number.MAX_VALUE; i--;) { Math.pow(12345, 123455); }}, 100);

setInterval(function() { var now = Date.now(); console.log("Han pasado", now - start, "ms"); start = now; console.log("Hola otra vez, Mundo del futuro!");}, 1000);

Ahora en serio: ¿Qué es Node.js?

var start = Date.now();

setTimeout(function() { var timesLeft = Number.MAX_VALUE, r; (function unPoquitoMas() { if (timesLeft-- > 0) { r = Math.pow(12345, 123455); } setTimeout(unPoquitoMas, 0); }());}, 100);

setInterval(function() { var now = Date.now(); console.log("Han pasado", now - start, "ms"); start = now; console.log("Hola otra vez, Mundo del futuro!");}, 1000);

Ahora en serio: ¿Qué es Node.js?

Nos surgen problemas curiosos...

• ¿Excepciones?

Ahora en serio: ¿Qué es Node.js?

Nos surgen problemas curiosos...

• ¿Excepciones?

try { throw new Error("Peté!");} catch(e) { console.log("Excepción!");}

Ahora en serio: ¿Qué es Node.js?

Nos surgen problemas curiosos...

• ¿Excepciones?

try { setTimeout(function() { throw new Error("Peté!"); }, 0);} catch(e) { console.log("Excepción!");}

EventEmitter

Nuestro código va a estar dirigido por eventos

• Node.js tiene su propio “estándar”

• Trae una implementación del patrón Observador (o Pub/Sub): EventEmitter

• Todas sus librerías (y casi todos los paquetes) siguen este modelo

EventEmitter

var EventEmitter = require("events").EventEmitter;

var pub = new EventEmitter();

pub.on("ev", function(m) { console.log("[ev]", m);});

pub.once("ev", function(m) { console.log("(ha sido la primera vez)");});

pub.emit("ev", "Soy un Emisor de Eventos!");pub.emit("ev", "Me vas a ver muy a menudo...");

EventEmitter

var EventEmitter = require("events").EventEmitter;

var pub = new EventEmitter();

pub.on("ev", function(m) { console.log("[ev]", m);});

pub.once("ev", function(m) { console.log("(ha sido la primera vez)");});

pub.emit("ev", "Soy un Emisor de Eventos!");pub.emit("ev", "Me vas a ver muy a menudo...");

require/exports

require(<paquete o ruta>)

• Importar módulos (paquetes, otros ficheros)

• Garantía: una única vez

• Devuelve el módulo!

require/exports

exports.propiedadPublica = <valor>

• El otro lado del mecanismo

• Se puede exportar cualquier valor

require/exports

codigo.js

libreria.js

var lib = require("./libreria");console.log(lib.propiedad);

console.log("una vez");exports.propiedad = "Pública";

Para que te confíes...

Haz un módulo “reloj”

• Que exporte una clase Reloj

• Emita eventos “segundo”, “minuto” y “hora”

var Reloj = require("./reloj").Reloj;var reloj = new Reloj();

reloj.on("segundo", function(fecha) { console.log("Un segundo! son las:", fecha); reloj.removeAllListeners("segundo");});

Para que te confíes...

Un truco:

• require(“util”).inherits• inherits(constructor, superConstructor)

var inherits = require("util").inherits;

function MiClase() { // ...}

function MiSubClase() { // ...}inherits(MiSubClase, MiClase);

JavaScript y el Universo

El JavaScript del navegador vive en un mundo ideal

• No hay SO con el que lidiar

• No hay datos binarios

• Todo es accesible con objetos y valores primitivos

• Apenas hay E/S, siempre con valores simples (strings)

• Un único usuario

JavaScript y el Universo

En Node.js, las cosas son de otra manera...

• Llamadas al sistema

• Mucha E/S

• Datos binarios (ficheros, sockets, etc)

• Descriptores de ficheros

• Puertos

• ...

JavaScript y el Universo

Tenemos que añadir nuevos conceptos a nuestro JS

• Streams (lectura, escritura o duplex)

• Buffers (representación de datos binarios)

• Procesos

• Rutas

• http://nodejs.org/api/index.html

JavaScript y el Universo

Buffers

• Una tira de bytes (datos binarios)

• Similar a un array de enteros

• Tamaño fijo

• Manipular datos directamente

- Sockets

- Implementar protocolos complejos

- Manipulación de ficheros/imágenes

- Criptografía

- ...

JavaScript y el Universo

Buffers

var buf = new Buffer(100);buf.write("abcd", 0, 4, "ascii");

console.log(buf.toString("ascii"));

JavaScript y el Universo

Buffers

var buf = new Buffer(100);buf.write("abcd", 0, 4, "ascii");

console.log(buf.toString("ascii"));

Tamaño del buffer

JavaScript y el Universo

Buffers

var buf = new Buffer(100);buf.write("abcd", 0, 4, "ascii");

console.log(buf.toString("ascii"));

Datos

Posición

Longitud

Codificación

JavaScript y el Universo

Buffers

var buf = new Buffer(100);buf.write("abcd", 0, 4, "ascii");

console.log(buf.toString("ascii"));

Codificación

JavaScript y el Universo

Buffers

JavaScript y el Universo

function Bitmap(w, h) { this.width = w; this.height = h; this.header = "P6\n" + w + " " + h + "\n255\n"; this.buffer = new Buffer(w*h*3+this.header.length); this.buffer.write(this.header, 0, this.header.length, "ascii");}Bitmap.prototype = { putPixel: function(x, y, color) { var pos = this.header.length + (y*this.width*3) + x*3; this.buffer.write(color, pos, 3, "hex"); }, fill: function(color) { this.buffer.fill(255, this.header.length); }, render: function() { process.stdout.write(this.buffer); }};

JavaScript y el Universo

var bitmap = new Bitmap(1, 1);bitmap.putPixel(0, 0, "000000");bitmap.render();

JavaScript y el Universo

var bitmap = new Bitmap(10, 10);bitmap.fill("ffffff");bitmap.putPixel(0, 0, "000000");bitmap.render();

JavaScript y el Universo

Streams

• “Chorros” de información

- Lectura / Escritura / Duplex

• Detrás de muchos mecanismos de Node.js

- stdin/stdout

- request HTTP

- sockets

- etc...

• Instancias de EventEmitter

• Acceso asíncrono

JavaScript y el Universo

Streams

• Es raro crear streams directamente

• Pero muchos recursos nos ofrecen este interfaz

JavaScript y el Universo

Streams de lectura

• Entrada de datos

• Eventos:

- readable: hay datos para leer

- data: se ha leído un trozo y está disponible

- end: se agotó el stream

- close: se cerró el stream

- error: algo malo sucedió leyendo los datos

JavaScript y el Universo

Streams de lectura

var fs = require("fs");

var readStream = fs.createReadStream("/etc/passwd", { flags: "r", encoding: "ascii", autoClose: true});

readStream.on("data", function(chunk) { console.log("He leído:", chunk.length);});

readStream.on("end", function() { console.log("ya está!");});

Una fácil

Haz un programa que cuente las líneas de un fichero

Una fácil

Haz un programa que cuente las líneas de un fichero

• Tienes los argumentos con los que se ha llamado al programa en process.argv

JavaScript y el Universo

Streams de escritura

• Salida de datos

• Operaciones:- write(chunk, [encoding], [callback])- end([chunk], [encoding], [callback])

• Eventos:

- drain: el buffer del stream está vacío (puedes escribir más)

- finish: se ha terminado de escribir toda la info y se ha cerrado el stream

JavaScript y el Universo

Streams de escritura

var fs = require("fs");

var writeStream = fs.createWriteStream(process.argv[2], { flags: "w", encoding: "utf-8"});

for (var i=100; i--;) { writeStream.write(i + " líneas más para terminar...\n");}writeStream.end("FIN");

writeStream.on("finish", function() { console.log("Listo!");});

¿Preguntas?

Un buen momento para despejar dudas antes de seguir...

HTTP(por fin...)

HTTP

Node.js trae un servidor web estupendo

• Asíncrono

- No bloquea la hebra

- Cada cliente conectado consume muy poquitos recursos

- Genial para miles de conexiones simultáneas

• Relativamente rápido

• Interfaz sencilla

• HTTP puro y duro, sin adornos

• Basado en streams y eventos

HTTP

var http = require("http");

var server = http.createServer();

server.on("request", function(req, res) { res.end("Hola, Mundo!");});

server.listen(3000);

HTTP

var http = require("http");

var server = http.createServer();

server.on("request", function(req, res) { res.end("Hola, Mundo!");});

server.listen(3000);

El módulo

HTTP

var http = require("http");

var server = http.createServer();

server.on("request", function(req, res) { res.end("Hola, Mundo!");});

server.listen(3000);

Respuesta(stream)

Request(objeto)

Eventos!

HTTP

El servidor HTTP

• Eventos:

- connection- request

• Operaciones:- createServer([requestCallback])- listen(puerto, [hostname], [backlog], [callback])- close([callback])

HTTP

http.IncomingMessage (parametro “req”)

• Representa la petición HTTP del cliente

• Propiedaes:

- req.headers: cabeceras de la petición

- req.method: verbo HTTP

- req.url: url de la petición

- req.conneciton.remoteAddress: ip del cliente

HTTP

http.ServerResponse (parametro “res”)

• Representa la respuesta del servidor

• Stream de escritura

• Operaciones adicionales:

- res.writeHead(statusCode, [headers]): código HTTP y cabeceras de la respuesta

- res.statusCode: [propiedad] Otra forma de establecer el código HTTP de la respuesta

- res.setHeader(name, value): Otra forma de establecer las cabeceras, de una en una

Manos a la obra

Escribe un servidor web que devuelva la hora

Un consejo: nodemon

Para mejorar el flow de trabajo:

• Editar, matar el proceso, volver a lanzarlo, probar... muy tedioso!

• Instálate nodemonnpm install -g nodemon

• Reinicia el servidor cada vez que cambia el fichero

$ nodemon <fichero.js>

HTTP

Node.js trae un módulo para parsear URLs

var http = require("http"), url = require("url"), inspect = require("util").inspect;

var server = http.createServer();

server.on("request", function(req, res) { var urlData = url.parse(req.url, true); res.end(inspect(urlData, {colors: false}));});

server.listen(3000);

HTTP

Node.js trae un módulo para parsear URLs

var http = require("http"), url = require("url"), inspect = require("util").inspect;

var server = http.createServer();

server.on("request", function(req, res) { var urlData = url.parse(req.url, true); res.end(inspect(urlData, {colors: false}));});

server.listen(3000);

HTTP

Un poco más difícil

Escribe un servidor de ficheros

• Lee el pathname de la URL

• Busca un fichero con esa ruta dentro de ./public

• Si existe, lo sirve

• Si no existe, devuelve 404

Un poco más difícil: notas

fs.exists(filePath, callback)

• Llama al callback con true si el fichero filePath existe

• O con false si no existe

var fs = require("fs");

fs.exists("./hola.txt", function(exists) { console.log(exists);});

Un poco más difícil: notas

fs.readFile(filePath, callback)

• Otra manera de leer ficheros

• Intenta leer filePath e invoca a callback con dos parámetros:

- err: null si todo ha ido bien o, si hubo error, el error

- data: todo el contenido del fichero (si fue posible leerlo)

var fs = require("fs");

fs.readFile("./hola.txt", function(err, data) { if (err) { /* error! */ } console.log(data);});

Un poco más difícil: epílogo

¿Cómo podríamos añadir un caché para no leer los ficheros del disco duro más de una vez?

¿Cómo podríamos hacer que los ficheros cacheados se liberaran después de x minutos?

¿Cómo podríamos escribir un registro de acceso?

Un poco más difícil: variaciones

Haz un contador de aperturas de emails

• Sirviendo un .gif de 1x1 y contando cuantas veces lo sirves

• Mejor aún, cuenta solo a cuántas IPs lo sirves

Haz un servicio de avatares que cambie según:

• La hora del día

• La frecuencia con que es pedido (popularidad)

Un poco más difícil: variaciones

Haz un “servidor hellban”

Si la IP está en la lista negra, todas las peticiones tienen un delay aleatorio que se va incrementando con cada petición consecutiva

Servidor A/B Testing

Cada vez que un cliente pide un recurso:

• Se comprueba si ya tiene caso asignado (por IP) o se le asigna uno

• Se construye la ruta del recurso según el caso asignado y se sirve

• Si pide “success.png”, se marca su caso como exitoso y se le sirve la imágen

• /stats devuelve un JSON con info sobre los casos (visitas totales y visitas exitosas por cada caso)

Servidor A/B Testing

Tenéis maqueta y recursos en :

/tema1/abtesting

Promesas

Node.js y CPS

Node.js maneja la asincronía utilizando callbacks

• Continuation Passing Style

• Continuaciones explícitas como funciones

• “Cuando termines, ejecuta esta otra función”

Node.js y CPS

Los callbacks tienen muchas ventajas

• Muy fáciles de entender e implementar

• Familiares para el programador JavaScript

• Extremadamente flexibles (clausuras, funciones de primer orden, etc, ...)

• Un mecanismo universal de asincronía/continuaciones

Pero...

Node.js y CPS

var fs = require("fs");

fs.exists("./hola.txt", function(exists) { if (exists) { fs.readFile("./hola.txt", function(err, data) { if (err) { // MANEJO DE ERROR } else { fs.writeFile("./copia.txt", data, function(err) { if (err) { // MANEJO DE ERROR } else { console.log("OK!"); } }) } }) } else { // MANEJO DE ERROR }});

Node.js y CPS

var fs = require("fs");

fs.exists("./hola.txt", function(exists) { if (exists) { fs.readFile("./hola.txt", function(err, data) { if (err) { // MANEJO DE ERROR } else { fs.writeFile("./copia.txt", data, function(err) { if (err) { // MANEJO DE ERROR } else { console.log("OK!"); } }) } }) } else { // MANEJO DE ERROR }});

Node.js y CPS

var fs = require("fs");

fs.exists("./hola.txt", function(exists) { if (exists) { fs.readFile("./hola.txt", function(err, data) { if (err) { // MANEJO DE ERROR } else { fs.writeFile("./copia.txt", data, function(err) { if (err) { // MANEJO DE ERROR } else {

console.log("OK!");}

}) } }) } else { // MANEJO DE ERROR }});

Node.js y CPS

var fs = require("fs");

fs.exists("./hola.txt", function(exists) { if (exists) { fs.readFile("./hola.txt", function(err, data) { if (err) { // MANEJO DE ERROR } else { fs.writeFile("./copia.txt", data, function(err) { if (err) { // MANEJO DE ERROR } else {

console.log("OK!");}

}) } }) } else { // MANEJO DE ERROR }});

Pyramid of DoomCallback Hell

Node.js y CPS

var fs = require("fs");

fs.exists("./hola.txt", function(exists) { if (exists) { fs.readFile("./hola.txt", function(err, data) { if (err) { // MANEJO DE ERROR } else { fs.writeFile("./copia.txt", data, function(err) { if (err) { // MANEJO DE ERROR } else { console.log("OK!"); } }) } }) } else { // MANEJO DE ERROR }});

CPS vs. Promesas

Promesas

Una manera alternativa de modelar asincronía

• Construcción explícita del flujo de ejecución

• Separación en bloques consecutivos

• Manejo de errores más controlado

• Combinación de diferentes flujos asíncronos

Promesas

Una promesa = Un flujo de ejecución

promesa.then(function() { // bloque return readFilePromise("./hola.txt");}).then(function(data) { // bloque return writeFilePromise("./copia.txt");}).then(function() { console.log("listo!");}).fail(function(err) { // MANEJO DEL ERROR});

Promesas

Una promesa = Un flujo de ejecución

promesa.then(function() { // bloque return readFilePromise("./hola.txt");}).then(function(data) { // bloque return writeFilePromise("./copia.txt");}).then(function() { console.log("listo!");}).fail(function(err) { // MANEJO DEL ERROR});

Promesas

Una promesa = Un flujo de ejecución

promesa.then(function() { // bloque return readFilePromise("./hola.txt");}).then(function(data) { // bloque return writeFilePromise("./copia.txt");}).then(function() { console.log("listo!");}).fail(function(err) { // MANEJO DEL ERROR});

Promesas

Una promesa = Un flujo de ejecución

promesa.then(function() { // bloque return readFilePromise("./hola.txt");}).then(function(data) { // bloque return writeFilePromise("./copia.txt");}).then(function() { console.log("listo!");}).fail(function(err) { // MANEJO DEL ERROR});

Promesas

Una promesa = Un flujo de ejecución

promesa.then(function() { // bloque return readFilePromise("./hola.txt");}).then(function(data) { // bloque return writeFilePromise("./copia.txt");}).then(function() { console.log("listo!");}).fail(function(err) { // MANEJO DEL ERROR});

Promesas

Una promesa = Un flujo de ejecución

promesa.then(function() { // bloque return readFilePromise("./hola.txt");}).then(function(data) { // bloque return writeFilePromise("./copia.txt");}).then(function() { console.log("listo!");}).fail(function(err) { // MANEJO DEL ERROR});

Promesas

Una promesa = Un flujo de ejecución

promesa.then(function() { // bloque return readFilePromise("./hola.txt");}).then(function(data) { // bloque return writeFilePromise("./copia.txt");}).then(function() { console.log("listo!");}).fail(function(err) { // MANEJO DEL ERROR});

Promesas

Una promesa = Un flujo de ejecución

promesa.then(function() { // bloque return readFilePromise("./hola.txt");}).then(function(data) { // bloque return writeFilePromise("./copia.txt");}).then(function() { console.log("listo!");}).fail(function(err) { // MANEJO DEL ERROR});

Promesas

Una promesa = Un flujo de ejecución

promesa.then(function() { // bloque return readFilePromise("./hola.txt");}).then(function(data) { // bloque return writeFilePromise("./copia.txt");}).then(function() { console.log("listo!");}).fail(function(err) { // MANEJO DEL ERROR});

Promesas

Una promesa = Un flujo de ejecución

Promesas

¡Pero aún no hemos ejecutado nada! Solamente hemos construído el flujo

Promesas

¿Ventajas?

• Código mucho más ordenado y más legible

• Mejor control de errores

• Podemos manipular el flujo

- Añadir nuevas etapas

- Devolverlo en funciones

- Pasarlo como parámetro

• Podemos combinar varios flujos

Promesas

function copyFile(from, to) { return readFilePromise(from); .then(function(data) { // bloque return writeFilePromise(to); });}

copyFile("./hola.txt", "./copia.txt").then(function() { return copyFile("./otraCosa.txt", "./copia2.txt");}).then(function() { console.log("listo!");}).fail(function(err) { console.log("Oops!");})

Promesas

function copyFile(from, to) { return readFilePromise(from); .then(function(data) { // bloque return writeFilePromise(to); });}

copyFile("./hola.txt", "./copia.txt").then(function() { return copyFile("./otraCosa.txt", "./copia2.txt");}).then(function() { console.log("listo!");}).fail(function(err) { console.log("Oops!");})

Promesas

function copyFile(from, to) { return readFilePromise(from); .then(function(data) { // bloque return writeFilePromise(to); });}

copyFile("./hola.txt", "./copia.txt").then(function() { return copyFile("./otraCosa.txt", "./copia2.txt");}).then(function() { console.log("listo!");}).fail(function(err) { console.log("Oops!");})

Promesas

function copyFile(from, to) { return readFilePromise(from); .then(function(data) { // bloque return writeFilePromise(to); });}

copyFile("./hola.txt", "./copia.txt").then(function() { return copyFile("./otraCosa.txt", "./copia2.txt");}).then(function() { console.log("listo!");}).fail(function(err) { console.log("Oops!");})

Promesas

function copyFile(from, to) { return readFilePromise(from); .then(function(data) { // bloque return writeFilePromise(to); });}

copyFile("./hola.txt", "./copia.txt").then(function() { return copyFile("./otraCosa.txt", "./copia2.txt");}).then(function() { console.log("listo!");}).fail(function(err) { console.log("Oops!");})

Promesas

function copyFile(from, to) { return readFilePromise(from); .then(function(data) { // bloque return writeFilePromise(to); });}

copyFile("./hola.txt", "./copia.txt").then(function() { return copyFile("./otraCosa.txt", "./copia2.txt");}).then(function() { console.log("listo!");}).fail(function(err) { console.log("Oops!");})

Promesas

function copyFile(from, to) { return readFilePromise(from); .then(function(data) { // bloque return writeFilePromise(to); });}

copyFile("./hola.txt", "./copia.txt").then(function() { return copyFile("./otraCosa.txt", "./copia2.txt");}).then(function() { console.log("listo!");}).fail(function(err) { console.log("Oops!");})

Promesas

function copyFile(from, to) { return readFilePromise(from); .then(function(data) { // bloque return writeFilePromise(to); });}

copyFile("./hola.txt", "./copia.txt").then(function() { return copyFile("./otraCosa.txt", "./copia2.txt");}).then(function() { console.log("listo!");}).fail(function(err) { console.log("Oops!");})

Promesas

.then(success, [error])

• Concatena bloques

• El nuevo bloque (success)...

- Sólo se ejecuta si el anterior se ha ejecutado sin errores

- Recibe como parámetro el resultado del bloque anterior

- Devuelve el valor que se le pasará el siguiente bloque➡ Si es un dato inmediato, se pasa tal cual

➡ Si es una promesa, se resuelve antes de llamar al siguiente bloque

• El segundo parámetro pone un manejador de error

- Equivalente a llamar a .fail(error)

• .then(...) siempre devuelve una nueva promesa

Promesas

var promesa = readFilePromise("./hola.txt");

promesa = promesa.then(function(data) { console.log("Contenido del fichero: ", data);}, function(err) { console.log("Ooops!", err);})

Promesas

var promesa = readFilePromise("./hola.txt");

promesa = promesa.then(function(data) { console.log("Contenido del fichero: ", data);}, function(err) { console.log("Ooops!", err);})

Promesas

var promesa = readFilePromise("./hola.txt");

promesa = promesa.then(function(data) { console.log("Contenido del fichero: ", data);}, function(err) { console.log("Ooops!", err);})

Promesas

var promesa = readFilePromise("./hola.txt");

promesa = promesa.then(function(data) { console.log("Contenido del fichero: ", data);}, function(err) { console.log("Ooops!", err);})

Promesas

var promesa = readFilePromise("./hola.txt");

promesa = promesa.then(function(data) { console.log("Contenido del fichero: ", data);}, function(err) { console.log("Ooops!", err);})

Promesas

var promesa = readFilePromise("./hola.txt");

promesa = promesa.then(function(data) { console.log("Contenido del fichero: ", data);}, function(err) { console.log("Ooops!", err);})

Promesas

var promesa = readFilePromise("./hola.txt");

promesa = promesa.then(function(data) { console.log("Contenido del fichero: ", data);}, function(err) { console.log("Ooops!", err);})

Promesas

var promesa = readFilePromise("./hola.txt");

promesa.then(function(data) { return 1;}).then(function(uno) { return 2;}, function(err) { console.log("Oh, oh...");}).then(function(dos) { return 3;}).fail(function(err) { console.log("Oops!");});

data

1

2

3

Promesas

var promesa = readFilePromise("./hola.txt");

var promesa2 = promesa.then(function(data) { return 1;});

var promesa3 = promesa.then(function(data) { return 2;});

data data

1 2

Promesas

data data

1 2

var promesa = readFilePromise("./hola.txt");

var promesa2 = promesa.then(function(data) { return 1;});

var promesa3 = promesa.then(function(data) { return 2;});

promesa3.then(function(dos) { console.log("Ping!");});

Promesas

data data

12

var promesa = readFilePromise("./hola.txt");

var promesa2 = promesa.then(function(data) { return 1;});

var promesa3 = promesa.then(function(data) { return 2;});

promesa3.then(function(dos) { console.log("Ping!");});promesa3.then(function(dos) { console.log("Pong!");});

2

Promesas

data data

12

var promesa = readFilePromise("./hola.txt");

var promesa2 = promesa.then(function(data) { return 1;});

var promesa3 = promesa.then(function(data) { return 2;});

promesa3.then(function(dos) { console.log("Ping!");});promesa3.then(function(dos) { console.log("Pong!");}, function(err) { console.log("Oh, oh...");});

2

Promesas: walled garden

Vamos a empezar a trastear con promesas...

• Pero, de momento, con una librería de mentira

• Para asentar conceptos

• (y perder el miedo)

Promesas: walled garden

var fakePromise = require("./fakePromise");

var promise = fakePromise.gimmePromise();

promise.then(function() { return "hola";}).then(function(msg) { console.log(msg); return "mundo";}).then(function(msg) { console.log(msg);});

Promesas: walled garden

var fakePromise = require("./fakePromise");

var promise = fakePromise.gimmePromise();

promise.then(function() { return "hola";}).then(function(msg) { console.log(msg); return "mundo";}).then(function(msg) { console.log(msg);});

Promesas: walled garden

¿Qué se muestra por consola al ejecutar esto?

var fakePromise = require("./fakePromise");

var promise = fakePromise.gimmePromise();

promise.then(function() { return "hola";}).then(function(msg) { console.log(msg); return "mundo";}).then(function(msg) { console.log(msg);});

Promesas: walled garden

¿Por qué?

var fakePromise = require("./fakePromise");

var promise = fakePromise.gimmePromise();

promise.then(function() { return "hola";}).then(function(msg) { console.log(msg); return "mundo";}).then(function(msg) { console.log(msg);});

Promesas: walled garden

Una promesa = un flujo de ejecución

• Configuramos un árbol de flujos e ejecución

• Añadimos bloques a promesas

• Pero... ¿Cuándo se empieza a ejecutar?

Promesas: walled garden

Una promesa = un flujo de ejecución

• Configuramos un árbol de flujos e ejecución

• Añadimos bloques a promesas

• Pero... ¿Cuándo se empieza a ejecutar?

• Cuando se resuelva la primera promesa del árbol

Promesas: walled garden

Las promesas se resuelven o se rechazan

Si se resuelven:

• Se resuelven a un valor (si es un bloque, su valor de retorno)

• Representan el estado “OK, puede seguir el siguiente”

Si se rechazan:

• Representan un error

• La ejecución cae hasta el siguiente manejador de errores

• Se saltan todos los estados desde el error hasta el manejador

Promesas: walled garden

var fakePromise = require("./fakePromise");

var promise = fakePromise.gimmePromise();

promise.then(function() { return "hola";}).then(function(msg) { console.log(msg); return "mundo";}).then(function(msg) { console.log(msg);});

promise.resolve(42);

Promesas: walled garden

var fakePromise = require("./fakePromise");

var promise = fakePromise.gimmePromise();

promise.then(function() { return "hola";}).then(function(msg) { console.log(msg); return "mundo";}).then(function(msg) { console.log(msg);}).fail(function(err) { console.log(err); return "MAL!"})

promise.reject(new Error("Oops!"));

Promesas: walled garden

Otra manera de rechazar una promesa es lanzar una excepción desde el interior de un bloque

promise.then(function() { return "hola";}).then(function(msg) { console.log(msg); throw new Error("Oh, oh..."); return "mundo";})

Promesas: walled garden

var fakePromise = require("./fakePromise");

var promise = fakePromise.gimmePromise();

promise.then(function() { return "hola";}).then(function(msg) { console.log(msg); throw new Error("Oh, oh..."); return "mundo";}).then(function(msg) { console.log(msg);}).fail(function(err) { console.log(err); return "MAL!"})

promise.resolve(42);

Promesas: walled garden

var fakePromise = require("./fakePromise");

var promise = fakePromise.gimmePromise();

promise.then(function() { return "hola";}).then(function(msg) { console.log(msg); throw new Error("Oh, oh..."); return "mundo";}).then(function(msg) { console.log(msg);}).fail(function(err) { console.log(err); return "MAL!"})

promise.resolve(42);

Promesas: walled garden

var fakePromise = require("./fakePromise");

var promise = fakePromise.gimmePromise();

promise.then(function() { return "hola";}).then(function(msg) { console.log(msg); throw new Error("Oh, oh..."); return "mundo";}).then(function(msg) { console.log(msg);}).fail(function(err) { console.log(err); return "MAL!"})

promise.resolve(42);

Promesas: walled garden

var fakePromise = require("./fakePromise");

var promise = fakePromise.gimmePromise();

promise.then(function() { return "hola";}).then(function(msg) { console.log(msg); throw new Error("Oh, oh..."); return "mundo";}).then(function(msg) { console.log(msg);}).fail(function(err) { console.log(err); return "MAL!"})

promise.resolve(42);

Promesas: walled garden

var fakePromise = require("./fakePromise");var promise = fakePromise.gimmePromise();

promise.then(function(msg) { console.log(msg); throw new Error("Oh, oh..."); return "mundo";}).then(function(msg) { console.log(msg); return "OK!";}).fail(function(err) { console.log(err); return "MAL!";}).then(function(msg) { console.log(msg);})

promise.resolve("hola");

Promesas: walled garden

Propagación de promesas

var fakePromise = require("./fakePromise");

var promise = fakePromise.gimmePromise(), promise2 = fakePromise.gimmePromise();

promise.then(function(val) { console.log("promise:", val); promise2.then(function(val) { console.log("promise2:", val); });});

promise.resolve(42);setTimeout(promise2.resolve.bind(promise2, 12), 2000);

Promesas: walled garden

Propagación de promesas

var fakePromise = require("./fakePromise");

var promise = fakePromise.gimmePromise(), promise2 = fakePromise.gimmePromise();

promise.then(function(val) { console.log("promise:", val); promise2.then(function(val) { console.log("promise2:", val); });});

promise.resolve(42);setTimeout(promise2.resolve.bind(promise2, 12), 2000);

Promesas: walled garden

Propagación de promesas

var fakePromise = require("./fakePromise");

var promise = fakePromise.gimmePromise(), promise2 = fakePromise.gimmePromise();

promise.then(function(val) { console.log("promise:", val); return promise2}).then(function(val) { console.log("promise2:", val);});

promise.resolve(42);setTimeout(promise2.resolve.bind(promise2, 12), 2000);

Promesas: walled garden

Propagación de promesas

var fakePromise = require("./fakePromise");

var promise = fakePromise.gimmePromise(), promise2 = fakePromise.gimmePromise();

promise.then(function(val) { console.log("promise:", val); return promise2}).then(function(val) { console.log("promise2:", val);});

promise.resolve(42);setTimeout(promise2.resolve.bind(promise2, 12), 2000);

Promesas: walled garden

¿Y al revés?

var fakePromise = require("./fakePromise");

var promise = fakePromise.gimmePromise(), promise2 = fakePromise.gimmePromise();

promise.then(function(val) { console.log("promise:", val); return promise2}).then(function(val) { console.log("promise2:", val);});

setTimeout(promise.resolve.bind(promise, 42), 2000);promise2.resolve(12);

Diferidos

En el mundo real, las cosas son un poco distintas

Las “promesas” están divididas en dos objetos:

• La promesa en sí

- Interfaz limitada a la construcción de flujos

- .then, .fail y algún método más

• Su diferido

- Interfaz limitada a controlar el estado

- .reject y .resolve

Diferidos

var Q = require("q");

var defer = Q.defer(), promise = defer.promise;

// flujo

promise.then(function(val) { console.log("val:", val);}, function(err) { console.log("Error!");});

// estado

defer.resolve(42);

Diferidos

var Q = require("q");

var defer = Q.defer(), promise = defer.promise;

// flujo

promise.then(function(val) { console.log("val:", val);}, function(err) { console.log("Error!");});

// estado

defer.resolve(42);

Diferidos

Cumplen dos roles diferentes:

• Diferido lo controla el gestor del recurso/proceso que se está modelando

• Promesa es el interfaz para que el consumidor del recurso pueda construir el flujo que necesita

Promesas

Un ejemplo realista: readFilePromise(file)• Implementa la función

• Basándote en fs.readFile()

• Devuelve una promesa

• Se resuelve con el contenido del fichero

• O se rechaza con el error

Promesas

function readFilePromise(filePath) { // ???}

readFilePromise("./hola.txt").then(function(contenido) { console.log("contenido:", contenido.toString());}, function(err) { console.log("ERROR!", err);});

Promesas

var fs = require("fs"), Q = require("q");

function readFilePromise(filePath) { var defer = Q.defer(); fs.readFile(filePath, function(err, data) { err ? defer.reject(err) : defer.resolve(data); }) return defer.promise;}

readFilePromise("./hola.txt").then(function(contenido) { console.log("contenido:", contenido.toString());}, function(err) { console.log("ERROR!", err);});

Promesas

var fs = require("fs"), Q = require("q");

function readFilePromise(filePath) { var defer = Q.defer(); fs.readFile(filePath, function(err, data) { err ? defer.reject(err) : defer.resolve(data); }) return defer.promise;}

readFilePromise("./hola.txt").then(function(contenido) { console.log("contenido:", contenido.toString());}, function(err) { console.log("ERROR!", err);});

Gestor del recurso

Consumidor del recurso

Promesas: paréntesis

Intenta modificar el servidor de ficheros estáticos para que utilice promesas:

• fileExistsPromise

• readFilePromise

Promesas: combinaciones

Q es una librería muy potente

• Trae un montón de funcionalidad

• Muy recomendable leer la documentación

• Nosotros vamos a ver dos cosas esenciales:

- Combinación de promesas en paralelo

- Adaptadores de llamadas con formato Node.js

Q.all([promise, promise, ...])

Crea una promesa que representa al conjunto

• La nueva promesa se resuelve cuando todas las del conjunto se hayan resuelto

• Su valor es un array con los valores de las promesas

• Si una del conjunto es rechazada, la nueva promesa también es rechazada

Q.all([promise, promise, ...])

var Q = require("q");

var def1 = Q.defer(), prom1 = def1.promise, def2 = Q.defer(), prom2 = def2.promise;

Q.all([prom1, prom2]).then(function(v) { console.log("Todas resueltas!"); console.log(v); // [42, 13]})

def1.resolve(42);setTimeout(def2.resolve.bind(def2, 13), 1000);

Q.all([promise, promise, ...])

Q.spread([v1, v2], callback)

“Reparte” valores de un array en parámetros

var Q = require("q");

Q.spread([1,2,3,4], function(a, b, c, d) { console.log(a, b, c, d); // 1 2 3 4})

Q.spread([v1, v2], callback)

Invoca automáticamente a Q.all(...)!

var Q = require("q");

var def1 = Q.defer(), def2 = Q.defer(), def3 = Q.defer(), pro1 = def1.promise, pro2 = def2.promise, pro3 = def3.promise;

pro1.then(function(v1) { console.log(v1); return [pro2, pro3];}).spread(function(v2, v3) { console.log(v2, v3);});

def1.resolve(42);setTimeout(def2.resolve.bind(def2, 13), 1000);setTimeout(def3.resolve.bind(def3, 71), 200);

Q.spread([v1, v2], callback)

Invoca automáticamente a Q.all(...)!

var Q = require("q");

var def1 = Q.defer(), def2 = Q.defer(), def3 = Q.defer(), pro1 = def1.promise, pro2 = def2.promise, pro3 = def3.promise;

pro1.then(function(v1) { console.log(v1); return [pro2, pro3];}).spread(function(v2, v3) { console.log(v2, v3);});

def1.resolve(42);setTimeout(def2.resolve.bind(def2, 13), 1000);setTimeout(def3.resolve.bind(def3, 71), 200);

Q.ninvoke(ctx, method, arg, [arg])

Adaptador para convertir llamadas Node.js en promesas

var Q = require("q"), fs = require("fs");

Q.ninvoke(fs, "readFile", "./hola.txt").then(function(data) { console.log("Contenido: ", data.toString());}).fail(function(err) { console.log("Oops!", err);});

Q(promiseOrValue)

Homogeneizar valores:

• Si es una promesa, se queda tal cual

• Si es un valor, se convierte en una promesa que se resuelve a ese valor

Q(42).then(function(v) { console.log(v); // 42})

Q(promise).then(function(v) { console.log(v); // resolución de promise})

Promesas: gotcha

¿Qué pasa aquí?

var d = Q.defer(), promise = d.promise;

promise.then(function(v) { console.log(v); throw new Error("Vaya por Dios!");}).then(function() { console.log("Hola?");});

d.resolve(42);

promise.done()

Finaliza el flujo, levantando los errores que no se hayan manejado

var d = Q.defer(), promise = d.promise;

promise.then(function(v) { console.log(v); throw new Error("Vaya por Dios!");}).then(function() { console.log("Hola?");}).done();

d.resolve(42);

promise.done()

La regla es:

• Si vas a devolver la promesa, déjala abierta

• Si eres el consumidor final de la promesa, asegúrate de cerrarla con .done()

¡A teclear!

Vamos el primer ejercicio complicadillo: un servidor de ficheros versionado

• La herramienta que hemos estado utilizando para compartir código

• Con promesas

• (Si alguien se atreve, que lo intente hacer sin promesas...)

Necesitas saber...

Manejar rutas: require(“path”)• path.resolve(base, ruta): ruta relativa a ruta

absoluta (partiendo de base)

• path.relative(base, ruta): ruta absoluta a ruta relativa (desde base)

Necesitas saber...

fs.readdir(ruta): Listar ficheros de un directorio

• Devuelve un array de strings con los nombres de los ficheros

• No es recursivo

• No hay manera de saber si una entrada es un fichero o un directorio

var fs = require("fs"), Q = require("q");

Q.ninvoke(fs, "readdir", ".").then(function(list) { console.log(list);})

Necesitas saber...

fs.stat(ruta): Info sobre un fichero/directorio

• stats.isDirectory(): true si es un directorio

• stats.mtime: Date de la última modificación

var fs = require("fs"), Q = require("q");

Q.ninvoke(fs, "stat", ".").then(function(stats) { console.log("es dir?", stats.isDirectory()); console.log("última modificación:", stats.mtime);})

Primer paso: listado recursivo

Escribe una función listAllFiles(ruta) que:

• Devuelva una promesa

• La promesa se resuelva con un listado recursivo de todos los ficheros que hay dentro del directorio

• Para cada fichero, genere un objeto del tipo{path: “/ruta/absoluta.txt”, stats: statsDelFichero}

listAllFiles(".").then(function(list) { console.log(list);}).done()

Primer paso: listado recursivo

Segundo paso: listener

Función somethingChanged(ruta):

• Devuelve true si algún fichero ha sido modificado desde la última vez que se invocó

• Impleméntalo utilizando stats.mtime como referencia

Utilizando esa función, escribe un “demonio” que monitorize un directorio y escriba un mensaje por consola cada vez que hay cambios

Tercer paso: volcado a memoria

Función readAllFiles(ruta):

• Completa el resultado de listAllFiles() añadiendo una tercera propiedad “contents” con los contenidos del fichero

Haz que el demonio lea todos los ficheros si detecta algún cambio y guarda el resultado en posiciones consecutivas de un array. Este va a ser nuestro control de versiones.

Cuarto paso: sirve los datos

El interfaz web tiene las siguientes rutas:

• /: listado de versiones

• /list?version=<n>: listado de ficheros de la versión n

• /ruta/al/fichero?version=<n>: busca el fichero con la ruta correspondiente en la versión n y lo sirve

• la versión “latest” siempre apunta a la versión más reciente

Virguerías opcionanes

Escribe un módulo simpleRoute de modo que podamos definir rutas así:

var routes = require("./simpleRoute");

routes.get("/", function(req, res) {})

routes.get("/list", function(req, res) {})

routes.default(function(req, res) {})

Virguerías opcionales

Además, simpleRoute modifica el parámetro req.url y lo sustituye por la url parseada

var routes = require("./simpleRoute");

routes.get("/list", function(req, res) { console.log(req.url.pathname); console.log(req.url.query.version);})

Express

¿Qué es express?

Un framework web para Node.js

• Estrictamente web (microframework)

• Sencillo y flexible

• Muy popular

• Se adapta muy bien a la filosofía de Node

• Similar a Sinatra, Sylex, Flask, Spark, ...

¿Qué es express?

Express nos va ayudar con...

• Rutas

• Parámetros

• Formularios y subida de ficheros

• Cookies

• Sesiones

• Templates

¿Qué es express?

Express NO nos va ayudar con...

• Base de datos / ORM

• Autenticación de usuarios

• Seguridad

• Migraciones

• Deployment

• Organización del código

¿Qué es express?

Más concretamente, express...

• Construye sobre http• Procesando la petición por un stack de middleware que

se encarga de decorar las peticiones

- Asocia rutas a manejadores

- Decorar los objetos req y res (parseo de parámetros, multipart, etc,...)

- Rendear templates

• Nosotros escogemos qué middlewares queremos usar, y en qué orden

¿Qué es express?

var express = require("express");

var app = express();

// configuración + rutas

app.listen(3000);

¿Qué es express?

Equivalente a:

var express = require("express"), http = require("http");

var app = express();

// configuración + rutas

http.createServer(app).listen(3000);

¿Qué es express?

Equivalente a:

var express = require("express"), http = require("http");

var app = express();

// configuración + rutas

http.createServer(app).listen(3000);

¿Qué es express?

var app = require("express")();

app.get("/", function(req, res) { res.end("Hola desde express!");});

app.listen(3000);

¿Qué es express?

var app = require("express")();

app.get("/", function(req, res) { res.end("Hola desde express!");});

app.listen(3000);

Verbo RutaManejador

¿Qué es express?

var app = require("express")();

app.get("/", function(req, res) { res.end("Hola desde express!");});

app.listen(3000);

Objeto Stream

¿Qué es express?

¿Qué nos aporta express, exactamente?

• Depende de los middlewares que usemos!

• Algunas cosas vienen por defecto

Request

req.params: parámetros de la ruta

app.get("/user/:id", function(req, res) { req.params.id;});

Request

req.query: la querystring, parseada

app.get("/search", function(req, res) { // GET /search?text=nodejs+express req.query.text;});

Request

• req.ip: IP del cliente conectado

• req.host: Hostname del servidor

• req.xhr: ¿Es ajax?

• req.acceptedLanguages: Array de locales

• req.host: Hostname del servidor

• Mas info en http://express.js.com/api.html

Response

res.cookie(nombre, valor, [opciones])• Modifica la cookie “nombre” con el valor “valor”

res.cookie("visitas", "1", {domain: ".ejemplo.com"});

Response

res.redirect([status], url)• Redirige a url

• El código de estado es opcional (302 por defecto)

res.redirect(301, "http://www.google.com");

Response

res.send([status], body)• Envía una respuesta (escribe en el buffer)

• Lo más adecuado para respuestas sencillas

- no streaming

• Automatiza ciertas cabeceras

- Content-Type, Content-Length

• Convierte objetos a JSON

res.send(500, {msg: "Oh, oh..."});

Response

Muchos otros métodos auxiliares:

• Cabeceras

• Envío de ficheros

• JSONP

• Content-Type

• ¡Lee la documentación!

http://expressjs.com/api.html

Application

app.configure([entorno], callback)• Configurar la aplicación

• Opcionalmente: configuración para un entorno

- “development”

- “testing”

- “production”

- etc

app.configure('development', function(){ app.set('db uri', 'localhost/dev');})

Application

¿Qué significa “configurar la aplicación”?

• Crear algunas propiedades globales

• Especificar el stack de middleware

Application

app.set(prop, value) / app.get(prop)

• Escribe/consulta valores globales en app

• Básicamente inútil...

• Excepto para cambiar alguna configuración más avanzada

app.set('title', 'Redradix');app.get('title');

Middleware

Middleware son módulos “plug and play” que se pueden apilar arbitrariamente en cualquier orden y proveen cierta funcionalidad

• Filtros: procesan tráfico entrate/saliente, pero no responden a ninguna request. (ejemplo: bodyParser)

• Proveedores: ofrecen respuestas automáticas a algún tipo de petición (ejemplo: static provider)

Middleware

app.configure(function() { app.use(express.favicon()); app.use(express.logger('dev')); app.use(express.bodyParser()); app.use(express.methodOverride());

app.use(express.cookieParser('your secret here')); app.use(express.session());

app.use(app.router); app.use(express.static(__dirname + '/public'));});

Middleware

app.configure(function() { app.use(express.favicon()); app.use(express.logger('dev')); app.use(express.bodyParser()); app.use(express.methodOverride());

app.use(express.cookieParser('your secret here')); app.use(express.session());

app.use(app.router); app.use(express.static(__dirname + '/public'));});

Middleware

app.configure(function() { app.use(express.favicon()); app.use(express.logger('dev')); app.use(express.bodyParser()); app.use(express.methodOverride());

app.use(express.cookieParser('your secret here')); app.use(express.session());

app.use(app.router); app.use(express.static(__dirname + '/public'));});

Middleware

Express trae unos cuantos preinstalados

• http://www.senchalabs.org/connect/

Una lista de módulos de terceros

• https://github.com/senchalabs/connect/wiki

express.favicon(ruta)

Sirve el favicon de la aplicación

• Debe ser el primero

• Para evitar capas inncesarias

• log

• parseo

• cookies

• etc...

express.logger([opciones])

Registro de actividad

• Muchas opciones...

http://www.senchalabs.org/connect/logger.html

• Se suele poner debajo de express.favicon()

express.cookieParser([secret])

Parsea las cookies de la petición

• Opcional: firmar cookies con secret

• Crea los objetos req.cookies y req.signedCookies

app.configure(function(){ app.use(express.cookieParser('secreto'));})

app.get("/", function(req, res) { console.log(req.cookies); console.log(req.signedCookies); res.send(200);})

express.bodyParser()

Parsea el cuerpo de las peticiones POST

• Decodifica

- application/json

- application/x-www-form-urlencoded

- multipart/form-data

• Crea el objeto req.body con los parámetros POST

• Crea el objeto req.files con los ficheros que se han subido desde un formulario

express.cookieSession([opciones])

Inicializa y parsea los datos de sesión del usuario

• Crea el objeto req.session

• Utilizando cookies como almacenamiento

• Opciones:

- secret: firma de segurdad para la cookie

- maxAge: duración, en ms (default: sin caducidad)

- path: ruta para la que es válida la cookie (default: /)

- httpOnly: protegida del cliente (default: true)

express.cookieSession([opciones])

var express = require("express"), app = express();

app.configure(function(){ app.use(express.cookieParser('secreto')); app.use(express.cookieSession());})

app.get("/", function(req, res) { req.session.visitas || (req.session.visitas = 0); var n = req.session.visitas++; res.send("Me has visitado: " + n + " veces!");})

app.listen(3000);

express.static(dir)

Sirve los ficheros estáticos dentro de dir• ¡Muy útil! Se pone cerca del final

• Cachea los ficheros

• La variable global __dirname contiene el directorio donde reside el script en ejecución

app.router

El enrutado de la aplicación

• Sirve para específicar exáctamente en qué momento quieres que se procesen las rutas de tu app

Middleware

app.configure(function() { app.use(express.favicon()); app.use(express.logger('dev')); app.use(express.bodyParser()); app.use(express.methodOverride());

app.use(express.cookieParser('your secret here')); app.use(express.cookieSession());

app.use(app.router); app.use(express.static(__dirname + '/public'));});

Templates

Express tiene un mecanismo para rendear templates

• Agnóstico

• Modular

• Simple

• NO trae ningún motor de templates por defecto

Templates

res.render(view, [locals], callback)• view: ruta del template

• locals: valores a interpolar

• callback: function(err, html) { ... }

Templates

Tenemos muchos motores de templates para elegir!

https://npmjs.org/browse/keyword/template

• Haml

• Hogan/Mustache

• Twig/Swig

• Ejs

• Jinja

• Jade

....

Ejs

<h1><%= title %></h1><ul> <% for(var i=0; i<supplies.length; i++) { %> <li> <a href='supplies/<%= supplies[i] %>'> <%= supplies[i] %> </a> </li> <% } %></ul>

Jade

Templates

Algunas propuestas originales/innovadoras:

CoffeeKup: http://coffeekup.org/

Weld: https://github.com/tmpvar/weld

Domo: http://domo-js.com/

Templates

Para utilizarlos desde express:

var express = require("express"), app = express();

app.configure(function() { app.engine("jade", require("jade").__express); app.set("views", "./views"); app.set("view engine", "jade");})

app.get("/", function(req, res) { res.render("welcome", {user: "Pepito"});})

app.listen(3000)

Templates

Para utilizarlos desde express:

var express = require("express"), app = express();

app.configure(function() { app.engine("jade", require("jade").__express); app.set("views", "./views"); app.set("view engine", "jade");})

app.get("/", function(req, res) { res.render("welcome", {user: "Pepito"});})

app.listen(3000)

Templates

Para utilizarlos desde express:

var express = require("express"), app = express();

app.configure(function() { app.engine("jade", require("jade").__express); app.set("views", "./views"); app.set("view engine", "jade");})

app.get("/", function(req, res) { res.render("welcome", {user: "Pepito"});})

app.listen(3000)

Templates

Para utilizarlos desde express:

var express = require("express"), app = express();

app.configure(function() { app.engine("jade", require("jade").__express); app.set("views", "./views"); app.set("view engine", "jade");})

app.get("/", function(req, res) { res.render("welcome", {user: "Pepito"});})

app.listen(3000)

Templates

Para utilizarlos desde express:

var express = require("express"), app = express();

app.configure(function() { app.engine("jade", require("jade").__express); app.set("views", "./views"); app.set("view engine", "jade");})

app.get("/", function(req, res) { res.render("welcome", {user: "Pepito"});})

app.listen(3000)

Templates

Donde /views/welcome.jade sería algo así:

doctype 5html(lang="es") body h1 Bienvenido, #{user}

Templates

Express es bastante listo (buenos defaults)

var express = require("express"), app = express();

app.get("/", function(req, res) { res.render("welcome.jade", {user: "Pepito"});})

app.listen(3000)

Blog en 15 minutos

Vamos a poner en práctica lo que hemos visto

• Vamos a hacer un “blog”, simplificado

• Sin BBDD

• Tiene 7 rutas (4 pantallas)

- GET /posts

- GET /posts/new

- POST /posts

- GET /posts/:id

- GET /posts/edit

- PUT /posts/:id

- DEL /posts/:id

Blog en 15 minutos

¿Sin base de datos?

• Guarda los datos en memoria, en un array

• Cuidado si usas nodemon, porque al guardar se resetea el servidor y se pierde el contenido del array

Un truco: app.param(...)

Mapear parámetros de url

app.param("postid", function(req, res, next, postId) { req.post = posts.find(postId); if (err || !req.post) { next(err || new Error("Post no encontrado ("+postId+")")); } else { next(); }});

app.get("/posts/:postid", function(req, res) { console.log(req.post);});

Más Middleware

Escribir middleware para express es muy sencillo:

• Una función que recibe tres parámetros:

- req

- res

- next

• Al terminar su tarea, tiene que invocar a next()- Sin parámetro: se invoca al siguiente middleware del stack

- Con parámetro: se cambia la ruta a lo que se pase como parámetro

Más Middleware

Por ejemplo, un logger simple:

app.configure(function(){ app.use(function(req, res, next) { console.log(" * %s: %s %s", req.connection.remoteAddress, req.method, req.url); next(); });})

Más Middleware

Haz un logger que muestre:

• Hora de la petición

• IP

• Método

• Ruta

• Tiempo de respuesta de la petición

Pero solo si el tiempo de respuesta está muy por encima de la media!

Más Middleware

Haz un módulo de “mensajes flash”

• Mensajes que solo están disponibles para la siguiente request

• Muy útiles para informar de ciertos sucesos

- “Post creado con éxito”

- “Email enviado”

- Errores

- etc...

Más Middleware

app.post("/posts", function(req, res) { var post = createPost(req.body); if (post) { req.flash.message("Post creado con éxito!"); } else { req.flash.error("No se ha podido crear..."); } res.redirect("/posts/" + post.id);})

app.get("/posts/:postid", function(req, res) { console.log(req.flash.message()); console.log(req.flash.error());})

Más Middleware

app.post("/posts", function(req, res) { var post = createPost(req.body); if (post) { req.flash.message("Post creado con éxito!"); } else { req.flash.error("No se ha podido crear..."); } res.redirect("/posts/" + post.id);})

app.get("/posts/:postid", function(req, res) { console.log(req.flash.message()); console.log(req.flash.error());})

Más Middleware

app.post("/posts", function(req, res) { var post = createPost(req.body); if (post) { req.flash.message("Post creado con éxito!"); } else { req.flash.error("No se ha podido crear..."); } res.redirect("/posts/" + post.id);})

app.get("/posts/:postid", function(req, res) { console.log(req.flash.message()); console.log(req.flash.error());})

Más Middleware: Errores

Un caso especial de middleware: una función que reciba un parámetro más

var express = require("express"), app = express();

app.get("/", function(req, res) { throw new Error("??")})

app.use(function(err, req, res, next) { console.log("* SOCORRO! ALGO VA MAL! ", err); res.send(500)})

app.listen(3000)

Más Middleware: Errores

Más correcto así:

var express = require("express"), app = express();

app.get("/", function(req, res, next) { next(new Error("esta app no hace nada"));})

app.use(function(err, req, res, next) { console.log("* SOCORRO! ALGO VA MAL! ", err); res.send(500)})

app.listen(3000)

Más Middleware: Errores

Escribe un manejador de errores para el blog

• Página 404 (página no encontrada o ruta no existe)

• Página 500 (mostrar en caso de error/excepción)

• Registra la fecha, la ruta y el mensaje de error en errors.log

Más Middleware

Dos maneras de activar middlewares

• Globalmente (app.use), activos para toda la app

• Locales para ciertas rutas

Más Middleware

var express = require("express"), app = express()

function logThis(req, res, next) { console.log(" -> ", req.url); next();}

app.get("/", logThis, function(req, res) { res.send("Ok!");})

app.listen(3000);

Más Middleware

Afinar la funcionalidad de cada ruta

• Pre-procesado de URL (parámetros, formato)

• Seguridad

• Caché

• Métricas

Más Middleware

Escribe un módulo para cachear la respuesta de una ruta durante X ms

Duración en ms

app.get("/", new StaticCache(5*1000), function(req, res) { res.send("Envío mi respuesta..." + new Date());})

Más Middleware

Una variación:

• Cachea a un fichero

• Sustituye res.write por el stream del fichero

• Cuando se dispare “finish”, sirve lo que has almacenado

• Puedes añadirlo al ejercicio del Blog para cachear las páginas de detalle de un post

SimpleAuth

Módulo para autenticar usuarios simpleAuth.js• Sencillo y flexible

• Independiente de la BBDD

• Utilizando las sesiones de express

• Middleware de ruta

SimpleAuth

El módulo provee 3 middlewares:

• createSession: comprueba los credenciales de usuario y crea una nueva sesión si son correctos

• requiresSession: protege una ruta y solo deja pasar a usuarios autenticados

• destroySession: desloguea a un usuario

SimpleAuth

var auth = require("./simpleAuth");

app.post("/login", auth.createSession({ redirect: "/secret" }))

app.get("/secret", auth.requiresSession, function(req, res) { res.end("Hola, " + req.user.email);})

app.get("/logout", auth.destroySession, function(req, res) { res.end("Te has deslogueado");});

SimpleAuth

El usuario ha de definir una estrategia:

serializeUser: ¿Cómo serializar un usuario?

deserializeUser: ¿Cómo des-serializar un usuario?

checkCredentials: ¿Son correctos los credenciales?

SimpleAuth

var auth = require("./simpleAuth")

var users = [{email: "admin@asdf.com", pass: "asdf", id: 0}];

auth.setStrategy({ serializeUser: function(user) { return user.id; }, deserializeUser: function(userId, cb) { cb(users[userId]); }, checkCredentials: function(email, pass, cb) { var admin = users[0]; if (email === admin.email && pass === admin.pass) { cb(null, admin); } else { cb(null, false); } }});

SimpleAuth

auth.createSession(config)• config.username: nombre del campo del formulario

• config.password: nombre del campo del formulario

• config.redirect: URL en caso de éxito

• config.failRedirect: URL en caso de error

- default: /login

• Devuelve: una función para usar como middleware

SimpleAuth

auth.createSession(config)• Genera una función que...

1. Extrae username y password según config de req.body

2. Llama a strategy.checkCredentials con username y password

3. Si son credenciales correctos:3.1. Crea una entrada en la sessión o cookie con el resultado de llamar a strategy.serializeUser(usuario), donde usuario es lo que devuelve strategy.checkCredentials

3.2. Redirige a la URL de éxito

4. Si no son correctos:4.1. Redirige a la URL de error

SimpleAuth

auth.requiresSession1. Se asegura de que exista la entrada adecuada en la

sesión o cookie

2. Llama a strategy.deserializeUser con el valor

3. Guarda el usuario des-serializado en req.user

4. En caso de error, borra la sesión o cookie

SimpleAuth

auth.destroySession1. Borra la entrada adecuada en la sesión o cookie

SimpleAuth

auth.setStrategy(strategy)1. Configura la estretagia:

- strategy.serializeUser

- strategy.deserializeUser

- strategy.checkCredentials

- strategy.loginRoute

SimpleAuth

Protege la edición del blog con simpleAuth• Utilizando un array de usuarios escrito directamente en

el código

• Página de login + link logout + solo los usuarios logueados pueden crear o editar posts

• Si te sobra tiempo, haz que los usuarios se puedan registrar

Redis

¿Qué es Redis?

Una base de datos NoSQL...

• Clave/valor

• Extremadamente rápida

• Persistencia y transacciones

• Estructuras de datos

- Listas

- Conjuntos

- Hashes

• Pub/sub

¿Qué es Redis?

Un servidor de estructuras de datos

• Sin tablas, ni queries ni JOINs

• Una manera de pensar muy diferente

• Muchas queries muy rápidas

• Muy fácil de aprender, muy fácil de usar mal

¿Qué es Redis?

Redis es una opción estupenda para...

• Datos de acceso inmediato

- Sesiones

- Cachés

• Escrituras muy rápidas

- Logs

- Conteos y estadísticas

• Canal de comunicación pub/sub

- Comunicación entre procesos (workers, big data)

- Chats :)

¿Qué NO es Redis?

Redis es una opción terrible para...

• Relaciones de datos complejas

• Búsquedas

¿Para qué sirve?

Mi consejo:

• NO bases tu app en redis

• A no ser que sea muy simple o sepas muy bien lo que haces

• Utiliza Redis como complemento a tu BBDD

- Cachés

- Estadísticas

- Workers

- Sesiones

- Registros

- Colas

La idea general

Redis es un gran Hash de claves

• No hay concepto de tabla/colección

• El valor fundamental es la cadena (string)

• Pero una clave puede contener un valor más complejo

- Hashes (string -> string)

- Listas (de strings)

- Sets (de strings)

- Sets ordenados (de strings)

Requisitos

Necesitas:

• Tener instalado y arrancado redis

• Tener a mano la documentación➡ http://redis.io/commands

• npm install hiredis redis• Como alternativa:

➡ c9.io

- nada-nix install redis- npm install hiredis redis- redis-server --port 16379 --bind $IP

Primeros pasos

Primero, necesitas conectarte al servidor

var redis = require("redis");

var client = redis.createClient();// c9.io:// var client = redis.createClient(16379, process.env.IP);

Primeros pasos

Ahora puedes mandar comandos a Redis

• client.nombreDelComando(arg, callback)• client.nombreDelComando(arg1, arg2, callback)• callback: function(err, value) { }

SET / GET

Guardar y recuperar un valor

var redis = require("redis"), client = redis.createClient();

client.set("miClave", "miValor", function(err, val) { console.log(arguments); client.get("miClave", function(err, value) { console.log("valor: ", value); });})

SET / GET

Guardar y recuperar un valor

var redis = require("redis"), client = redis.createClient();

client.set("miClave", "miValor", function(err, val) { console.log(arguments); client.get("miClave", redis.print);})

SET / GET

Guardar y recuperar un valor

var redis = require("redis"), client = redis.createClient();

client.set("miClave", "miValor", function(err, val) { console.log(arguments); client.get("miClave", function(err, value) { console.log("valor: ", value); });})

SET / GET

Guardar y recuperar un valor

var redis = require("redis"), client = redis.createClient();

client.set("miClave", "miValor", function(err, val) { console.log(arguments); client.get("miClave", function(err, value) { console.log("valor: ", value); });})

SET / GET

Guardar y recuperar un valor

var redis = require("redis"), Q = require("q"), client = redis.createClient();

Q.ninvoke(client, "set", "miClave", "miValor").then(function() { return Q.ninvoke(client, "get", "miClave");}).then(function(value) { console.log("Valor: ", value);}).done();

SET / GET

Guardar y recuperar un valor

Un truco: redis-cli monitor

Para ver qué está pasando en Redis

Redis = Strings!!

Cuidado con los valores. Han de ser siempre strings.

var redis = require("redis"), Q = require("q"), client = redis.createClient();

Q.ninvoke(client, "set", "miClave", {un: "objeto"}).then(function() { return Q.ninvoke(client, "get", "miClave");}).then(function(value) { console.log("Valor: ", value); // Valor: [object Object]}).done();

Redis = Strings!!

var obj = {propiedad: "valor"};

Q.ninvoke(client, "set", "miClave", JSON.stringify(obj)).then(function() { return Q.ninvoke(client, "get", "miClave");}).then(function(value) { console.log("Valor: ", value); // Valor: {"propiedad": "valor"}}).done();

Redis = Strings!!

var serializable = { toString: function() { return JSON.stringify(this); }};

var obj = Object.create(serializable, { propiedad: { enumerable: true, value: "valor" }});

Q.ninvoke(client, "set", "miClave", obj).then(function() { return Q.ninvoke(client, "get", "miClave");}).then(function(value) { console.log("Valor: ", value); // Valor: {"propiedad": "valor"}}).done();

Redis = Strings!!

var serializable = { toString: function() { return JSON.stringify(this); }};

var obj = Object.create(serializable);obj.propiedad = "valor";

Q.ninvoke(client, "set", "miClave", obj).then(function() { return Q.ninvoke(client, "get", "miClave");}).then(function(value) { console.log("Valor: ", value); // Valor: {"propiedad": "valor"}}).done();

Redis = Strings!!

Una solución más radical:

Object.prototype.toString = function() { return JSON.stringify(this);};

var obj = {propiedad: "valor"};

Q.ninvoke(client, "set", "miClave", obj).then(function() { return Q.ninvoke(client, "get", "miClave");}).then(function(value) { console.log("Valor: ", value); // Valor: {"propiedad": "valor"}}).done();

DEL / EXISTS / TYPE / RENAME

Operaciones con claves

• DEL: borrar una clave

• EXSITS: comprobar si una clave existe

• TYPE: el tipo de valor almacenado en una clave

• RENAME: cambiar el nombre de la calve

DEL / EXISTS / TYPE / RENAME

var redis = require("redis"), Q = require("q"), client = redis.createClient();

Q.ninvoke(client, "exists", "MiClave").then(function(exists) { console.log( exists? "Existe!" : "No existe..." ); return Q.ninvoke(client, "set", "MiClave", "MiValor");}).then(function() { return Q.ninvoke(client, "rename", "MiClave", "MyKey");}).then(function() { return Q.ninvoke(client, "type", "MyKey");}).then(function(type) { console.log("MyKey es de tipo", type);}).done();

Operaciones con cadenas

• APPEND: añade el valor a la cadena

• DECR/INCR: Decrementa/incrementa el valor en 1

• DECRBY/INCRBY: Dec/inc el valor en N

• GETSET: Modifica el valor y devuelve el viejo

• STRLEN: Longitud del valor

Operaciones con cadenas

var op = Q.ninvoke.bind(Q, client);

op("set", "miClave", 1).then(function() { return op("incrby", "miClave", 10);}).then(function() { return op("decr", "miClave");}).then(function() { return op("getset", "miClave", "fin");}).then(function(valor) { console.log("VALOR: ", valor); return op("strlen", "miClave")}).then(function(len) { console.log("Len: ", len);}).done();

Operaciones múltiples

• MGET: Trae el valor de varias claves

• MSET: Modifica el valor de varias claves

var op = Q.ninvoke.bind(Q, client);

op("mset", "miClave", 1, "otraClave", 2).then(function() { return op("mget", "miClave", "otraClave");}).then(function(values) { console.log(values[0], ",", values[1]);}).done()

Listas

• LPUSH/RPUSH key value [value ...]• LPOP/RPOP key• LINDEX key index• LSET key index value• LLEN key• LRANGE key start stop: trae el rango de elementos

• LTRIM key start stop: limita al rango start-stop

• RPOPLPUSH source dest: RPOP sour + LPUSH dest

Listas

var op = Q.ninvoke.bind(Q, client), makeOp = function() { var args = arguments; return function() { return op.apply(null, args); } };

op("del", "miClave").then(makeOp("rpush", "miClave", 1, 2, 3, 4)).then(makeOp("lrange", "miClave", 0, 2)).then(function(values) { console.log(values); return op("ltrim", "miClave", 0, 1);}).then(makeOp("llen", "miClave")).then(function(len) { console.log(len);}).done()

A teclear un poco!

Modifica el ejercicio del blog del tema anterior...

• Para que utilice Redis como BD

• Guardar los posts como objetos JSON

• Guarda los usuarios en claves tipo:

- “user:admin@asdf.com”: <JSON del usuario>

• Modifica la estrategia del autenticación

Hashes

• HSET key field value: Modifica el valor de campo field del hash en value

• HGET key field: Consulta el valor de campo field del hash en value

• HEXISTS key field: Existe el campo field?

• HKEYS/HVALS key: Todos los campos/valores

• HGETALL: Trae el hash entero

• HINCRBY key field n: Incrementa el campo en n

• HMGET/HMSET: Operaciones múltiples

Hashes

op("del", "miClave").then(function() { return op("hmset", "miClave", "a", 1, "b", 2, "c", 3);}).then(function() { return op("hincrby", "miClave", "c", 100);}).then(function() { return op("hgetall", "miClave");}).then(function(hash) { console.log(hash); // { a: '1', b: '2', c: '103' }}).done()

Conjuntos

• SADD key member [member ...]: añadir miembros

• SREM key member [member ...]: quitar miembros

• SCARD key: cardinal (número de elementos)

• SDIFF key [key ...]• SINTER key [key ...]• SUNION key [key ...]• SISMEMBER key member: ¿es miembro?

• SMEMBERS key: todos los miembros

Conjuntos

var op = Q.ninvoke.bind(Q, client), makeOp = function() { var args = arguments; return function() { return op.apply(null, args); } };

op("sadd", "miConjunto", 1, 1, 2, 3, 5, 8, 13).then(makeOp("sadd", "miConjunto2", 1, 3, 5, 7, 9, 11, 13)).then(makeOp("sinter", "miConjunto", "miConjunto2")).then(function(values) { console.log("Intersección:", values); return op("sdiff", "miConjunto", "miConjunto2");}).then(function(values) { console.log("Diferencia:", values);}).done()

Ejercicio: acotador de URLs

Escribe un acortador de URLs con Redis

• Registro y login de usuarios utilizando simpleAuth

• Redirección automática de urls

• Estadísticas de visita

- Cada IP cuenta una sola vez

- ¿Estadísticas por fecha? ¿Tendencias?

- ¿Geotracking de visitas (freegeoip.net)?

http.get("http://freegeoip.net/json/83.44.23.171", function(res) { res.on("data", function(data) { console.log(JSON.parse(data)); });});

MongoDB

¿Qué es MongoDB?

Una base de datos NoSQL...

• Sin esquema

• Alto rendimiento

• Almacena documentos BSON

• Enfocada en escalabilidad horizontal

• Lenguaje de consultas potente

• Sin transacciones

• Agregado de datos

¿Qué es MongoDB?

Una base de datos de documentos

• No impone forma a los datos

• No necesita migraciones/reestructuración de la BBDD

• Permite estructuras muy complejas

• Herramientas potentes de agregado con JavaScript

- Map-Reduce

- Aggregation Pipeline

¿Qué es MongoDB?

Con algunos extras interesantes...

• Índices geoespaciales

• Búsquedas FTS

• Almacenamiento eficiente de blobs y ficheros

• Sharding automático

• Replicación

¿Qué es MongoDB?

Es una buena alternativa para... ¡muchas cosas!

• Prototipos y aplicaciones simples

• Hacer la transición de front a back

• Aplicaciones con mucha carga de escritura

• Agregado de datos a un nivel medio/alto

• Aplicaciones con datos muy heterogéneos

• Enormes colecciones de datos (sharding)

• Almacenar ficheros (sharding)

¿Qué NO es MongoDB?

No te dejes seducir demasiado:

• Mongo no puede hacer JOINs!

• El lenguaje de consulta menos potente que SQL

• No tiene transacciones!

• La velocidad baja al subir la seguridad (escritura)

• Ten cuidado:

- Es muy fácil empezar con MongoDB

- Si tu app crece mucho... vas a necesitar JOINs

La idea general

Un almacen de documentos

• Básicamente, objetos JSON (BSON)

• Dividido en colecciones

• Consultas basadas en la estructura del documento

• ¡Se integra genial con JavaScript!

Requisitos

Necesitas

• Instalar y arrancar mongod

‣ mongod --dbpath <path> --nojournal• Tener la documentación a mano

➡ http://docs.mongodb.org/manual/

➡ http://mongodb.github.io/node-mongodb-native/

• npm install mongodb• En c9.io

•<comandos para instalar mongodb>

Primeros pasos

Para conectarte al servidor

var MongoClient = require("mongodb").MongoClient , ObjectID = require("mongodb").ObjectID;

var client = Q.ninvoke(MongoClient, "connect", "mongodb://127.0.0.1:27017/dbname");

client.fail(function(e) { console.log("ERROR conectando a Mongo: ", e)});

Primeros pasos

La BD está dividida en colecciones. Para abrir una colección:

var collection = client.then(function(db) { return db.collection("coleccion");});

Documento

Un documento es un objeto BSON

{ "_id" : ObjectId("524872a99c50880000000001"), "email" : "test@asdf.com", "password" : "asdf1234", "name" : "Test User", "date" : 1380479657300, "token" : "hm6ly43v.0o1or"}

Documento

Un documento es un objeto BSON

{ "_id" : ObjectId("524872a99c50880000000001"), "email" : "test@asdf.com", "password" : "asdf1234", "name" : "Test User", "date" : 1380479657300, "token" : "hm6ly43v.0o1or"}

Documento

Un documento puede contener arrays y otros documentos

{ "_id" : ObjectId("5249a2e9b90687d56453b2f3"), "text" : "Soy un comentario", "user" : { "_id" : ObjectId("524872a99c50880000000001"), "nombre" : "Test User", "avatar" : "/img/as09a8sd09.jpg" }, "tags" : [ "test", "prueba" ]}

Documento

Un documento puede contener arrays y otros documentos

{ "_id" : ObjectId("5249a2e9b90687d56453b2f3"), "text" : "Soy un comentario", "user" : { "_id" : ObjectId("524872a99c50880000000001"), "nombre" : "Test User", "avatar" : "/img/as09a8sd09.jpg" }, "tags" : [ "test", "prueba" ]}

Documento

Un documento puede contener arrays y otros documentos

{ "_id" : ObjectId("5249a2e9b90687d56453b2f3"), "text" : "Soy un comentario", "user" : { "_id" : ObjectId("524872a99c50880000000001"), "nombre" : "Test User", "avatar" : "/img/as09a8sd09.jpg" }, "tags" : [ "test", "prueba" ]}

Documentos

MongoDB no puede hacer JOINs

• Sin embargo, se pueden empotrar documentos y arrays

• Profundidad y complejidad arbitraria

• El límite: documento < 16MB

• Se suelen favorecer los diseños desnormalizados

✓ Mucho más cómodos

✓ Más eficientes en Mongo (con cuidado)

๏ Redundancia...

๏ Posible inconsistencia

๏ Actualizar los datos a mano cuando cambian

Colecciones

Una colección es una agrupación de documentos

• Puede alojar cualquier documento (no impone estructura)

• Puede alojar documentos con diferentes formas

• Operaciones de consulta

• Es donde se ponen los índices

Colecciones

Operaciones sobre una colección:

• collection.save: guardar/actualizar un documento

• collection.insert: inserta un documento

• collection.findOne: recuperar un documento

• collection.find: recuperar varios documentos

• collection.remove: borrar uno o varios documentos

• collection.drop: elimina la colección

• collection.rename: cambia de nombre la colección

• collection.count: número de documentos

Colecciones

MongoDB trae un cliente de línea de comandos

• mongo <host>/<dbname>• Ejecuta JavaScript

• Muy práctico para explorar

Colecciones

Colecciones

Colecciones

Desde Node.js

var MongoClient = require("mongodb").MongoClient , ObjectID = require("mongodb").ObjectID , Q = require("q");

Q.ninvoke(MongoClient, "connect", "mongodb://127.0.0.1:27017/dbname").then(function(db) { var micoleccion = db.collection("micoleccion"), op = Q.ninvoke.bind(Q, micoleccion);

op("insert", {uno: 1, dos: 2}) .then(function() { return op("insert", {tres: 3, cuatro: [4]}); }) .then(function() { return op("findOne", {uno: 1}); }) .then(function(doc) { console.log(doc); }) .done();});

Consulta

Dos operaciones fundamentales:

•findOne: devuelve un documento

•find: devuelve varios documentos en un cursor

Consulta

Ambos reciben como parámetro las conditiones que tiene que cumplir el documento:

var micoleccion = db.collection("micoleccion"), op = Q.ninvoke.bind(Q, micoleccion);

Q.ninvoke(micoleccion, "findOne", {uno: 1}) .then(function(doc) { console.log(doc); });

Q.ninvoke(micoleccion, "findOne", {dos: {$gt: 0}}) .then(function(doc) { console.log(doc); });

Consulta

Los operadores de búsqueda:

• $gt / $gte: mayor/mayor o igual

• $lt / $lte: menor/menor o igual

• $ne: diferente

• $in / $nin: en/no en array de valores

micol.findOne({ valor: {$in: [ 5, 15 ] }}, cb)

Consulta

Los operadores lógicos:

• $or: se cumple alguna cláusula

• $and: se cumplen todas las cláusulas

• $nor: el resultado opuesto

• $not: no se cumplen todas las cláusulas

micol.findOne({$or: [{valor: 5},{precio: {$gt: 15 }}

]}, callback)

Consulta

Más operadores interesantes:

• Operadores de evaluación

- Regex

- Código JavaScript arbitrario

• Operaciones geoespaciales➡ http://docs.mongodb.org/manual/reference/operator/nav-query/

Cursores

El operador find(...) devuelve un cursor

• Representa un conjunto de resultados

• cursor.count(callback): cantidad de documentos

• cursor.limit(n): limitar a n documentos

• cursor.skip(n): saltarse los n primeros documentos

• cursor.nextObject(callback): siguiente documento

• cursor.each(callback): para cada doc, en orden

• cursor.toArray(callback): convierte el cursor en array

Cursores

cursor.sort(opciones, [callback])• Ordenar los resultados

• Opciones del tipo:

‣ [[“campo”, 1], [“otroCampo”, -1]]

‣ 1 para ascendente, -1 para descendente

coleccion.find() .sort([['a', -1]]) .nextObject(function(err, item) { // ... })

Modificación

El operador más sencillo para modificar: save• Si el documento es nuevo (no tiene _id), lo inserta

• Si el documento ya existe, lo modifica

db.micol.save({ nombre: "Test User" })

Modificación

insert(<documento o array>)• Inserta uno o varios documentos en la colección

db.micol.insert([ { nombre: "Test User" }, { nombre: "Test User 2" }])

Escritura

remove(<patrón>)• Elimina los documentos que satisfagan la búsqueda

op("insert", {a: 1}) .then(function() { return op("remove", {a: 1}); }) .then(function(doc) { return op("count"); }) .then(function(n) { console.log(n); }) .done()

Ejercicio: Clón de Digg

Vamos a hacer un clon de Digg/HN/Reddit/...

• El usuario se puede registrar/loguear

• Puede añadir links (url+título+descripción)

• Puede votar +1/-1 los links (solo una vez)

• Puede comentar los links posteados

• Puede votar +1/-1 los comentarios

• Recientes/Populares

Ejercicio: Clón de Digg

La mecánica es ligeramente distinta al ej. anterior

• El cliente es una Single Page App en Backbone que sólo hace peticiones a una API JSON (autenticadas por token)

• No hay vistas, solo respondemos con datos JSON

• Algunas rutas tienen parámetros extra...

Ejercicio: Clón de Digg

Peculiaridades:

• /me: información sobre el usuario logueado

• /posts?s=<seccion>&page=<page>- seccion == “hottest”: ordenados por voto

- else: ordenados por fecha desc.

• /posts/:postsid/vote/up• /posts/:postsid/vote/down• /comments/:commentsid/vote/up• /comments/:commentsid/vote/down

Ejercicio: Clón de Digg

Para loguearse:

• El usuario manda user+pass a POST /session

• El servidor:

1. Genera un token para el usuario

2. Lo guarda en user.token

3. Devuelve el JSON del usuario al cliente

Ejercicio: Clón de Digg

Datos del usuario

{ "email" : "asdf@asdf.com", "name" : "Test User", "date" : 1380479657300, "_id" : ObjectId("524872a99c50880000000001"), "token" : "hm6ly43v.0o1or"}

Ejercicio: Clón de Digg

Datos del post

{ "_id" : ObjectId("524889c998ea730000000001"), "date" : 1380485577004, "description" : "asdf", "link" : "http://www.google.com", "ncomments" : 9, "title" : "titulo", "user" : { "name" : "Test User", "_id" : ObjectId("524872a99c50880000000001") }, "votes" : 0}

Ejercicio: Clón de Digg

Datos del comentario

{ "text" : "Comentario!", "post_id" : ObjectId("524889c998ea730000000001"), "user" : { "name" : "Test User", "_id" : ObjectId("524872a99c50880000000001") }, "votes" : 0, "date" : 1380485577013, "_id" : ObjectId("524889c998ea730000000002")}

Socket.io

¿Qué son websockets?

Protocolo de comunicación

• Full-duplex

• Una sola conexión permanente

• Stream de mensajes

• Contenido en tiempo real

¿Qué son websockets?

Es decir...

• El cliente puede enviar y recibir datos en tiempo real

• Orientado a “eventos” (mensajes)

• Siempre conectado

• Baja latencia

Websockets y Node.js

Funcionan especialemente bien con Node.js

• El servidor maneja muchas conexiones simultáneas

• Buena integración con JSON

• Eventos

¿Para qué sirven?

Fundamentalmente, para:

• Actividades colaborativas

• Juegos multijugador

• Acelerar ciertas operaciones

• Enviar datos

• Cargar recursos

• En resumen: tiempo real en vez de “a petición”

Socket.io

Vamos a usar Socket.io

• Una librería para manipular websockets

• Muy popular

• Fallback para navegadores obsoletos

• Muy fácil de usar

➡ http://socket.io/

Socket.io

Socket.io tiene dos partes:

• Servidor (Node.js):

• Cliente:

var express = require("express"), server = require("http").createServer(), io = require("socket.io").listen(server), app = express();

server.on("request", app).listen(3000);

<script src="socket.io/socket.io.js"></script>

Socket.io

Los sockets emiten eventos

• Un evento = un “mensaje”

• Se pueden pasar parámetros

• socket.on(mensaje, callback)• socket.emit(mensaje, [param1, param2, ...])

Socket.io

server.js

var express = require("express"), app = express(), server = require("http").createServer(app), io = require("socket.io").listen(server);

app.use(express.static(__dirname + "/public"));

io.sockets.on("connection", function(socket) { socket.emit("ping"); socket.on("pong", function() { console.log("PONG!"); });});

server.listen(3000);

Socket.io

server.js

var express = require("express"), app = express(), server = require("http").createServer(app), io = require("socket.io").listen(server);

app.use(express.static(__dirname + "/public"));

io.sockets.on("connection", function(socket) { socket.emit("ping"); socket.on("pong", function() { console.log("PONG!"); });});

server.listen(3000);

Socket.io

server.js

var express = require("express"), app = express(), server = require("http").createServer(app), io = require("socket.io").listen(server);

app.use(express.static(__dirname + "/public"));

io.sockets.on("connection", function(socket) { socket.emit("ping"); socket.on("pong", function() { console.log("PONG!"); });});

server.listen(3000);

Socket.io

index.html

<html> <head> <script src="/socket.io/socket.io.js"></script> <script type="text/javascript"> var socket = io.connect("http://localhost:3000");

socket.on("ping", function() { console.log("PING!"); socket.emit("pong"); }); </script> </head> <body></body></html>

Socket.io

index.html

<html> <head> <script src="/socket.io/socket.io.js"></script> <script type="text/javascript"> var socket = io.connect("http://localhost:3000");

socket.on("ping", function() { console.log("PING!"); socket.emit("pong"); }); </script> </head> <body></body></html>

Socket.io

index.html

<html> <head> <script src="/socket.io/socket.io.js"></script> <script type="text/javascript"> var socket = io.connect("http://localhost:3000");

socket.on("ping", function() { console.log("PING!"); socket.emit("pong"); }); </script> </head> <body></body></html>

Socket.io

Eventos reservados (servidor):

• io.sockets.on(“connection”, cb)• socket.on(“message”, cb)• socket.on(“disconnect”, cb)

Cliente:• socket.on(“connect”, cb)• socket.on(“disconnect”, cb)• socket.on(“error”, cb)• socket.on(“message”, cb)

Socket.io

Métodos (servidor)

• socket.broadcast.emit(msg)- les llega a todos menos el emisor

• socket.disconnect()• socket.emit(msg) / socket.on(msg)

Métodos (client)

• var socket = io.connect(host)• socket.disconnect()• socket.emit(msg) / socket.on(msg)

Un Chat! (simple)

Vamos a hacer un chat sencillo:

• Los usuarios se loguean eligiendo un nick

• Todo el mundo escribe en la misma sala común

• No tenemos indicador de presencia

Un Chat! (simple)

En el cliente:

• Chat.registerHandler(cb): callback cuando el usuario escribe

• Chat.postMsg(user, msg): Muestra un mensaje de otro

• Chat.showMyMsg(user, msg): Muestra un mensaje propio

Donde:

• user: {avatar: <string>, name: <string>}• msg: {text: <string>, time: <date>}

Canales

Con Socket.io podemos crear canales o namespaces para agrupar los receptores

var express = require("express"), app = express(), server = require("http").createServer(app), io = require("socket.io").listen(server);

app.use(express.static(__dirname + "/public"));

io.of("/canal").on("connection", function(socket) { socket.emit("ping");});

server.listen(3000);

Canales

En el cliente:

<script src="/socket.io/socket.io.js"></script> <script type="text/javascript"> var socket = io.connect("http://localhost:3000/canal");

socket.on("ping", function() { console.log("PING!"); });

</script>

Canales

Podemos tener varios canales simultáneos (multiplexando el mismo websocket)

io.of("/canal").on("connection", function(socket) { socket.emit("ping");});

io.of("/otro").on("connection", function(socket) { socket.emit("bang!");});

Canales

En el cliente:

var canal = io.connect("http://localhost:3000/canal"), otro = io.connect("http://localhost:3000/otro");

canal.on("ping", function() { console.log("PING!"); });

otro.on("bang!", function() { console.log("Estoy herido!"); });

Ahora, multisala

Utilizando namespaces, los usuarios pueden:

• Loguearse/registrarse (simpleauth)

• Crear salas

• Unirse y salirse de las salas creadas

• Escribir en la sala en la que estén

Ahora, multisala

Consejos:

• Guarda los sockets de cada usuario en un objeto

• Utiliza la sesión (o req.user) para saber en qué sala está un usuario (clave del objeto de sockets + canal)

• Crea mensajes para:

- Un usuario ha entrado en la sala

- Un usuario ha salido de la sala

- Alguien postea un mensaje