hx-multipart
Потоковая передача HTML с multipart/mixed
Расширение hx-multipart позволяет multipart/mixed HTTP-ответу передавать несколько частей.
Установка
<script src="https://cdn.jsdelivr.net/npm/htmx.org@4.0.0/dist/htmx.min.js"></script> <script src="https://cdn.jsdelivr.net/gh/bigskysoftware/htmx@four/src/ext/hx-multipart.min.js"></script>
Использование
Обновление элемента
Начните с типичного запроса htmx, используя hx-get:
<button hx-get="/ping"> Ping </button>
Вместо text/html верните ответ с multipart/mixed:
HTTP/1.1 200 OK Content-Type: multipart/mixed; boundary=... --... Content-Type: text/html Pong --...--
Библиотеки для бэкенда
-
Python:
scriptogre/multipart-responseдля Django, FastAPI, FastHTML и Starlette
Пример:
from fastapi import FastAPI
from multipart_response.fastapi import MultipartResponse, Part
app = FastAPI()
@app.get("/ping", response_class=MultipartResponse)
async def ping():
yield Part("Pong", media_type="text/html") Планируется добавить поддержку других бэкендов.
Содержимое кнопки заменяется этой частью:
<button hx-get="/ping"> Pong </button>
htmx использует те же правила, что и для ответа text/html:
Потоковое обновление
Сервер может передавать HTML потоком, используя несколько частей:
HTTP/1.1 200 OK Content-Type: multipart/mixed; boundary=... --... Content-Type: text/html P --... Content-Type: text/html Po --... Content-Type: text/html Pon --... Content-Type: text/html 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: multipart/mixed; boundary=... --... Content-Type: text/html Hello --... Content-Type: text/html , world --... Content-Type: text/html ! --...--
hx-swap="beforeend" добавляет их в <output>:
<output>Hello, world!</output>
Каждая часть наследует значения запроса для hx-target, hx-swap и hx-select.
Обновление элементов
Используйте обычный запрос htmx, чтобы обновить несколько элементов:
<button hx-get="/events">Connect</button> <div id="feed"></div> <div id="status">Offline</div>
Заголовки частей определяют, где будет заменено содержимое каждой части:
HTTP/1.1 200 OK Content-Type: multipart/mixed; boundary=... --... Content-Type: text/html HX-Target: #status Online --... Content-Type: text/html HX-Target: #feed <p>New</p> --...--
Страница приобретает следующий вид:
<button hx-get="/events">Connect</button> <div id="feed"> <p>New</p> </div> <div id="status">Online</div>
Как и hx-swap-oob и <hx-partial>, заголовки частей позволяют одному ответу обновлять несколько элементов.
Постоянные соединения
Постоянные соединения переподключаются после завершения ответа.
Открытие соединений
Используйте hx-multipart:connect для постоянного GET-соединения:
<div hx-multipart:connect="/events"></div> <div id="status">Offline</div>
Сервер отправляет часть:
HTTP/1.1 200 OK Content-Type: multipart/mixed; boundary=... --... Content-Type: text/html HX-Target: #status Online --...--
Страница приобретает следующий вид:
<div hx-multipart:connect="/events"></div> <div id="status">Online</div>
Сервер может оставить ответ открытым и отправить дополнительные части. Если ответ завершится, произойдёт переподключение hx-multipart:connect.
Соединения открываются при load. Используйте hx-trigger, чтобы подключиться позже:
<button id="connect">Connect</button>
<div hx-multipart:connect="/events"
hx-trigger="click from:#connect">
</div> Поддерживаются все модификаторы hx-trigger.
Закрытие соединений
Закройте соединение, когда часть вызывает именованное событие:
<div hx-multipart:connect="/progress"
hx-multipart:close="done"
hx-target="#status"></div>
<div id="status">Working</div> Сервер отправляет HX-Trigger вместе с последней частью:
HTTP/1.1 200 OK Content-Type: multipart/mixed; boundary=... --... Content-Type: text/html HX-Trigger: done Complete --...--
Часть всё равно заменяет содержимое, после чего соединение закрывается:
<div id="status">Complete</div>
Возобновление соединений
Идентифицируйте каждую часть, чтобы при переподключении можно было продолжить после последнего успешно выполненного обновления:
--... Content-Type: text/html HX-Part-ID: event-42 <p>New message</p>
После завершения действий и замены содержимого части при следующем переподключении отправляется:
HX-Last-Part-ID: event-42
Сервер должен вернуть части, следующие за этим идентификатором. Клиент обрабатывает каждую полученную часть, не удаляя дубликаты идентификаторов.
Сбросьте указатель, передав пустой заголовок:
HX-Part-ID:
Отсутствующий HX-Part-ID не изменяет указатель. Неудачная или отменённая часть не обновляет его.
Настройка соединений
Настроить hx-multipart можно в трёх местах:
-
<meta name="htmx-config">задаёт глобальные значения по умолчанию в HTML.<meta name="htmx-config" content="multipart.reconnectDelay:1s multipart.reconnectMaxAttempts:5"> -
htmx.config.multipartзадаёт глобальные значения по умолчанию в JavaScript.htmx.config.multipart.reconnectDelay = '1s' htmx.config.multipart.reconnectMaxAttempts = 5
-
hx-configпереопределяет значения по умолчанию для одного соединения.<div hx-multipart:connect="/events" hx-config="multipart.reconnectMaxAttempts:2"> </div>
Эти значения считываются при начале обработки multipart.
Перекрывающиеся замены
Используйте multipart/parallel, чтобы задержка swap или settle не блокировала обработку следующих частей:
Content-Type: multipart/parallel; boundary=... --... HX-Target: #one HX-Swap: innerHTML swap:1s First --... HX-Target: #two Second --...--
С параметром multipart/parallel:
-
Secondзаменяется немедленно. - Через секунду заменяется
First.
С параметром multipart/mixed:
- В течение секунды ничего не заменяется.
- Затем
FirstиSecondзаменяются по порядку.
Используйте multipart/parallel только если любая из замен может завершиться первой.
Смешивание типов содержимого
При использовании multipart/mixed каждая часть может иметь любой медиа-тип в своём Content-Type, включая:
- JSON:
application/json - Аудио:
audio/mpeg - Видео:
video/mp4 - Двоичные данные общего назначения:
application/octet-stream - Пользовательский тип:
application/vnd.example.binary
HTTP/1.1 200 OK
Content-Type: multipart/mixed; boundary=...
--...
Content-Type: text/html
<p>Done</p>
--...
Content-Type: application/json
{"sentiment":"positive","confidence":0.94}
--...
Content-Type: audio/mpeg
<audio bytes>
--...-- Один HTTP-ответ может заменить содержимое HTML, сгенерированным LLM, вернуть структурированные данные и передать потоком необработанное аудио.
Самостоятельная обработка частей
Перехватите обработку типа содержимого с помощью htmx:multipart:before:part:
document.addEventListener('htmx:multipart:before:part', event => {
let { part } = event.detail
if (part.headers.get('Content-Type') !== 'application/json') return
event.preventDefault()
event.detail.waitUntil(part.json().then(update))
}) preventDefault() пропускает встроенную замену. waitUntil() ожидает завершения асинхронной обработки.
Прочитайте тело как:
- JSON:
part.json() - Blob:
part.blob() - Байты:
part.bytes() - Поток:
part.body
Атрибуты
hx-get/hx-post/hx-put/…
Любой запрос htmx может обрабатывать multipart-ответ. Обычные запросы завершаются после получения одного ответа.
<button hx-post="/generate" hx-target="#result"> Generate </button>
Каждая часть наследует настройки замены запроса, если их не переопределяют собственные заголовки части.
hx-multipart:connect
Открывает постоянный GET-запрос:
<div hx-multipart:connect="/events"></div>
Значения по умолчанию:
При удалении элемента его соединение закрывается.
hx-multipart:close
Закрывает соединение после того, как часть вызывает именованное событие HX-Trigger:
<div hx-multipart:connect="/events"
hx-multipart:close="done"></div> Заголовки
HX-Part-ID
Идентифицирует часть ответа для переподключения:
HX-Part-ID: event-42
Расширение записывает значение после завершения действий и замены содержимого части. Если заголовок отсутствует, текущий идентификатор не меняется. Пустое значение сбрасывает его.
При использовании multipart/parallel записанный идентификатор продвигается только после успешного завершения этой части и всех предыдущих принятых частей.
HX-Last-Part-ID
Идентифицирует последнюю завершённую часть в запросе переподключения:
HX-Last-Part-ID: event-42
Сервер определяет, как возобновить обработку после этого идентификатора. Заголовок не передаётся, если HX-Part-ID сбрасывает указатель.
Заголовки HX-*
Оболочка — это обычный HTTP-ответ. Она может содержать стандартные HTTP-заголовки и любые заголовки ответа htmx.
Заголовки замены оболочки задают значения по умолчанию для каждой части:
HTTP/1.1 200 OK Content-Type: multipart/mixed; boundary=... HX-Target: #feed HX-Swap: beforeend HX-Select: .item --... Content-Type: text/html <div class="item">First</div> --... Content-Type: text/html <div class="item">Second</div> --...--
Обе части выбирают .item и добавляют его в #feed.
Заголовки частей переопределяют значения оболочки по умолчанию:
--... Content-Type: text/html HX-Retarget: #status HX-Reswap: innerHTML HX-Reselect: .message HX-Trigger: ready <p class="message">Ready</p> --...--
Эта часть выбирает .message, заменяет #status и вызывает ready.
Multipart-оболочки и части поддерживают следующие заголовки ответа htmx:
| Заголовок | Действие |
|---|---|
HX-Target |
Задаёт цель |
HX-Retarget |
Задаёт цель и переопределяет HX-Target
|
HX-Swap |
Задаёт способ замены |
HX-Reswap |
Задаёт способ замены и переопределяет HX-Swap
|
HX-Select |
Выбирает содержимое |
HX-Reselect |
Выбирает содержимое и переопределяет HX-Select
|
HX-Trigger |
Вызывает события |
HX-Location |
Выполняет последующий GET-запрос htmx |
HX-Redirect |
Переходит на другую страницу с полной загрузкой |
HX-Refresh |
Перезагружает страницу, если задано значение true
|
HX-Push-Url |
Добавляет запись в историю браузера |
HX-Replace-Url |
Заменяет URL текущей записи в истории браузера |
HX-Push |
Устарел; используйте HX-Push-Url
|
Приоритет настроек замены (от высшего к низшему):
TARGET part HX-Retarget --> part HX-Target --> envelope HX-Retarget --> envelope HX-Target --> hx-target SWAP part HX-Reswap --> part HX-Swap --> envelope HX-Reswap --> envelope HX-Swap --> hx-swap SELECT part HX-Reselect --> part HX-Select --> envelope HX-Reselect --> envelope HX-Select --> hx-select
- Начальные заголовки замены оболочки остаются значениями по умолчанию после переподключения.
- Действия оболочки выполняются после завершения обработки multipart. Действия частей выполняются по мере поступления частей.
-
HX-Locationпропускает замену для своей части. Соединение остаётся открытым. -
HX-RefreshиHX-Redirectзаменяют страницу, закрывая соединение.
Accept
Объявляет поддержку multipart во всех запросах htmx, пока загружено расширение:
Accept: text/html, multipart/mixed, multipart/parallel
Существующие значения сохраняются.
Content-Type
Multipart-ответ должен содержать свой медиа-тип и границу:
Content-Type: multipart/mixed; boundary=... Content-Type: multipart/parallel; boundary=...
Тела частей всегда поступают в порядке передачи. Если первая замена выполняется медленно, порядок обработки различается:
TIME ------------------------------------------------------------> multipart/mixed Part 1 [read][actions][--------- swap ---------][finished] Part 2 [read][actions][swap][finished] multipart/parallel Part 1 [read][actions][--------- swap ---------][finished] Part 2 [read][actions][swap][finished]
multipart/mixed ждёт завершения замены каждой части, прежде чем читать тело следующей. multipart/parallel выполняет действия частей в порядке их поступления, запускает замену, а затем читает тело следующей части, не дожидаясь завершения этой замены.
Указатель HX-Part-ID по-прежнему продвигается в порядке передачи. Завершившаяся позже часть ждёт успешного завершения всех предшествующих принятых частей.
Если каждая замена завершается мгновенно, оба формата обычно выглядят одинаково. Для наглядного сравнения см. раздел «Перекрывающиеся замены».
Используйте multipart/parallel, если:
- Части обновляют независимые цели.
- Последующее обновление полезно до завершения предыдущей замены.
- Задержка замены или фаза CSS settle не должны блокировать последующие обновления.
Используйте multipart/mixed, если:
- Части обновляют одну и ту же цель.
- Часть зависит от DOM, созданного предыдущей частью.
- Порядок поступления должен совпадать с порядком завершения.
Нативные переходы представлений не выполняются параллельно. htmx глобально ставит замены transition:true в очередь, поэтому multipart/parallel может раньше поставить следующий переход в очередь, но сами переходы всё равно выполняются по одному.
Расширение включает парсер Response.prototype.parts() для обоих форматов.
События
Данные события доступны в event.detail.
События соединения содержат:
event.detail.connection = {
url,
config,
lastPartId,
attempt,
status,
cancelled
} htmx:multipart:before:connection
Вызывается перед чтением htmx первоначального ответа или началом переподключения.
document.addEventListener('htmx:multipart:before:connection', event => {
if (event.detail.connection.attempt > 5) event.preventDefault()
}) htmx:multipart:after:connection
Вызывается, когда первоначальный ответ или переподключение готовы к передаче потоком.
document.addEventListener('htmx:multipart:after:connection', event => {
console.log('Connected:', event.detail.connection.url)
}) htmx:multipart:before:part
Вызывается перед чтением htmx тела части или выполнением её действий.
Перехватите обработку части и используйте detail.waitUntil() для асинхронной работы:
document.addEventListener('htmx:multipart:before:part', event => {
let { part, waitUntil } = event.detail
event.preventDefault()
waitUntil(handle(part))
}) Отменить обработку можно одним из двух способов:
- вызвать
event.preventDefault() - установить
event.detail.cancelledв значениеtrue
Детали события содержат:
-
connection: multipart-соединение -
ctx: контекст запроса htmx -
part:BodyPart -
cancelled: установите в значениеtrue, чтобы отменить обработку без вызоваpreventDefault() -
waitUntil(promise): ожидание завершения пользовательской обработки перед завершением этой части
htmx:multipart:after:part
Вызывается после выполнения htmx действий и замены содержимого части. Не вызывается, если before:part отменено.
document.addEventListener('htmx:multipart:after:part', event => {
console.log('Handled:', event.detail.part.headers)
}) Детали события содержат connection, ctx и part.
htmx:multipart:close
Вызывается при закрытии соединения.
document.addEventListener('htmx:multipart:close', event => {
console.log('Closed:', event.detail.reason)
}) reason — это part, removed, cancelled или ended.
htmx:multipart:error
Вызывается после ошибки потока, сети, статуса или типа содержимого.
document.addEventListener('htmx:multipart:error', event => {
console.error('Multipart error:', event.detail.error)
}) -
connection: соединение, в котором произошла ошибка -
error: значение ошибки -
status: HTTP-статус, если он доступен
Конфигурация
multipart.reconnect
Управляет автоматическим переподключением после завершения или сбоя ответа.
<meta name="htmx-config" content="multipart.reconnect:false">
По умолчанию — true для hx-multipart:connect и false для обычных запросов htmx.
multipart.reconnectDelay
Задаёт время ожидания перед первой попыткой переподключения.
<meta name="htmx-config" content="multipart.reconnectDelay:1s">
По умолчанию — 500 миллисекунд. После каждой неудачной попытки задержка удваивается. Значение можно задать в миллисекундах или строкой времени, например 500ms, 1s или 2m.
multipart.reconnectMaxDelay
Ограничивает время ожидания между попытками переподключения.
<meta name="htmx-config" content="multipart.reconnectMaxDelay:30s">
По умолчанию — 60000 миллисекунд. Используйте миллисекунды или строку времени.
multipart.reconnectMaxAttempts
Ограничивает количество последовательных неудачных попыток переподключения.
<meta name="htmx-config" content="multipart.reconnectMaxAttempts:5">
По умолчанию — Infinity.
multipart.reconnectJitter
Распределяет попытки переподключения во времени, чтобы множество клиентов не повторяло запрос одновременно.
<meta name="htmx-config" content="multipart.reconnectJitter:0">
По умолчанию — 0.3: случайное изменение каждой задержки составляет до ±30%. Используйте 0 для точных значений задержки.
multipart.pauseOnBackground
Закрывает запрос, пока страница скрыта, и переподключается, когда она становится видимой.
<meta name="htmx-config" content="multipart.pauseOnBackground:false">
По умолчанию — true для hx-multipart:connect и false для обычных запросов htmx. Используйте HX-Part-ID и воспроизведение на стороне сервера, чтобы восстановить части, отправленные во время отключения.
Как работает multipart/mixed
Обычный HTML-ответ содержит одно тело:
HTTP/1.1 200 OK Content-Type: text/html <p>One update</p>
Multipart-ответ помещает множество пар «заголовок/тело» внутрь этого тела:
HTTP/1.1 200 OK Content-Type: multipart/mixed; boundary=... --... Content-Type: text/html HX-Target: #one First --... Content-Type: text/html HX-Target: #two Second --...--
| Пример | Значение |
|---|---|
HTTP/1.1 200 OK |
Статус всего ответа |
boundary=... |
Обозначает границу |
--... |
Начинает следующую часть |
Content-Type и HX-Target
|
Заголовки этой части |
| Пустая строка | Начинает тело части |
First и Second
|
Тела частей |
--...-- |
Завершает ответ |
Строка статуса появляется один раз. У частей нет кодов статуса.
Как заканчивается тело части?
--... Content-Type: text/plain Content-Length: 5 Hello --...--
| Заголовок части | Что читает htmx |
|---|---|
Content-Length: 5 |
Следующие пять байтов |
Без Content-Length
|
До следующей границы |
После тела всегда следует граница. Если граница — part, строка тела, начинающаяся с --part, будет ошибочно принята за следующую часть.
Licensed under the Zero-Clause BSD License.
https://four.htmx.org/extensions/hx-multipart/