Новый перевод от команды Spring АйО расскажет вам, каких проблем можно избежать, если пользоваться подходом «Рендеринг на стороне сервера» и в чем преимущества такого подхода в целом по сравнению с подходом Single Page Application.
Мне действительно нравится использовать Spring Boot, Thymeleaf и htmx в качестве продуктивного стека для веб приложений. Однако, по большей части моя повседневная работа состоит из написания REST API бэкендов (точнее, JSON Data API) для фронтендов на Angular или на React. Во время такой работы я иногда не могу не думать, «У нас не было бы этой проблемы, если бы мы использовали рендеринг на стороне сервера, а не JavaScript Single Page Application.». В этой статье я объясню, почему я так думаю, более подробно.
Я признаю, что для большинства этих проблем существуют решения, но суть в том, что во многих случаях эти решения вам не нужны, поскольку и проблема, как таковая, не существует. Основная цель данной статьи в том, чтобы заставить людей думать о том, какую технологию они выбирают для создания веб приложения и какие последствия несет в себе тот или иной выбор.
Версионирование API
Первое о чём вам следует подумать при создании REST API — это версионирование. Поскольку клиент является приложением, отдельным от серверной части с SPA (Single Page Application, одностраничное приложение), у них разные жизненные циклы . Разные версии клиента могут взаимодействовать с одним и тем же сервером. Я слышал истории о людях, которые никогда не закрывают свой компьютер или браузер и держат одно и то же SPA открытым месяцами. Они никогда не получают обновленную версию клиента, потому что никогда не обновляют страницу. Рано или поздно все начинает ломаться, потому что клиент уже больше не совместим с сервером.
Добавив версионирование (что можно сделать через сегмент URL или через заголовок), вы можете поддерживать несколько версий клиента. Это приятное преимущество, и вам реально необходимо сделать это, если, например, вы создаете мобильные клиентские приложения.
Но если у вас просто веб приложения, эти меры необязательны. При использовании рендеринга на стороне сервера (server-side rendering, SSR) страница обновляется при каждом взаимодействии, так что у вас всегда будет открыта в браузере новейшая версия страницы.
Валидация
Поскольку вам не следует доверять клиенту, даже вашему собственному фронтенду, вам необходимо производить валидацию входящих запросов на серверной стороне. Используя Spring MVC, вы можете добавлять аннотации для валидации в код на Java, а Thymeleaf может отобразить их, если что-то пойдет не так. Используя htmx, вы даже можете отправлять запросы на сервер по поводу проблем валидации и динамически показывать их, пока вы печатаете (примеры здесь).
Если вы пользуетесь SPA, вам необходимо дублировать правила, уже заданные языком программирования, на котором написан ваш бэкенд, в JavaScript или TypeScript. Вы также должны следить за тем, чтобы эти правила были совершенно одинаковы, дублируя все сделанные изменения в двух местах, если правила валидации поменялись.
Безопасность
Любое нетривиальное приложении имеет пользователей и роли, которые определяют, что конкретный пользователь системы может или не может делать. Если приложение использует SSR, сервер решает на своей стороне, что может и что не может делать текущий пользователь. Например, довольно распространенной практикой является не отрисовывать кнопку “Удалить”, если пользователь не является администратором. Теги <button>
или <form>
просто не появляются в коде страницы, и, соответственно, пользователь не может выполнить это действие.
При использовании SPA клиентское приложение вынуждено решать, какой HTML отображать, на основании входных данных в формате JSON. В то время как HATEOAS предоставляет хорошее решение для этой проблемы, большинство REST API с ним несовместимы. Некоторые приложения считывают набор ролей, имеющихся у пользователя, из JSON токена и решают полностью на клиентской стороне, что отрисовывать, а что нет. Эта функциональность является дубликатом логики, которая уже присутствует на сервере.
Похожий пример — это кнопка, которую надо сделать недоступной в некоторых случаях. Если клиент проверяет какие-то флаги статуса в JSON, чтобы принять решение по этому поводу, вы опять-таки дублируете логику, уже существующую на сервере. Когда движок темплейтов занимается рендерингом HTML на сервере, вы можете использовать эту серверную логику для отключения доступности кнопки, при этом избежав дублирования логики на клиенте. Браузер с удовольствием отобразит кнопку как недоступную, и никакой JavaScript для этой цели не понадобится.
Безопасное скачивание файлов
Предоставление пользователю права на скачивание файла в приложении, использующем SPA — это на удивление нетривиальная задача. В приложении с SSR вы можете использовать стандартный код <a href=".."/>
, а вопросом безопасности будет заниматься серверная сторона через механизм сессий. Работая с SPA, вы обычно используете заголовок Authentication
для передачи JWT токена. Но это нельзя сделать с нормальной гиперссылкой.
В качестве решения вам необходимо вписать JWT токен, например, в куки, либо сделать AJAX-вызов, чтобы сначала записать документ в память, и только потом сохранить его на диск. Обходные пути работают, но, как вы понимаете, это все-таки обходные пути. 🙂
Документация
Хорошая документация абсолютно необходима, чтобы гарантировать, что команда, занимающаяся фронтендом, знает, как вызывать REST API, какие вызовы доступны, каких ответов следует ожидать и т.д. Вы можете использовать Swagger или Spring REST Docs (мой любимый вариант), чтобы это сделать. Реальность же состоит в том, что многие команды, работающие над серверной частью, ненавидят писать такую документацию и предоставляют команде фронтенда догадываться, какие эндпоинты существуют или какие значения для enum можно использовать для тех или иных полей внутри JSON запросов.
При использовании SSR документация становится не нужна, поскольку HTML и CSS становятся частью серверной стороны приложения. Если возникает потребность в документации, она решается на том же уровне, как и наличие Javadoc в других частях кода. Тем не менее, иногда вам может оказаться непонятно, какие переменные доступны внутри темплейта Thymeleaf, если вы не будете осторожны. Этот вопрос решается довольно приятным образом, например, в JTE. Вы в явном виде заявляете, что именно является доступным, как вы сделали бы это в обычном методе на Java.
Переводы
Если вы сделаете свое приложение многоязычным, вы сможете достичь более широкой аудитории и улучшить пользовательский опыт. Переводы можно сделать исключительно на фронтенде, и на первый взгляд это кажется логичным выбором. Однако, если вам потребуется сделать пагинацию на сервере, переводы тоже придется перенести на бэкенд. Подумайте о колонке статуса в отсортированной таблице, разбитой на страницы. Пользователь будет ожидать от приложения, чтобы сортировка соответствовала переведенному на его язык строковому значению, а сортировать строки сначала на английском и потом уже переводить их было бы неправильно.
При использовании рендеринга на серверной стороне выбор очевиден. Все переводы живут на сервере.
Медленная первая загрузка
Привлекательность SPA подхода состоит в том, что у вас есть первоначальная стоимость загрузки фреймворков и приложения, но после этого вы можете общаться с сервером с помощью легких JSON сообщений. Причина того, что SPA первоначально работает медленнее состоит в том, что браузеру сначала необходимо загрузить библиотеку JavaScript, а также весь JavaScript код из самого приложения. После этого требуется распарсить этот JavaScript и собрать приложение. Только после этого отправляется запрос к серверу, чтобы получить реальные данные для приложения через вызов REST API. JavaScript парсит JSON и превращает его в HTML, который затем визуализируется браузером.
В случае с рендерингом на сервере, отправляется только тот HTML код, который необходим для отображения страницы. Браузеры исключительно быстры в обработке HTML, так что время, необходимое для получения так называемого Largest Contentful Paint обычно сильно снижается.
При работе с SSR вам надо отправлять больше данных, так как страница в HTML-представлении содержит больше байтов, предназначенных к отправке. Это утверждение может быть верно в теории, но на практике в большинстве случаев это работает иначе.
Если пользователь открывает ссылку внутри приложения в новой вкладке, затраты на первоначальную загрузку повторятся. Я также вижу в большинстве приложений, что большое количество данных загружается предварительно, на случай, если они понадобятся пользователю в будущем. Это еще больше снижает потенциальную выгоду от использования JSON.
Даже если пользователи не открывают по несколько вкладок в приложении, затраты на первоначальную загрузку могут повториться. Chrome, например, может проактивно в фоновом режиме избавляться от вкладок, которые в течение какого-то времени оставались неиспользованными. Если пользователь вернется к этой вкладке, приложению придется перезагрузиться и запуститься заново.
Некоторое количество дополнительных байтов — это, на самом деле, не тот параметр, от которого зависит, быстрым или медленным нам кажется тот или иной сайт. Сайт McMaster-Carr воспринимается пользователями как исключительно быстрый, и он использует SSR. Простого добавления тега hx-boost
к вашему приложению может быть достаточно, чтобы произвести впечатление на некоторых людей:
“Нам дали задание сделать простое CRUD приложение, и на моего учителя произвело сильное впечатление то, каким быстрым оказался наш сайт, особенно учитывая, что мы использовали школьный Wi-Fi. Я просто вкинул
hx-boost="true"
в HTML тег, но мне норм.”
Если учесть все эти наблюдения, SPA действительно имеет смысл только в том случае, если ваш пользователь проводит в приложении по много часов за раз.
Реализация поведения браузера заново
Ах, эта печально знаменитая кнопка Back. В вашем приложении на Spring Boot с Thymeleaf, кнопка Back просто работает, пока вы перемещаетесь со страницы на страницу. При использовании SPA (будь это React, Angular или Vue), кнопка Back из коробки не работает. Вот что один из пользователей Reddit сказал на эту тему:
Всего лишь на прошлой неделе я проверял код для Vue, который пытался воспроизвести то, что естественным образом происходит, когда пользователь нажимает кнопку Back или когда их браузер обновляет страницу. Такой невероятный бардак, и все только для того, чтобы попытаться заставить работать то, что работает естественным образом без этого SPA мусора.
Но кнопка Back — это лишь один пример. Для SPA часто требуется заново реализовывать многие функции, которые браузеры предоставляют по умолчанию:
Обработка форм: браузеры имеют встроенную валидацию форм, обработку отправки и сообщения об ошибках. SPA-приложения обычно реализуют все это заново при помощи JavaScript библиотек, таких как Formik или React Hook Form.
Управление фокусом: браузеры естественным образом управляют фокусом при переходе со страницы на страницу. SPA приложениям нужны комплексные решения по управлению фокусом, особенно для целей accessibility.
Навигация: помимо кнопки Back, SPA приложениям необходимо управлять обновлениями URL-ов, восстановлением позиции скроллинга и состоянием навигации. Именно поэтому библиотеки вроде React Router нуждаются в комплексных API для реализации возможностей, которые работают автоматически в традиционных веб приложениях.
Состояния загрузки: браузеры показывают нативные индикаторы загрузки во время навигации. SPA приложения вынуждены реализовывать свои собственные крутящиеся индикаторы загрузки и прогресс бары.
Моментальные появления неверных состояний
Вы когда-нибудь встречали веб приложение, где изначально появляется что-то одно, но через секунду или две, появляется что‑то другое или что‑то одно заменяется на что‑то другое? Скорее всего, это приложение является SPA. Вы можете сказать, что оно плохо написано, и ничего не должно появляться, пока приложение не определилось, залогинен пользователь или нет (к примеру). Но, к сожалению, эта ошибка кажется весьма распространенной.
В качестве примера, посмотрите на страницу GitLab, предназначенную для редактирования черновика pull request‑а. Чекбокс для «Mark as draft» изначально показывается пользователю непомеченным. Затем, после подгрузки JavaScript, он уже отображается правильно, как помеченный, чтобы показать нам, что PR является черновиком. Гифка внизу показывает, как это выглядит на специально замедленном 4G подключении, чтобы происходящее стало еще более очевидным:

Ни в чем не виним GitLab, других веб приложений с такой же проблемой предостаточно.
Если бы вся веб страница рендерилась на сервере и затем отправлялась в браузер, непомеченный чекбокс никогда не увидел бы свет.
Состав команды
При создании Single Page Application вы обычно создаете специальную команду под фронтенд, или, как минимум, выделяете на фронтенд одного разработчика. Из-за водораздела между разработчиками бэкенда и фронтенда вам надо следить за тем, чтобы нагрузка на членов команд была примерно равной в каждом спринте. Однако, иногда спринты по естественным причинам оказываются перегружены проблемами бэкенда. При наилучшем сценарии команды могут потратить это время, чтобы поработать над какими-то техническими долгами, но при худшем сценарии это будет означать замедление поставки новой функциональности, так как не все в команде могут работать над важными задачами.
При работе над приложением с рендерингом на стороне сервера людям проще работать над всем приложением в целом. Специализация все еще будет присутствовать, в том смысле, что кого-то всегда будут просить проконсультировать по сложным проблемам с CSS, по тому же принципу, что кто-то в команде всегда помогает другим решать проблемы с базой данных. Но со временем разработчики бэкенда лучше познакомятся с кодом фронтенда, а разработчики фронтенда будут лучше знать код бэкенда.
Пример из реального мира: миграция приложения, написанного на React, на Python/Django/htmx:

За время миграции все стали full-stack разработчиками, что сильно упрощает ситуацию для продакт менеджера и тимлида при распределении различных задач, которые необходимо сделать.
Публичные и приватные API
Преимущество использования REST API состоит в том, что вы можете создать с их помощью любой клиент. Не только Single Page Application, но и мобильное приложение и десктоп приложение. Однако, в большинстве проектов из реального мира, которые мне приходилось видеть, мобильные приложения всегда имеют пользовательскую базу, отличающуюся от пользовательской базы веб приложения. Веб приложение чаще всего является более административным приложением, и эндпоинты, которые оно использует, относятся к так называемому «внутреннему» API. В большинстве случаев разработчики такого приложения будут менее строги к обратной совместимости, поскольку бэкенд и фронтэнд будут выходить в релиз одновременно, к тому же пользователи могут просто «обновить браузер».
Недостатком такого подхода является то, что как только приложение начинает расти, становится не так очевидно, какой API используется каким клиентом. Становится сложно отвечать на вопросы типа «Можем ли мы поменять этот эндпоинт? Что сломается, если мы это сделаем?».
Если вы разрабатываете веб приложение с рендерингом на сервере, и у вас есть выделенный API только для мобильного приложения, становится очевидно, какая часть должна считаться приватной, а какая является публичным API. Если вы следуете практике написания кода, при которой контроллеры являются очень тонким слоем и просто делегируют сервисам или сценариям использования, тогда никакого дублирования происходить не должно.
Добавляйте сложность только тогда, когда она нужна
Каждый, кто работал с реальными готовыми к продакшен SPA приложениями, скажет вам, что им свойственно много сложностей. Вам необходимо выбрать правильный инструмент для сборки, правильную библиотеку для управления состояниями, правильную библиотеку для маршрутизации и т. д. И все это надо выбрать еще до старта реализации чего бы то ни было.
Если вы используете SSR с htmx, вы можете начать с простого HTML и CSS. По мере роста приложения вы можете решить, во что инвестировать ваш отведенный на сложность бюджет.
Исключением из этого правила является создание веб приложений, подобных Miro или Figma. Такого рода приложения действительно обязаны быть SPA приложениями. Но эти виды приложений являются исключениями из общего случая административных бизнес-приложений, которыми занимается большинство разработчиков.
Добавление нескольких htmx взаимодействий в тех местах, где это имеет наибольший смысл, является компромиссом, на который охотно пойдет любая команда. Важным моментом здесь является гарантия того, что команда продолжит все контролировать. Команда решает, стоит ли дополнительная сложность времени и усилий, которых придется на неё потратить. При использовании SPA сложность будет с вами, хотите вы того или нет.
Заключение
Рендеринг на стороне сервера (SSR) с прогрессивным расширением предлагает заманчивую альтернативу подходу Single Page Application для многих бизнес приложений. В то время как SPA приложения имеют свою сферу применения, особенно в высоко интерактивных приложениях типа Miro или Figma, привносимые ими дополнительные сложности не всегда могут быть оправданы теми преимуществами, которые они обеспечивают.
Выбирая SSR с выборочными улучшениями на стороне клиента, осуществляемыми при помощи инструментов типа htmx, команды смогут многих распространенных проблем: сложностей с версионированием, дублирования логики валидации, угроз безопасности и сложности управления состояниями. Этот подход позволяет командам начинать с простого и постепенно прибавлять больше сложности, но только там, где это дает очевидные преимущества для пользователей.
Самое главное — дело не в выборе между «современной» и «традиционной» разработкой, а в подборе инструментов, подходящих именно для ваших задач. Рендеринг на серверной стороне с прогрессивным расширением может обеспечить прекрасный пользовательский опыт, и при этом кодовая база останется простой в поддержке, а команды смогут работать продуктивнее. Прежде чем автоматически выбирать SPA-фреймворк для следующего проекта, подумайте, не окажется ли более простое решение полезнее для ваших пользователей и вашей команды.
Веб платформа продолжает развиваться, пополняясь новыми возможностями, и фреймворки, подобные htmx, показывают нам, как использовать эти возможности, при этом сохраняя простоту и надежность, которые сделали Всемирную паутину такой успешной с самого начала. Иногда меньшими усилиями можно достичь большего.

Присоединяйтесь к русскоязычному сообществу разработчиков на Spring Boot в телеграм — Spring АйО, чтобы быть в курсе последних новостей из мира разработки на Spring Boot и всего, что с ним связано.