Événements envoyés par le serveur: une étude de cas

Bonne journée, mes amis!



Dans ce didacticiel, nous examinerons les événements envoyés par le serveur: une classe EventSource intégrée qui vous permet de maintenir une connexion au serveur et de recevoir des événements de celui-ci.



Vous pouvez lire ce qu'est l'ESS et à quoi il sert ici .



Qu'allons-nous faire exactement?



Nous écrirons un serveur simple qui, à la demande du client, lui enverra les données de 10 utilisateurs aléatoires, et le client utilisera ces données pour générer des cartes utilisateurs.



Le serveur sera implémenté dans Node.js , le client en JavaScript. Bootstrap sera utilisé pour le style , et Random User Generator sera utilisé comme API .



Le code du projet est ici...



Si vous êtes intéressé, suivez-moi.



Entraînement



Créez un répertoire sse-tut:



mkdir sse-tut


On y va et on initialise le projet:



cd sse-tut

yarn init -y
// 
npm init -y


Installer axios:



yarn add axios
// 
npm i axios


axios sera utilisé pour obtenir les données utilisateur.



Édition package.json:



"main": "server.js",
"scripts": {
    "start": "node server.js"
},


Structure du projet:



sse-tut
    --node_modules
    --client.js
    --index.html
    --package.json
    --server.js
    --yarn.lock


Contenu index.html:



<head>
    <!-- Bootstrap CSS -->
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" integrity="sha384-JcKb8q3iqJ61gNV9KGb8thSsNjpSL0n8PARn9HuZOnIxN0hoP+VmmDGMN5t9UJ0Z" crossorigin="anonymous">
    <style>
        .card {
            margin: 0 auto;
            max-width: 512px;
        }
        img {
            margin: 1rem;
            max-width: 320px;
        }
        p {
            margin: 1rem;
        }
    </style>
</head>

<body>
    <main class="container text-center">
        <h1>Server-Sent Events Tutorial</h1>
        <button class="btn btn-primary" data-type="start-btn">Start</button>
        <button class="btn btn-danger" data-type="stop-btn" disabled>Stop</button>
        <p data-type="event-log"></p>
    </main>

    <script src="client.js"></script>
</body>


Serveur



Commençons par implémenter le serveur.



Nous ouvrons server.js.



Nous connectons http et axios, définissons le port:



const http = require('http')
const axios = require('axios')
const PORT = process.env.PORT || 3000


Nous créons une fonction de réception des données utilisateur:



const getUserData = async () => {
    const response = await axios.get('https://randomuser.me/api')
    //   
    console.log(response)
    return response.data.results[0]
}


Créez un compteur pour le nombre d'utilisateurs envoyés:



let i = 1


Nous écrivons la fonction d'envoi de données au client:



const sendUserData = (req, res) => {
    //   - 200 
    //    
    //   -  
    //  
    res.writeHead(200, {
        Connection: 'keep-alive',
        'Content-Type': 'text/event-stream',
        'Cache-Control': 'no-cache'
    })

    //     2 
    const timer = setInterval(async () => {
        //   10 
        if (i > 10) {
            //  
            clearInterval(timer)
            //   ,    10 
            console.log('10 users has been sent.')
            //      -1
            //  ,    
            res.write('id: -1\ndata:\n\n')
            //  
            res.end()
            return
        }

        //  
        const data = await getUserData()

        //    
        // event -  
        // id -  ;    
        // retry -   
        // data - 
        res.write(`event: randomUser\nid: ${i}\nretry: 5000\ndata: ${JSON.stringify(data)}\n\n`)

        //   ,   
        console.log('User data has been sent.')

        //   
        i++
    }, 2000)

    //    
    req.on('close', () => {
        clearInterval(timer)
        res.end()
        console.log('Client closed the connection.')
      })
}


Nous créons et démarrons le serveur:



http.createServer((req, res) => {
    //     CORS
    res.setHeader('Access-Control-Allow-Origin', '*')

    //    - getUser
    if (req.url === '/getUsers') {
        //  
        sendUserData(req, res)
    } else {
        // ,   ,     ,
        //   
        res.writeHead(404)
        res.end()
    }

}).listen(PORT, () => console.log('Server ready.'))


Code serveur complet:
const http = require('http')
const axios = require('axios')
const PORT = process.env.PORT || 3000

const getUserData = async () => {
    const response = await axios.get('https://randomuser.me/api')
    return response.data.results[0]
}

let i = 1

const sendUserData = (req, res) => {
    res.writeHead(200, {
    Connection: 'keep-alive',
    'Content-Type': 'text/event-stream',
    'Cache-Control': 'no-cache'
    })

    const timer = setInterval(async () => {
    if (i > 10) {
        clearInterval(timer)
        console.log('10 users has been sent.')
        res.write('id: -1\ndata:\n\n')
        res.end()
        return
    }

    const data = await getUserData()

    res.write(`event: randomUser\nid: ${i}\nretry: 5000\ndata: ${JSON.stringify(data)}\n\n`)

    console.log('User data has been sent.')

    i++
    }, 2000)

    req.on('close', () => {
    clearInterval(timer)
    res.end()
    console.log('Client closed the connection.')
    })
}

http.createServer((req, res) => {
    res.setHeader('Access-Control-Allow-Origin', '*')

    if (req.url === '/getUsers') {
    sendUserData(req, res)
    } else {
    res.writeHead(404)
    res.end()
    }

}).listen(PORT, () => console.log('Server ready.'))




Nous exécutons la commande yarn startou npm start. Le terminal affiche le message «Serveur prêt». Ouverture http://localhost:3000: Nous en avons







terminé avec le serveur, allez du côté client de l'application.



Client



Ouvrez le fichier client.js.



Créez une fonction pour générer un modèle de carte personnalisé:



const getTemplate = user => `
<div class="card">
    <div class="row">
        <div class="col-md-4">
            <img src="${user.img}" class="card-img" alt="user-photo">
        </div>
        <div class="col-md-8">
            <div class="card-body">
                <h5 class="card-title">${user.id !== null ? `Id: ${user.id}` : `User hasn't id`}</h5>
                <p class="card-text">Name: ${user.name}</p>
                <p class="card-text">Username: ${user.username}</p>
                <p class="card-text">Email: ${user.email}</p>
                <p class="card-text">Age: ${user.age}</p>
            </div>
        </div>
    </div>
</div>
`


Le modèle est généré à l'aide des données suivantes: ID utilisateur (le cas échéant), nom, connexion, adresse e-mail et âge de l'utilisateur.



Nous commençons à implémenter la fonctionnalité principale:



class App {
    constructor(selector) {
        //   - 
        this.$ = document.querySelector(selector)
        //   
        this.#init()
    }

    #init() {
        this.startBtn = this.$.querySelector('[data-type="start-btn"]')
        this.stopBtn = this.$.querySelector('[data-type="stop-btn"]')
        //     
        this.eventLog = this.$.querySelector('[data-type="event-log"]')
        //    
        this.clickHandler = this.clickHandler.bind(this)
        //   
        this.$.addEventListener('click', this.clickHandler)
    }

    clickHandler(e) {
        //    
        if (e.target.tagName === 'BUTTON') {
            //   
            //       ,
            //   
            const {
                type
            } = e.target.dataset

            if (type === 'start-btn') {
                this.startEvents()
            } else if (type === 'stop-btn') {
                this.stopEvents()
            }

            //   
            this.changeDisabled()
        }
    }

    changeDisabled() {
        if (this.stopBtn.disabled) {
            this.stopBtn.disabled = false
            this.startBtn.disabled = true
        } else {
            this.stopBtn.disabled = true
            this.startBtn.disabled = false
        }
    }
//...


Tout d'abord, nous implémentons la fermeture de la connexion:



stopEvents() {
    this.eventSource.close()
    //   ,    
    this.eventLog.textContent = 'Event stream closed by client.'
}


Passons à l'ouverture du flux d'événements:



startEvents() {
    //          
    this.eventSource = new EventSource('http://localhost:3000/getUsers')

    //   ,   
    this.eventLog.textContent = 'Accepting data from the server.'

    //        -1
    this.eventSource.addEventListener('message', e => {
        if (e.lastEventId === '-1') {
            //  
            this.eventSource.close()
            //   
            this.eventLog.textContent = 'End of stream from the server.'

            this.changeDisabled()
        }
        //       
    }, {once: true})
}


Nous gérons l'événement personnalisé "randomUser":



this.eventSource.addEventListener('randomUser', e => {
    //   
    const userData = JSON.parse(e.data)
    //  
    console.log(userData)

    //     
    const {
        id,
        name,
        login,
        email,
        dob,
        picture
    } = userData

    //   ,     
    const i = id.value
    const fullName = `${name.first} ${name.last}`
    const username = login.username
    const age = dob.age
    const img = picture.large

    const user = {
        id: i,
        name: fullName,
        username,
        email,
        age,
        img
    }

    //  
    const template = getTemplate(user)

    //    
    this.$.insertAdjacentHTML('beforeend', template)
})


N'oubliez pas de mettre en œuvre la gestion des erreurs:



this.eventSource.addEventListener('error', e => {
    this.eventSource.close()

    this.eventLog.textContent = `Got an error: ${e}`

    this.changeDisabled()
}, {once: true})


Enfin, nous initialisons l'application:



const app = new App('main')


Code client complet:
const getTemplate = user => `
<div class="card">
    <div class="row">
        <div class="col-md-4">
            <img src="${user.img}" class="card-img" alt="user-photo">
        </div>
        <div class="col-md-8">
            <div class="card-body">
                <h5 class="card-title">${user.id !== null ? `Id: ${user.id}` : `User hasn't id`}</h5>
                <p class="card-text">Name: ${user.name}</p>
                <p class="card-text">Username: ${user.username}</p>
                <p class="card-text">Email: ${user.email}</p>
                <p class="card-text">Age: ${user.age}</p>
            </div>
        </div>
    </div>
</div>
`

class App {
    constructor(selector) {
        this.$ = document.querySelector(selector)
        this.#init()
    }

    #init() {
        this.startBtn = this.$.querySelector('[data-type="start-btn"]')
        this.stopBtn = this.$.querySelector('[data-type="stop-btn"]')
        this.eventLog = this.$.querySelector('[data-type="event-log"]')
        this.clickHandler = this.clickHandler.bind(this)
        this.$.addEventListener('click', this.clickHandler)
    }

    clickHandler(e) {
        if (e.target.tagName === 'BUTTON') {
            const {
                type
            } = e.target.dataset

            if (type === 'start-btn') {
                this.startEvents()
            } else if (type === 'stop-btn') {
                this.stopEvents()
            }

            this.changeDisabled()
        }
    }

    changeDisabled() {
        if (this.stopBtn.disabled) {
            this.stopBtn.disabled = false
            this.startBtn.disabled = true
        } else {
            this.stopBtn.disabled = true
            this.startBtn.disabled = false
        }
    }

    startEvents() {
        this.eventSource = new EventSource('http://localhost:3000/getUsers')

        this.eventLog.textContent = 'Accepting data from the server.'

        this.eventSource.addEventListener('message', e => {
            if (e.lastEventId === '-1') {
                this.eventSource.close()
                this.eventLog.textContent = 'End of stream from the server.'

                this.changeDisabled()
            }
        }, {once: true})

        this.eventSource.addEventListener('randomUser', e => {
            const userData = JSON.parse(e.data)
            console.log(userData)

            const {
                id,
                name,
                login,
                email,
                dob,
                picture
            } = userData

            const i = id.value
            const fullName = `${name.first} ${name.last}`
            const username = login.username
            const age = dob.age
            const img = picture.large

            const user = {
                id: i,
                name: fullName,
                username,
                email,
                age,
                img
            }

            const template = getTemplate(user)

            this.$.insertAdjacentHTML('beforeend', template)
        })

        this.eventSource.addEventListener('error', e => {
            this.eventSource.close()

            this.eventLog.textContent = `Got an error: ${e}`

            this.changeDisabled()
        }, {once: true})
    }

    stopEvents() {
        this.eventSource.close()
        this.eventLog.textContent = 'Event stream closed by client.'
    }
}

const app = new App('main')




Redémarrez le serveur au cas où. Nous ouvrons http://localhost:3000. Cliquez sur le bouton "Démarrer":







Nous commençons à recevoir les données du serveur et à rendre les cartes utilisateur.



Si vous cliquez sur le bouton «Arrêter», l'envoi des données sera suspendu:







Appuyez à nouveau sur «Démarrer», l'envoi des données se poursuit.



Lorsque la limite (10 utilisateurs) est atteinte, le serveur envoie un identifiant avec une valeur de -1 et ferme la connexion. Le client, à son tour, ferme également le flux d'événements:







comme vous pouvez le voir, SSE est très similaire aux websockets. L'inconvénient est que les messages sont unidirectionnels: les messages ne peuvent être envoyés que par le serveur. L'avantage est la reconnexion automatique et la facilité de mise en œuvre.



La prise en charge de cette technologie est aujourd'hui de 95%:







J'espère que vous avez apprécié l'article. Merci de votre attention.



All Articles