hx-sse
Передача HTML-потоком с помощью text/event-stream (SSE)
Расширение hx-sse позволяет передавать множество событий, отправляемых сервером (SSE) в одном HTTP-ответе.
Если вы использовали sse в htmx 2.0, см. примечания по миграции.
Установка
<script src="https://cdn.jsdelivr.net/npm/htmx.org@4.0.0/dist/htmx.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/htmx.org@4.0.0/dist/ext/hx-sse.min.js"></script>
Использование
Обновление элемента
Начните с обычного запроса htmx с использованием hx-get:
<button hx-get="/ping"> Ping </button>
Вместо text/html ответьте потоком text/event-stream:
HTTP/1.1 200 OK Content-Type: text/event-stream data: Pong
У этого события без имени есть поле data:, но нет поля event:.
Библиотеки для серверной части
-
Python: FastAPI или
sse-starlette -
Go:
go-sse - PHP: потоки событий Laravel
Событие заменяет содержимое кнопки:
<button hx-get="/ping"> Pong </button>
htmx использует те же правила, что и для ответа text/html:
Передача обновления потоком
Также можно передавать HTML-потоком с помощью нескольких событий без имени:
HTTP/1.1 200 OK Content-Type: text/event-stream data: P data: Po data: Pon data: Pong
Кнопка изменяется по мере поступления событий:
Ping → P → Po → Pon → Pong
Выбор способа замены
Используйте hx-swap и hx-target, чтобы выбрать, как и где заменять содержимое при обновлении:
<button hx-post="/generate"
hx-target="next output"
hx-swap="beforeend">
Generate
</button>
<!-- LLM tokens stream here -->
<output></output> Каждое событие без имени содержит один текстовый фрагмент:
HTTP/1.1 200 OK Content-Type: text/event-stream data: Hello data: , world data: !
hx-swap="beforeend" накапливает их в <output>:
<output>Hello, world!</output>
Также можно использовать:
-
hx-select, чтобы выбрать содержимое для замены -
hx-select-oob, чтобы выбрать дополнительные элементы для замены
Обновление элементов
Используйте обычный запрос htmx, чтобы обновить несколько элементов:
<button hx-get="/events">Connect</button> <div id="feed"></div> <div id="status">Offline</div>
Сервер отправляет две дополнительные замены с помощью hx-swap-oob и <hx-partial>:
HTTP/1.1 200 OK Content-Type: text/event-stream data: <div id="status" hx-swap-oob="true">Online</div> data: <hx-partial hx-target="#feed"><p>New</p></hx-partial>
Страница становится такой:
<button hx-get="/events">Connect</button> <div id="feed"> <p>New</p> </div> <div id="status">Online</div>
Почему не использовалась обычная замена?
После извлечения дополнительных замен htmx обычная замена оказывается пустой:
(empty)
Элементы hx-swap-oob и <hx-partial> извлекаются до обычной замены. По умолчанию swapEmpty:false оставляет элемент подключения без изменений.
Сервер может сочетать дополнительные замены с обычным HTML:
HTTP/1.1 200 OK Content-Type: text/event-stream data: <p>New event</p> data: <hx-partial hx-target="#status">Busy</hx-partial>
Абзац следует за hx-target и hx-swap элемента запроса. Частичное обновление обновляет #status.
Чтобы отключить замену элемента запроса, установите hx-swap="none":
<button hx-get="/events" hx-swap="none">Connect</button>
Замены hx-swap-oob и <hx-partial> всё равно выполняются.
Постоянные подключения
Используйте постоянное подключение, чтобы продолжать получать обновления с сервера.
Открытие подключений
Добавьте hx-sse:connect к элементу, который будет их получать:
<div hx-sse:connect="/events"></div>
Используйте hx-trigger, чтобы подключиться после события:
<button id="connect">Connect</button>
<div hx-sse:connect="/events"
hx-trigger="click from:#connect">
</div> Поддерживаются все модификаторы hx-trigger.
Закрытие подключений
Закройте подключение при получении события с определённым именем:
<div hx-sse:connect="/progress" hx-sse:close="done"></div>
Сервер отправляет:
HTTP/1.1 200 OK Content-Type: text/event-stream event: done data: Complete
Обработчики на стороне клиента для события done выполняются до закрытия подключения.
Настройка подключений
Настроить hx-sse можно в трёх местах:
-
<meta name="htmx-config">задаёт глобальные значения по умолчанию в HTML.<meta name="htmx-config" content="sse.reconnectDelay:1s sse.reconnectMaxAttempts:5"> -
htmx.config.sseзадаёт глобальные значения по умолчанию в JavaScript.htmx.config.sse.reconnectDelay = '1s' htmx.config.sse.reconnectMaxAttempts = 5
-
hx-configпереопределяет значения по умолчанию для одного подключения.<div hx-sse:connect="/events" hx-config="sse.reconnectMaxAttempts:2"> </div>
Эти значения считываются при начале обработки потока.
Повторная отправка сообщений
Добавьте id:, чтобы получать сообщения, пропущенные во время отключения:
HTTP/1.1 200 OK Content-Type: text/event-stream id: event-42 data: <p>New message</p>
Поток сохраняет текущий идентификатор события:
- Событие без
idнаследует текущий идентификатор. - Пустое
id:очищает идентификатор. - Блок только с идентификатором, завершающийся пустой строкой, обновляет или очищает идентификатор, не отправляя сообщение.
При повторном подключении hx-sse включает Last-Event-ID, если текущий идентификатор не пуст:
Last-Event-ID: event-42
Это сообщает серверу, на каком месте остановился клиент, чтобы сервер мог повторно отправить пропущенные сообщения.
Server Client
event-42 -------------------> received
event-43 --------X disconnected
<------------------- Last-Event-ID: event-42
event-43 -------------------> replayed Сервер решает, какие сообщения отправлять повторно. hx-sse обрабатывает каждое полученное событие и не удаляет дубликаты повторно отправленных сообщений.
Вызов событий на стороне клиента
Поле event события SSE вызывает событие DOM вместо замены его данными:
HTTP/1.1 200 OK Content-Type: text/event-stream event: progress data: 50 id: task-5
Обработайте его с помощью hx-on:
<button hx-get="/progress"
hx-on:progress="htmx.find('#progress').value = event.detail.data">
Start
</button>
<progress id="progress" max="100" value="0"></progress> Событие всплывает от источника запроса и предоставляет:
event.detail = {
data: '50',
id: 'task-5'
} Именованное событие также может инициировать другой запрос htmx:
<div hx-get="/status" hx-trigger="progress from:body"></div>
Атрибуты
hx-get/hx-post/hx-put/…
Расширение hx-sse расширяет возможности:
Если в ответе используется Content-Type: text/event-stream, htmx обрабатывает каждое событие SSE по мере его поступления.
hx-sse:connect
Открывает постоянное SSE-подключение с помощью GET-запроса:
<div hx-sse:connect="/events"></div>
Подключение использует:
-
hx-headersиhx-valsдля GET-запроса -
hx-target,hx-swap,hx-selectиhx-select-oobдля каждого сообщения
Значения по умолчанию:
hx-trigger="load"sse.reconnect:truesse.pauseOnBackground:truesse.releaseOn:immediate-
swapEmpty:falseдля каждого сообщения
hx-sse:close
Закрывает подключение после события с соответствующим именем:
<div hx-sse:connect="/events" hx-sse:close="done"></div>
HTTP/1.1 200 OK Content-Type: text/event-stream event: done data: Complete
Заголовки
Accept
Объявляет о поддержке SSE во всех запросах htmx, пока загружено расширение.
Accept: text/html, text/event-stream
Ответ передаётся потоком, если его Content-Type содержит text/event-stream.
Last-Event-ID
Указывает последнее полученное событие SSE при повторном подключении.
Last-Event-ID: event-42
Расширение отправляет этот заголовок, пока текущий идентификатор события не пуст. События без id наследуют этот идентификатор, а пустое id: удаляет заголовок при последующих подключениях.
Сервер решает, как повторно отправлять последующие сообщения. Клиент не удаляет дубликаты.
События
Данные события доступны в event.detail.
События подключения предоставляют:
event.detail.connection = {
url,
config,
lastEventId,
attempt,
status,
cancelled
} События сообщений предоставляют:
event.detail = {
connection,
message: {
data,
event,
id
},
waitUntil(), // before processing only
cancelled // before processing only
} htmx:sse:before:connection
Вызывается перед тем, как htmx запускает начальный поток или планирует повторное подключение.
document.addEventListener('htmx:sse:before:connection', event => {
if (event.detail.connection.attempt > 5) event.preventDefault()
}) Начальный HTTP-ответ уже получен. Отменить подключение можно любым из способов:
- вызвать
event.preventDefault() - установить
event.detail.connection.cancelledв значениеtrue
htmx:sse:after:connection
Вызывается, когда начальный ответ или повторное подключение готовы к передаче потока.
document.addEventListener('htmx:sse:after:connection', event => {
console.log('Connected:', event.detail.connection.url)
}) connection.status содержит HTTP-статус.
htmx:sse:before:message
Вызывается перед обработкой сообщения SSE.
document.addEventListener('htmx:sse:before:message', event => {
let message = event.detail.message
if (message.event === 'heartbeat') event.preventDefault()
else message.data = sanitize(message.data)
}) Изменение message.data меняет данные замены или именованного события. Изменение message.event меняет то, будет ли сообщение заменять содержимое или вызывать событие DOM.
detail.waitUntil(promise) откладывает обработку до завершения асинхронных операций. Отменить её можно с помощью event.preventDefault() или detail.cancelled = true.
htmx:sse:after:message
Вызывается после того, как htmx заменяет содержимое или вызывает событие SSE.
document.addEventListener('htmx:sse:after:message', event => {
console.log('Received:', event.detail.message.data)
}) htmx:sse:close
Вызывается при закрытии потока SSE.
document.addEventListener('htmx:sse:close', event => {
console.log('Closed:', event.detail.reason)
}) reason может иметь одно из следующих значений:
-
message:hx-sse:closeсовпало с именованным событием -
removed: исходный элемент удалён из DOM -
ended: поток завершился или исчерпал попытки повторного подключения -
cancelled: начальное подключение к потоку отменено -
cleanup: htmx очистил исходный элемент
htmx:sse:error
Вызывается при сбое чтения потока SSE или повторного подключения к нему.
document.addEventListener('htmx:sse:error', event => {
console.error('SSE error:', event.detail.error)
}) -
connection: неудачное подключение, если оно доступно -
url: URL SSE, если настройка завершилась ошибкой до создания подключения -
error: значение ошибки -
status: HTTP-статус неудачного ответа при повторном подключении, если он доступен
hx:release
Событие, отправляемое сервером, которое досрочно завершает жизненный цикл запроса. Полезно только вместе с sse.releaseOn:end.
event: hx:release data:
Когда сервер отправляет это событие, htmx сразу скрывает индикаторы и повторно включает элементы. Поток продолжает работать в фоновом режиме.
Это полезно при потоковой передаче ответов LLM, когда нужно:
- показывать индикатор загрузки, пока модель не начнёт отвечать
- позволить пользователю взаимодействовать со страницей, пока продолжается передача токенов
<button hx-post="/generate"
hx-target="#output"
hx-swap="beforeend"
hx-config="sse.releaseOn:end"
hx-indicator="#spinner">
Generate
</button> Сервер передаёт поток:
data: First token event: hx:release data: data: more tokens...
Индикатор скрывается после hx:release, но токены продолжают добавляться.
Конфигурация
sse.reconnect
Управляет автоматическим переподключением закрытого потока.
<meta name="htmx-config" content="sse.reconnect:false">
По умолчанию — true для hx-sse:connect и false для обычных запросов htmx.
sse.reconnectDelay
Задаёт время ожидания перед первой попыткой переподключения.
<meta name="htmx-config" content="sse.reconnectDelay:1s">
По умолчанию — 500 миллисекунд. После каждой неудачной попытки задержка удваивается. Значение можно указать в миллисекундах или в виде строки времени, например 500ms, 1s и 2m.
Сервер может переопределить это значение для потока:
HTTP/1.1 200 OK Content-Type: text/event-stream retry: 2000 data: Reconnect after two seconds
sse.reconnectMaxDelay
Ограничивает время ожидания между попытками переподключения.
<meta name="htmx-config" content="sse.reconnectMaxDelay:30s">
По умолчанию — 60000 миллисекунд. Укажите значение в миллисекундах или в виде строки времени.
sse.reconnectMaxAttempts
Ограничивает количество попыток переподключения закрытого потока.
<meta name="htmx-config" content="sse.reconnectMaxAttempts:5">
По умолчанию — Infinity.
sse.reconnectJitter
Распределяет попытки переподключения во времени, чтобы множество клиентов не повторяли попытку одновременно.
<meta name="htmx-config" content="sse.reconnectJitter:0">
По умолчанию — 0.3: задержка каждой попытки случайным образом изменяется не более чем на ±30%. Используйте 0 для точных задержек.
sse.pauseOnBackground
Закрывает поток, когда страница скрыта, и переподключается, когда она снова становится видимой.
<meta name="htmx-config" content="sse.pauseOnBackground:false">
По умолчанию — true для hx-sse:connect и false для обычных запросов htmx. Используйте идентификаторы событий и повторное воспроизведение на стороне сервера, чтобы получить сообщения, отправленные во время отключения.
sse.releaseOn
Управляет моментом завершения жизненного цикла запроса (индикаторы скрываются, элементы снова становятся активными).
<meta name="htmx-config" content="sse.releaseOn:first">
Значения:
-
immediate: завершить, когда SSE перехватывает управление (после получения заголовков) -
first: завершить после подстановки первого сообщения -
end: завершить, когда поток закроется
По умолчанию — immediate для hx-sse:connect и end для обычных запросов htmx.
При значении по умолчанию end индикаторы остаются видимыми, а hx-disable оставляет элементы отключёнными, пока поток не закроется. Это удобно для потоковой передачи от LLM, когда нужно предотвратить повторную отправку запросов.
Используйте first, если хотите, чтобы интерфейс стал интерактивным сразу после поступления контента:
<button hx-post="/generate"
hx-config="sse.releaseOn:first">
Generate
</button> Сервер также может завершить обработку раньше, отправив событие hx:release:
event: hx:release data:
Миграция
htmx 2.0
В htmx 2.0 использовался EventSource, а именованные сообщения выбирались с помощью sse-swap:
<div sse-connect="/chat" sse-swap="message"></div>
В htmx 4 используется обычный запрос htmx, а неименованные сообщения подставляются автоматически:
<div hx-sse:connect="/chat"></div>
HTTP/1.1 200 OK Content-Type: text/event-stream data: <p>New message</p>
Теперь именованные сообщения вызывают события DOM вместо выбора целевого элемента для подстановки:
HTTP/1.1 200 OK Content-Type: text/event-stream event: progress data: 50
<div hx-sse:connect="/progress"
hx-on:progress="updateProgress(event.detail.data)">
</div> Чтобы запустить другой запрос, используйте обычное имя события:
<!-- htmx 2 --> <div hx-get="/status" hx-trigger="sse:progress"></div> <!-- htmx 4 --> <div hx-get="/status" hx-trigger="progress from:body"></div>
Атрибуты
Изменились следующие атрибуты:
| htmx 2.x | htmx 4.x | Совместимость |
|---|---|---|
sse-connect |
hx-sse:connect |
Работает с предупреждением |
sse-swap |
Неименованные сообщения подставляются автоматически | Удалён; выводится предупреждение |
sse-close |
hx-sse:close |
Работает с предупреждением |
События
Изменились следующие события:
| htmx 2.x | htmx 4.x |
|---|---|
htmx:sseOpen |
htmx:sse:after:connection |
htmx:sseError |
htmx:sse:error |
htmx:sseBeforeMessage |
htmx:sse:before:message |
htmx:sseMessage |
htmx:sse:after:message |
htmx:sseClose |
htmx:sse:close |
В htmx 4 вместо EventSource используются fetch() и ReadableStream. Поэтому в запросах с ответами SSE можно использовать любые HTTP-методы htmx, значения запросов и заголовки.
От бета-версии до RC1
В RC1 события жизненного цикла SSE получили пространство имён:
| Бета-версия | RC1 |
|---|---|
htmx:before:sse:connection |
htmx:sse:before:connection |
htmx:after:sse:connection |
htmx:sse:after:connection |
htmx:before:sse:message |
htmx:sse:before:message |
htmx:after:sse:message |
htmx:sse:after:message |
Отмена обработки сообщения перенесена из detail.message.cancelled в detail.cancelled. Хуки перед обработкой сообщения могут отложить обработку с помощью detail.waitUntil(promise).
Licensed under the Zero-Clause BSD License.
https://four.htmx.org/extensions/hx-sse/