Vom Server gesendete Ereignisse: Eine Fallstudie

Guten Tag, Freunde!



In diesem Lernprogramm werden wir uns die vom Server gesendeten Ereignisse ansehen: eine integrierte EventSource-Klasse, mit der Sie eine Verbindung zum Server aufrechterhalten und Ereignisse von diesem empfangen können.



Sie können lesen , was über SSE ist und wofür es verwendet wird , hier .



Was genau machen wir jetzt?



Wir werden einen einfachen Server schreiben, der ihm auf Anfrage des Kunden Daten von 10 zufälligen Benutzern sendet, und der Kunde wird diese Daten verwenden, um Benutzerkarten zu generieren.



Der Server wird in Node.js implementiert , der Client in JavaScript. Bootstrap wird für das Styling verwendet und Random User Generator wird als API verwendet .



Der Projektcode ist hier...



Wenn Sie interessiert sind, folgen Sie mir bitte.



Ausbildung



Erstellen Sie ein Verzeichnis sse-tut:



mkdir sse-tut


Wir gehen darauf ein und initialisieren das Projekt:



cd sse-tut

yarn init -y
// 
npm init -y


Installieren axios:



yarn add axios
// 
npm i axios


Axios werden verwendet, um Benutzerdaten abzurufen.



Bearbeitung package.json:



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


Projektstruktur:



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


Inhalt 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>


Server



Beginnen wir mit der Implementierung des Servers.



Wir öffnen server.js.



Wir verbinden http und axios, definieren den Port:



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


Wir erstellen eine Funktion zum Empfangen von Benutzerdaten:



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


Erstellen Sie einen Zähler für die Anzahl der gesendeten Benutzer:



let i = 1


Wir schreiben die Funktion des Sendens von Daten an den Kunden:



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.')
      })
}


Wir erstellen und starten den Server:



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.'))


Vollständiger Servercode:
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.'))




Wir führen den Befehl yarn startoder aus npm start. Das Terminal zeigt die Meldung "Server bereit" an. Öffnen http://localhost:3000: Wir sind







mit dem Server fertig, gehen Sie zur Client-Seite der Anwendung.



Klient



Öffnen Sie die Datei client.js.



Erstellen Sie eine Funktion zum Generieren einer benutzerdefinierten Kartenvorlage:



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>
`


Die Vorlage wird unter Verwendung der folgenden Daten generiert: Benutzer-ID (falls vorhanden), Name, Login, E-Mail-Adresse und Alter des Benutzers.



Wir beginnen mit der Implementierung der Hauptfunktionalität:



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
        }
    }
//...


Zuerst implementieren wir das Schließen der Verbindung:



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


Fahren wir mit dem Öffnen des Ereignisstroms fort:



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})
}


Wir behandeln das benutzerdefinierte Ereignis "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)
})


Vergessen Sie nicht, die Fehlerbehandlung zu implementieren:



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

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

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


Schließlich initialisieren wir die Anwendung:



const app = new App('main')


Vollständiger Client-Code:
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')




Starten Sie den Server für alle Fälle neu. Wir öffnen http://localhost:3000. Klicken Sie auf die Schaltfläche "Start":







Wir empfangen Daten vom Server und rendern Benutzerkarten.



Wenn Sie auf die Schaltfläche "Stopp" klicken, wird das Senden von Daten angehalten:







Drücken Sie erneut auf "Start", um das Senden der Daten fortzusetzen.



Wenn das Limit (10 Benutzer) erreicht ist, sendet der Server eine Kennung mit dem Wert -1 und schließt die Verbindung. Der Client schließt wiederum den Ereignisstrom:







Wie Sie sehen können, ist SSE Websockets sehr ähnlich. Der Nachteil ist, dass Nachrichten unidirektional sind: Nachrichten können nur vom Server gesendet werden. Der Vorteil ist die automatische Wiederverbindung und die einfache Implementierung.



Die Unterstützung für diese Technologie beträgt heute 95%:







Ich hoffe dir hat der Artikel gefallen. Danke für die Aufmerksamkeit.



All Articles