Desarrollo de una red social Como sabemos, las redes sociales han tomado un protagonismo muy fuerte en el mundo de la comunicación, debido a la experiencia que generan en los usuarios de eventos en tiempo real y escalabilidad. Desarrollaremos un proyecto utilizando Node como lenguaje de programación y Redis como modelo de persistencia.
▼
El porqué del proyecto .............. 2
Lista de amigos conectados ............. 40 Informar cuando
▼
Definición del proyecto.............. 2
se conecta un usuario....................... 43
Creación del proyecto ........................ 3
Creación del sistema de chat............ 48
Configuración del archivo app.js......... 4
Creación del sistema de posts .......... 53
Registro y login de usuarios ............. 12
Vista de la base de datos.................. 59
Creación de la página principal ........ 25 Definición de los eventos
▼
Resumen................................... 61
▼
Actividades............................... 62
para un usuario logueado ................. 27 Envío de una solicitud de amistad .... 31
Servicio de atención al lector:
[email protected]
2
APÉNDICE. DESARROLLO DE UNA RED SOCIAL
El porqué del proyecto La razón por la cual desarrollaremos una red social es porque reúne todas las características y condiciones que deben tener los sistemas escalables estudiados a lo largo de este libro. Una red social tiene que ser capaz de manejar eventos de interacción con los usuarios en tiempo real y debe funcionar bajo un modelo de persistencia que permita operar con millones de registros y solicitudes en el menor tiempo posible. A nuestro ejemplo lo llamaremos SocialRedis y utilizará las siguientes tecnologías:
• • • • • • •
*Node.js *Socket.IO *Express *Redis *Crypto (módulo para encriptación) *Ejs (motor de plantillas) *Session.socket.io (módulo para manejo de sesiones en Socket.IO) El sistema se encargará de funcionar de manera autónoma como
servidor propio, y ofrecerá todas las características específicas (tales como ruteo, manejo de plantillas y estilos css, eventos de tiempo real e interacción con la base de datos).
Definición del proyecto Consiste en el desarrollo de una red social similar a Facebook, donde los usuarios podrán registrarse; desde el primer momento se mostrará en tiempo real la cantidad de usuarios registrados. Consideremos que podrán enviar solicitudes de amistad y contar con un sistema de notificaciones, que permitirá aceptar o rechazar una solicitud en el mismo instante en que otro usuario la envía. Además, se podrán crear posts para compartir entre los amigos. Por último, implementaremos un sistema de chat donde los usuarios podrán tener múltiples conversaciones en forma simultánea.
www.redusers.com
3
SISTEMAS WEB ESCALABLES
Figura 1. En la pantalla principal los usuarios pueden compartir información e interactuar en tiempo real con los demás usuarios.
Instalación de herramientas previas Para comenzar, necesitamos una herramienta que nos permita administrar la base de datos. Como vamos a trabajar exclusivamente con Redis y Node, podemos utilizar el módulo Redis Commander instalándolo mediante el siguiente comando: npm install -g redis-commander
GRACIAS A NODEMON NO SERÁ NECESARIO
También es recomendable utilizar Nodemon,
REINICIAR LA
ya que nos evitaremos reiniciar la aplicación en cada cambio que hagamos. Lo podemos instalar mediante el siguiente comando:
APLICACIÓN LUEGO DE CADA CAMBIO
npm install -g nodemon
Creación del proyecto Para dar inicio a nuestro ejemplo debemos abrir una consola y dirigirnos a la unidad C:; una vez ahí, ejecutaremos los siguientes comandos para crear la aplicación e instalar los módulos necesarios:
www.redusers.com
4
APÉNDICE. DESARROLLO DE UNA RED SOCIAL
express –e SocialRedis cd SocialRedis && npm install npm install redis npm install crypto npm install socket.io npm install session.socket.io
Utilizamos el parámetro –e para crear la aplicación, con lo cual indicamos que vamos a utilizar el módulo ejs como motor de plantillas; luego instalamos los módulos redis, crypto, socket.io y sesión.socket.io. Una vez creado el entorno de trabajo, podemos ejecutar la aplicación con Nodemon y comprobar que está en funcionamiento. En la consola escribimos el siguiente comando y posteriormente abrimos un navegador para verificar su funcionamiento: nodemon app.js Por defecto, el sistema va a funcionar en la siguiente dirección: http://127.0.0.1:3000.
Configuración del archivo app.js Ahora trabajaremos directamente en la aplicación. Lo primero que haremos es configurar el archivo principal, dirigiéndonos a la raíz del proyecto y abriendo el archivo app.js. En este punto tenemos que realizar algunos cambios importantes, por lo que vamos a borrar todo el contenido del archivo e iremos agregando el código necesario. Para comenzar, vamos a incluir los módulos:
APLICACIONES DE EJEMPLO DE EXPRESS Express provee una extensa lista de ejemplos que cubren todos los aspectos del framework, como autenticación, manejo de plantillas, cookies, solución de errores, parámetros de entrada y administración de sesiones, entre otros. Podemos acceder a los ejemplos en la siguiente dirección: https://github.com/ visionmedia/express/tree/master/examples.
www.redusers.com
5
SISTEMAS WEB ESCALABLES
// definicion de modulos var express = require(‘express’) , routes
= require(‘./routes’)
, user
= require(‘./routes/user’)
, http
= require(‘http’)
, path
= require(‘path’)
, redis
= require(‘redis’)
, crypto , ssio
= require(‘crypto’) = require(‘session.socket.io’);
Inicializamos las variables agregando lo siguiente: // inicializacion de variables var app
= express()
, server
= http.createServer(app)
, io
= require(‘socket.io’).listen(server)
, sessionStore = new express.session.MemoryStore() , cookieParser = express.cookieParser(‘!@#$%^&*()1234567890qwerty’) , sessionIO
= new ssio(io, sessionStore, cookieParser);
Para continuar definimos el puerto que utilizará el sistema, el directorio y también el motor de plantillas: // configuraciones para todos los entornos app.set(‘port’, process.env.PORT || 3000); app.set(‘views’, __dirname + ‘/views’); app.set(‘view engine’, ‘ejs’);
SELECCIONAR UN FRAMEWORK Actualmente los desarrolladores nos encontramos con una gran cantidad de frameworks MVC para organizar y estructurar las aplicaciones en JS. Para ayudarnos a resolver el problema existe un proyecto llamado TodoMVC, el cual ofrece una funcionalidad similar para varios frameworks como Backbone, Ember o AngularJS.
www.redusers.com
6
APÉNDICE. DESARROLLO DE UNA RED SOCIAL
Para continuar nos encargamos de establecer las herramientas o middlewares de Express que utilizaremos: // middlewares de Express app.use(express.favicon()); app.use(express.logger(‘dev’)); app.use(express.bodyParser()); app.use(cookieParser); app.use(express.session({ store: sessionStore })); app.use(express.methodOverride()); app.use(app.router); app.use(express.static(path.join(__dirname, ‘public’)));
Posteriormente realizamos la definición del uso del manejador de errores para el entorno de desarrollo: // configuraciones para el entorno de desarrollo if (‘development’ == app.get(‘env’)) { app.use(express.errorHandler()); }
Para continuar, determinamos las variables globales que serán visibles desde todos los archivos del sistema: // definicion de variables globales global.usrEnLinea = []; global.titulo
= ‘Social Redis’;
global.autor
= ‘Carlos Alberto Benitez [2013]’;
global.db
= redis.createClient(6379, ‘localhost’);
global.io
= io;
global.sessionIO = sessionIO; global.crypto
= crypto;
La variable usrEnLinea servirá para almacenar los usuarios conectados al sistema; titulo y autor son parte de la leyenda del sitio; db es un objeto
www.redusers.com
7
SISTEMAS WEB ESCALABLES
que contendrá la conexión a la base de datos; io
LA VARIABLE CRYPTO
es también un objeto y contendrá la instancia de
PERMITIRÁ LA
Socket.io; en sessionIO almacenamos la instancia del objeto sesión.socket.io, que nos servirá para acceder
ENCRIPTACIÓN DE
a las variables de sesión desde una conexión por
LAS CLAVES
socket; por último, la variable crypto nos servirá para encriptar las claves de acceso.
DE ACCESO
A continuación debemos realizar la definición de las rutas que manejaremos en el sistema. Primero ruteamos las solicitudes por GET para el acceso y salida del sistema: // GET - definicion de las rutas app.get(‘/’, routes.index); //app.get(‘/salir’, user.logout);
Notemos que, para el ruteo de la página principal, utilizamos el método index del archivo index.js y, para la salida, el método logout del archivo user.js. Consideremos que esta última está comentada ya que aún no realizamos la definición del método. Luego, a través del método POST, determinamos el ruteo para las acciones de registro, login, creación de un post, solicitud de amistad y respuesta a una solicitud de amistad. // POST - definicion de las rutas //app.post(‘/registro’, user.registro); //app.post(‘/login’, user.login); //app.post(‘/setPost’, user.setPost); //app.post(‘/setSolicitud’, user.setSolicitud); //app.post(‘/setRespuestaSolicitud’, user.setRespuestaSolicitud);
Todas las acciones definidas utilizarán los métodos del archivo user.js, pero como todavía no las hemos creado están comentadas. A medida que vayamos creando los métodos iremos descomentando el ruteo para cada una de ellas. Por último, ponemos en marcha el servidor indicándole el puerto que vamos a utilizar:
www.redusers.com
8
APÉNDICE. DESARROLLO DE UNA RED SOCIAL
// listen server.listen(app.get(‘port’), function(){ console.log(‘Express server listening on port ‘ + app.get(‘port’)); });
Definición de la apariencia del sistema Vamos a establecer la apariencia del sistema. Primero debemos dirigirnos al directorio public/ y renombrar las carpetas images por img, javascripts por js y stylesheets por css; haremos estos cambios únicamente para simplificar la legibilidad de los archivos. Dentro de la carpeta css/, vamos a crear un archivo con el nombre style.css, con el siguiente código: body{background: url(“/img/bg.jpg”) repeat scroll 0 0 transparent; font: 12px/1.5em Arial,Helvetica,sans-serif; margin: 0; padding: 0; text-align: center; color: #444444;} header{height: 50px; padding: 20px 5px;} header #buscador{width: 370px; float: left;} header #buscador #tBuscar{width: 270px;} header #buscador #bBuscar{height: 28px;} header #notificacion{float: right; padding: 9px 0 0; width: 203px;} header #notificacion #usr{float: left; font-size: 14px; height: 100%; margin: 5px 0; width: 150px;} h1{background: url(“/img/icon-profile.png”) no-repeat scroll left top transparent; color: #666; float: left; font-size: 29px; line-height: 55px; margin: 0; padding: 0 10px 0 42px; text-align: left; text-shadow: 1px 1px 0 #FFF; width: 175px;} input[type=”text”],
www.redusers.com
9
SISTEMAS WEB ESCALABLES
input[type=”password”]{margin: 7px 0 0;} a{color: #444444; display: block; text-decoration: none;} nav{clear: both; float: left; width: 140px; padding: 0 10px;} nav ul{list-style: none outside none; padding: 0; margin: 0;} nav ul li{border: 1px solid transparent; cursor: pointer; font-size: 15px; height: 100%; line-height: 46px; padding: 0; width: auto;} nav ul li:hover,nav ul .active {background-color: #EEEEEE; backgroundimage: -moz-linear-gradient(center top , #EEEEEE, #E0E0E0); border: 1px solid #CCCCCC; box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1) inset; color: #333333;} input[type=”submit”]{background: url(“/img/bg-verde.png”) repeat-x left top #47C516; border: 1px solid #149E1F; border-radius: 2px 2px 2px 2px; color: #FFFFFF; padding: 5px; text-align: center;} footer{clear: both; height: 80px; padding: 10px; text-align: center;} footer p{margin: 0 auto; padding: 30px 0; width: 300px;} .contenedor{background: url(“/img/bg-sombras.png”) no-repeat scroll center top transparent; margin: 0 auto; min-height: 600px; width: 850px;} .marco{border: 1px solid #CCCCCC; border-radius: 5px; -moz-border-radius:5px; -webkit-border-radius: 5px; padding: 7px;} .marco.blanco{background-color: #FFFFFF;} .marco.gris{background-color: #F8F8F8;} .marco.celeste{background-color: #BAD9F1;} .login{float: right; padding: 5px 15px; text-align: right; width: 401px; font-size: 15px;}
www.redusers.com
10
APÉNDICE. DESARROLLO DE UNA RED SOCIAL
.tecnologias {float: left; height: 400px; width: 400px;} .tecnologias img{margin: 20px 0;} .registro{min-height: 215px; padding: 5px 15px;} .registro input{display: block; width: 395px;} .error {background: url(“/img/icono_error.gif”) no-repeat 10px 50% #FAEBE7; border: 1px solid #F16048; color: #DF280A; padding: 3px 3px 3px 30px; textalign: justify;} .columna_derecha{float: right; margin: 0 5px; width: 431px;} .usuarios_registrados{font-size: 15px; margin: 15px 0; padding: 5px 15px; width: 399px;} .usuario{text-align: right; min-width: 140px; float: left; padding: 11px 10px 0; font-size: 14px;} .contenido{width: 438px; float: left;} .usuarios{float: left; margin: 0 5px; width: 180px; text-align: left;} .usuarios span{font-size: 13px; font-weight: bold;} #notificacion ul, .usuarios ul{margin: 0; min-height: 40px; list-style: none; padding: 0;} #notificacion ul li, .usuarios ul li{border-bottom: 1px solid #CCCCCC; margin: 10px 0; cursor: pointer; padding: 5px;} #notificacion ul li:hover, .usuarios ul li:hover{background-color: #47C516;} .chat{clear: both; height: 181px; bottom: 0; margin: 0 5px; padding: 0 3px; width: 828px; text-align: left; position: absolute; z-index: 1000;}
www.redusers.com
11
SISTEMAS WEB ESCALABLES
.ventana{margin: 0 2px; float: left; width: 186px;} .ventana input[type=”text”]{margin: 0 2px; width: 179px;} .usuariosLista{min-height: 400px;} .avatar{float: left; width: 30px; height: 30px;} .avatar img{width: 30px; height: 30px;} .bloque{text-align: left; margin-bottom: 5px; float: left; width: 421px;} .bloque .header{line-height: 45px; height: 30px; border-bottom: 1px solid #CCC; text-align: left; margin-bottom: 10px;} .bloque .header img{margin-right: 10px; float: left; width: 30px; height: 30px;} .bloque .header .bloque_usuario{font-weight: bold; margin: 0 5px 0 0;} .bloque .header .bloque_fecha{float: right;} .contador{background: url(“/img/bg-azul.png”) repeat-x left top #53AFEC; border: 1px solid #315EA8; border-radius: 2px 2px 2px 2px; color: #FFFFFF; padding: 5px; text-align: center; display: block; float: left; font-size: 18px; fontweight: bold; height: 17px; width: 30px;} #postFrm{width: 436px;} #postFrm textarea {display: block; height: 63px; width: 422px;} #postFrm input[type=”submit”]{margin-right: 0; float: right;} #solicitudes{height: 200px; width: 200px; background-color: #F8F8F8; position: absolute; z-index: 100; display: none; top: 64px;} .bSalir{display: inline-block; position: relative; cursor: pointer; text-shadow: 1px 1px 0 #FFF; top: -57px; width: 81px; right: -106px;}
www.redusers.com
12
APÉNDICE. DESARROLLO DE UNA RED SOCIAL
Dado que solo hemos definido los aspectos visuales, no se va a explicar el código en detalle.
Registro y login de usuarios Ahora vamos a trabajar sobre la primera pantalla. Para esto, primero crearemos las plantillas que se reutilizarán en la aplicación. Generaremos un archivo nuevo llamado header.ejs, que guardaremos en el directorio views/ con el siguiente contenido: <meta charset=”utf-8” />
Simplemente hemos creado una estructura HTML básica que luego incluiremos en las otras plantillas. El elemento contendrá la variable titulo, que hemos definido anteriormente en el archivo app.js. Vamos a crear un nuevo archivo llamado footer.ejs con código que sigue:
<script type=”text/javascript” src=”/socket.io/socket.io.js”> <script type=”text/javascript” src=”http://ajax.googleapis.com/ajax/libs/jquery/1.9.0/jquery.min.js”> <script type=”text/javascript” src=”/js/script.js”>
www.redusers.com
13
SISTEMAS WEB ESCALABLES
Al igual que header.ejs, este archivo contendrá las variables globales titulo y autor definidas en el archivo app.js; además, se incluyen los archivos JavaScript que se necesitarán del lado del cliente. El archivo html5.js permite utilizar elementos HTML5 en versiones de Internet Explorer menores a 9. Ya estamos en condiciones de crear la pantalla de acceso. Generaremos un archivo dentro del directorio views/ con el nombre index.ejs y el siguiente contenido:
REPLICAR EL EJEMPLO UTILIZANDO PHP Como hemos visto, Express ofrece un entorno en el cual podemos, entre otras cosas, manejar el ruteo, y con Node es posible crear nuestro propio servidor web. Una interesante propuesta es desarrollar el mismo sistema utilizando PHP y Apache para identificar las ventajas y desventajas de cada uno y definir cuándo utilizar una arquitectura u otra.
www.redusers.com
14
APÉNDICE. DESARROLLO DE UNA RED SOCIAL
En este código, primero incluimos la plantilla header y definimos el título mediante la variable global. Luego, creamos un archivo llamado home.ejs y lo guardamos en el mismo directorio que index.ejs, por el momento sin contenido. También generamos el formulario de login y, más abajo, agregamos los logos de Redis y Node para lograr una apariencia más atractiva. Creamos el formulario de registro de usuarios y, debajo, un elemento que permite mostrar la cantidad de usuarios registrados en el sistema. Por último, incluimos la plantilla footer.
www.redusers.com
15
SISTEMAS WEB ESCALABLES
Una vez definida la plantilla de inicio, necesitamos crear la función que se encargará de renderizar el contenido cuando se accede al sistema. Para esto, abrimos el archivo index.js dentro del directorio routes/ y reemplazamos todo el contenido por lo siguiente:
exports.index = function(req, res){ // inicializamos la variable se sesion de usuario req.session.usr = req.session.usr || ‘’; // definimos la plantilla a renderizar plantilla = req.session.usr === ‘’ ? ‘index’ : ‘home’; // renderizamos la palntilla res.render(plantilla, { titulo: titulo, autor: autor, usuario: req.session.usr } ); };
En este código exportamos la función index para que sea accesible desde cualquier archivo y, dentro de ella, verificamos si la variable req. session.ur tiene algún contenido: como aún no la hemos definido, está vacía. Por lo tanto, renderizaremos la plantilla index.ejs pasándole las variables globales titulo y autor; la variable usuario, en esta instancia, está vacía. Si ejecutamos la aplicación y accedemos a la dirección http://127.0.0.1:3000 deberemos ver lo que muestra la Figura 2.
GRUPO DE USUARIOS DE EXPRESS Siempre es buena idea formar parte de los grupos de usuarios de las tecnologías que nos interesan. Express posee una lista que actualmente cuenta con alrededor de dos mil usuarios, discutiendo cerca de cinco mil temas. El grupo utiliza la plataforma Google Groups y podemos unirnos a través del siguiente enlace: https://groups.google.com/forum/#!forum/express-js.
www.redusers.com
16
APÉNDICE. DESARROLLO DE UNA RED SOCIAL
Figura 2. Al ingresar al sistema, los usuarios podrán registrarse o iniciar sesión para poder interactuar con amigos. A continuación definiremos el archivo que manejará las acciones del usuario, creando un archivo llamado script.js, que guardaremos en el directorio js/ con el siguiente contenido:
$(document).ready(function(){ // registro de usuario $(document).on(‘submit’, ‘#nuevoFrm’, function(e){ e.preventDefault(); setUsuario($(this)); }); });
// funcion para mostrar el mensaje de error en los formularios function mostrarMensajeFormulario(form, mensaje){ if (form.find(‘p’).size()) form.find(‘p’).show().html(mensaje); else
www.redusers.com
17
SISTEMAS WEB ESCALABLES
form.prepend(‘
’ + mensaje + ‘
’); form.find(‘p’).fadeOut(7000); } // metodo POST de registro function setUsuario(form) { $.post(“/registro”, form.serialize(), function(respuesta){ if (respuesta.codigo === 201) window.location = respuesta.mensaje; else mostrarMensajeFormulario(form, respuesta.mensaje); } ); }
En el código definimos la acción para el formulario de registro, donde ejecutamos la función setUsuario() para enviar los datos al servidor. Si la respuesta del servidor es 201, redirigimos al usuario a la pantalla principal del sistema; en el caso contrario, ejecutamos la función mostrarMensajeFormulario(), que creará un elemento
para mostrar el mensaje recibido. En la función setUsuario() hacemos un POST por Ajax a la ruta /registro (en el archivo app.js
UNA RESPUESTA 201 REDIRIGE AL USUARIO A LA PANTALLA PRINCIPAL DEL SISTEMA
tenemos comentado el ruteo para esta solicitud). Ahora simplemente tenemos que descomentarla, pasando de esto: //app.post(‘/registro’, user.registro); a esto: app.post(‘/registro’, user.registro);
www.redusers.com
18
APÉNDICE. DESARROLLO DE UNA RED SOCIAL
Debemos, ahora, definir el método registro, para lo cual abrimos el archivo user.js (ubicado en el directorio routes/), borramos todo el contenido y escribimos lo siguiente: // registramos el usuario exports.registro = function(req, res) { var nombre = req.param(‘nombre’), apellido = req.param(‘apellido’), usuario = req.param(‘usuario’), clave
= req.param(‘clave’),
correo = req.param(‘correo’); if (nombre.length < 1 || apellido.length < 1 || usuario.length < 1 || clave.length < 1 || correo.length < 1 ){ res.send({codigo: 204, mensaje: ‘Complete todos los campos’ }); return; }else{ // guardar en DB db.get(‘usuario:’ + usuario + ‘:uid’, function (err, reply) { if (reply === null){ // obtenemos el proximo uid db.incr(‘global:ultimoUid’, function (err, uid) { // seteamos el uid al usuario db.set(‘usuario:’ + usuario + ‘:uid’, uid); // encripto las claves var hashClave = crypto.createHash(‘sha256’).update(clave).digest(‘hex’); // seteamos los campos al usuario db.hmset(‘uid:’ + uid, {‘nombre’ : nombre, ‘apellido’ : apellido, ‘usuario’ : usuario, ‘clave’
: hashClave,
‘correo’ : correo } ); // incremento la cantidad de usuarios (ver si dejar o no) db.sadd(‘usuarios’,uid);
www.redusers.com
19
SISTEMAS WEB ESCALABLES
// seteamos las variables de session req.session.usr = {‘uid’
: uid,
‘nombre’ : nombre, ‘apellido’ : apellido, ‘usuario’ : usuario }; // emitimos el total de usuarios getTotalUsuarios(); res.send({codigo: 201, mensaje: ‘/’ }); }); }else res.send({codigo: 204, mensaje: ‘Error: El usuario ya existe’ }); }); } };
En este código obtenemos los parámetros enviados del formulario y verificamos que ya no esté registrado el usuario; incrementamos la clave global:ultimoUid y creamos un usuario con este valor. Encriptamos la clave y guardamos los atributos en la base de datos, agregamos el uid a usuarios y creamos la variable de sesión con uid, nombre, apellido y usuario. Hacemos una llamada
LUEGO DE ENCRIPTAR LA CLAVE DEBEMOS GUARDAR LOS ATRIBUTOS DE LA BASE DE DATOS
a la función getTotalUsuarios(), y retornamos el código 201 con la ruta para la redirección. A continuación, debemos definir la función getTotalUsuarios() que hemos utilizado anteriormente: // emitimos el total de usuarios var getTotalUsuarios = function () { db.scard(‘usuarios’, function (err, cant) { io.sockets.emit(“setTotalUsuarios”, cant); }); }
www.redusers.com
20
APÉNDICE. DESARROLLO DE UNA RED SOCIAL
Utilizaremos la función getTotalUsuarios() para obtener la cantidad de usuarios registrados y emitir el resultado a través del evento llamado setTotalUsuarios, utilizando Socket.IO. Al final del archivo exportamos la función para que pueda ser invocada desde cualquier archivo del sistema: // exportamos las funciones exports.getTotalUsuarios = getTotalUsuarios;
En síntesis, cuando se registra un usuario, emitimos un evento para informar a todos los miembros la cantidad de usuarios registrados. El paso siguiente es retornar al archivo script.js para capturar el evento setTotalUsuarios. Primero nos conectamos al socket, incluyendo la siguiente línea al principio del archivo: // nos conectamos al socket var sockets
= io.connect(‘http://127.0.0.1:3000’);
Luego, dentro de la función $(document).ready(), capturamos el evento: // io - cuando se registra un usuario muestro el total sockets.on(‘setTotalUsuarios’, mostrarTotalUsuarios);
En este código hemos indicado que, cuando se reciba el evento, se ejecutará la función mostrarTotalUsuarios(), que definimos de la siguiente manera: // cuando recibe el total de usuarios lo muestra en el contenedor function mostrarTotalUsuarios(data){ $(‘#totalUsuarios’).html(data); }
En este código obtenemos la cantidad de usuarios y los mostramos en el elemento. Podemos probar cómo se ve el proyecto abriendo un navegador y registrando un usuario. Si todo salió bien, el usuario
www.redusers.com
21
SISTEMAS WEB ESCALABLES
nuevo será redirigido, por ahora, a una página en blanco o, en caso contrario, se mostrará un error.
Figura 3. Para que un usuario pueda registrarse debe completar todos los campos; de otra manera, se mostrará un mensaje de error. A continuación vamos a crear el login de usuario. En el archivo script. js, dentro de la función $(document).ready(), agregamos lo siguiente:
// login $(document).on(‘submit’, ‘#loginFrm’, function(e){ e.preventDefault(); login($(this)); });
CONTRIBUIR CON REDIS Redis provee una vía para testear, agregar y mejorar características de la base de datos: solo es necesario clonar el repositorio oficial de Github. Otra alternativa para contribuir con Redis es reportar bugs o resolver los existentes. Ambas opciones son definidas en detalle en el enlace http://redis.io/community.
www.redusers.com
22
APÉNDICE. DESARROLLO DE UNA RED SOCIAL
En el mismo archivo agregamos la función login(): // metodo POST de login function login(form) { $.post(“/login”, form.serialize(), function(respuesta){ if (respuesta.codigo === 201) window.location = respuesta.mensaje; else mostrarMensajeFormulario(form, respuesta.mensaje); } ); }
En el código anterior hicimos un POST por Ajax a la ruta /login, en el cual enviamos los datos del formulario al igual que el registro. En el caso de que la respuesta del servidor sea 201, redireccionamos al usuario a la ruta indicada; en el caso contrario, mostramos el mensaje mediante la función mostrarMensajeFormulario(). Ahora descomentamos el ruteo para /login en el archivo principal (es decir, en app.js), pasando de esto: //app.post(‘/login’, user.login); a esto: app.post(‘/login’, user.login); Necesitamos crear la función login(). Escribiremos lo siguiente en el archivo user.js:
// verificamos el usuario y creamos la variable de sesion exports.login = function(req, res){ var usuario = req.param(‘usuario’), clave = req.param(‘clave’);
www.redusers.com
23
SISTEMAS WEB ESCALABLES
if (usuario.length < 1 || clave.length < 1){ res.send({codigo: 204, mensaje: ‘Complete todos los campos’ }); return; }else{ db.get(‘usuario:’ + usuario + ‘:uid’, function (err, uid) { if (uid === null) res.send({codigo: 204, mensaje: ‘Error: El Usuario no existe’ }); else{ // obtenemos todos los atributos del usuario db.hgetall(‘uid:’ + uid, function (err, usuario) { // encriptamos la clave var hashClave = crypto.createHash(‘sha256’).update(clave).digest(‘hex’); if (hashClave == usuario.clave) { // creamos la variable se sesion req.session.usr = {‘uid’
: uid,
‘nombre’ : usuario.nombre, ‘apellido’ : usuario.apellido, ‘usuario’ : usuario.usuario }; res.send({codigo: 201, mensaje: ‘/’ }); }else res.send({codigo: 204, mensaje: ‘Error: Usuario o clave incorrectos’ }); }); } }); } }
En este código, primero verificamos que los parámetros sean diferentes a vacío y luego comprobamos que el usuario exista: en caso de existir obtenemos los atributos del usuario y comparamos la clave. Si la clave almacenada en la base de datos coincide con la ingresada, creamos la variable de sesión con los datos del usuario, devolvemos el código 201 y la ruta para redireccionar. En el caso contrario, retornamos el código 204 y generamos un mensaje de error.
www.redusers.com
24
APÉNDICE. DESARROLLO DE UNA RED SOCIAL
Figura 4. Cuando un usuario intente acceder sin completar los campos, se mostrará una advertencia. Es importante considerar que cada vez que se intente acceder con un nombre de usuario que no se encuentre registrado en el sistema, se procederá a mostrar un mensaje de carácter informativo a los usuarios que están intentando conectarse.
Figura 5. En el caso de que un usuario no exista en la base de datos, simplemente mostramos otro mensaje. www.redusers.com
25
SISTEMAS WEB ESCALABLES
Creación de la página principal Una vez que el usuario se ha registrado o ha iniciado sesión, es redireccionado a la página principal con la variable de sesión creada. Por lo tanto, renderizamos la plantilla home.ejs. A continuación abriremos este archivo y agregaremos el siguiente código:
En este código hemos incluido el archivo header.ejs y definimos el título con el contenido de la variable global y los elementos para el buscador. Luego determinamos el área para las notificaciones y la lista de solicitudes, y definimos el enlace para salir del sistema y el contenedor donde el usuario verá la lista de amigos conectados. Creamos un contenedor para el formulario y el listado de posts. Definimos el contenedor para los usuarios que no son amigos del usuario actual. Generamos el contenedor de las ventanas de chat e incluimos footer.ejs. Una vez creada la plantilla podemos acceder para ver su estructura.
www.redusers.com
27
SISTEMAS WEB ESCALABLES
Figura 6. Cuando el usuario inicie sesión, verá la lista de amigos, el área central para los posts y la columna con usuarios nuevos.
Definición de los eventos para un usuario logueado A continuación vamos a crear los eventos que emitirá y recibirá el usuario cuando inicia sesión a través de Socket.IO. Para esto, abrimos el archivo index.js y agregamos lo siguiente en la primera línea para tener acceso a las funciones definidas en el archivo user.js: var user = require(‘./user.js’); Luego, dentro de la función index(), vamos a utilizar el objeto sessionIO para acceder a la variable de sesión y desencadenar los eventos. Para esto escribimos lo siguiente:
sessionIO.on(‘connection’, function (err, socket, session) { //usuarios logueados if(session.usr != ‘’){ // guardamos el socket.id del usuario actual
www.redusers.com
28
APÉNDICE. DESARROLLO DE UNA RED SOCIAL
usrEnLinea[session.usr.uid] = socket.id; // obtenemos los usuarios que no son amigos user.getUsuariosNuevos(session.usr.uid); }else{ // obtenemos la cantidad de usuarios user.getTotalUsuarios(); } });
En este código, capturamos la conexión y verificamos si la variable session.usr contiene información. En el caso de que un usuario se haya logueado, almacenamos el identificador del socket en el array global usrEnLinea con su uid como identificador, lo que nos permitirá identificar el socket mediante el cual está conectado cada usuario para poder enviarle mensajes privados. Luego, vamos a invocar a la función getUsuariosNuevos() del archivo user.js, al cual le pasaremos como parámetro el uid del usuario actual. Por lo tanto, debemos crear esta función en el archivo user.js: // obtenemos los usuarios que no son amigos var getUsuariosNuevos = function(uid){ db.sdiff(‘usuarios’, ‘uid:’ + uid + ‘:amigos’, function (err, usuarios) { if (usuarios) { var usrNuevos = []; usuarios.forEach(function(id){ // verificamos que no sea el usuario actual if (uid != id){ // verificamos que no tenga una solicitud pendiente db.sismember(‘uid:’ + id + ‘:solicitudes’, uid, function (err, solicitudEnviada) { if(solicitudEnviada == 0){ // obtenemos la informacion del usuario db.hgetall(‘uid:’ + id, function (err, usuario) { usrNuevos.push({‘uid’: id,
www.redusers.com
29
SISTEMAS WEB ESCALABLES
‘nombre’ : usuario.nombre, ‘apellido’ : usuario.apellido }); // emitimos los usuarios if (usrNuevos.length == usuarios.length - 1) io.sockets.socket(usrEnLinea[uid]).emit(‘setUsuariosNuevos’, usrNuevos); }); } }); } }); } }); }
En este código utilizamos el comando sdiff de Redis para obtener los usuarios que no son amigos del usuario actual; luego, con cada uno verificamos que no sea el uid del usuario conectado, y comprobamos que no tenga una solicitud de amistad pendiente. Obtenemos los datos y los almacenamos en un array. Cuando finaliza la lista, emitimos el evento setUsuariosNuevos al usuario actual a través del array usrEnLinea con el índice identificado por el uid pasado como parámetro de la función. Para que la función esté disponible desde cualquier lugar, agregamos lo siguiente al final del archivo: exports.getUsuariosNuevos = getUsuariosNuevos; Ahora necesitamos capturar el evento del lado del cliente. Para esto, agregamos lo siguiente al método $(document).ready() del archivo script.js: //io - mostramos los usuarios que no son amigos sockets.on(‘setUsuariosNuevos’, mostrarUsuariosNuevos);
En el código, recibimos el evento y ejecutamos la función mostrarUsuariosNuevos(), que definiremos de la siguiente manera:
www.redusers.com
30
APÉNDICE. DESARROLLO DE UNA RED SOCIAL
// muestra los usuarios nuevos function mostrarUsuariosNuevos(data){ if (data.length) { $(‘#usuariosNuevos’).html(‘
’); data.forEach(function (usuario) { var nombreUsuario = usuario.nombre + ‘ ‘ + usuario.apellido ; $(‘#usuariosNuevos ul’).append(‘- ’ + nombreUsuario + ‘
’); }); $(‘#usuariosNuevos’).append(‘
’); }; }
En el código que acabamos de presentar agregamos al elemento #usuariosNuevos, de esta manera podremos acceder a la lista recibida de los usuarios que aún no son amigos del que se encuentra conectado actualmente. Podemos efectuar la prueba de esta funcionalidad registrando varios usuarios.
Figura 7. Cuando el usuario inicie sesión obtendrá una lista con los usuarios que no son amigos, a quienes les podrá enviar una solicitud. www.redusers.com
31
SISTEMAS WEB ESCALABLES
Envío de una solicitud de amistad Vamos a desarrollar la funcionalidad que nos permitará que el usuario envíe una solicitud de amistad.
Figura 8. Para enviar una solicitud de amistad solo será necesario hacer clic en algún nombre de la lista de usuarios. Necesitamos capturar el evento click de la lista. Para esto, agregamos lo siguiente en el método $(document).ready() del archivo script.js: // se envia una solicitud de amistad $(document).on(‘click’, ‘#usuariosNuevos ul li’, function(){ setSolicitud($(this).attr(‘uid’)); });
INSTALAR EXPRESS CON EJS Por defecto, Express usa el motor de plantillas Jade, el cual es muy bueno pero abstrae de una manera significante la utilización de elementos HTML. Una alternativa es utilizar EJS, más amigable y simple al crear estructuras complejas. Para instalarlo cuando creamos una aplicación, debemos escribir el siguiente comando: express -e nombreDeLaApp.
www.redusers.com
32
APÉNDICE. DESARROLLO DE UNA RED SOCIAL
En este código hemos capturado el evento click de la lista de usuarios y ejecutamos la función setSolicitud(), que definimos en el mismo archivo de la siguiente manera: // se envia una solicitud de amistad function setSolicitud(uid) { $.post(“/setSolicitud”, ‘uid=’ + uid, function(respuesta){ if (respuesta.codigo === 201) $(‘#usuariosNuevos ul li[uid=”’ + uid + ‘”]’).fadeOut(1000); else $(‘#usuariosNuevos ul li[uid=”’ + uid + ‘”]’).html(respuesta.mensaje); } ); }
Aquí hicimos un POST por Ajax a la ruta /
CON UN CÓDIGO 201 EN LA RESPUESTA QUITAMOS AL USUARIO DE
setSolicitud, enviando el uid del usuario a que se le mandará la solicitud. En el caso de tener como respuesta el código 201, quitamos al usuario de la lista, y en el caso contrario, mostramos el mensaje en el mismo lugar. Para continuar es necesario que descomentemos el ruteo para la petición /
LA LISTA
setSolicitud en el archivo principal, para lo cual abriremos y cambiaremos el archivo app.js. Pasaremos de esto: //app.post(‘/setSolicitud’, user.setSolicitud); a esto: app.post(‘/setSolicitud’, user.setSolicitud); Ya sabemos qué hacer en caso de recibir un POST a esta ruta. Lo
siguiente es definir la función setSolicitud() en el archivo user.js, del modo que mostramos a continuación:
www.redusers.com
33
SISTEMAS WEB ESCALABLES
// registramos y envia una solicitud de amistad exports.setSolicitud = function(req, res){ var uid = req.param(‘uid’); if (uid.length < 1){ res.send({codigo: 204, mensaje: ‘Usuario Invalido’ }); }else{ // agregamos el usuario a la lista de solicitudes db.sadd(‘uid:’ + uid + ‘:solicitudes’, req.session.usr.uid); // obtenemos la solicitudes y la enviamos al usuario getSolicitudes(uid); res.send({codigo: 201, mensaje: ‘’ }); } }
En el código, recibimos el uid del usuario receptor, al que le agregamos el usuario actual en la clave solicitudes. Luego invocamos el método getSolicitudes() con el uid del usuario receptor como parámetro para obtener las solicitudes pendientes del usuario. Crearemos la función getSolicitudes()en el mismo archivo de la siguiente manera:
// obtenemos la cantidad de solicitudes pendientes var getSolicitudes = function(uid){ db.smembers(‘uid:’ + uid + ‘:solicitudes’, function (err, solicitudesRecibidas) { if (solicitudesRecibidas) { var solicitudes = []; // obtenemos los datos de cada uid solicitudesRecibidas.forEach(function(id){ db.hgetall(‘uid:’ + id, function (err, usuario) { solicitudes.push({‘uid’: id, ‘nombre’ : usuario.nombre, ‘apellido’ : usuario.apellido });
www.redusers.com
34
APÉNDICE. DESARROLLO DE UNA RED SOCIAL
// emitimos las solicitudes if (solicitudes.length == solicitudesRecibidas.length) io.sockets.socket(usrEnLinea[uid]).emit(‘getSolicitudes’, solicitudes); }); }); } }); }
En el código anterior verificamos las solicitudes pendientes del uid pasado por parámetro y, por cada una de ellas, obtenemos la información. Al obtener todas las solicitudes, emitimos el evento getSolicitudes con la lista completa al usuario recibido. Luego, al final del archivo, exportamos la función: exports.getSolicitudes
= getSolicitudes;
Necesitamos capturar el evento getSolicitudes del lado del cliente; por lo tanto, agregamos lo siguiente a la función $(document).ready() del archivo script.js: // io - cuando se recibe una solicitud sockets.on(‘getSolicitudes’, mostrarSolicitudes);
Cuando ocurra getSolicitudes, vamos a ejecutar la función mostrarSolicitudes(), que debemos definir como sigue:
DOCUMENTACIÓN DE EXPRESS 2X Si bien actualmente Express se encuentra en la versión 3, debido a que se han creado miles de aplicaciones con la versión 2 fue necesario dejar la documentación accesible para consultas de desarrolladores que necesitan mantener sistemas escritos con esta versión. Para ver la documentación correspondiente a Express en su versión 2, deberemos acceder a la página que se encuentra en la dirección http://expressjs.com/2x.
www.redusers.com
35
SISTEMAS WEB ESCALABLES
// se muestran las solicitudes de amistad function mostrarSolicitudes(solicitudes) { if (solicitudes.length) { $(‘#solicitudes’).html(‘’); solicitudes.forEach(function (usuario) { var contenido = ‘’; contenido += ‘
’; contenido += ‘ ’ + usuario.nombre + ‘ ‘ + usuario.apellido + ‘ quiere ser tu amigo!’ + ‘’; contenido += ‘ ’; contenido += ‘
’; contenido += ‘
’; contenido += ‘
’; contenido += ‘’; $(‘#solicitudes’).prepend(contenido); }); $(‘#valorContador’).fadeOut(500, function() { $(this).html(solicitudes.length).fadeIn(500) }); } }
En el código que mostramos arriba recibimos las solicitudes y, por cada una de ellas, creamos un ítem en la lista con el nombre del usuario que ha enviado la solicitud original. Además, se procede a agregar los botones que necesitamos para aceptarla y también para cancelarla. Posteriormente, incrementamos el valor del contador con la
POR CADA SOLICITUD DE AMISTAD RECIBIDA CREAMOS UN ÍTEM EN LA LISTA DEL USUARIO CONECTADO
cantidad de solicitudes que han sido recibidas. Cuando un usuario reciba una solicitud de amistad, verá incrementado el contador.
www.redusers.com
36
APÉNDICE. DESARROLLO DE UNA RED SOCIAL
Figura 9. Por cada solicitud de amistad recibida, el contador incrementará su valor. Debemos lograr que, cuando el usuario haga clic en el contador, se muestre la ventana con las solicitudes; para lograrlo, agregamos lo siguiente al método $(document).ready() del archivo script.js: // contador de solicitudes $(document).on(‘click’, ‘.contador’, function(){ $(‘#solicitudes’).fadeToggle(500); });
En el código, mostramos la lista de solicitudes con un efecto de aparición suave y, en caso de que la lista ya esté visible, la ocultamos.
SOCKET.IO EN OTROS LENGUAJES Sockt.IO es una librería en lenguaje JavaScript, pero también es posible utilizar otras versiones, escritas en diferentes lenguajes, como Java, Objective-C, C, C++, Go, Python y PHP, entre otros. Esto es, sin duda, una gran ventaja, ya que es posible desarrollar sistemas basados en lenguajes como PHP en tiempo real. Podemos ver la lista en https://github.com/learnboost/socket.io/wiki.
www.redusers.com
37
SISTEMAS WEB ESCALABLES
Figura 10. Al recibir una solicitud el usuario puede optar por aceptarla o cancelarla. Antes definimos que, cuando se envía una solicitud de amistad, el usuario la recibe en tiempo real. Ahora debemos asegurarnos de que el usuario no conectado la reciba al conectarse. Para esto, agregamos el código siguiente en el archivo index.js, en el método sessionIO.on(), dentro del condicional de sesión: // obtenemos las solicitudes pendientes user.getSolicitudes(session.usr.uid);
Con este código, cada vez que se conecta, el usuario puede obtener el listado de las solicitudes pendientes.
PROYECTOS USANDO SOCKET.IO En el repositorio oficial de Socket.IO hay una lista con varios sistemas que pueden servirnos como base para el desarrollo de algún proyecto en tiempo real. Un ejemplo muy útil es collabshot, una herramienta colaborativa de edición de imágenes, notas y chat. Podemos acceder a la lista de proyectos a través del siguiente enlace: https://github.com/LearnBoost/Socket.IO/wiki/Projects-using-Socket.IO.
www.redusers.com
38
APÉNDICE. DESARROLLO DE UNA RED SOCIAL
Respuesta a una solicitud de amistad Vamos a definir las acciones que puede tomar el usuario cuando recibe una solicitud de amistad. Primero, agregamos lo siguiente al método $(document).ready() del archivo script.js: // boton de aceptar/cancelar solicitudes $(document).on(‘click’, ‘#solicitudes input[type=”button”]’, function(){ setRespuestaSolicitud($(this)); });
En este código capturamos el evento click para aceptar o cancelar una solicitud, mediante el cual ejecutamos la función setRespuestaSolicitud(), que debemos definir en el mismo archivo del siguiente modo: // se responde una solicitud de amistad function setRespuestaSolicitud(boton) { $.post(“/setRespuestaSolicitud”, ‘uid=’ + boton.attr(‘uid’) + ‘&accion=’ + boton. attr(‘value’), function(respuesta){ if (respuesta.codigo === 201){ $(‘#solicitudes li[uid=”’ + boton.attr(‘uid’) + ‘”]’).fadeOut(1000); $(‘#valorContador’).fadeOut(500, function() { $(this).html(parseInt($(this).html()) - 1).fadeIn(500) }); }else $(‘#solicitudes li[uid=”’ + boton.attr(‘uid’) + ‘”]’).html(respuesta.mensaje); } ); }
En el código anterior, enviamos un POST por Ajax a la ruta /setRespuestaSolicitud, con el uid del usuario al cual se responde la solicitud y con la acción (es decir, Aceptar o Cancelar). Cuando se recibe la respuesta, si se obtiene el código 201 se elimina el nombre del usuario y se descuenta el contador. De ocurrir lo contrario, se muestra el mensaje.
www.redusers.com
39
SISTEMAS WEB ESCALABLES
Lo siguiente es habilitar el ruteo para /setRespuestaSolicitud, para lo cual descomentamos la siguiente línea en el archivo app.js, pasando de esto: //app.post(‘/setRespuestaSolicitud’, user.setRespuestaSolicitud); al código que presentamos a continuación: app.post(‘/setRespuestaSolicitud’, user.setRespuestaSolicitud); Necesitaremos definir la función setRespuestaSolicitud en el archivo user.js, de la siguiente manera:
// respondemos la solicitud de amistad exports.setRespuestaSolicitud = function(req, res){ var uid
= req.param(‘uid’),
accion = req.param(‘accion’); db.get(‘uid:’ + uid , function (err, reply) { if (reply === null){ res.send({codigo: 204, mensaje: ‘Usuario Invalido’ }); }else{ if (accion == ‘Aceptar’){ // agregamos como amigo a ambos usuarios db.sadd(‘uid:’ + uid + ‘:amigos’, req.session.usr.uid); db.sadd(‘uid:’ + req.session.usr.uid + ‘:amigos’, uid); } // eliminamos la solicitud actual db.srem(‘uid:’ + req.session.usr.uid + ‘:solicitudes’, uid); res.send({codigo: 201, mensaje: ‘’ }); } }); }
En este código, verificamos si existe el usuario recibido y, en caso de que se haya aceptado la solicitud, se agregan como amigos
www.redusers.com
40
APÉNDICE. DESARROLLO DE UNA RED SOCIAL
mutuamente a través de la clave amigos. Luego se elimina la solicitud en ambos casos, es decir, tanto si se ha aceptado la solicitud como si no se ha hecho.
Figura 11. Una solicitud de amistad permanecerá en la lista hasta que el usuario realice una de las dos acciones definidas.
Lista de amigos conectados Vamos a crear una función para obtener los amigos que están conectados cuando un usuario inicia sesión. Para hacerlo, agregamos el código siguiente en el archivo llamado index.js, en la función sessionIO.on(), dentro del condicional de sesión:
// obtenemos los usuarios conectados user.getAmigosConectados(session.usr.uid);
En el código definimos que, cuando un usuario se conecta, se ejecuta la función getAmigosConectados(). Por lo tanto, vamos a definirla en el archivo user.js de la siguiente manera:
www.redusers.com
41
SISTEMAS WEB ESCALABLES
// obtenemos los amigos conectados var getAmigosConectados = function(uid){ db.smembers(‘uid:’ + uid + ‘:amigos’, function (err, amigos) { if (amigos) { var usrConectados = []; var i = 0; amigos.forEach(function(id){ i++; // verificamos que el usuario se encuentre conectado if (usrEnLinea[id]){ // obtenemos la informacion del usuario db.hgetall(‘uid:’ + id, function (err, usuario) { usrConectados.push({‘uid’: id, ‘nombre’ : usuario.nombre, ‘apellido’ : usuario.apellido }); // emitimos los amigos if (i == amigos.length) io.sockets.socket(usrEnLinea[uid]).emit(‘setAmigosConectados’, usrConectados); }); } }); } }); }
HERRAMIENTAS PARA FRONTEND Fred Sarmento ha creado un portal con recursos para los frontend, donde se incluyen librerías y plugins como jQuery, Normalize.css, herramientas de debug y testeo como Firebug y Chrome Developer Tools, tutoriales en línea, y editores de código como Sublime Text3. Podemos ver la lista completa en el siguiente enlace: http://fredsarmento.me/frontend-tools.
www.redusers.com
42
APÉNDICE. DESARROLLO DE UNA RED SOCIAL
En el código anterior hemos obtenido los amigos del usuario actual y, para cada uno, verificamos si se encontraba conectado, es decir que existiera en el array usrEnLinea. De estos usuarios hemos obtenido el uid, el nombre y el apellido. Cuando terminamos de obtener los amigos, emitimos el evento setAmigosConectados con la lista. En este punto debemos exportar la función para que esté disponible desde cualquier lugar del sistema. Para ello, agregamos el siguiente código al final del archivo llamado user.js: exports.getAmigosConectados = getAmigosConectados; Vamos a capturar este evento del lado del cliente, definiendo lo que sigue en el archivo script.js dentro del método $(document).ready():
// io - mostramos los amigos conectados sockets.on(‘setAmigosConectados’, mostrarAmigosConectados);
Hasta este momento hemos definido que, cuando se recibe el evento setAmigosConectados, se ejecuta la función mostrarAmigosConectados(); por lo tanto, debemos crearla en el mismo archivo: // muestra los amigos conectados function mostrarAmigosConectados(data){ if (data.length) { $(‘#usuariosAmigos’).html(‘
’); data.forEach(function (usuario) { var nombreUsuario = usuario.nombre + ‘ ‘ + usuario.apellido; $(‘#usuariosAmigos ul’).prepend(‘
’ + nombreUsuario + ‘’); }); }; }
Con este código obtenemos la lista de amigos conectados, agregando a cada uno de ellos en el contenedor #usuariosAmigos.
www.redusers.com
43
SISTEMAS WEB ESCALABLES
Figura 12. Cuando los usuarios inicien sesión verán en la columna izquierda la lista de los amigos conectados.
Informar cuando se conecta un usuario Vamos a crear una función para informar a los amigos, en tiempo real, que un usuario se ha conectado. Primero nos encargamos de agregar el siguiente código en el archivo denominado index.js, en la función sessionIO.on(), dentro del condicional de sesión: // informamos a los amigos que se ha conectado el usuario user.setAmigoConectado(session.usr);
INTERNET Según los últimos informes de Cisco, para el año 2017 habrá cerca de 3.600 millones de usuarios de Internet, esto se puede resumir como casi el 50% de la población mundial. Implicaría un aumento del tráfico mundial por tres, donde el servicio será accesible desde notebooks, netbooks, smartphones, tablets y televisores inteligentes.
www.redusers.com
44
APÉNDICE. DESARROLLO DE UNA RED SOCIAL
En el código anterior definimos que, cuando un usuario se conecte, se ejecutará la función setAmigoConectado(), a la que le pasamos como parámetro todos los datos del usuario actual. En este momento hacemos la definición de la función setAmigoConectado() en el archivo user.js:
// informamos a los amigos que se ha conectado el usuario var setAmigoConectado = function(usr){ // obtenemos todos los amigos db.smembers(‘uid:’ + usr.uid + ‘:amigos’, function (err, amigos) { amigos.forEach(function(id){ // verificamos que el usuario se encuentre conectado if (usrEnLinea[id]) io.sockets.socket(usrEnLinea[id]).emit(‘setAmigoConectado’, usr); }); }); }
En el código anterior, obtenemos los amigos del usuario actual de la base de datos y, a cada uno de ellos, le informamos los datos del usuario mediante el evento setAmigoConectado. Luego, exportamos la función para que esté disponible en todo el sistema: exports.setAmigoConectado
= setAmigoConectado;
Vamos a capturar el evento del lado del cliente mediante el siguiente código, en el $(document).ready() del archivo script.js:
// io - mostramos cuando se conecta un amigo sockets.on(‘setAmigoConectado’, mostrarAmigoConectado);
En el código anterior, definimos que se ejecutará la función mostrarAmigoConectado() cuando se reciba el evento setAmigoConectado. Por lo tanto, la definimos en el mismo archivo:
www.redusers.com
45
SISTEMAS WEB ESCALABLES
// agrega un usuario a la lista de amigos cuando se conecta function mostrarAmigoConectado(data){ if ($(‘#usuariosAmigos ul li[uid=”’ + data.uid + ‘”]’).size() === 0){ // creamos la lista si no existe if($(‘#usuariosAmigos p’).size()){ $(‘#usuariosAmigos p’).remove(); $(‘#usuariosAmigos’).append(‘
’); } var nombreUsuario = data.nombre + ‘ ‘ + data.apellido; $(‘#usuariosAmigos ul’).prepend(‘
’ + nombreUsuario + ‘’); } }
En este código nos encargamos de verificar que el usuario recibido no exista en la lista de amigos y lo agregamos. También podemos corroborar si existe el elemento
, para eliminarlo en caso de que sea el primer amigo que se muestre en la lista.
Informar cuando se desconecta un usuario Vamos a crear una función para informar a los amigos cuando un usuario se desconecta. Si observamos home.ejs veremos que hemos definido el enlace salir y apunta a la ruta /salir. Para que nuestro sistema pueda efectuar alguna acción cuando recibe esta ruta, necesitaremos descomentar la siguiente línea del archivo app.js, pasando de esto: //app.get(‘/salir’, user.logout); a esto: app.get(‘/salir’, user.logout);
www.redusers.com
46
APÉNDICE. DESARROLLO DE UNA RED SOCIAL
En este código pudimos definimos que, cuando se solicite la ruta /salir, se deberá ejecutar la función logout(). Por lo tanto, debemos proceder a crearla en el archivo llamado user.js:
// logout exports.logout = function(req, res){ // informamos a los amigos que se ha desconectado el usuario setAmigoDesconectado(req.session.usr.uid); // eliminamos el uid del array de usuarios y la session delete usrEnLinea[req.session.usr.uid]; // eliminamos la clave se session req.session.usr = ‘’; // redireccionamos res.redirect(‘/’); }
En el código nos encargamos de ejecutar la función setAmigoDesconectado(), que vamos a crear a continuación; luego, borramos el usuario del array de usuarios en línea, vaciamos la variable de sesión y redirigimos al usuario a la pagina inicial. A continuación vamos a definir la función setAmigoDesconectado() en el mismo archivo con el que estamos trabajando:
// informamos a los amigos que se ha desconectado el usuario var setAmigoDesconectado = function(uid){ // obtenemos todos los amigos db.smembers(‘uid:’ + uid + ‘:amigos’, function (err, amigos) { amigos.forEach(function(id){ // verificamos que el usuario se encuentre conectado if (usrEnLinea[id]) io.sockets.socket(usrEnLinea[id]).emit(‘setAmigoDesconectado’, uid); }); }); }
www.redusers.com
47
SISTEMAS WEB ESCALABLES
Aquí hemos obtenido todos los amigos del usuario y, para cada uno de ellos, hemos verificado si estaba conectado, emitiendo el evento setAmigoDesconectado con el uid del usuario actual. Para continuar nos encargamos de exportar la función para que esté disponible desde cualquier lugar del sistema: exports.setAmigoDesconectado = setAmigoDesconectado; Ahora necesitaremos realizar la captura del evento desde el lado del cliente. Para efectuar esta tarea debemos proceder a escribir lo que mostramos a continuación, en el método denominado $(document). ready() dentro del archivo script.js:
// io - mostramos cuando se desconecta un amigo sockets.on(‘setAmigoDesconectado’, mostrarAmigoDesconectado);
Con el código establecemos que, cuando se reciba el evento setAmigoDesconectado, se deberá ejecutar la función mostrarAmigoDesconectado(), que definiremos en el mismo archivo:
// elimina un usuario de la lista de amigos cuando se desconecta function mostrarAmigoDesconectado(data){ // eliminamos el amigo de la lista $(‘#usuariosAmigos ul li[uid=”’ + data + ‘”]’).remove(); // si no hay amigos mostramos el mensaje if ($(‘#usuariosAmigos ul li’).size() == 0){ $(‘#usuariosAmigos ul’).remove(); $(‘#usuariosAmigos’).prepend(‘
No hay amigos conectados...
’); } // eliminamos la ventana de chat si existe if ($(‘#ventana-’ + data).size()) $(‘#ventana-’ + data).remove(); }
www.redusers.com
48
APÉNDICE. DESARROLLO DE UNA RED SOCIAL
Eliminamos el ítem de la lista de amigos y verificamos si existen otros conectados; en el caso contrario mostramos un elemento
con el mensaje de que no hay amigos conectados y, luego, verificamos si existe una ventana de chat abierta y la borramos. A continuación trabajaremos en el chat con amigos.
Creación del sistema de chat Para continuar vamos a desarrollar el sistema de chat entre los amigos que están conectados. La idea es que, al hacer clic en un amigo de la lista, se abra una ventana típica de chat. Primero vamos a capturar el evento click. En el método $(document). ready(), en el archivo script.js, agregamos lo siguiente: // abre ventanas de chat $(document).on(‘click’, ‘#usuariosAmigos ul li’, function(){ abrirVentanaChat($(this).attr(‘uid’)); });
Así, definimos que, cuando se haga clic en un elemento de la lista, se ejecutará la función abrirVentanaChat(), y pasamos como parámetro el atributo uid del usuario de la lista. A continuación debemos proceder a definir la función denominada abrirVentanaChat() en el mismo archivo:
// abre una ventana de chat function abrirVentanaChat(uid) { if (!$(‘#ventana-’ + uid).size()){ var nombre = $(‘#usuariosAmigos ul li[uid=”’ + uid + ‘”]’).html(); var ventana = ‘
’; ventana += ‘
’+ nombre + ‘’;
ventana += ‘
’;
www.redusers.com
49
SISTEMAS WEB ESCALABLES
ventana += ‘
’;
ventana += ‘
’; $(‘.chat’).append(ventana); } }
En el código, obtenemos el uid del usuario con el que se va a chatear, y se crea una ventana que contendrá un elemento para escribir y un
LUEGO DE OBTENER EL UID DEL USUARIO
elemento para mostrar los mensajes;
SE CREA UNA
agregamos toda la ventana al elemento chat. Vamos a definir el evento que va a capturar el mensaje para enviarlo al receptor. Para hacerlo, agregamos el siguiente código en el método
VENTANA APTA PARA UNA SESIÓN DE CHAT
$(document).ready() del mismo archivo: // cuando el usuario presiona enter emite el mensaje $(document).on(‘keypress’, ‘.chat-text’, function(e){ if (e.which == 13) enviarMensaje($(this).parent().attr(‘uid’), $(this).val()); });
En este código simplemente ejecutamos la función enviarMensaje() cuando se presiona la tecla ENTER, pasándole como parámetros el uid del usuario receptor y el mensaje. Debemos definir la función enviarMensaje() en el mismo archivo:
EXTENSIONES PARA EXPRESS Además de las herramientas que vienen integradas por defecto en Express, integrantes de la comunidad oficial han creado decenas de extensiones muy útiles para ser utilizadas en los desarrollos y ahorrar tiempo de programación. Podemos conocerlas accediendo al siguiente enlace: https://github.com/ senchalabs/connect/wiki.
www.redusers.com
50
APÉNDICE. DESARROLLO DE UNA RED SOCIAL
// se envia un mensaje en el chat function enviarMensaje(uid, msg) { var data = { para : uid, mensaje: msg, fecha : new Date() }; // emitimos el mensaje sockets.emit(‘enviarMensaje’, data); // agregamos el mensaje al textarea $(‘#ventana-’+ uid +’ textarea’).val($(‘#ventana-’+ uid +’ textarea’).val() + ‘yo: ‘ + msg + ‘\r\n’); // limpiamos la caja de texto $(‘#ventana-’+ uid +’ .chat-text’).val(‘’); }
En el código, creamos un objeto con la clave para (donde le asignamos el uid receptor, el mensaje y la fecha de emisión) y luego emitimos el mensaje enviarMensaje al servidor; por último, agregamos a el mensaje y limpiamos la caja de texto. Ahora necesitamos capturar el evento del lado del servidor. Debemos agregar lo que sigue en el archivo index.js, en la función sessionIO.on() dentro del condicional de sesión: // Emitimos el mensaje al usuario socket.on(‘enviarMensaje’, function (data){ data.de
= session.usr.uid;
data.nombre = session.usr.nombre + ‘ ‘ + session.usr.apellido; user.enviarMensaje(data); });
En el código hemos capturado el evento enviarMensaje y hemos agregado la clave de con el uid del usuario actual, y la clave nombre con los atributos nombre y apellido concatenados. Ejecutamos la función enviarMensaje() pasándole como parámetro el objeto recibido. Lo que sigue es crear la función enviarMensaje() en el archivo user.js:
www.redusers.com
51
SISTEMAS WEB ESCALABLES
// se envia el mensaje del chat var enviarMensaje = function (data){ io.sockets.socket(usrEnLinea[data.para]).emit(‘mensajeRecibido’, data); }
Simplemente, enviamos el objeto recibido al usuario receptor a través del evento mensajeRecibido. Luego, exportamos la función para que esté disponible desde cualquier lugar del sistema: exports.enviarMensaje
= enviarMensaje;
Necesitamos capturar este evento del lado del cliente, dentro de la función $(document).ready(): // io - mostramos el mensaje recibido del chat sockets.on(‘mensajeRecibido’, mostrarMensajeRecibido);
En este código hemos definido que, cuando se recibe el evento mensajeRecibido, se ejecuta la función mostrarMensajeRecibido(), por lo que debemos definirla en el mismo archivo. Antes de la función vamos a establecer una variable global llamada ultimaFechaMsg: var ultimaFechaMsg = 0; // se muestra un mensaje recibido del chat function mostrarMensajeRecibido(data){ // si se recibe mensajes duplicados if (ultimaFechaMsg == data.fecha) return; else{ // actualizamos la fecha del ultimo mensahe recibido ultimaFechaMsg = data.fecha; // si no existe la ventana la creamos if ($(‘#ventana-’ + data.de).size() == 0) abrirVentanaChat(data.de);
www.redusers.com
52
APÉNDICE. DESARROLLO DE UNA RED SOCIAL
// agregamos la informacion del mensaje $(‘#ventana-’ + data.de + ‘ span’).html(data.nombre); $(‘#ventana-’ + data.de + ‘ textarea’).val($(‘#ventana-’ + data.de + ‘ textarea’).val() + data.nombre + ‘: ‘ + data.mensaje + ‘\r\n’); } }
En el código anterior, declaramos una variable global que utilizaremos en la función para comparar la fecha de recepción de mensajes en el caso de que existan mensajes duplicados. Luego, si la ventana de chat no existe, la abrimos y agregamos el mensaje.
Figura 13. En la imagen podemos ver la ventana de chat con amigos.
ESTADÍSTICAS MEDIANTE STATCOUNTER StatCounter es un sitio que publica estadísticas globales acerca de diferentes tecnologías. Entre las opciones que ofrece se encuentra la posibilidad de ver qué tecnología comparar, permite seleccionar el tipo de gráfico (líneas, barras o mapa) y además permite descargar el gráfico en formato JPG o CSV. Podemos conocerlo mejor a través del siguiente enlace: http://gs.statcounter.com.
www.redusers.com
53
SISTEMAS WEB ESCALABLES
Debemos destacar que el sistema de chat desarrollado permite al usuario tener múltiples conversaciones en simultáneo, ya que en el servidor mantenemos el id del Socket que identifica a cada uno.
Figura 14. Mediante el sistema de chat desarrollado, los usuarios pueden mantener varias conversaciones a la vez.
Creación del sistema de posts Vamos a desarrollar el sistema de publicación de post con amigos. Primero necesitamos poder enviar al servidor los posts escritos, para lo cual debemos capturar el contenido del formulario. En el archivo script.js, dentro del método $(document).ready(), escribiremos lo siguiente: // nuevo post $(document).on(‘submit’, ‘#postFrm’, function(e){ e.preventDefault(); setPost($(this)); });
Así, definimos que, cuando se envía el formulario, se ejecuta la función setPost() enviando como parámetro el mismo objeto. A continuación definimos la función setPost() en el mismo archivo:
www.redusers.com
54
APÉNDICE. DESARROLLO DE UNA RED SOCIAL
// metodo POST cuando se escrine un post nuevo function setPost(form) { $.post(“/setPost”, form.serialize(), function(respuesta){ if (respuesta.codigo === 201){ var bloque = ‘
’; bloque += ‘ ’; bloque += ‘ ’ + $(‘#postFrm textarea’).val() + ‘’; bloque += ‘’; $(‘#posts’).prepend(bloque); $(‘#postFrm textarea’).val(‘’); }else mostrarMensajeFormulario(form, respuesta.mensaje); } ); }
En el código hacemos un POST por Ajax a la ruta /setPost con los elementos del formulario serializado. Si la respuesta es 201, creamos un bloque con el post escrito y se lo mostramos al mismo usuario. En el caso contrario, mostramos el mensaje de error. Como estamos haciendo un POST descomentamos la siguiente línea en app.js. De esto: //app.post(‘/setPost’, user.setPost); debemos pasar a lo que mostramos a continuación: app.post(‘/setPost’, user.setPost);
www.redusers.com
55
SISTEMAS WEB ESCALABLES
En este código nos encargamos de definir que el ruteo deberá ejecutar la función setPost(). Por esta razón, tendremos que realizar esta definición en el archivo denominado user.js:
// registramos el post para el usuario actual y los amigos exports.setPost = function(req, res){ var post = req.param(‘post’); if (post.length < 1){ res.send({codigo: 204, mensaje: ‘Mensaje Invalido’ }); return; }else{ db.incr(‘global:ultimoPid’, function (err, pid) { var fecha = formatearFecha(); var uid = req.session.usr.uid; // seteamos el post y la fecha/hora actual db.hmset(‘post:’ + pid, {‘uid’ : uid, ‘fecha’ : fecha, ‘post’ : post } ); // incremento la cantidad de posts para el usuario actual db.incr(‘uid:’ + uid + ‘:nposts’); var postID = pid; // obtenemos los amigos db.smembers(“uid:” + uid + “:amigos”, function (err, amigos) { // agrego el usuario actual amigos.push(uid); // agrego el id del post a cada amigo amigos.forEach(function(sid){ db.lpush(‘uid:’ + sid + ‘:posts’, postID); }); });
www.redusers.com
56
APÉNDICE. DESARROLLO DE UNA RED SOCIAL
res.send({codigo: 201, mensaje: ‘Mensaje publicado’}); }); } };
En el código anterior, recibimos el post y verificamos que tenga contenido; luego, incrementamos la clave ultimoPid y llamamos a la función formatearFecha() para obtener la fecha actual formateada. Después, obtenemos el usuario actual y guadamos en la base de datos el post con la información obtenida. Seguidamente, nos encargamos de incrementar la cantidad de posts para este usuario y la guardamos en simultáneo para cada uno de los amigos. A continuación vamos a definir la función formatearFecha():
// funcion para formatear la fecha function formatearFecha(fecha) { var d dia
= new Date(fecha || Date.now()), = d.getDate(),
mes
= (d.getMonth() + 1),
anio
= d.getFullYear(),
hora
= d.getHours(),
minuto = d.getMinutes(), segundo = d.getSeconds(); if (mes.length < 2) mes += ‘0’; if (dia.length < 2) dia += ‘0’; return [dia, mes, anio].join(‘-’) + ‘ ‘ + [hora, minuto, segundo].join(‘:’); }
En el código anterior hemos obtenido la fecha actual, en el caso de que no la pasemos por parámetro, y la formateamos de manera legible. Lo que nos queda, ahora, es obtener los posts cuando el usuario inicia sesión. Para esto, agregamos el siguiente código en el archivo index.js, en la función sessionIO.on(), dentro del condicional de sesión:
www.redusers.com
57
SISTEMAS WEB ESCALABLES
// obtenemos los posts user.getPosts(session.usr.uid);
De este modo definimos que, al conectarse un usuario, se ejecutará la función getPosts(), a la cual le pasaremos como parámetro el uid actual. Debemos definir la función que corresponde en el archivo llamado user.js de la siguiente manera: var getPosts = function(uid){ db.lrange(‘uid:’ + uid + ‘:posts’, 0, 10, function (err, posts) { if (posts) { var arrayPosts = []; var i = 0; // obtenemos los atributos de cada post posts.forEach(function(pid) { db.hgetall(‘post:’ + pid, function (err, post) { var usuarioNombre; // obtenemos los atributos del usuario db.hgetall(‘uid:’ + post.uid, function (err, usuario) { i++; arrayPosts.push({‘uid’
: post.uid,
‘nombre’ : usuario.nombre, ‘apellido’: usuario.apellido, ‘fecha’ : post.fecha, ‘mensaje’ : post.post }); // al final de la lista de post se emite al usuario if (i == posts.length) io.sockets.socket(usrEnLinea[uid]).emit(‘setPosts’, arrayPosts); }); }); }); } }); }
www.redusers.com
58
APÉNDICE. DESARROLLO DE UNA RED SOCIAL
Hemos obtenido los últimos diez posts del usuario actual y, de cada uno de ellos, la información completa y del usuario que la escribió, que almacenamos en un array. Por último, debemos realizar la emisión del evento llamado setPosts con el array creado. A continuación debemos capturar el evento setPosts del lado del cliente. Para realizar esto agregaremos lo que sigue en el archivo script.js en el método $(document).ready():
// io - mostramos los posts sockets.on(‘setPosts’, mostrarPosts);
Así determinamos que, cuando ocurra el evento setPosts, ejecutaremos la función mostrarPosts(), que definiremos en el mismo archivo tal como mostramos: // muestra los posts en el contenedor function mostrarPosts(data){ if (data.length) { $(‘#posts’).html(‘’); data.forEach(function (post) { var nombreUsuario = post.nombre + ‘ ‘ + post.apellido; var bloque = ‘
’; bloque += ‘ ’; bloque += ‘ ’ + post.mensaje + ‘
’; bloque += ‘’; $(‘#posts’).append(bloque); }); }; }
www.redusers.com
59
SISTEMAS WEB ESCALABLES
En el código nos hemos encargado de obtener
POR CADA POST SE
los posts correspondientes y, por cada uno de
CREA UN BLOQUE
ellos, creamos un bloque con el usuario que lo escribió, la fecha y el mensaje, y lo agregamos al contenedor #posts. Por último, recordemos que había quedado pendiente explicar la llamada a la función user. getTotalUsuarios() del archivo index.js, en caso de
CON EL USUARIO QUE LO ESCRIBIÓ Y OTROS DATOS IMPORTANTES
que el usuario haya iniciado sesión. Ya hemos desarrollado esta función, que simplemente realiza la devolución del total de usuarios registrados en el sistema (debemos considerar que serán mostrados en la parte inferior de la pantalla principal del sistema).
Figura 15. En la imagen podemos observar la apariencia de los posts y que el usuario tiene acceso a los posts de sus amigos.
Vista de la base de datos Inspeccionando la base de datos de nuestro sistema podemos ver la estructura generada; por ejemplo, los usuarios registrados y conectados, las listas de amigos generadas para cada usuario, los posts creados por los usuarios y también los contadores.
www.redusers.com
60
APÉNDICE. DESARROLLO DE UNA RED SOCIAL
Figura 16. Cada usuario contiene, además de sus datos, una lista de amigos, una lista de posts y un contador de posts. También podemos observar qué información tenemos de cada post. Entre los datos que es posible identificar para los post escritos encontramos el autor, la fecha en que se creó y también el mensaje que corresponde, así como el identificador del usuario.
Figura 17. En la base de datos guardamos la fecha, el mensaje y el identificador del usuario que lo escribió. www.redusers.com
61
SISTEMAS WEB ESCALABLES
Hasta aquí hemos realizado el desarrollo completo de un sistema medianamente complejo, que nos sirve como base para cualquier tipo de sistema escalable con funciones y características en tiempo real. Como sabemos, una red social cumple los requisitos mencionados, lo que la convierte en el ejemplo ideal para el tema que hemos tratado a lo largo de los capítulos que componen esta obra.
RESUMEN Hemos aplicado todos los conocimientos y temas tratados a lo largo del libro mediante el desarrollo de una red social que propone un gran cambio en la manera de pensar las acciones, ya que la mayoría de las interacciones deben ser reflejadas en tiempo real y desencadenan un efecto en los demás usuarios. Las bases de datos NoSQL, como Redis, cuentan con ventaja en la disponibilidad para grandes volúmenes de datos, ofreciendo un gran desempeño en el funcionamiento de cualquier sistema; al mismo tiempo, mediante Socket.IO es posible emitir y recibir eventos completamente personalizados, que entregan un aspecto único a la interacción de los usuarios con los sistemas.
www.redusers.com
62
APÉNDICE. DESARROLLO DE UNA RED SOCIAL
Actividades TEST DE AUTOEVALUACIÓN 1
¿Express ofrece la posibilidad de definir un sistema autosuficiente?
2
¿Es posible definir diferentes entornos de funcionamiento en Express?
3
¿Con qué característica debe contar una variable para tener un alcance global?
4
¿ Qué característica debe tener una función para tener un alcance global?
5
¿Socket.IO maneja sesiones? Justificar.
6
¿Es posible utilizar un mecanismo de cookies, en vez de sesiones, para manejar usuarios conectados?
7
¿on() y emit() son métodos de Socket.IO de Express?
8
¿Qué utilidad tiene la exportación de las funciones?
9
¿Los métodos on() y emit() pueden ser utilizados en cliente y en servidor indistintamente?
10
¿Un sistema desarrollado con Node y Express necesita de un servidor web como Apache?
EJERCICIOS PRÁCTICOS 1
Implemente un buscador para localizar posts, un sistema de comentario con Socket.IO para notificar a los amigos y un sistema de puntuación para los posts.
2
Instale el módulo nodemailer para enviar correos electrónicos.
3
Genere la función Ver y editar el perfil actual y Acceder al perfil de los amigos.
4
Implemente un sistema de cierre de sesión y notificación cuando el usuario cierra el navegador.
PROFESOR EN LÍNEA Si tiene alguna consulta técnica relacionada con el contenido, puede contactarse con nuestros expertos:
[email protected]
www.redusers.com