Сайт с ощущением SPA
Сайт работает быстро, но каждый щелчок по-прежнему перезагружает HTML-страницу целиком. Традиционный ответ — построить одностраничное приложение (SPA): JavaScript-приложение, написанное на другом стеке и общающееся с API. Это означает второе приложение, которое нужно разрабатывать, защищать, развёртывать и поддерживать в синхронизации с основным.
У Symfony есть другой ответ. Symfony UX — это инициатива, которая приносит опыт SPA в приложения с серверным рендерингом: продолжайте писать шаблоны Twig и контроллеры Symfony, добавляя JavaScript только там, где он приносит пользу.
Знакомство с Symfony UX
Хорошая новость? Мы используем Symfony UX с самого первого шага этой книги. Скелет webapp включает два его пакета: symfony/stimulus-bundle и symfony/ux-turbo. Загляните в importmap.php:
1 2 3 4 5 6
return [
'app' => ['path' => './assets/app.js', 'entrypoint' => true],
'@hotwired/stimulus' => ['version' => '3.2.2'],
'@symfony/stimulus-bundle' => ['path' => './vendor/symfony/stimulus-bundle/assets/dist/loader.js'],
'@hotwired/turbo' => ['version' => '8.0.23'],
];
Stimulus — это небольшой JavaScript-фреймворк, который связывает JavaScript-контроллеры с HTML-элементами через data--атрибуты. Файл assets/stimulus_bootstrap.js запускает приложение Stimulus и автоматически регистрирует любой контроллер из директории assets/controllers/.
Turbo строится на этом фундаменте, делая навигацию мгновенной.
Мгновенная навигация с Turbo
Turbo Drive всё это время незаметно работал: он перехватывает каждый щелчок по ссылке и каждую отправку формы, загружает страницу в фоне и подменяет <body> без полной перезагрузки страницы. Браузер сохраняет те же JavaScript и CSS, поэтому навигация ощущается мгновенной.
Откройте инструменты разработчика браузера, перейдите на вкладку "Network" и пощёлкайте по сайту: смены страниц — это fetch-запросы, а не полные загрузки документа.
Взамен Turbo просит только одно: корректную HTTP-семантику для форм. Успешная отправка должна перенаправлять (наша так и делает благодаря redirectToRoute()), а неудачная — использовать код состояния 4xx. Последнее Symfony делает автоматически: когда отправленная форма недействительна, render() отвечает кодом состояния 422 Unprocessable Entity.
Обновление части страницы с Turbo Frames
Turbo Drive избегает полных перезагрузок страницы. Turbo Frames идут на шаг дальше: они позволяют фрагменту страницы перемещаться независимо от остальной её части.
Список комментариев — идеальный кандидат. При листании страниц комментариев по ссылкам "Previous" и "Next" меняться должен только список; перерисовывать шапку, форму и подвал — напрасная работа. Оберните комментарии во фрейм:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
--- i/templates/conference/show.html.twig
+++ w/templates/conference/show.html.twig
@@ -17,3 +17,4 @@
<div class="row">
<div class="col-12 col-lg-8">
+ <turbo-frame id="comments" data-turbo-action="advance">
{% if comments|length > 0 %}
@@ -56,5 +57,6 @@
No comments have been posted yet for this conference.
</div>
{% endif %}
+ </turbo-frame>
</div>
<div class="col-12 col-lg-4">
Ссылки внутри элемента <turbo-frame> обновляют только этот фрейм: при щелчке по "Next" Turbo загружает страницу в фоне и подменяет соответствующий фрейм, не трогая остальную часть страницы.
Атрибут data-turbo-action="advance" превращает навигацию фрейма в полноценный переход: URL в адресной строке обновляется (включая параметр запроса offset), поэтому страницы комментариев остаются доступными для обмена ссылками, а кнопка "назад" браузера работает как положено.
Предпросмотр фото комментария со Stimulus
Пришло время написать наши первые строки JavaScript за всю книгу. Участники, отправляющие фото, не могут увидеть его до публикации комментария. Давайте показывать предпросмотр, когда они выбирают файл.
Создайте контроллер Stimulus (имя файла определяет имя контроллера, photo-preview):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
import { Controller } from '@hotwired/stimulus';
export default class extends Controller {
show() {
const [photo] = this.element.files;
if (!photo) {
this.preview?.remove();
this.preview = null;
return;
}
if (!this.preview) {
this.preview = document.createElement('img');
this.preview.className = 'img-thumbnail mt-2';
this.preview.style.maxHeight = '150px';
this.element.insertAdjacentElement('afterend', this.preview);
}
this.preview.src = URL.createObjectURL(photo);
}
}
Это весь нужный нам JavaScript. Stimulus обнаруживает и регистрирует контроллер автоматически. Подключите его к полю фото в форме комментария через data--атрибуты:
1 2 3 4 5 6 7 8 9 10 11 12 13
--- i/src/Form/CommentType.php
+++ w/src/Form/CommentType.php
@@ -26,6 +26,10 @@ class CommentType extends AbstractType
->add('photo', FileType::class, [
'required' => false,
'mapped' => false,
+ 'attr' => [
+ 'data-controller' => 'photo-preview',
+ 'data-action' => 'change->photo-preview#show',
+ ],
'constraints' => [
new Image(maxSize: '1024k')
],
Атрибут data-controller привязывает контроллер к полю выбора файла, а data-action вызывает его метод show() при каждом изменении значения поля. Выберите фото на странице конференции и наслаждайтесь предпросмотром.
Контроллеры Stimulus работают рука об руку с Turbo: поскольку Drive и Frames обновляют страницу без перезагрузки, контроллеры автоматически подключаются и отключаются по мере того, как элементы появляются в DOM и покидают его.
А как же мобильные приложения?
Мы получили опыт SPA, не строя второго приложения: ни отдельного JavaScript-стека, ни второго веб-сервера, ни настройки CORS, ничего нового для развёртывания.
А если вам когда-нибудь понадобится нативное мобильное приложение, API, созданный на предыдущем шаге, — правильная точка входа: он публичный, документированный и не зависит от фреймворка. Сайт и мобильное приложение могут развиваться независимо, разделяя один и тот же бэкенд.