Mes conseils d'utilisation de Socket.io

socket.io est la librairie la plus utilisée pour faire du real-time avec un serveur node. Lorsque les sockets embarquent une multitude d’évènements, il devient de plus en plus difficile de les maintenir. Voici quelques tips que j’utilise personnellement afin d’éviter au maximum ce problème.

Avant de commencer cet article, il est préférable de connaitre les bases de fonctionnement de socket.io.

Respecter une norme de nommage

La première étape avant de commencer quoi que ce soit est de mettre au point une norme de nommage des events de vos sockets. Pour ma part, j’utilise le format domaine:action.
Par exemple, lorsque vous voulez envoyer un message sur le tchat, le nom de l’event sera message:send. Si vous souhaitez rafraichir la liste des messages, l’event sera message:refresh. Si nous avons la possibilité de gérer une liste d’amis, alors nous pourrions avoir des évènements qui auront pour nom friends:add ou encore friends:remove.
Ce format permet dans un premier temps d’éviter les doublons et surtout d’avoir une meilleure lisibilité sur le rôle de l’évènement. Le fait de scinder les évènements en domaine permet également de scinder logiquement ses fonctions en plusieurs fichiers correspondant à leurs domaines respectifs.

Utilisation de “handlers”

Les handlers ne sont ni plus ni moins que des fichiers où l’on va implémenter les logiques des évènements. Le but est de séparer votre code en respectant la même logique que pour le nommage (c’est-à-dire, en créant un fichier par domaine). Voici comment je sépare la logique de mes sockets :

- socket/
  - index.js
  - message.js
  - friends.js

Comme expliqué dans le chapitre précédent, vos handlers seront regroupés par domaine. dans le fichier index.js c’est ici que vous aurez toute la connexion de vos sockets et l’implémentations de vos handlers. Vos handlers quant-à-eux n’implémenteront que les logiques liées à leur domaine.
Dans notre exemple, tous les events liés à message (ex: message:send, message:refresh) se feront dans le fichier message.js.

Valider vos events: Joi à la rescousse

Etant un grand fan de hapi, cela me frustrait particulièrement de ne pas pouvoir utiliser tout l’aspect de validation mise en place par hapi sur les routes.
Une des forces de ces validations reste l’utilisation de Joi, une librairie permettant de vérifier qu’un objet respecte un schéma bien précis. Ces schémas se présentent sous cette forme :

Joi.object().keys({
  message: Joi.string().required().description("The content of the message"),
  to: Joi.string().optional().description("The name of the user to send the message")
});

Ce schéma ci-dessus permet de vérifier le contenu de notre event message:send. Ici nous pouvons voir que notre event attend un objet avec l’attribut message qui est obligatoire. Un attribut to est optionnel et permet d’envoyer un message à seulement une personne.

Aller plus loin: vous pouvez également externaliser vos schémas dans un autre fichier. vous pourrez donc avoir une architecture qui ressemble à ça :

- socket/
  - message/
    - index.js
    - schemas.js
  - index.js

Ajouter une couche supplémentaire : les Helpers

Pour terminer en beauté, nous allons créer des helpers dans un autre fichier (idéalement situé dans un dossier helpers suivi d’un fichier nommé socket.js) qui vont nous permettre d’assembler tout ce que nous avons vu précédemment. En effet, nous n’allons pas réécrire tout notre bout de code à chaque fois que nous allons créer un évènement.
Tout d’abord, nous allons créer un helper qui va nous permettre de créer un évènement et valider le payload de l’event avec un schéma Joi. Voici un exemple d’helper qui répond à cette problématique :

// helpers/socket.js
/**
 * Create an event to be implemented into sockets
 * @param {String} name - The name of the event
 * @param {object} rules - Object containing Joi validation rules
 * @param {Function} fn - The function to be called on event
 * @returns {*} The event Object
 */
export const createEvent = (name, rules, fn) => {
  Hoek.assert(!!name, "helpers - socket.createEvent() must have a name");
  Hoek.assert(typeof fn === "function", "helpers - socket.createEvent() must have a function");
  return {
	  name,
	  fn,
	  validation: rules && Joi.object().keys(rules)
  };
};

Hoek est un utilitaire utilisé dans l’univers hapi. On s’en sert principalement pour la fonction assert qui permet d’éviter les “if throw” qui prennent beaucoup de lignes inutilement.
Avec ce helper, nous allons pouvoir maintenant créer notre event message:send:

// socket/message.js
export const sendMessage = createEvent("message:send", {
  message: Joi.string().required().description("The content of the message"),
  to: Joi.string().optional().description("The name of the user to send the message")
}, async (socket, { message, to }) => {
  // Insert your logic here
});

Nous venons de créer notre premier event, mais maintenant il faut pouvoir l’implémenter dans la socket, pour cela nous allons créer un deuxième helper qui va interpréter le résultat de notre fonction createEvent :

// helpers/socket.js
/**
 * Bind an event to a socket
 * @param {String} name - The name of the event
 * @param {any} validation - A Joi object validation
 * @param {Function} fn - The function to be called on event
 */
export const bindEvent = (socket, {name, validation, fn}) => {
  socket.on(name, (payload = {}) => {
    if (validation) {
      Joi.validate(payload, validation, (error) => {
        if (error) {
          return socket.emit(name, {error});
        }
        fn(socket, payload);
      });
    }
    return fn(socket, payload);
  });
};

bindEvent permet de créer l’évènement sur la socket, de vérifier le payload (si on lui a passer un schéma), de renvoyer une erreur si celui-ci n’est pas valide, et enfin de faire appel à la callback en rajoutant la socket actuelle dedans.
Dans votre fichier principal, vous n’aurez donc plus qu’à utiliser notre méthode créer précédemment pour que nos events soient maintenant disponible dans la socket

// socket/index.js
import Io from "socket.io";
import { bindEvent } from "./../helpers/socket";
import * as messageHandlers from "./message";
const handlers = Object.values({
  ...messageHandlers
});
export default (listener) => {
  const io = Io.listen(listener);
  io.on("connection", (socket) => {
    handlers.forEach((handler) => {
      bindEvent(socket, handler);
    });
  });
};

Conclusion

En suivant les tips, vous devrez avoir une architecture serveur ressemblant à ça :

- helpers/
  - index.js
  - socket.js
- socket/
  - index.js
  - friends.js
  - message.js
- index.js

Ces méthodes ne sont pas à prendre au pied de la lettre, elles ont pour but de vous donner quelques conseils afin de vous permettre de mieux organiser la logique de vos sockets.
N’hésitez pas à écrire des commentaires si vous connaissez d’autres techniques ou si vous avez moyen d’améliorer davantage les tips cités ci-dessus.
Article écrit par Christophe Brochard