Потоковая передача HTML – асинхронные обновления DOM без JavaScript
Разработчики стремятся создавать адаптивные веб-приложения, чтобы обеспечить лучший пользовательский опыт. Пользователи веб-приложений ожидают, что страницы будут загружаться быстро, чего может быть трудно достичь, если страница требует данных из медленного источника или выполняет операции с интенсивными вычислениями. В этих случаях разработчики могут сначала загрузить страницу с базовыми стилями и данными быстрой загрузки, а затем обновить страницу асинхронно, когда станут доступны более медленные данные.
Обновление страницы, когда данные становятся доступными, почти всегда требует использования JavaScript. Одностраничные приложения (SPA) используются чаще всего, но новые платформы, которые хорошо взаимодействуют с серверным рендерингом, такие как Remix, Next.js, HTMX или Turbo, становятся все более распространенными. Однако каждое решение JavaScript усложняет приложение.
Благодаря развитию декларативного теневого DOM и потоковой передачи тел HTTP разработчики получили новые методы асинхронного обновления страницы без использования JavaScript. Эти методы можно применять в приложениях, отображаемых на стороне сервера, чтобы повысить скорость реагирования при сохранении низкой сложности.
JavaScript-решения
Одностраничные приложения
Одностраничное приложение (например, созданное с помощью React) по-прежнему является наиболее распространенной архитектурой, которую разработчики используют для асинхронного обновления страницы. Преимущество SPA заключается в том, что они широко используются и знакомы большинству разработчиков. Они чрезвычайно гибки и обеспечивают богатое и оперативное взаимодействие с пользователем.
Однако SPA вносят значительные сложности. SPA — это отдельное приложение, отличное от вашей серверной части. Создание и поддержка SPA аналогичны сложности мобильных приложений. SPA должны быть тщательно протестированы отдельно и интегрированы с серверной частью приложения.
Трудно найти разработчиков, которые умело работают с SPA и бэкэнд-фреймворками, поэтому разработчиков часто делят на фронтенд- и бэкенд-команды.
Такое разделение приводит к необходимости дополнительной коммуникации, большей зависимости внутри команды и более низкому пониманию того, как функционирует система.
Рендеринг на стороне сервера
Тела потоковой передачи HTTP позволяют браузерам отображать части HTML-документа до получения всего ответа. Новые платформы, такие как Remix и Next.js, используют эту функцию, отображая большую часть страницы на сервере и передавая дополнительные данные по тому же соединению, когда они становятся доступными. Каждая платформа также предоставляет клиентский JavaScript, который считывает потоковые данные и обновляет DOM.
Эти платформы проще, чем SPA, но сохраняют гибкость SPA, позволяя разработчикам запускать произвольный код в браузере. Они немного сложны, поскольку большая часть кода приложения должна работать как в браузере, так и на серверной части. Кроме того, серверная часть должна быть написана на JavaScript или на чем-то, что компилируется в JavaScript, что может быть нежелательно для многих команд.
Легкие интерфейсные библиотеки
Облегченные интерфейсные библиотеки, такие как HTMX и Turbo, предоставляют решение, которое позволяет разработчикам использовать атрибуты HTML для выполнения HTTP-вызовов и замены части DOM своим ответом. Они могут добавить легкий уровень интерактивности к приложению, отображаемому на стороне сервера, без специального кода JavaScript. Эти библиотеки просты, но их интерактивность ограничена по сравнению с решениями JavaScript. Их также сложно тестировать, потому что их необходимо тестировать, используя в браузере фреймворк, аналогичный Cypress или Playwright.
Другой подход
Декларативный теневой DOM
Функции шаблонов и слотов, представленные Shadow DOM, добавляют в HTML шаблоны многократного использования. К сожалению, до недавнего времени единственным способом использования Shadow DOM была функция AttachShadow JavaScript. Современные браузеры теперь поддерживают декларативную теневую модель DOM, которая позволяет разработчикам создавать теневой корень непосредственно в HTML.
В приведенном ниже примере показан теневой корень, объявленный непосредственно в HTML. Элемент шаблона содержит тег слота, заполненный элементом с атрибутом слота.
<section>
<template shadowrootmode="open">
<slot name="heading"></slot>
</template>
<h1 slot="heading">Hello world</h1>
</section>
The browser renders this as follows.
<section>
#shadow-root
<h1 slot="heading">Hello world</h1>
</section>
Добавление потоковой передачи
Шаблоны облегчают асинхронную загрузку страниц, заполняя элемент в одном месте содержимым элемента в другом. Предположим, мы создаем приложение, в котором получение адреса электронной почты пользователя является медленной операцией, и мы определяем нашу разметку следующим образом.
<header><!-- header content -->/header>
<footer><!-- footer content -->/footer>
<main>
<template shadowrootmode="open">
<slot name="heading">
<h1>Loading</h1>
</slot>
</template>
<!-- additional main content -->
<h1 slot="heading">Welcome, user@example.com!</h1>
</main>
Это позволяет нам визуализировать всю страницу с данными, которые быстро загружаются, прежде чем мы отобразим электронную почту пользователя. Используя потоковую передачу HTTP, мы сначала отправляли в браузер все, что находится перед тегом <h1 slot="heading">
, чтобы браузер отображал частичное представление страницы.
<header><!-- header content -->/header>
<footer><!-- footer content -->/footer>
<main>
#shadow-root
<h1>Loading</h1>
<!-- additional main content -->
Как только мы получаем электронное письмо пользователя, мы отправляем его в поток и закрываем соединение, чтобы пользователь мог видеть остальную часть содержимого страницы.
<header><!-- header content -->/header>
<footer><!-- footer content -->/footer>
<main>
#shadow-root
<h1 slot="heading">Welcome, user@example.com!</h1>
<!-- additional main content -->
</main>
Стилизация теневого DOM
Одна из мощных (или сводящих с ума, в зависимости от вашего варианта использования) особенностей теневого DOM заключается в том, что каждый теневой корень инкапсулирует свои собственные стили. Это означает, что на элементы, отображаемые в теневом DOM, не влияют глобальные стили CSS, а стили, примененные в данном теневом корне, не влияют на элементы за пределами теневого корня. Это может быть желательно, если все ваши стили ограничены лишь компонентами, но на практике большинство приложений имеют несколько глобальных стилей, которые следует применять повсюду.
Специалисты по сопровождению спецификаций продолжают обсуждать, как лучше всего разрешить глобальным стилям влиять на шаблонные HTML-документы, но на данный момент рекомендуемый подход состоит в том, чтобы дублировать глобальные стили в каждом теневом корне, используя тег ссылки, который ссылается на те же файлы, что и в голова.
<link rel="stylesheet" href="style.css">
<header><!-- header content -->/header>
<footer><!-- footer content -->/footer>
<main>
<template shadowrootmode="open">
<link rel="stylesheet" href="style.css">
<slot name="heading">
<h1>Loading</h1>
</slot>
</template>
<!-- additional main content -->
<h1 slot="heading">Welcome, user@example.com!</h1>
</main>
Поскольку браузер уже получил и проанализировал файлы глобального стиля, производительность не страдает. Обратной стороной является то, что это обременительно для разработчиков, а глобальные стили не будут применяться к границам светлого и теневого DOM.
Преимущества
Основное преимущество этого подхода в том, что он прост. Большая часть кода приложения идентична обычному приложению, отображаемому на стороне сервера (SSR). Как и в случае с другими SSR, отладка проще, поскольку весь код выполняется в одном и том же месте.
Кроме того, тесты приложения просты. Их можно писать так же, как и тесты любого другого приложения, отображаемого на стороне сервера, с учетом конечного результата рендеринга шаблонов и не беспокоясь о потоках.
Приложения, созданные с использованием такого подхода, работают быстро. Все данные извлекаются одним запросом и отображаются на экране по мере готовности. В традиционном SPA мы должны дождаться обработки первоначального запроса в браузере, а затем выполнить дополнительные запросы для получения дополнительных данных.
Недостатки
Одним из основных недостатков этого подхода является то, что он (пока) не получил широкого распространения. Существует не так много фреймворков, использующих его методы, а некоторые языки шаблонов его не поддерживают. Поскольку декларативный теневой DOM относительно новый, документации недостаточно.
Кроме того, приложения, созданные с использованием такого подхода, не обеспечивают такую же интерактивность, как SPA. После закрытия первоначального HTTP-запроса приложение ведет себя как традиционное приложение SSR.
При таком подходе обработка ошибок также может быть затруднена. Он сохраняет первоначальное HTTP-соединение страницы открытым до тех пор, пока не загрузятся более медленные данные. Чем дольше открыто соединение, тем больше риск того, что соединение будет прервано, возможно, страница останется в частично загруженном состоянии. В этом случае приложения должны найти способ отобразить ошибку пользователю.
Пример использования Go
Далее мы рассмотрим пример приложения и кодовой базы, в которых используется декларативный теневой DOM и потоковая передача HTTP-ответов, чтобы увидеть, как применить этот метод на практике. Большинство HTTP-серверов изначально поддерживают потоковую передачу HTTP, поэтому мы сможем создать пример, используя любую комбинацию языка и платформы. Go — естественный выбор из-за встроенного http.Server, примитивов параллелизма и того, что шаблоны Go постепенно записывают выходные данные в Writer по мере анализа шаблона.
Буферизованный писатель
http.Server Go
записывает тела ответа в соединение через буфер записи размером 4 КБ. Даже если мы поэтапно запишем результат разбора шаблона, результат не будет отправлен пользователю, пока мы не запишем 4 КБ данных.
Чтобы гарантировать, что обновления отправляются пользователю как можно быстрее, мы должны очистить данные буферизованного записывающего устройства перед ожиданием длительной операции. Это будет отображать как можно большую часть страницы для пользователя, ожидая разрешения более медленных данных.
Интерфейс http.ResponseWriter
не имеет метода сброса, но он есть в большинстве реализаций (включая ту, которая предоставляется http.Server
). Если возможно, используйте метод сброса в http.ResponseController
для безопасного сброса средства записи ответов.
Имейте в виду, что браузер или другие элементы сетевого уровня также могут буферизовать поток тела ответа, поэтому сбросы не могут гарантировать, что они действительно отправят данные пользователю. Приложения, использующие этот подход, следует тестировать в производственной инфраструктуре, чтобы убедиться в их правильном работе. На практике этот подход, как правило, хорошо поддерживается.
Простой обработчик
В нашем примере приложения есть один обработчик индекса, который отображает простое сообщение. Чтобы проиллюстрировать, как наш сервер Go может передавать медленные ответы, мы добавили поставщику сообщений искусственную задержку в одну секунду.
data := make(chan []string)
go func() {
data <- provider.FetchAll()
}()
_ = websupport.Render(w, Resources, "index", model{Message: deferrable.New(w, data)})
В фоновом режиме мы ждем сообщения и отправляем его в канал, когда оно будет готово. Канал оборачивается объектом с возможностью отсрочки (о котором мы поговорим далее) перед его передачей в модель шаблона.
Отложенный
Отложенная структура принимает в качестве свойств записывающее устройство и канал. После вызова GetOne
отложенный объект сбрасывает записывающее устройство (используя http.ResponseController
, как обсуждалось выше), прежде чем ждать результата из канала и возвращать его.
type Deferrable[T any] struct {
writer http.ResponseWriter
channel chan T
}
func (d Deferrable[T]) GetOne() T {
d.flush()
return <-d.channel
}
func (d Deferrable[T]) flush() {
_ = http.NewResponseController(d.writer).Flush()
}
Сброс позволяет визуализировать шаблон до ожидания медленного сообщения, что означает, что пользователь может просматривать контент во время ожидания сообщения.
Потоковый шаблон
Как обсуждалось выше, шаблон декларативно создает теневой корень и включает глобальные стили. Слот содержит содержимое-заполнитель, которое отображается до тех пор, пока слот не будет выбран позже. Мы вызываем GetOne, чтобы средство записи сбрасывалось перед отправкой медленного сообщения пользователю.
Как только сообщение получено, GetOne
возвращается, и остальная часть шаблона, включая содержимое слота, отображается для пользователя.
<template shadowrootmode="open">
<link rel="stylesheet" href="/static/style/application.css">
<!-- header content -->
<section>
<slot name="content">
<h2>Wait for it...</h2>
</slot>
</section>
</template>
{{ $items := .Message.GetOne }}
<div slot="content">
<h2>
Success!
</h2>
<ul class="bulleted">
{{range $item := $items}}
<li>{{$item}}</li>
{{end}}
</ul>
</div>
Вот и все! Остальная часть приложения состоит из стандартного шаблона сервера Go, но если вам интересно, посмотрите исходный код.
Заключение
В этой статье показан практический пример использования Shadow DOM, слотов шаблонов и тел потокового ответа для создания адаптивного приложения без JavaScript. В следующий раз, когда вы захотите добавить немного отзывчивости приложению, рассмотрите возможность использования этого подхода, прежде чем переходить к более тяжелому решению на основе JavaScript. Это дает разработчикам простоту, возможность тестирования и обслуживания приложения, отображаемого на стороне сервера, одновременно обеспечивая лучший пользовательский опыт.