Chat con socket.io

En esta lección aprenderéis cómo hacer un chat en tiempo real con Node.js, y socket.io. Aprenderemos qué es eso de websockets y para qué es útil.

Websockets es una tecnología de comunicación en tiempo real bidireccional entre los navegadores y servidores. Es útil por ejemplo para hacer server-push en ciertos eventos, algo que hasta ahora sólo se podía hacer con short/long polling.

socket.io es una herramienta alojada en la web homónima cuyo objetivo es facilitar el uso de websockets y que proporciona funcionalidades más avanzadas como namespaces. Esto es muy útil para hacer chats más complejos ya que permite por ejemplo hacer una habitación en cada namespace.

Instalación

Primero que nada seguir la guia de instalación de Introducción y Preparación. Después, una vez estemos en nuestra carpeta del proyecto y hayamos hecho npm init, instalaremos la librería propia server, que nos servirá para crear nuestro back-end:

npm install server --save

Back-end

El back-end va a tener dos funcionalidades. Va a ser el encargado de cargar y mostrar los archivos necesarios (como un servidor estático) y va a actuar como capa de publicación de mensajes. Esto último quiere decir que va a recibir un mensaje de cierto tipo desde un usuario y se lo comunica al resto de usuarios conectados al back-end.

Vamos a usar las funciones que provee server, que ya incluye la librería socket.io. Como el back-end va a ser sencillo vamos a hacerlo en sólo 2 archivos de código. Primero creamos nuestro punto de entrada, index.js:

// index.js
const server = require('server');
const { get, socket } = server.router;
const { file } = server.reply;
const chat = require('./chat');

// Lanzar el servidor en el puerto 3000
server([

  // Mostrar el archivo principal a todo el mundo
  get('/', file('./public/index.html')),

  // Rutas para el chat
  socket('login',      chat.login),
  socket('message',    chat.message),
  socket('logout',     chat.logout),  // Manual
  socket('disconnect', chat.logout),  // Accidental/cerrar ventana

  // En caso que haya alguna petición sin responder
  get('*', ctx => 404)
]);

Y después creamos la lógica del chat.js:

// Guardar el nombre del usuario en el back-end y comunicarlo a todos
exports.login = ctx => {
  ctx.socket.user = ctx.data;
  console.log('Login:', ctx.socket.user);
  return ctx.io.emit('login', {
    user: ctx.socket.user,
    time: new Date()
  });
};

// Cuando alguien envía un mensaje, reenviarlo a todo el mundo
exports.message = ctx => {
  console.log('Message:', ctx.data);
  ctx.io.emit('message', {
    user: ctx.socket.user,
    text: ctx.data,
    time: new Date()
  });
};

// Cuando alguien hace logout mostrarselo también a todo el mundo
exports.logout = ctx => {
  console.log('Logout:', ctx.socket.user);
  if (!ctx.socket.user) return; // Para los que entran y salen sin hacer login
  return ctx.io.emit('logout', {
    user: ctx.socket.user,
    time: new Date()
  });
};

Front-end

Ahora que ya tenemos listo el back-end, vamos a proceder con el front-end. Este es el código que cada usuario ejecuta en su navegador y que sirve tanto para que los usuarios envíen información cómo para que la reciban.

HTML

El HTML es bastante sencillo, incluimos algunas dependencias a través de CDNs y creamos la estructura básica incluyendo el modal de login en /public/index.html:

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>Chat!</title>
  <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
  <link href="https://cdnjs.cloudflare.com/ajax/libs/materialize/0.98.2/css/materialize.min.css" rel="stylesheet">
  <link href="/style.css" rel="stylesheet">
</head>
<body>
  <nav>
    <div class="nav-wrapper">
      <a href="#" class="brand-logo left">Maker Chat</a>
      <ul id="nav-mobile" class="right">
        <li><a href="#" class="logout waves-effect waves-light">Logout</a></li>
      </ul>
    </div>
  </nav>



  <!-- Contenido principal de la página web -->
  <main>

    <!-- Donde se irán añadiendo los mensajes -->
    <div class="messages"></div>

    <!-- Para poder escribir un mensaje nosotros también -->
    <form class="message">
      <div class="input-field">
        <input id="message" type="text">
        <label for="message">Escribe tu mensaje aquí:</label>
      </div>
      <button class="btn btn-large btn-floating waves-effect waves-light">
        <i class="material-icons">send</i>
      </button>
    </form>
  </main>



  <!-- Modal Structure -->
  <form id="login" class="modal">
    <div class="modal-content">
      <h4>Nombre de usuario</h4>
      <p>Tu identidad para que otros sepan quién eres:</p>
      <div class="input-field">
        <i class="material-icons prefix">account_circle</i>
        <input id="username" class="username" type="text" placeholder="Nombre">
        <label for="username">Nombre de usuario:</label>
      </div>
      <p>
        <button class="btn waves-effect left">Aceptar</button>
      </p>
    </div>
  </form>

  <script src="https://unpkg.com/[email protected]"></script>
  <script src="https://code.jquery.com/jquery-2.1.1.min.js"></script>
  <script src="https://unpkg.com/[email protected]/dist/socket.io.slim.min.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/materialize/0.98.2/js/materialize.min.js"></script>
  <script src="/javascript.js"></script>
</body>
</html>

La mayoría es estándar de cualquier página web así que vamos a explicar la parte intermedia. El <main> contiene lo que es el chat en sí, que será la columna central. El div con la clase .messages es al que le iremos añadiendo los mensajes a medida que los vayamos recibiendo y por último el formulario es para que nosotros también podamos enviar mensajes al resto de usuarios.

La hoja de estilos style.css se queda fuera de la explicación de este taller pero la pondremos en /public/style.css y la podéis encontrar aquí.

Conexión y login

Vamos a la parte más interesante, el javascript de front-end en public/javascript.js. Primero que nada iniciamos socket.io y nos ponemos a escuchar mensajes:

// Conectamos al servidor con socket.io
var socket = io();

socket.on('login', function(message) {
  console.log(message);
});

socket.on('message', function(message) {
  console.log(message);
});

socket.on('logout', function(message) {
  console.log(message);
});

Con esto ya estaríamos escuchando a los mensajes y mostrándolos por pantalla pero todavía no tenemos ninguna forma de escribirlos. Primero que nada vamos a crear la funcionalidad de login con ayuda de jQuery y el modal de Materialize:

// Usar el modal de login con el usuario al entrar
$('#login').modal({ dismissible: false }).submit(function (e) {
  e.preventDefault();
  var user = $('#login input').val();

  // Continuar sólo si sí que hay usuario
  if (!user) return;
  cookies({ user });
  $('#login').modal('close');

  // Enviar el evento al resto de usuarios
  socket.emit('login', user);
});

También vamos a comprobar al cargar la página por primera vez si ya hay un usuario o no, y en caso de que lo haya cerrar el modal y comunicar al resto de usuarios quién se ha unido al chat:

// Comunicar el usuario al resto de gente o pedir login
if (cookies('user')) {
  socket.emit('login', cookies('user'));
} else {
  $('#login').modal('open');
}

Lo juntamos todo en javascript.js dentro de nuestra carpeta public:

// Conectamos al servidor con socket.io
var socket = io();

socket.on('login', function(message) {
  console.log(message);
});

socket.on('message', function(message) {
  console.log(message);
});

socket.on('logout', function(message) {
  console.log(message);
});


// Comunicar el usuario al resto de gente o pedir login
if (cookies('user')) {
  socket.emit('login', cookies('user'));
} else {
  $('#login').modal('open');
}


// Usar el modal de login con el usuario al entrar
$('#login').modal({ dismissible: false }).submit(function (e) {
  e.preventDefault();
  var user = $('#login input').val();

  // Continuar sólo si sí que hay usuario
  if (!user) return;
  cookies({ user });
  $('#login').modal('close');

  // Enviar el evento al resto de usuarios
  socket.emit('login', user);
});

Para probarlo, ejecutar npm start en nuestra raíz del proyecto y abrimos una ventana del navegador en http://localhost:3000/ y abrimos la consola. Nos pedirá el nombre de usuario, se lo proporcionamos y debería de hacer un log() en la terminal.

Abrimos otra ventana a la misma web y observamos que en la inicial vuelve a hacer un log() desde la terminal. Ya tenemos lo básico funcionando.

Enviar mensajes

Para enviar un mensaje interceptaremos el submit del formulario de mensajes, enviamos el mismo por websockets y borramos el valor del input:

// El usuario envía un mensaje
$('form.message').submit(function(e){
  e.preventDefault();
  var $input = $(e.target).find('input');
  var text = $input.val();

  // Borrar el mensaje del input
  $input.val('');

  // Enviar el mensaje a todos
  socket.emit('message', text);
});

No añadimos el mensaje aquí ya que el mismo lo volveremos a recibir desde el server, validando que se ha enviado de esta forma.

Mostrar mensajes

Por último nos definimos las funciones que mostraban por la consola para que lo muestren ahora en la pantalla. Usamos un par de pequeñas funciones de utilidad que nos hemos creado para facilitar las cosas:

// Evitar XSS usando escape()
var escape = function(html) {
  return $('<div>').text(html).html();
};


// Añadir un mensaje a la lista y hacer scroll
var add = function(html) {
  var toScroll = $('.messages').prop("scrollHeight") - 50 < $('.messages').scrollTop() + $('.messages').height();
  $('.messages').append(html);

  // Hacer scroll sólo si mantenemos la conversación abajo, si hemos subido no scrollear
  if (toScroll) {
    $('.messages').stop(true).animate({
      scrollTop: $('.messages').prop("scrollHeight")
    }, 500);
  }
};


socket.on('login', function(message) {
  add('<div class="msg login">\
    <span class="user">' + escape(message.user) + '</span> logged in.\
  </div>');
});

socket.on('message', function(message) {
  add('<div class="msg">\
    <span class="user">' + escape(message.user) + ':</span> \
    <span class="msg">' + escape(message.text) + '</span>\
  </div>');
});

socket.on('logout', function(message) {
  add('<div class="msg logout">\
    <span class="user">' + escape(message.user) + '</span> logged out.\
  </div>');
});

Ya lo tenemos! Podemos ver el código final en el repositorio:

Repositorio de Github

Le hemos añadido unos pequeños extras y detalles, pero sigue la estructura general que hemos explicado aquí.

Subir a Heroku

Esta sección se expandirá (todavía no) en otra lección.

Para subir sólo lo que nos interesa creamos .gitignore en el root de nuestro proyecto:

# Logs
logs
*.log
npm-debug.log*

# Dependency directories
node_modules

# dotenv environment variables file
.env

Por último iniciamos Git, Heroku y subimos la web:

git init

# Añadir nuestros archivos a Git
git add .gitignore
git commit -m "Ignoring some files"
git add .
git commit -m "Added full project"

# Crear la app de Heroku y añadir el remote
heroku create

# Subir la web a Heroku
git push heroku

¡Ya está! Ahora podemos abrir la URL que nos ha mostrado Heroku y ver nuestro chat en tiempo real.