Русский
Русский
English
Статистика
Реклама

Eventsource

Server-Sent Events пример использования

21.09.2020 14:20:44 | Автор: admin


Доброго времени суток, друзья!

В этом туториале мы рассмотрим Server Sent Events: встроенный класс EventSource, который позволяет поддерживать соединение с сервером и получать от него события.

О том, что такое SSE и для чего он используется можно почитать здесь.

Что конкретно мы будем делать?

Мы напишем простой сервер, который будет по запросу клиента отправлять ему данные 10 случайных пользователей, а клиент с помощью этих данных будет формировать карточки пользователей.

Сервер будет реализован на Node.js, клиент на JavaScript. Для стилизации будет использоваться Bootstrap, в качестве API Random User Generator.

Код проекта находится здесь.

Поиграть с кодом можно здесь.

Если вам это интересно, прошу следовать за мной.

Подготовка


Создаем директорию sse-tut:

mkdir sse-tut

Заходим в нее и инициализируем проект:

cd sse-tutyarn init -y// илиnpm init -y

Устанавливаем axios:

yarn add axios// илиnpm i axios

axios будет использоваться для получения данных пользователей.

Редактируем package.json:

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

Структура проекта:

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

Содержание index.html:

<head>    <!-- Bootstrap CSS -->    <link rel="stylesheet" href="http://personeltest.ru/aways/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.js.

Подключаем http и axios, определяем порт:

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')    // проверяем полученные данные    console.log(response)    return response.data.results[0]}

Создаем счетчик количества отправленных пользователей:

let i = 1

Пишем функцию отправки данных клиенту:

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

Создаем и запускаем сервер:

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

Полный код сервера:
const http = require('http')const axios = require('axios')const PORT = process.env.PORT || 3000const getUserData = async () => {    const response = await axios.get('https://randomuser.me/api')    return response.data.results[0]}let i = 1const 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.'))


Выполняем команду yarn start или npm start. В терминале появляется сообщение Server ready.. Открываем http://localhost:3000:



С сервером закончили, переходим к клиентской части приложения.

Клиент


Открываем файл client.js.

Создаем функцию генерации шаблона пользовательской карточки:

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

Сначала реализуем закрытие соединения:

stopEvents() {    this.eventSource.close()    // сообщаем о том, что соединение закрыто пользователем    this.eventLog.textContent = 'Event stream closed by client.'}

Переходим к открытию потока событий:

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

Обрабатываем кастомное событие 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)})

Не забываем реализовать обработку ошибок:

this.eventSource.addEventListener('error', e => {    this.eventSource.close()    this.eventLog.textContent = `Got an error: ${e}`    this.changeDisabled()}, {once: true})

Наконец, инициализируем приложение:

const app = new App('main')

Полный код клиента:
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')


На всякий случай перезапускаем сервер. Открываем http://localhost:3000. Нажимаем на кнопку Start:



Начинаем получать данные от сервера и рендерить карточки пользователей.

Если нажать на кнопку Stop, отправка данных будет приостановлена:



Снова нажимаем Start, отправка данных продолжается.

При достижении лимита (10 пользователей) сервер отправляет идентификатор со значением -1 и закрывает соединение. Клиент, в свою очередь, также закрывает поток событий:



Как видите, SSE очень похож на веб-сокеты. Недостатком является однонаправленность сообщений: сообщения могут отправляться только сервером. Преимущество состоит в автоматическом переподключении и простоте реализации.

Поддержка данной технологии на сегодняшний день составляет 95%:



Надеюсь, статья вам понравилась. Благодарю за внимание.
Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru