In questo articolo vedremo come implementare una semplice chat con ExpressJS e Redis.

Struttura e funzionamento di una chat

Una chat è costituita da due tipi di dati: una lista di utenti ed una lista di messaggi. In JavaScript entrambi i tipi di dati possono essere rappresentati come array.

Quando un utente entra nella chat, sceglie un nome utente (username). Se il nome utente non è già in uso, il suo nome utente viene aggiunto alla lista degli utenti. A quel punto l'utente può visualizzare la lista dei messaggi già inviati ed inviarne a sua volta. Per farlo, l'utente scriverà il messaggio all'interno di un'area di testo.

Ciascun messaggio a sua volta contiene due dati: lo username dell'autore del messaggio ed il testo del messaggio. In JavaScript possiamo rappresentare i messaggi come oggetti letterali aventi come proprietà appunto lo username ed il testo del messaggio.

L'utente può anche decidere di abbandonare la chat cliccando su un apposito pulsante. A quel punto il suo nome utente verrà rimosso dalla lista degli utenti e se vorrà dovrà nuovamente entrare nella chat.

Per poter implementare questa struttura abbiamo bisogno che lato backend vi sia uno storage comune dove memorizzare le liste degli utenti e dei messaggi. Questo storage è Redis.

Uso di Redis

Redis è uno storage in-memory, ossia i dati vengono salvati solo sulla memoria, anche se è possibile configurare Redis per effettuare un salvataggio su disco.

I dati vengono salvati come stringhe sotto forma di coppie formate da una chiave e da un valore.

Quello che dobbiamo fare è trasformare gli array contenenti gli utenti ed i messaggi in stringhe JSON in scrittura e riconvertirli in array in lettura utilizzando rispettivamente i metodi JSON.stringify() e JSON.parse(). In questo modo possiamo aggiornare tali dati quando è necessario.

Il codice del backend

Possiamo incapsulare la logica e la struttura della chat in una classe specifica. Usando l'object composition, questa classe avrà come unica dipendenza il modulo NPM redis per poter lavorare con il server Redis. I metodi che gestiranno le API JSON del frontend accetteranno come argomenti gli oggetti request e response di ExpressJS.

class ChatServer {
    constructor(pubSubLib) {
        this.pubSubLib = pubSubLib;
        this.client = pubSubLib.createClient();
        this.users = [];
        this.messages = [];

        this.init();
    }

    init() {
        let self = this;
        self.client.once('ready', () => {
            self.client.flushdb();
            self.client.get('users', (err, reply) => {
                if (reply) {
                    self.users = JSON.parse(reply);
                }
            });
    
            self.client.get('messages', (err, reply) => {
                if (reply) {
                    self.messages = JSON.parse(reply);
                }
            });
       });
    }

    join(req, res) {
        const {username} = req.body;
        if (!this.users.includes(username)) {
            this.users.push(username);
            this.client.set('users', JSON.stringify(this.users));
            res.send({
                users: this.users
            });
        } else {
            res.send({
                error: 'Already joined.'
            });
        }
    }

    leave(req, res) {
        const {username} = req.body;
        this.users.splice(this.users.indexOf(username), 1);
        this.client.set('users', JSON.stringify(this.users));
        res.send({
            done: true
        });
    }

    send(req, res) {
        const {username, message} = req.body;
        this.messages.push({
            username,
            message
        });
        this.client.set('messages', JSON.stringify(this.messages));
        res.send({
            done: true
        });
    }

    getMessages(req, res) {
        res.send(this.messages);
    }

    getUsers(req, res) {
        res.send(this.users);
    }
}

module.exports = ChatServer;

pubSubLib è il riferimento al modulo NPM redis. Questo riferimento ci permette di creare un client Redis con cui operare (createClient()). In fase di avvio (init()) svuotiamo il database (flushdb()) e risincronizziamo gli array di utenti e messaggi.

In join() riceviamo in POST uno username e se questo non è presente nell'array degli utenti, lo aggiungiamo e salviamo l'array in Redis con la chiave users e il valore uguale alla stringa JSON dell'array degli utenti. Altrimenti restituiamo un errore.

Quando l'utente vuole lasciare la chat viene chiamato in POST il metodo leave() che riceve il nome utente e lo rimuove dall'array degli utenti. Quindi salviamo nuovamente in Redis l'array modificato.

Per inviare un messaggio viene effettuata una richiesta POST al metodo send() che riceve il nome utente ed il testo del messaggio. Viene aggiornato l'array dei messaggi e salvato in Redis.

I metodi getMessages() e getUsers() gestiscono due richieste GET che restituiscono al frontend rispettivamente l'elenco dei messaggi e quello degli utenti.

A questo punto possiamo usare questa classe nel file principale della nostra app collegando i metodi che gestiscono le API JSON con le route di ExpressJS.

const path = require('path');
const express = require('express');
const bodyParser = require('body-parser');
const app = express();
const redis = require('redis');
const port = process.env.PORT || 3000;
const ChatServer = require('./classes/ChatServer');

const chat = new ChatServer(redis);

app.disable('x-powered-by');

app.use('/public', express.static(path.join(__dirname, 'public')));
app.use(bodyParser.json());

app.get('/', (req, res) => {
    res.sendFile(__dirname + '/views/index.html');
});

app.post('/api/join', (req, res) => { chat.join(req, res); });
app.post('/api/leave', (req, res) => { chat.leave(req, res); });
app.post('/api/send', (req, res) => { chat.send(req, res); });
app.get('/api/messages', (req, res) => { chat.getMessages(req, res); });
app.get('/api/users', (req, res) => { chat.getUsers(req, res); });


app.listen(port);

È da notare che usando bodyParser.json() stiamo di fatto vincolando l'uso degli endpoint in POST all'invio di una richiesta in formato JSON, ossia il cui Content Type è application/json e il suo corpo un oggetto JSON valido.

Codice sorgente

GitHub