In questa lezione della nostra guida a Node.js parleremo, attraverso un esempio, di una delle librerie Javascript più interessanti. Si tratta della libreria Socket.io che permette di realizzare in maniera semplice e intuitiva applicazioni web Real-Time, abilitando un canale di comunicazione bidirezionale fra Client e Server. Infatti, Socket.io usa la tecnologia dei WebSocket per tutti i browser compatibili, ripiegando su altre tecniche per i browser più obsoleti. Gli sviluppatori non dovranno così preoccupparsi di problemi di compatibilità.
Socket.io consiste di due componenti separate: una lato Client e una libreria lato Server per Node.js. L’API delle due è simile e si basa sull’uso degli eventi, in modo simile a quanto abbiamo già visto quando abbiamo parlato degli Event Emitter. Le funzionalità messe a disposizione da Socket.io sono tante. In questa lezione ci limiteremo a un’introduzione in cui impiegheremo i due metodi base della libreria, ovvero socket.emit() e socket.on().
Esempio di applicazione Real-Time
Vediamo come poter utilizzare la libreia Socket.io per realizzare una applicazione web Real-Time. Realizzeremo un sito internet su cui è possibile vendere degli oggetti usati. Ogni utente può fare un’offerta dopo aver inserito il proprio nome. Ovviamente in questo esempio realizzeremo una sola pagina in cui è stato messo in vendita un Macintosh del 1984. Ciascun utente visualizzerà un messaggio in cui gli viene comunicato in tempo reale quanti altri utenti stanno visualizzando quell’oggetto. Quando un utente effettuerà un’offerta, a tutti gli altri sarà mostrata una notifica con il nome dell’utente che ha fatto l’offerta e la somma proposta in euro.
Per realizzare la nostra applicazione, creiamo una nuova cartella, ci spostiamo al suo interno e lanciamo il comando npm init -y per creare un file package.json con le impostazioni predefinite. Installiamo poi le seguenti dipendenze: Socket.io, Express e Concurrently.
npm install socket.io concurrently express --save
Modifichiamo poi il file package.json come riportato in basso.
{
"name": "socket_example",
"version": "1.0.0",
"description": "",
"main": "app.js",
"scripts": {
"start": "node app.js",
"browser-sync": "browser-sync start --proxy 127.0.0.1:7777 --ws --files 'views/*' 'public' ",
"nodemon": "nodemon app.js",
"dev": "concurrently -k "npm run nodemon" "npm run browser-sync" "
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"concurrently": "^3.5.0",
"express": "^4.16.1",
"socket.io": "^2.0.3"
}
}
Nel nostro caso abbiamo anche installato globalmente (npm install -g browser-sync nodemon) Browser-sync e Nodemon. Questo passaggio è opzionale, ma consente di velocizzare la fase di sviluppo dell’applicazione. Nodemon infatti resta in ascolto per eventuali modifiche lato server e rilancia in automatico l’applicazione. Abbiamo invece utilizzato Browser-sync per aggiornare il browser ogni volta viene fatta una modifica a uno dei file presenti nella cartella views o public. Inoltre, Browser-sync permette di avere più finestre aperte (anche di Browser o dispositivi diversi) con la stessa applicazione e provvede a sincronizzarle. Con il comando npm run browser-sync avviamo Browser-sync specificando che abbiamo già lanciato un server HTTP all’indirizzo 127.0.0.1: 7777. Usiamo l’opzione –ws per indicare che nella nostra applicazione abbiamo usato i WebSocket. Infine Concurrently dà la possibilità di eseguire i due script in parallelo.
Analizziamo quindi quella che sarà la struttura del nostro progetto.
.
├── app.js
├── node_modules
├── package-lock.json
├── package.json
├── public
│ ├── css
│ │ └── style.css
│ ├── imgs
│ │ └── macintosh.jpg
│ └── js
│ └── socket.io.js
├── socket.js
└── views
└── index.html
Partiamo dalla cartella public in cui abbiamo inserito alcune risorse statiche come l’immagine che useremo nel corso dell’esempio, un file style.css e la componente lato client della libreria Socket.io (socket.io-client). I file su cui concentreremo invece la nostra attenzione sono: index.html (lato client), socket.js e app.js (lato server).
Partiamo col file app.js che è il più semplice dei tre.
// app.js
const express = require('express');
const app = express();
const http = require('http');
const server = http.createServer(app);
const initSocket = require('./socket');
initSocket(server);
app.use('/js', express.static(__dirname + '/public/js'));
app.use('/css', express.static(__dirname + '/public/css'));
app.use('/imgs', express.static(__dirname + '/public/imgs'));
app.get('*', function(req, res) {
res.sendFile(`${__dirname}/views/index.html`);
});
server.listen(7777);
Nel file app.js abbiamo importato Express, il modulo HTTP e la funzione che abbiamo definito nel file socket.js. Invochiamo quest’ultima passando come argomento un’istanza di http.Server, restituita dalla funzione http.createServer(app), che a sua volta riceve come argomento l’oggetto Express Application (app). Usiamo il middleware express.static per indicare dove sono collocati i vari asset statici. Inoltre, prima di lanciare il server con il metodo server.listen(7777), chiediamo a Express di servire sempre la stessa pagina index.html per qualsiasi percorso richiesto.
// file socket.js
const Socket = require('socket.io');
const sockets = {};
let currentPrice = 1;
function initSocket(server) {
const io = Socket(server);
io.on('connection', (socket) => {
const other_users = Object.keys(sockets).length;
sockets[socket.id] = socket;
io.emit('users', other_users);
io.emit('firstPrice', currentPrice);
socket.on('updatePrice', (data) => {
const name = data.name;
const amount = data.amount;
if (amount > currentPrice) {
currentPrice = amount;
io.emit('newPrice', {id: socket.id, name, amount})
}
});
socket.on('disconnect', (reason) => {
console.log('disconnected');
delete sockets[socket.id];
const other_users = Object.keys(sockets).length - 1;
io.emit('users', other_users);
});
});
}
module.exports = initSocket;
All’interno del file socket.js abbiamo definito la funzione initSocket() in cui creiamo un oggetto SocketServer a cui passiamo come argomento il server HTTP creato nel file app.js. Tale oggetto resta in attesa che un Client si colleghi al server. Per far ciò abbiamo registrato una funzione listener per l’evento ‘connection’ emesso dallo stesso oggetto Server (io). L’unico argomento di questa funzione listener è il socket relativo al Client che si è collegato al server. All’interno dell’oggetto sockets salviamo un riferimento a ciascun socket. Il server lancerà quindi due eventi a tutti (io.emit()) i socket attivi. Nel file index.html registreremo le opportune funzioni listener per entrambi gli eventi ‘users’ e ‘firstPrice’ che utilizzeremo per comunicare ai Client quanti altri utenti sono attivi al momento in cui avviene la connessione al server e qual è il prezzo corrente dell’oggetto in vendita. Abbiamo inoltre registrato due funzioni listener per gli eventi ‘updatePrice’ e ‘disconnect’ emessi da un Client. Il primo evento updatePrice, verrà emesso da un Client quando effettuerà una nuova offerta. Se la somma offerta supera il prezzo corrente, il server emetterà un evento in broadcast per notificare a tutti i Client connessi che il prezzo dell’oggetto è stato aggiornato. Ciascun Client riceverà come argomento anche l’oggetto contenente l’id e il nome del Client che ha fatto la proposta e la sommma offerta. Infine, verrà lanciato l’evento ‘disconnect’ quando un Client si disconnette. In questo caso il server provvede a rimuovere il socket corrispondente dalla lista dei socket attivi e invia ai Client rimanenti il numero aggiornato degli utenti attivi.
<!-- file index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<link rel="stylesheet" href="css/style.css">
<title>Esempio Socket.io</title>
</head>
<body>
<main>
<div class="img-container">
<img src="/imgs/macintosh.jpg" alt="macintosh 1984" >
</div>
<div class="alert-message">
<span></span>
</div>
<section class="content">
<h1>Macintosh - 1984</h1>
<p>
Nullam quis risus eget urna mollis ornare vel eu leo.
Etiam porta sem malesuada magna mollis euismod.
Integer posuere erat a ante venenatis dapibus posuere velit aliquet.
Cum sociis natoque penatibus et magnis dis parturient montes,
nascetur ridiculus mus.
</p>
<div class="price-container">
<small>Prezzo corrente</small>
<div class="price"></div>
</div>
<form id="form" action="#" method="post">
<fieldset>
<legend>Fai un'offerta per aggiudicati l'oggetto in vendita</legend>
<div>
<label for="name">Nome</label>
<input
type="text"
name="name" id="name"
placeholder="Inserisci il tuo nome">
</div>
<div>
<label for="amount">Cifra da offrire (€)</label>
<input
type="number"
name="amount"
id="amount"
placeholder="Inserisci la cifra da offrire">
</div>
<input type="submit" id="submit" value="Conferma offerta">
</fieldset>
</form>
</section>
<div class="snackbar">
<p></p>
</div>
</main>
<script src="/js/socket.io.js" ></script>
<script>
const nameField = document.querySelector('#name');
const amountField = document.querySelector('#amount');
const submit = document.querySelector('#submit');
const form = document.querySelector('#form');
const alertMessage = document.querySelector('.alert-message span');
const currentPrice = document.querySelector('.price');
// usata per mostrare le notifiche in seguito all'aggiornamento del prezzo
const snackbar = document.querySelector('.snackbar');
const snackbarContent = document.querySelector('.snackbar p');
const socket = io.connect('http://127.0.0.1:7777');
socket.on('connect', function(){console.log('connected');});
socket.on('users', function(noOfUsers) {
const message = noOfUsers === 1 ?
`Un altro utente sta visualizzando questo oggetto` :
`Altri ${noOfUsers} utenti stanno visualizzando questo oggetto`
alertMessage.textContent = message;
});
socket.on('firstPrice', (amount) => {
let price = parseFloat(amount);
currentPrice.textContent = price.toLocaleString('it-IT', { style: 'currency', currency: 'EUR' });
})
socket.on('disconnect', function(){console.log('disconnected')});
socket.on('newPrice', (data) => {
const name = data.name;
let utente = name[0] + '****' + name[name.length - 1];
let price = parseFloat(data.amount);
price = price.toLocaleString('it-IT', { style: 'currency', currency: 'EUR' });
currentPrice.textContent = price;
if (data.id !== socket.id) {
snackbar.classList.toggle('active');
snackbarContent.textContent = `L'utente ${utente} ha fatto un'offerta di ${price}`;
setTimeout(() => snackbar.classList.remove('active'), 3500);
}
});
// invia i dati al server
form.addEventListener('submit', (e) => {
e.preventDefault();
const name = nameField.value;
const amount = amountField.value;
if (name && amount) {
socket.emit('updatePrice', {name, amount});
}
});
</script>
</body>
</html>
Nel file index.html abbiamo inserito tutto il codice del Client. Analizziamo il frammanto di codice Javascript racchiuso fra i tag <script>. Nella prima parte salviamo un riferimento a diversi elementi della pagina. Il Client si collega poi al server e in questa occasione viene emesso l’evento ‘connect’. Quando si disconnette viene invece emesso l’evento ‘disconnect’. Se viene premuto il pulsante ‘CONFERMA OFFERTA’, viene emesso l’evento ‘updatePrice’ con le informazioni relative al nome dell’utente e al nuovo prezzo digitato nel campo ‘Cifra da offrire’. Se ricordate, sul server registravamo una funzione listener per eseguire le opportune operazioni in conseguenza a questo tipo di eventi, al termine dei quali veniva emesso l’evento ‘newPrice’. Quando il server emette un evento ‘newPrice’, ogni Client provvede ad aggiornare il prezzo corrente dell’oggetto in vendita. Abbiamo anche registrato una funzione listener per l’evento ‘firstPrice’, sempre emesso dal server, con il quale aggiorniamo il prezzo corrente dell’oggetto non appena il Client si collega. Con l’evento ‘users’ otteniamo invece il numero degli altri utenti che stanno attualmente visualizzando lo stesso oggetto. Infine, tutti i Client, tranne colui che ha effettuato l’offerta (data.id !== socket.id), visualizzano una notifica contenente il nome dell’utente che ha fatto l’ultima proposta.
Nella GIF sottostante potete vedere come viene aggiornato il numero di utenti quando un Client si disconnette.
E come detto in precedenza, quando un Client effettua una nuova offerta, tutti gli altri utenti visualizzeranno una notifica.
Conclusioni
Abbiamo visto un esempio in cui abbiamo introdotto la libreria Socket.io la quale offre una ricca API che ci permette di realizzare applicazioni Real-time complesse. Nella prossima e ultima lezione vedremo alcune soluzioni hosting disponibili sul mercato che ci consentono di pubblicare una applicazione web realizzata con Node.js.