Spec-Zone.ru › htmx

hx-ws

Передача HTML-потоком и отправка данных через WebSocket

Расширение hx-ws открывает соединения WebSocket, заменяет входящий HTML и отправляет данные формы в формате JSON.

Если вы использовали ws в 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-ws.min.js"></script>

Использование

Обновление элемента

Откройте постоянное соединение WebSocket:

<div hx-ws:connect="/chat">
  ...
</div>

Браузер получает это сообщение WebSocket:

<p>New message</p>

Результат:

<div hx-ws:connect="/chat">
  <p>New message</p> <!-- Swapped in -->
</div>

htmx использует те же правила, что и для ответа text/html:

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

Выбор способа замены

Используйте hx-swap и hx-target, чтобы выбрать, как и где выполнять замену при обновлении:

<div hx-ws:connect="/chat"
     hx-target="#messages"
     hx-swap="beforeend">

  <div id="messages">
    <p>Old message</p>
    <!-- Content goes here -->
  </div>

</div>

Сервер отправляет:

<p>New message</p>

Результат:

<div hx-ws:connect="/chat"
     hx-target="#messages"
     hx-swap="beforeend">

  <div id="messages">
    <p>Old message</p>
    <p>New message</p> <!-- Appended -->
  </div>

</div>

Также можно использовать:

  • hx-select, чтобы выбрать содержимое для замены
  • hx-select-oob, чтобы выбрать дополнительные элементы для замены

Обновление элементов

Начните с элементов страницы, которые нужно обновить:

<div hx-ws:connect="/chat"></div>

<div id="feed">
  <p>Old</p>
</div>
<div id="status">Offline</div>

Мы отправим элемент hx-swap-oob и элемент <hx-partial> (новинка в версии 4.0):

<!-- Match by ID -->
<div id="status" hx-swap-oob="true">Online</div>

<!-- Append -->
<hx-partial hx-target="#feed" hx-swap="beforeend">
  <p>New</p>
</hx-partial>

Страница станет такой:

<div hx-ws:connect="/chat"></div>

<div id="feed">
  <p>Old</p>
  <p>New</p>
</div>
<div id="status">Online</div>
Почему не использовалась обычная замена?

После извлечения дополнительных замен htmx обычная замена может оказаться пустой:

(empty)

Элементы hx-swap-oob и <hx-partial> извлекаются до обычной замены. По умолчанию swapEmpty:false оставляет соединение без изменений.

Сервер также может объединять эти обновления с обычным HTML:

<p>New chat content</p>

<!-- Also replace #status -->
<hx-partial hx-target="#status">Busy</hx-partial>

Для первого элемента используются целевой элемент и способ замены соединения. Частичные обновления #status.

Чтобы отключить замену соединения, задайте hx-swap="none":

<div hx-ws:connect="/chat" hx-swap="none">
  ...
</div>

Замены hx-swap-oob и <hx-partial> по-прежнему выполняются.

Отправка сообщения

Добавьте hx-ws:send к полю ввода внутри соединения:

<div hx-ws:connect="/chat" hx-target="#messages">
  <div id="messages"></div>
  <input hx-ws:send type="button" name="message" value="Hello">
</div>

Исходящее сообщение:

{
  "headers": {
    "HX-Request": "true",
    "HX-Request-Type": "partial",
    "HX-Source": "input",
    "HX-Target": "div#messages",
    "HX-Current-URL": "https://example.com/chat"
  },
  "message": "Hello"
}

headers зарезервирован для метаданных. Значения формы и hx-vals используют другие ключи верхнего уровня.

Чтобы отправить массив, повторите поле формы:

<form hx-ws:send>
  <input name="tag" value="urgent">
  <input name="tag" value="public">
  <button>Send</button>
</form>

Исходящее сообщение:

{
  "headers": { /* ... */ },
  "tag": ["urgent", "public"]
}

hx-vals переопределяет значения формы, не преобразуя их типы:

<form hx-ws:send hx-vals="count:2">
  <input name="count" value="1">
  <button>Send</button>
</form>
{ "headers": { /* ... */ }, "count": 2 }

Переопределение входящей замены

Используйте JSON, чтобы переопределить способ замены соединения:

{
  "content": "<p class=\"message\">New message</p>",
  "target": "#messages",
  "swap": "beforeend settle:10ms",
  "select": ".message"
}
  • content: HTML-код для замены
  • target: место замены
  • swap: сериализованная спецификация hx-swap
  • select: что выбрать из content

HTTP-заголовки HX-Re* заменяют значения, уже выбранные для запроса.

Сообщение WebSocket может поступить без запроса, поэтому его поля JSON могут задавать эти значения сразу:

Поле JSON Заголовок ответа HTTP Значение элемента по умолчанию
target HX-Retarget hx-target
swap HX-Reswap hx-swap
select HX-Reselect hx-select

content использует те же атрибуты hx-target, hx-swap и hx-select, что и обычный HTML. Элементы hx-swap-oob и <hx-partial> внутри него по-прежнему создают независимые замены.

Поля JSON переопределяют соответствующие атрибуты:

TARGET
JSON target  -->  hx-target  -->  connection element

SWAP
JSON swap    -->  hx-swap    -->  defaultSwap

SELECT
JSON select  -->  hx-select  -->  all content

hx-select-oob остается настройкой элемента. Вместо этого сервер может использовать hx-swap-oob или <hx-partial> внутри content.

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

JSON без content не заменяется:

{
  "type": "notification",
  "text": "New message"
}

Обработайте его с помощью htmx:ws:before:message:incoming:

document.addEventListener('htmx:ws:before:message:incoming', async event => {
  let message = await event.detail.message.json()
  if (message.type === 'notification') showNotification(message)
})

Отмените событие, чтобы самостоятельно обработать пользовательские или двоичные данные:

document.addEventListener('htmx:ws:before:message:incoming', async event => {
  event.preventDefault()
  handleCustomMessage(await event.detail.message.text())
})

Изначально message.data содержит исходную строку, Blob или ArrayBuffer.

Преобразуйте его текущее значение:

await message.text()
await message.json()

Отмените событие, чтобы пропустить встроенную обработку. Двоичные сообщения автоматически не заменяются.

Постоянные соединения

Соединения WebSocket остаются открытыми для входящих и исходящих сообщений.

Открытие соединений

Используйте hx-trigger, чтобы открыть соединение позже, чем load:

<button id="connect">Connect</button>

<div hx-ws:connect="/chat"
     hx-trigger="click from:#connect">
</div>

Поддерживаются все модификаторы hx-trigger.

Открытие при отправке

Укажите оба атрибута на одном элементе, чтобы при отправке открыть его соединение:

<button hx-ws:connect="/actions"
        hx-ws:send
        hx-trigger="click"
        name="action"
        value="refresh">
  Refresh
</button>

При нажатии кнопки открывается /actions и через это соединение отправляется action=refresh.

Отправка во время повторного подключения

Пользователь может инициировать отправку сообщений, пока соединение устанавливается повторно:

<div hx-ws:connect="/actions">
  <button hx-ws:send name="action" value="save">Save</button>
  <button hx-ws:send name="action" value="refresh">Refresh</button>
</div>

Если пользователь нажмет Сохранить, а затем Обновить, htmx отправит оба сообщения после открытия соединения:

// First
{ "headers": { /* ... */ }, "action": "save" }

// Then
{ "headers": { /* ... */ }, "action": "refresh" }
Использование общих соединений

Поместите несколько элементов hx-ws:send внутрь одного hx-ws:connect:

<div hx-ws:connect="/actions">
  <button hx-ws:send name="action" value="save">Save</button>
  <button hx-ws:send name="action" value="delete">Delete</button>
</div>

<div id="save-result"></div>

<div id="delete-result"></div>

Обе кнопки используют соединение, принадлежащее <div>. Для входящих сообщений используется <div>, а не кнопка, отправившая сообщение.

Маршрутизация входящих сообщений

Укажите target во входящем сообщении JSON, чтобы направить замену:

{
  "content": "<p>Saved</p>",
  "target": "#save-result"
}

Используйте hx-swap-oob или <hx-partial>, если одно сообщение обновляет несколько целевых элементов.

Закрытие соединений

Закройте соединение с кодом 1000, чтобы отключить повторное подключение:

socket.close(1000, 'done')

Для кодов из ws.reconnectCodes выполняется повторное подключение.

Настройка соединений

Настроить hx-ws можно в трех местах:

  • <meta name="htmx-config"> задает глобальные значения по умолчанию в HTML.

    <meta name="htmx-config"
          content="ws.reconnectDelay:1s ws.reconnectMaxAttempts:5">
  • htmx.config.ws задает глобальные значения по умолчанию в JavaScript.

    htmx.config.ws.reconnectDelay = '1s'
    htmx.config.ws.reconnectMaxAttempts = 5
  • hx-config переопределяет значения по умолчанию для одного соединения.

    <div hx-ws:connect="/ws"
         hx-config="ws.reconnectMaxAttempts:2">
    </div>

Эти значения считываются при создании соединения.

Атрибуты

hx-ws:connect

Открывает соединение WebSocket:

<div hx-ws:connect="/chat"></div>

Для входящего HTML используются следующие наследуемые атрибуты замены:

  • hx-target: по умолчанию — элемент соединения
  • hx-swap: по умолчанию — htmx.config.defaultSwap
  • hx-select: выбирает содержимое для замены соединения
  • hx-select-oob: выбирает дополнительные элементы для замены

Значения по умолчанию:

  • swapEmpty:false; чтобы переопределить его, укажите явно в hx-swap
  • hx-trigger="load"; используйте hx-trigger, чтобы изменить его
  • ws.reconnect:true
  • ws.pauseOnBackground:true

Каждый элемент hx-ws:connect владеет своим соединением. Разные элементы открывают разные соединения, даже если используют один и тот же URL.

hx-ws:send

Отправляет данные формы и hx-vals в формате JSON.

<div hx-ws:connect="/chat">
  <form id="chat-form" hx-ws:send hx-target="#messages">
    <input name="message">
    <button>Send</button>
  </form>
  <div id="messages"></div>
</div>

hx-ws:send использует ближайший hx-ws:connect, в том числе указанный на том же элементе. Его значение игнорируется.

Значение hx-trigger по умолчанию:

  • change для текстовых полей ввода, <textarea> и <select>
  • submit для <form>
  • click для кнопок и других элементов

События

Данные события доступны в event.detail.

События подключения и закрытия:

event.detail.connection = {
  url,
  config,
  socket,     // WebSocket or null
  queue,      // outgoing messages waiting to send
  attempt,    // reconnect count
  cancelled
}

События входящих сообщений:

event.detail = {
  connection,
  message: {
    data,          // current string, Blob, or ArrayBuffer
    text(),
    json()
  },
  waitUntil(),     // before processing only
  cancelled        // before processing only
}

События исходящих сообщений предоставляют:

event.detail = {
  connection,
  message: {
    headers,       // htmx metadata
    values,        // form values and hx-vals
    data            // replacement payload before send; sent payload afterward
  },
  waitUntil(),     // before sending only
  cancelled        // before sending only
}

События входящих сообщений отправляются элементом, владеющим соединением. События исходящих сообщений отправляются элементом, инициировавшим отправку.

htmx:ws:before:connection

Срабатывает перед первоначальным подключением и каждым повторным подключением.

document.addEventListener('htmx:ws:before:connection', event => {
  event.detail.connection.config.protocols = 'graphql-transport-ws'
})

В любом случае событие можно отменить:

  • вызовите event.preventDefault()
  • установите event.detail.connection.cancelled в true

htmx:ws:after:connection

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

document.addEventListener('htmx:ws:after:connection', event => {
  event.detail.connection.socket.binaryType = 'arraybuffer'
})

htmx:ws:before:message:outgoing

Срабатывает перед отправкой исходящего сообщения.

document.addEventListener('htmx:ws:before:message:outgoing', event => {
  let message = event.detail.message
  message.headers.Authorization = `Bearer ${token}`

  if (!isValid(message.values)) event.preventDefault()
})
  • message.headers: изменяемые метаданные htmx
  • message.values: изменяемые значения формы и hx-vals
  • message.data: необязательные данные для замены

detail.waitUntil(promise) откладывает сериализацию и отправку до завершения асинхронной работы.

Обычно {...values, headers} сериализуется в JSON. Установите message.data, чтобы вместо этого отправить строку, Blob, ArrayBuffer или представление типизированного массива:

document.addEventListener('htmx:ws:before:message:outgoing', event => {
  let message = event.detail.message
  message.data = encodeMessagePack({
    ...message.values,
    headers: message.headers
  })
})

Включите message.headers в заменяющие данные, если серверу нужны метаданные запроса htmx.

htmx:ws:after:message:outgoing

Срабатывает после отправки исходящего сообщения.

document.addEventListener('htmx:ws:after:message:outgoing', event => {
  console.log('Outgoing:', event.detail.message.data)
})

message.data — это значение, переданное в WebSocket.send().

htmx:ws:before:message:incoming

Срабатывает перед обработкой входящего сообщения.

document.addEventListener('htmx:ws:before:message:incoming', event => {
  let { message, waitUntil } = event.detail

  waitUntil(message.json().then(data => {
    if (!isValid(data)) event.detail.cancelled = true
  }))
})

detail.waitUntil(promise) откладывает встроенную обработку до завершения асинхронной работы.

Установите message.data, чтобы заменить данные перед встроенной обработкой. Также применяются изменения, внесенные через waitUntil().

Синхронную обработку можно отменить любым из способов:

  • вызовите event.preventDefault()
  • установите event.detail.cancelled в true

htmx:ws:after:message:incoming

Срабатывает после обработки входящего сообщения расширением.

document.addEventListener('htmx:ws:after:message:incoming', event => {
  console.log('Incoming:', event.detail.message.data)
})

htmx:ws:close

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

document.addEventListener('htmx:ws:close', event => {
  console.log('Closed:', event.detail.reason, event.detail.code)
})
  • reason: closed, removed или cancelled
  • code: код закрытия WebSocket или null

Код из ws.reconnectCodes запускает повторное подключение, если ws.reconnect:true.

htmx:ws:error

Срабатывает при ошибках подключения и отправки.

document.addEventListener('htmx:ws:error', event => {
  console.error('WebSocket error:', event.detail.error)
})
  • connection: соединение, в котором произошла ошибка, если оно доступно
  • url: запрошенный URL, если соединения нет
  • error: значение ошибки

Настройки

ws.reconnect

Определяет, выполняется ли автоматическое повторное подключение при разрешенных кодах закрытия.

<meta name="htmx-config" content="ws.reconnect:false">

По умолчанию — true.

ws.reconnectCodes

Выберите коды закрытия WebSocket, при которых выполняется повторное подключение:

<meta name="htmx-config"
      content='{"ws":{"reconnectCodes":[1001,1005,1006,1011,1012,1013,1014]}}'>

По умолчанию:

  • 1001: уход
  • 1005: код состояния не получен
  • 1006: ненормальное закрытие
  • 1011: внутренняя ошибка
  • 1012: перезапуск службы
  • 1013: повторите попытку позже
  • 1014: ошибка шлюза

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

ws.reconnectDelay

Задайте время ожидания перед первой попыткой повторного подключения.

<meta name="htmx-config" content="ws.reconnectDelay:1s">

По умолчанию — 500 миллисекунд. После каждой неудачной попытки задержка удваивается; значения можно указывать в миллисекундах или в виде временных строк, например 500ms, 1s и 2m.

ws.reconnectMaxDelay

Ограничивает время ожидания между попытками повторного подключения.

<meta name="htmx-config" content="ws.reconnectMaxDelay:30s">

По умолчанию — 60000 миллисекунд. Используйте миллисекунды или временную строку.

ws.reconnectMaxAttempts

Ограничивает количество попыток повторного подключения после закрытия соединения.

<meta name="htmx-config" content="ws.reconnectMaxAttempts:5">

По умолчанию — Infinity.

ws.reconnectJitter

Распределяет попытки повторного подключения во времени, чтобы множество клиентов не повторяли их одновременно.

<meta name="htmx-config" content="ws.reconnectJitter:0">

По умолчанию — 0.3: задержка каждой попытки случайным образом изменяется в пределах ±30%. Используйте 0 для точных задержек.

ws.maxOutgoingMessagesQueueSize

Ограничивает количество исходящих сообщений, ожидающих повторного подключения.

<meta name="htmx-config" content="ws.maxOutgoingMessagesQueueSize:20">

По умолчанию — 100. Для последующих сообщений генерируется событие htmx:ws:error, и они не отправляются. Используйте 0, чтобы отключить очередь.

ws.pauseOnBackground

Закрывает соединения, пока страница скрыта, и повторно подключается, когда она становится видимой.

<meta name="htmx-config" content="ws.pauseOnBackground:false">

По умолчанию — true.

ws.protocols

Задайте подпротоколы WebSocket для рукопожатия.

<meta name="htmx-config" content="ws.protocols:graphql-transport-ws">

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

Миграция

htmx 2.0

В htmx 2.0 каждый входящий элемент считается неявным hx-swap-oob:

<!-- interpreted as hx-swap-oob="true" by default -->
<div id="notifications">
  New message
</div>

В htmx 4.0 на соединении используются hx-target и hx-swap:

<div hx-ws:connect="/notifications"
     hx-target="#notifications"
     hx-swap="outerHTML">
</div>

<div id="notifications"></div>

Входящее сообщение содержит обычный HTML:

<div id="notifications">
  New message
</div>

В htmx 4.0 для каждой дополнительной замены требуется явный синтаксис:

  • hx-swap-oob или <hx-partial> для дополнительных замен
  • JSON для выбора целевого элемента и способа замены соединения

Исходящие сообщения

В htmx 2 к значениям формы добавлялся HEADERS:

{
  "message": "Hello",
  "HEADERS": {
    "HX-Request": "true"
  }
}

В htmx 4 для метаданных зарезервирован headers, а значения размещаются на верхнем уровне:

{
  "headers": {
    "HX-Request": "true"
  },
  "message": "Hello"
}

Атрибуты и API

Названия изменились следующим образом:

htmx 2.x htmx 4.x
ws-connect hx-ws:connect
ws-send hx-ws:send
htmx.config.wsReconnectDelay htmx.config.ws.reconnectDelay
createWebSocket Удален
wsBinaryType Удален
socketWrapper Удален

ws-connect и ws-send по-прежнему работают, но выводится предупреждение.

События

События изменились следующим образом:

htmx 2.x htmx 4.x
htmx:wsConnecting Удалено
htmx:wsOpen htmx:ws:after:connection
htmx:wsClose htmx:ws:close
htmx:wsError htmx:ws:error
htmx:wsBeforeMessage htmx:ws:before:message:incoming
htmx:wsAfterMessage htmx:ws:after:message:incoming
htmx:wsConfigSend htmx:ws:before:message:outgoing
htmx:wsBeforeSend htmx:ws:before:message:outgoing
htmx:wsAfterSend htmx:ws:after:message:outgoing

От бета-версии к RC1

В RC1 изменены форматы сообщений, события и поведение повторного подключения по сравнению с бета-версией.

Исходящие сообщения

Формат передачи изменен с {headers, body: values} на {...values, headers}. headers зарезервирован, поэтому поле формы или запись hx-vals с таким именем не отправляется.

Данные события изменены с {headers, body} на {message: {headers, values, data}, waitUntil, cancelled}. Установите message.data, чтобы заменить данные. Используйте waitUntil(promise), чтобы завершить асинхронную работу до сериализации, постановки в очередь или отправки.

Входящие сообщения

Сопоставление сообщений через HX-Request-ID и request_id удалено.

Данные события изменены с {message: {text, json, cancelled}} на {message: {data, text(), json()}, waitUntil, cancelled}. Поля преобразования теперь являются методами. Установите message.data, чтобы заменить входящие данные. Отмените обработку с помощью event.preventDefault() или event.detail.cancelled = true.

События

Бета-версия RC1
htmx:before:ws:connection htmx:ws:before:connection
htmx:after:ws:connection htmx:ws:after:connection
htmx:before:ws:request htmx:ws:before:message:outgoing
htmx:after:ws:request htmx:ws:after:message:outgoing
htmx:before:ws:message htmx:ws:before:message:incoming
htmx:after:ws:message htmx:ws:after:message:incoming

htmx:ws:error предоставляет connection, если ошибка связана с соединением. Если соединения нет, предоставляется url.

Повторное подключение

В бета-версии повторное подключение выполнялось после каждого закрытия. В RC1 оно выполняется, только если ws.reconnect имеет значение true, код закрытия входит в ws.reconnectCodes, а подключенный элемент все еще существует.

По умолчанию код 1000 прекращает попытки повторного подключения. Сообщения, созданные во время открытия или повторного подключения, ставятся в очередь и отправляются по порядку.

Владение соединением

Каждый элемент hx-ws:connect владеет своим соединением. Разные элементы больше не используют общее соединение на основе URL.

Значение hx-ws:send больше не открывает соединение и игнорируется. Поместите hx-ws:send внутрь hx-ws:connect или укажите оба атрибута на одном элементе.

Другие изменения

Бета-версия RC1 Совместимость
htmx.config.websockets htmx.config.ws Удалено
ws.pendingRequestTTL Удалено Удалено
ws.reconnectJitter:true/false ws.reconnectJitter:0.3/0 Удалено
payload content Работает, но выводится предупреждение

Примечания

  • hx-ws:connect принимает:

    • URL относительно корня: /ws
    • URL относительно текущего пути: events
    • URL относительно протокола: //api.example.com/ws
    • URL HTTP(S): https://example.com/ws
    • URL WebSocket: wss://example.com/ws

    URL HTTP(S) преобразуются в соответствующие URL WebSocket.

  • Каждое соединение обрабатывает входящие и исходящие сообщения по порядку. Обработка входящих сообщений ожидает завершения каждой замены.

  • Для всех замен через WebSocket используется htmx.swap().

  • Используйте hx-ws-connect и hx-ws-send там, где двоеточия не поддерживаются, например в JSX.

См. также

  • hx-swap
  • hx-target
  • hx-select
  • hx-select-oob
  • hx-trigger
  • htmx.swap()
  • hx-sse

Licensed under the Zero-Clause BSD License.
https://four.htmx.org/extensions/hx-ws/

Spec-Zone.ru

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