Зробити так, щоб вебсайт відчувався як SPA
Вебсайт швидкий, але кожен клік усе ще перезавантажує цілу HTML-сторінку. Традиційна відповідь — побудувати односторінковий застосунок (англ. Single-Page Application, 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 ідуть на крок далі: вони дозволяють фрагменту сторінки навігувати незалежно від решти.
Список коментарів — ідеальний кандидат. Під час перегляду сторінок коментарів за допомогою посилань "Попередня" та "Наступна" має змінюватися лише список; повторний рендеринг заголовка, форми та підвалу — це марна робота. Огорніть коментарі у фрейм:
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> оновлюють лише цей фрейм: під час кліку на "Наступна" 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, створений на попередньому кроці, є правильною точкою входу: він публічний, задокументований і незалежний від фреймворку. Вебсайт і мобільний застосунок можуть розвиватися незалежно, спільно використовуючи той самий бекенд.