Live reload: реализация через Server-Sent Events

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

Live Reload автоматизирует этот процесс. После изменения файлов браузер самостоятельно выполняет перезагрузку страницы или обновляет отдельные ресурсы.

В экосистеме Esbuild отсутствует встроенный механизм Live Reload, однако благодаря высокой скорости сборки реализовать такую функциональность достаточно просто. Одним из наиболее удобных способов является использование технологии Server-Sent Events (SSE).


Что такое Server-Sent Events

Server-Sent Events — это стандарт браузеров для получения непрерывного потока событий от сервера по HTTP-соединению.

Сервер поддерживает открытое соединение и периодически отправляет клиенту сообщения:

Сервер → Браузер

В отличие от WebSocket:

SSE WebSocket
Односторонняя передача данных Двунаправленная передача
Работает поверх обычного HTTP Использует специальный протокол
Простая реализация Более сложная инфраструктура
Идеален для Live Reload Подходит для чатов и игр

Для механизма автоматической перезагрузки страницы двусторонний обмен не нужен. Серверу достаточно уведомить браузер о том, что сборка завершена.


Общая схема работы

Типичный цикл выглядит следующим образом:

Изменение файла
        ↓
Esbuild запускает пересборку
        ↓
Сборка завершена
        ↓
SSE-сервер отправляет событие
        ↓
Браузер получает уведомление
        ↓
location.reload()

Таким образом достигается автоматическое обновление страницы после каждого изменения исходников.


Архитектура решения

Минимальная реализация состоит из трех компонентов:

  1. Сервер разработки.
  2. SSE-канал уведомлений.
  3. Клиентский скрипт для перезагрузки страницы.

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

project/
├── src/
│   ├── index.js
│   └── live-reload.js
├── public/
│   └── index.html
├── build.js
└── package.json

Создание сервера разработки

Для примеров используется встроенный HTTP-модуль Node.js.

const http = require('http');
const fs = require('fs');
const path = require('path');

const server = http.createServer((req, res) => {
    const filePath = path.join(
        __dirname,
        'public',
        req.url === '/' ? 'index.html' : req.url
    );

    fs.readFile(filePath, (err, data) => {
        if (err) {
            res.writeHead(404);
            res.end();
            return;
        }

        res.end(data);
    });
});

server.listen(3000);

Сервер обслуживает статические файлы и будет использоваться для разработки.


Создание SSE-эндпоинта

Теперь необходимо создать маршрут, который будет удерживать открытое соединение.

const clients = [];

const server = http.createServer((req, res) => {
    if (req.url === '/esbuild-events') {

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

        clients.push(res);

        req.on('close', () => {
            const index = clients.indexOf(res);

            if (index >= 0) {
                clients.splice(index, 1);
            }
        });

        return;
    }

    // остальная логика
});

После открытия страницы браузер подключается к маршруту:

http://localhost:3000/esbuild-events

Соединение остается открытым до закрытия вкладки.


Отправка сообщений клиентам

Для передачи события используется специальный формат:

data: reload

Обязательным является двойной перевод строки после сообщения.

Функция рассылки:

function broadcastReload() {
    for (const client of clients) {
        client.write('dat a: reload\n\n');
    }
}

Вызов этой функции приведет к отправке уведомления всем подключенным браузерам.


Клиентская часть Live Reload

В браузере SSE подключается через объект EventSource.

Создадим файл:

// live-reload.js

const events = new EventSource('/esbuild-events');

events.onmess age = () => {
    location.reload();
};

После получения любого сообщения выполняется полная перезагрузка страницы.


Подключение клиента в HTML

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
</head>
<body>

<h1>Esbuild Live Reload</h1>

<script src="/live-reload.js"></script>

</body>
</html>

Теперь браузер постоянно ожидает события от сервера.


Настройка режима наблюдения Esbuild

Для автоматической пересборки используется Watch Mode.

const esbuild = require('esbuild');

esbuild.context({
    entryPoints: ['src/index.js'],
    bundle: true,
    outfile: 'public/app.js'
})
.then(async (ctx) => {

    await ctx.watch();

});

После изменения исходников Esbuild будет запускать пересборку автоматически.


Использование плагина для отслеживания завершения сборки

Необходимо определить момент, когда сборка успешно завершилась.

Для этого применяется API плагинов.

const reloadPlugin = {
    name: 'reload-plugin',

    setup(build) {

        build.onEnd(result => {

            if (result.errors.length === 0) {
                broadcastReload();
            }

        });

    }
};

Подключение:

esbuild.context({
    entryPoints: ['src/index.js'],
    bundle: true,
    outfile: 'public/app.js',
    plugins: [reloadPlugin]
});

Каждая успешная сборка вызывает рассылку события всем браузерам.


Полный пример build.js

const esbuild = require('esbuild');
const http = require('http');
const fs = require('fs');
const path = require('path');

const clients = [];

function broadcastReload() {
    for (const client of clients) {
        client.write('dat a: reload\n\n');
    }
}

const server = http.createServer((req, res) => {

    if (req.url === '/esbuild-events') {

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

        clients.push(res);

        req.on('close', () => {
            const index = clients.indexOf(res);

            if (index >= 0) {
                clients.splice(index, 1);
            }
        });

        return;
    }

    const filePath = path.join(
        __dirname,
        'public',
        req.url === '/' ? 'index.html' : req.url
    );

    fs.readFile(filePath, (err, data) => {

        if (err) {
            res.writeHead(404);
            res.end();
            return;
        }

        res.end(data);

    });

});

server.listen(3000);

const reloadPlugin = {
    name: 'reload-plugin',

    setup(build) {

        build.onEnd(result => {

            if (result.errors.length === 0) {
                broadcastReload();
            }

        });

    }
};

(async () => {

    const ctx = await esbuild.context({
        entryPoints: ['src/index.js'],
        bundle: true,
        outfile: 'public/app.js',
        plugins: [reloadPlugin]
    });

    await ctx.watch();

})();

Такой скрипт обеспечивает:

  • запуск сервера;
  • автоматическую сборку;
  • наблюдение за файлами;
  • уведомление браузеров;
  • автоматическую перезагрузку страницы.

Отправка именованных событий

SSE поддерживает не только сообщения по умолчанию, но и именованные события.

Сервер:

client.write(`
event: rebuild
data: done

`);

Клиент:

const events = new EventSource('/esbuild-events');

events.addEventListener('rebuild', () => {
    location.reload();
});

Подход становится удобным при расширении функциональности.


Передача дополнительной информации

Вместе с событием можно отправлять полезные данные.

Сервер:

client.write(`
data: {
  "type":"reload",
  "time":"12:45:10"
}

`);

Клиент:

events.onmess age = event => {

    const data = JSON.parse(event.data);

    console.log(data);

};

Такой механизм позволяет выводить диагностические сообщения, время сборки и другую служебную информацию.


Индикация успешной сборки

Вместо немедленной перезагрузки можно отображать уведомление.

Сервер:

client.write(`
event: build
data: success

`);

Клиент:

events.addEventListener('build', () => {

    const notice = document.createElement('div');

    notice.textContent = 'Сборка завершена';

    document.body.appendChild(notice);

});

Подобная техника часто используется совместно с Hot Module Replacement.


Обработка ошибок сборки

При наличии ошибок автоматическая перезагрузка обычно не выполняется.

build.onEnd(result => {

    if (result.errors.length > 0) {

        console.error('Build failed');

        return;
    }

    broadcastReload();

});

Это предотвращает обновление страницы на нерабочую версию приложения.


Автоматическая переподключаемость SSE

Одно из преимуществ EventSource заключается в автоматическом восстановлении соединения.

Если сервер перезапустился:

Сервер остановлен
        ↓
Соединение разорвано
        ↓
Сервер запущен снова
        ↓
EventSource переподключается автоматически

Дополнительный код обычно не требуется.


Отправка heartbeat-сообщений

Некоторые прокси и серверы могут закрывать неактивные соединения.

Распространенная практика — периодическая отправка heartbeat.

setInterval(() => {

    for (const client of clients) {
        client.write(': heartbeat\n\n');
    }

}, 30000);

Строка, начинающаяся с двоеточия, считается комментарием и игнорируется браузером.


Перезагрузка только при изменении определённых файлов

Иногда требуется обновлять страницу лишь при изменении конкретных ресурсов.

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

build.onEnd(result => {

    const changed = result.errors.length === 0;

    if (!changed) {
        return;
    }

    broadcastReload();

});

Более сложные реализации сравнивают хэши файлов или список измененных модулей.


Полная перезагрузка и Hot Module Replacement

Live Reload и HMR решают похожие задачи, но работают по-разному.

Live Reload

Изменение файла
        ↓
Пересборка
        ↓
Обновление страницы

Преимущества:

  • простая реализация;
  • минимальный объем кода;
  • отсутствие зависимости от фреймворка.

Недостатки:

  • потеря состояния приложения;
  • полная перезагрузка DOM.

HMR

Изменение файла
        ↓
Пересборка
        ↓
Замена модуля

Преимущества:

  • сохранение состояния;
  • мгновенное обновление интерфейса.

Недостатки:

  • сложная реализация;
  • дополнительная интеграция с инструментами разработки.

Для большинства небольших и средних проектов связка Esbuild + SSE Live Reload обеспечивает оптимальный баланс между скоростью, простотой и удобством разработки.


Преимущества SSE для Live Reload в Esbuild

Минимальный объем кода

Реализация занимает несколько десятков строк.

Отсутствие внешних зависимостей

Достаточно стандартных возможностей Node.js и браузера.

Низкое потребление ресурсов

Поддерживается одно долгоживущее HTTP-соединение.

Простая интеграция с плагинами Esbuild

События пересборки легко перехватываются через build.onEnd().

Хорошая масштабируемость

Один сервер способен обслуживать множество подключенных вкладок браузера одновременно.

Предсказуемое поведение

После завершения успешной сборки браузер всегда получает уведомление и отображает актуальную версию приложения.