Во время разработки веб-приложений постоянно происходят изменения исходного кода, стилей и шаблонов. После каждого сохранения файлов требуется обновление страницы браузера для отображения актуального состояния проекта. Ручное обновление быстро превращается в рутинную операцию, особенно при интенсивной разработке.
Live Reload автоматизирует этот процесс. После изменения файлов браузер самостоятельно выполняет перезагрузку страницы или обновляет отдельные ресурсы.
В экосистеме Esbuild отсутствует встроенный механизм Live Reload, однако благодаря высокой скорости сборки реализовать такую функциональность достаточно просто. Одним из наиболее удобных способов является использование технологии Server-Sent Events (SSE).
Server-Sent Events — это стандарт браузеров для получения непрерывного потока событий от сервера по HTTP-соединению.
Сервер поддерживает открытое соединение и периодически отправляет клиенту сообщения:
Сервер → Браузер
В отличие от WebSocket:
| SSE | WebSocket |
|---|---|
| Односторонняя передача данных | Двунаправленная передача |
| Работает поверх обычного HTTP | Использует специальный протокол |
| Простая реализация | Более сложная инфраструктура |
| Идеален для Live Reload | Подходит для чатов и игр |
Для механизма автоматической перезагрузки страницы двусторонний обмен не нужен. Серверу достаточно уведомить браузер о том, что сборка завершена.
Типичный цикл выглядит следующим образом:
Изменение файла
↓
Esbuild запускает пересборку
↓
Сборка завершена
↓
SSE-сервер отправляет событие
↓
Браузер получает уведомление
↓
location.reload()
Таким образом достигается автоматическое обновление страницы после каждого изменения исходников.
Минимальная реализация состоит из трех компонентов:
Структура проекта:
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);
Сервер обслуживает статические файлы и будет использоваться для разработки.
Теперь необходимо создать маршрут, который будет удерживать открытое соединение.
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');
}
}
Вызов этой функции приведет к отправке уведомления всем подключенным браузерам.
В браузере SSE подключается через объект
EventSource.
Создадим файл:
// live-reload.js
const events = new EventSource('/esbuild-events');
events.onmess age = () => {
location.reload();
};
После получения любого сообщения выполняется полная перезагрузка страницы.
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
</head>
<body>
<h1>Esbuild Live Reload</h1>
<script src="/live-reload.js"></script>
</body>
</html>
Теперь браузер постоянно ожидает события от сервера.
Для автоматической пересборки используется 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]
});
Каждая успешная сборка вызывает рассылку события всем браузерам.
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();
});
Это предотвращает обновление страницы на нерабочую версию приложения.
Одно из преимуществ EventSource заключается в автоматическом восстановлении соединения.
Если сервер перезапустился:
Сервер остановлен
↓
Соединение разорвано
↓
Сервер запущен снова
↓
EventSource переподключается автоматически
Дополнительный код обычно не требуется.
Некоторые прокси и серверы могут закрывать неактивные соединения.
Распространенная практика — периодическая отправка 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();
});
Более сложные реализации сравнивают хэши файлов или список измененных модулей.
Live Reload и HMR решают похожие задачи, но работают по-разному.
Изменение файла
↓
Пересборка
↓
Обновление страницы
Преимущества:
Недостатки:
Изменение файла
↓
Пересборка
↓
Замена модуля
Преимущества:
Недостатки:
Для большинства небольших и средних проектов связка Esbuild + SSE Live Reload обеспечивает оптимальный баланс между скоростью, простотой и удобством разработки.
Минимальный объем кода
Реализация занимает несколько десятков строк.
Отсутствие внешних зависимостей
Достаточно стандартных возможностей Node.js и браузера.
Низкое потребление ресурсов
Поддерживается одно долгоживущее HTTP-соединение.
Простая интеграция с плагинами Esbuild
События пересборки легко перехватываются через
build.onEnd().
Хорошая масштабируемость
Один сервер способен обслуживать множество подключенных вкладок браузера одновременно.
Предсказуемое поведение
После завершения успешной сборки браузер всегда получает уведомление и отображает актуальную версию приложения.