Spec-Zone.ru › htmx

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:

  • hx-target="this"
  • hx-swap="innerHTML" (из htmx.config.defaultSwap)

Потоковое обновление

Сервер может передавать 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-trigger="load"
  • multipart.reconnect:true
  • multipart.pauseOnBackground:true

При удалении элемента его соединение закрывается.

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/

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API