Lorsque l’on souhaite mettre en place une communication en temps réel entre nos applications et nos APIs, on pense souvent à l’utilisation des web sockets, mais ce n’est pas toujours la bonne solution. Il existe une autre fonctionnalité souvent méconnue qui se nomme SSE pour Server-Sent Events.
Note : Avant de poursuivre, je tiens à préciser que les exemples de cet article sont fonctionnels, mais sont simplifiés au maximum pour se concentrer sur la mise en œuvre de Server-Sent Event, je pense notamment à la partie qui concerne la mise en place côté back-end. Je ne présenterais pas l’accès à la base de données ni comment architecturer correctement l’application, je préfère me concentrer sur les concepts clés c’est pourquoi certains raccourcis seront pris, libre à vous d’adapter en fonction de vos projets. J’ai tout de même créé un projet complet qui est disponible sur GitHub si vous êtes intéressé.
Pourquoi mettre en place du temps réel ?
De nos jours, la plupart des sites web ou applications que vous utilisez quotidiennement que ce soit Twitter, Facebook, Discord ou encore Snapchat ont toutes un point en commun : ils proposent une expérience temps réel aux utilisateurs.
Si je vous dis “FOMO” ça ne vous dit peut-être rien, mais c’est l’acronyme de “Fear Of Missing Out” que l’on peut traduire par la peur de rater quelque chose. Bon, on ne va pas faire un cours de marketing, mais ce sentiment de peur est très utilisé par les sociétés pour vous rendre dépendants à leurs applications comme c’est le cas avec les réseaux sociaux.
Nos applications se doivent donc d’être interactives et transmettre de nouvelles informations aux utilisateurs sans intervention de leur part. Il est aujourd’hui impensable de devoir recharger une page web pour mettre à jour celle-ci.
Des solutions existent…
Nous devons donc mettre en place un moyen de communication en “temps réel” entre nos applications et nos APIs. De nombreuses solutions existent, parmi les plus connues nous avons :
- Polling : Cette solution, vous l’avez tous utilisée, notre application va interroger toutes les x secondes notre API pour voir si de nouvelles données sont disponibles;
- Long polling : C’est une variante de la précédente solution. La différence est que notre API va cette fois-ci garder la connexion ouverte avec notre application et renvoyer une réponse une fois que de nouvelles données sont disponibles;
- Web sockets : Cette fois-ci une connexion persistante est établie entre notre application et notre API. Les échanges peuvent se faire simultanément dans les deux sens (full duplex).
Les deux premières solutions présentent de nombreux inconvénients notamment une consommation en bande passante et en ressources importante, c’est pourquoi on pense souvent à l’utilisation des web sockets. Mais l’on n’a pas toujours besoin d’une communication bidirectionnelle, c’est le cas par exemple pour :
- Afficher le compteur des “j’aime” sur les réseaux sociaux;
- Mettre à jour l’affichage du stock d’un produit;
- Afficher les résultats d’événements sportifs;
- Recevoir des notifications;
- etc.
Nous allons donc voir l’utilisation d’une solution trop peu méconnue, vous vous en doutez je parle de Server-Sent Events.
Il y a bien longtemps…
Pour la petite histoire, la technologie Server-Sent Events est apparue en 2006 en tant que fonctionnalité expérimentale du navigateur Opéra, c’est-à-direc’est-à-dire deux ans avant les web sockets qui sont apparus quant à eux en 2008. Elle a par la suite été standardisée au sein de HTML 5. Autant dire que ça ne date pas d’hier !
Comment ça fonctionne ?
Tout comme web sockets, la connexion entre le client et le serveur est persistante, mais contrairement à celui-ci, Server-Sent Event établi une communication unidirectionnelle, les messages transitent uniquement du serveur vers le client.
Server-Sent Event repose entièrement sur le protocole HTTP contrairement à web socket qui est un protocole à part entière. La communication se fait comme suit :
- Le client se connecte sur le serveur via une URL;
- Le serveur maintient la connexion ouverte et envoie une réponse au client avec le header
Content-Type: text/event-stream
; - Dès que des données sont disponibles, le serveur les envoie au client.
- Si la connexion est perdue avec le serveur, le client envoie une demande de reconnexion au serveur avec le header
Last-Event-ID
qui permet d’indiquer au serveur qu’elle est le dernier message reçu; - Le client ou le serveur peut mettre fin à la connexion.
Format des messages
Contrairement aux web sockets dont le type de données peut être soit du binaire, soit une chaîne de caractères encodée en UTF-8, Server-Sent Event ne gère que le dernier cas. De plus, les messages doivent respecter un format précis décrit dans la norme :
1 2 3 4 5 | id: <ID du message - optionnel>\n event: <Le type de message- optionnel>\n retry: <Le délai de reconnexion - Optionnel>\n data: <La donnée du message - Obligatoire>\n \n\n |
Chaque message est constitué d’une paire clé/valeur séparée par les deux points :
et terminé par un saut de ligne \n
. Le message doit lui-même être terminé par deux sauts de ligne \n\n
.
Voyons voir à quoi correspond chacune des paires clé/valeur :
id
(optionnel) : L’ID unique du message, utilisé par le client lors d’une reconnexion (avec le headerLast-Event-ID
) pour éventuellement récupérer les données manquantes;event
(optionnel) : Le type de message reçu permettant notamment au client de séparer ses traitements en fonction du type de message, la valeur par défaut estmessage
;retry
(optionnel) : Le délai en millisecondes avant une tentative de reconnexion au serveur de la part du client;data
(obligatoire) : Le contenu du message qui peut être du texte ou du JSON.
Implémentation
Voyons dès maintenant l’implémentation de Server-Sent Event côté client dans un premier temps et côté serveur dans un second temps.
Côté client
La mise en place de Server-Sent Event côté client est extrêmement simple. En effet, les navigateurs proposent la classe EventSource
, il suffit de créer une nouvelle instance en passant en paramètre du constructeur l’URL de notre source :
1 | const eventSource = new EventSource('/stream'); |
Si la connexion s’est bien déroulée, un événement open
est déclenché. Il est possible d’enregistrer un gestionnaire pour cet événement en passant par la méthode onopen
(coucou l’absence de camelcase…) ou via la méthode addEventListener
:
1 2 3 4 5 6 | /* Soit via l'utilisation de la méthode "onopen" */ eventSource.onopen = () => { console.log('connected'); }; /* Ou via l'ajout d'un gestionnaire d'événement "open" */ eventSource.addEventListener('open', () => console.log('connected')); |
Il est également possible de vérifier l’état de la connexion via la propriété readyState
de l’instance d’EventSource
qui a pour valeur :
- 0 (ou
EventSource.CONNECTING
) : Connexion ou reconnexion en cours; - 1 (ou
EventSource.OPEN
) : Connecté; - 2 (ou
EventSource.CLOSE
) : Connexion fermée.
Si une erreur se produit, nous pouvons enregistrer un gestionnaire d’événement via la méthode onerror
(recoucou l’absence de camel case…) ou via la méthode addEventListener
afin de traiter cette erreur :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | /* Soit via l'utilisation de la méthode "onerror" */ eventSource.onerror = event => { console.log(event); if (eventSource.readyState === EventSource.CLOSED) { /* Traitement en cas de perte de connexion définitif avec le serveur */ } if (eventSource.readyState === EventSource.CONNECTING) { /* En cas de perte de connexion temporaire avec le serveur */ } }; /* Ou via l'ajout d'un gestionnaire d'événement "error" */ eventSource.addEventListener('error', event => { console.log(event); if (eventSource.readyState === EventSource.CLOSED) { /* Traitement en cas de perte de connexion définitive avec le serveur */ } if (eventSource.readyState === EventSource.CONNECTING) { /* En cas de perte de connexion temporaire avec le serveur */ } }); |
Récupération des messages
Pour récupérer les messages envoyés par le serveur, rien de plus simple, il suffit de faire appel à la méthode onmessage
(décidément l’absence de camel case…):
1 2 3 | eventSource.onmessage = event => { console.log(event.data); }; |
Le contenu du message est disponible dans la propriété data
de l’événement.
Par contre comme nous l’avons vu tout à l’heure, le serveur peut définir des types de messages via la propriété event
permettant au client de séparer ses traitements en fonction du type de message.
Avec la méthode onmessage
il est uniquement possible de récupérer les messages ayant le type par défaut message
. Pour récupérer les autres types de messages, il suffit d’enregistrer un gestionnaire d’événements pour chacun des types :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | /* Récupération des messages de type "alert" */ eventSource.addEventListener('alert', event => { console.log('Alert message', event.data); }); /* Récupération des messages de type "comment" */ eventSource.addEventListener('comment', event => { console.log('Comment message', event.data); }); /* Récupération des messages de type "like" */ eventSource.addEventListener('like', event => { console.log('Like message', event.data); }); |
Lors de la réception d’un message possédant un id, le client assigne la valeur de cette id à la propriété lastEventId
de notre instance d’EventSource
. Nous allons voir juste après à quoi sert cette propriété.
Gestion de la reconnexion
Server-Sent Event fournit un mécanisme de reconnexion automatique en cas de déconnexion avec le serveur. Le client s’occupe automatiquement de la reconnexion après quelques secondes ou via un délai défini arbitrairement par le serveur via un message reçu avec la propriété retry
définie.
Lors de la reconnexion au serveur, le client envoi via le header Last-Event-ID
, la valeur de la propriété lastEventId
, contenant l’id du dernier message reçu , permettant au serveur d’envoyer les éventuels messages non reçus par le client.
Fermeture de la connexion
Pour fermer la connexion avec le serveur, il suffit de faire appel à la méthode close
:
1 | eventSource.close(); |
Côté serveur
Il existe déjà des librairies implémentant Server-Sent Event (sse.js ou express-sse) mais le but de cet article est de comprendre comment fonctionne Server-Sent Event et rien de tel que de mettre les mains dans le cambouis, bref commençons !
Gestion de la connexion
Commençons dans un premier temps par gérer la connexion d’un client à notre serveur. Pour cela nous allons créer une classe SSEClient
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 | class SSEClient { /** * @param {Object} context - La réponse HTTP */ constructor(context) { this.context = context; } /** * Initialise la connexion avec le client * @function initialize */ initialize() { const headers = { /* Permet d'indiquer au client qu'il s'agit d'une connexion SSE */ 'Content-Type': 'text/event-stream', /* Permet d'indiquer au client que la connexion est persistente */ Connection: 'keep-alive', /* Permet d'empêcher la mise en cache des messages */ 'Cache-Control': 'no-cache' }; /* On envoie les headers au client */ this.context.writeHead(200, headers); } /** * Envoie un message au client * @function send * @params {Object} message - Le message à envoyer au client * @params {number|string} [message.id] - L'identifiant unique du message * @params {string} [message.type='message'] - Le type de message * @params {number} [message.retry] - Le délai en millisecondes avant une tentative de reconnexion au serveur * @params {string} message.data - Le contenu du message */ send(message) { const { id, type = 'message', retry, data } = message; if (id) { this.context.write(`id: ${id}\n`); } if (type) { this.context.write(`event: ${type}\n`); } if (retry) { this.context.write(`retry: ${retry}\n`); } this.context.write(`data: ${typeof data === 'object' ? JSON.stringify(data) : data}\n\n`); } } module.exports = SSEClient; |
Bon jusque là rien de bien compliqué hein ? Et bien, dites-vous que l’on a presque terminé, c’est vraiment aussi simple que ça !
Création du serveur HTTP
Créons à présent notre serveur HTTP à l’aide d’Express et ajoutons une route permettant à un client de se connecter via Server-Sent Event :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | const express = require('express'); const cors = require('cors'); const SSEClient = require('./SSEClient'); const app = express(); /* On active la prise en charge des requêtes CORS */ app.use(cors()); app.get('/stream/hello', async (req, res) => { /* On crée notre client */ const client = new SSEClient(res); /* On initialise la connexion */ client.initialize(); /* On attends 5 secondes ... */ setTimeout(() => { /* ... et on envoie un message au client */ client.send({ id: Date.now(), type: 'message', data: 'hello' }); }, 5000); }); app.listen(8080, () => { console.log('App is running ! Go to http://localhost:8080'); }); |
Testons avec le client
Vérifions que tout cela fonctionne en nous connectons à notre route avec notre navigateur web :
1 2 3 4 5 | const eventSource = new EventSource('http://localhost:8080/stream/hello'); eventSource.addEventListener('message', event => { console.log('Message', event.data); }); |
Vous pouvez utiliser la console de votre navigateur pour voir les messages reçus. Sur Chrome, il suffit de se rendre dans l’onglet “Network” puis “EventStream”.
Centraliser la gestion des connexions
Pour le moment, notre exemple est tout simple, dès qu’un client se connecte on lui envoie un message de bienvenue. Maintenant, comment gérer le cas où l’on souhaite envoyer des messages à une liste de clients en particulier ?
Nous allons ajouter une classe SSEManager
permettant de gérer les connexions et l’envoie des messages aux clients :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 | const SSEClient = require('./SSEClient'); class SSEManager { constructor() { /* On garde une liste de tous les clients connectés */ this.clients = new Map(); } /** * Initialise une nouvelle connexion avec un client * @function open * @param {number|string} clientId - L'identifiant du client * @param {Object} context - La réponse HTTP */ open(clientId, context) { const client = new SSEClient(context); client.initialize(); this.clients.set(clientId, client); } /** * Supprime un client * @function delete * @param {number|string} clientId - L'identifiant du client */ delete(clientId) { this.clients.delete(clientId); } /** * Supprime tous les clients * @function deleteAll */ deleteAll() { this.clients.clear(); } /** * Envoie un message à un seul client * @function unicast * @param {number|string} clientId - L'identifiant du client * @params {Object} message - Le message à envoyer au client * @params {number|string} [message.id] - L'identifiant unique du message * @params {string} [message.type='message'] - Le type de message * @params {number} [message.retry] - Le délai en millisecondes avant une tentative de reconnexion au serveur * @params {string} message.data - Le contenu du message */ unicast(clientId, message) { const client = this.clients.get(clientId); if (client) { client.send(message); } } /** * Envoie un message à tout les clients * @function broadcast * @params {Object} message - Le message à envoyer aux clients * @params {number|string} [message.id] - L'identifiant unique du message * @params {string} [message.type='message'] - Le type de message * @params {number} [message.retry] - Le délai en millisecondes avant une tentative de reconnexion au serveur * @params {string} message.data - Le contenu du message */ broadcast(message) { for (const [id] of this.clients) { this.unicast(id, message); } } /** * Envoie un message à une liste de client * @function multicast * @param {Array} clientIds - Les identifiants des clients * @params {object} message - Le message à envoyer aux clients * @params {number|string} [message.id] - L'identifiant unique du message * @params {string} [message.type='message'] - Le type de message * @params {number} [message.retry] - Le délai en millisecondes avant une tentative de reconnexion au serveur * @params {string} message.data - Le contenu du message */ multicast(clientIds, message) { for (const id of clientIds) { this.unicast(id, message); } } /** * Retourne le nombre de clients connectés * @function count * @returns {number} */ count() { return this.clients.size; } } module.exports = SSEManager; |
L’idée est que notre manager soit accessible au sein de notre application, plusieurs solutions s’offrent à nous comme l’injection de dépendances, l’utilisation d’un conteneur de dépendances, la création d’un singleton que nous importons via l’instruction require,
etc.
Nous allons utiliser sur une solution très simple afin de rester concentrer sur le fonctionnement de notre manager, libre à vous par la suite d’architecturer votre application comme bon vous semble.
Créons une nouvelle instance de notre manager et enregistrons là dans notre application Express afin que celui-ci soit accessible dans nos routes :
1 2 3 4 5 | const sseManager = new SSEManager(); /* On enregistre notre instance dans notre application Express, il sera lors possible de récupérer celle-ci via la méthode "get" */ app.set('sseManager', sseManager); |
Créons ensuite notre route :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | app.get('/stream/live', async (req, res) => { /* On récupère notre manager */ const sseManager = req.app.get('sseManager'); /* Notre route étant publique nous n'avons pas l'identité de l'utilisateur, nous générons donc un identifiant aléatoirement */ const id = crypto.randomBytes(16).toString('hex'); /* On ouvre la connexion avec notre client */ sseManager.open(id, res); /* On envoie le nombre de clients connectés à l'ensemble des clients */ sseManager.broadcast({ id: Date.now(), type: 'count', data: sseManager.count() }); /* en cas de fermeture de la connexion, on supprime le client de notre manager */ req.on('close', () => { /* En cas de deconnexion on supprime le client de notre manager */ sseManager.delete(id); /* On envoie le nouveau nombre de clients connectés */ sseManager.broadcast({ id: Date.now(), type: 'count', data: sseManager.count() }); }); }); |
Testons maintenant tout ça côté client :
1 2 3 4 5 6 | const eventSource = new EventSource('http://localhost:8080/stream/live'); /* Récupération des messages de type "count" */ eventSource.addEventListener('count', event => { console.log(`Il y a actuellement ${event.data} personne(s) connectée(s) sur le live`); }); |
À chaque connexion/déconnexion, le compteur de client se met à jour.
Sécurité
On a vu dans deux précédents articles comment sécuriser une API REST, je vous invite à aller les lire, je vous mets les liens ici : Sécuriser une API REST (1/3) : Théorie et Sécuriser une API REST (2/3) : Implémentation en Node.js.
Outre le fait qu’il soit indispensable d’utiliser une connexion sécurisée via l’utilisation de HTTPS, il est également nécessaire d’authentifier un utilisateur lorsque l’on souhaite avoir certaines routes privées de notre API utilisant Server-Sent Event.
Dans le cas de l’utilisation de cookies, il suffit de définir l’option withCredentials
à true
lors de l’instanciation de la classe EventSource
:
1 2 3 | const eventSource = new EventSource('http://localhost:8080/stream/live', { withCredentials: true }); |
Mais comment faire pour définir nous-même des en-têtes HTTP par exemple l’en-tête Authorization
avec comme valeur un JWT ? Et bien ce n’est malheureusement pas possible avec la classe native EventSource
. Il est possible néanmoins d’utiliser des librairies permettant d’envoyer les en-têtes HTTP, mais cela sort du cadre du standard Server-Sent Event.
Une autre solution consiste à récupérer un token, que l’on va appeler “stream token”, à l’aide d’un JWT en effectuant une requête HTTP sur une route de notre API, puis de se connecter sur le flux privé à l’aide de celui-ci.
Authentification via un “stream token”
Implémentons donc cette solution. Créons notre route permettant de récupérer le “stream token” à l’aide d’un JWT, nous n’allons pas revenir sur comment récupérer un JWT je vous invite à aller lire l’article qui lui est dédié :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | /* On récupère notre modèle permettant d'interagir avec la BDD */ const StreamToken = require('./models/StreamToken'); const { expiresIn } = require('./config'); const { auth } = require('./middleware'); app.post('/stream/token', auth, async (req, res, next) => { try { /* On récupère l'utilisateur authentifié */ const { user } = req; /* On génère un token */ const streamToken = crypto.randomBytes(64).toString('hex'); /* On enregistre ce token associé à l'utilisateur en base de données */ await StreamToken.create({ userId: user.id, token: streamToken, expiresAt: Date.now() + expiresIn }); /* On envoie ce token au client */ res.json({ streamToken, expiresIn: expiresIn }); } catch (err) { next(err); } }); |
Créons ensuite un middleware streamToken
permettant de récupérer un utilisateur à partir d’un “stream token”:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | /* On récupère notre modèle permettant d'interagir avec la BDD */ const StreamToken = require('./models/StreamToken'); module.exports = async (req, res, next) => { try { /* On récupère le token qui se trouve en paramètre de l'URL */ const { token: tokenParam } = req.params; /* On récupère le token avec l'utilisateur associé en base de données */ const token = await StreamToken.findOne({ where: { token: tokenParam }, include: ['user'] }); /* On vérifie que le token existe */ if (!token) { return res.status(401).json({ message: 'Invalid token' }); } /* On vérifie que le token n'est pas expiré */ if (token.expiresAt < new Date()) { return res.status(401).json({ message: 'Expired token' }); } /* On passe l'utilisateur dans notre requête afin que celui-ci soit disponible pour les prochains middlewares */ req.user = token.user; /* On appelle le prochain middleware */ return next(); } catch (err) { return res.status(500).json({ message: 'Internal server' }); } }; |
Modifions notre précédente route en utilisant cette fois-ci le middleware que l’on vient de créer :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | app.get('/stream/live/:token', streamToken, async (req, res) => { /* On récupère notre application Express et l'utilisateur */ const { app, user } = req; /* On récupère notre manager */ const sseManager = app.get('sseManager'); /* On ouvre la connexion avec notre client */ sseManager.open(user.id, res); /* On envoie le nombre de clients connectés à l'ensemble des clients */ sseManager.broadcast({ id: Date.now(), type: 'count', data: sseManager.count() }); /* En cas de fermeture de la connexion, on supprime le client de notre manager */ req.on('close', () => { sseManager.delete(user.id); /* On envoie le nouveau nombre de clients connectés */ sseManager.broadcast({ id: Date.now(), type: 'count', data: sseManager.count() }); }); }); |
Terminons avec la partie coté client, récupérons le “stream token” et connectons nous :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | async function connect() { /* On récupère le JWT */ const jwt = ... ; /* On crée le header avec le JWT */ const headers = new Headers(); headers.append('Authorization', `Bearer ${jwt}`); /* On créer les options de notre requête */ const options = { method: 'POST', headers: headers, mode: 'cors' }; /* On fait un appel sur notre API pour récupérer le stream token */ const response = await fetch('https://localhost:9001/stream/token', options); const { streamToken } = await response.json(); /* On se connecte à au flux Server-Sent Event */ const eventSource = new EventSource(`https://localhost:9001/stream/live/${streamToken}`); return eventSource; } |
Le “stream token” possède une durée de validité et il est également possible, pour plus de sécurité, de supprimer celui-ci une fois la connexion établie. Le souci avec cette approche est qu’en cas de déconnexion, la reconnexion automatique sera confrontée à une erreur 401 car le “stream token” n’existe plus. Il sera alors nécessaire d’intercepter cette erreur côté client et de procéder à une nouvelle connexion afin de récupérer un “stream token” valide.
Tout cela peut se réaliser en ajoutant un gestionnaire d’événement d’erreur :
1 2 3 4 5 6 7 8 9 10 11 12 13 | /* Il est également possible d'utiliser la méthode "onerror" */ eventSource.addEventListener('error', event => { console.log(event); if (eventSource.readyState === EventSource.CLOSED) { /* Traitement en cas de perte de connexion définitive avec le serveur */ } if (eventSource.readyState === EventSource.CONNECTING) { /* On ferme la connexion */ eventSource.close(); /* Et on se reconnecte */ connect(); } }); |
Pour finir
On a vu tout au long de cet article qu’il était simple de mettre en place Server-Sent Event aussi bien coté back que front.
Il y a de nombreux cas d’utilisations de Server-Sent Event, par exemple Facebook l’utilise pour mettre à jour en temps réels les réactions et les commentaires sur les vidéos en direct. On pense souvent à l’utilisation de web sockets alors que ce n’est pas toujours la meilleure solution notamment lorsque l’on souhaite uniquement recevoir des informations du serveur.
J’espère vous avoir fait découvrir cette fonctionnalité qui est malheureusement trop méconnue de la plupart des développeurs.
Pour les plus curieux, un petit projet, qui reprend ce que l’on vient de voir dans cet article, est disponible sur GitHub, allez y jeter un œil !
Je suis lead developer dans une boîte spécialisée dans l’univers du streaming/gaming, et en parallèle, je m’éclate en tant que freelance. Passionné par l’écosystème JavaScript, je suis un inconditionnel de Node.js depuis 2011. J’adore échanger sur les nouvelles tendances et partager mon expérience avec les autres développeurs.
Si vous avez envie de papoter, n’hésitez pas à me retrouver sur Twitter, m’envoyer un petit email ou même laisser un commentaire.
1 commentaire