Der Website das Gefühl einer SPA geben
Die Website ist schnell, aber jeder Klick lädt immer noch eine vollständige HTML-Seite neu. Die traditionelle Antwort darauf ist, eine Single-Page Application (SPA) zu bauen: eine JavaScript-Anwendung, geschrieben in einem anderen Stack, die mit der API kommuniziert. Das bedeutet eine zweite Anwendung, die entwickelt, abgesichert, deployt und mit der Hauptanwendung synchron gehalten werden muss.
Symfony hat eine andere Antwort. Symfony UX ist eine Initiative, die das SPA-Erlebnis in servergerenderte Anwendungen bringt: Schreibe weiterhin Twig-Templates und Symfony-Controller, und streue JavaScript nur dort ein, wo es einen Mehrwert bietet.
Symfony UX entdecken
Die gute Nachricht? Wir verwenden Symfony UX seit dem allerersten Schritt dieses Buches. Das Webapp-Skeleton bringt zwei seiner Pakete mit: symfony/stimulus-bundle und symfony/ux-turbo. Wirf einen Blick auf 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 ist ein kleines JavaScript-Framework, das JavaScript-Controller über data--Attribute mit HTML-Elementen verbindet. Die Datei assets/stimulus_bootstrap.js startet die Stimulus-Anwendung und registriert automatisch jeden Controller, der im Verzeichnis assets/controllers/ liegt.
Turbo baut auf diesem Fundament auf, um die Navigation augenblicklich zu machen.
Sofort navigieren mit Turbo
Turbo Drive war die ganze Zeit still im Einsatz: Es fängt jeden Link-Klick und jede Formular-Übermittlung ab, holt die Seite im Hintergrund und tauscht den <body> aus, ohne die Seite vollständig neu zu laden. Der Browser behält dieselben JavaScript- und CSS-Ressourcen am Leben, sodass sich die Navigation unmittelbar anfühlt.
Öffne die Browser-Entwicklertools, wechsle zum "Network"-Tab und klicke Dich durch die Website: Seitenwechsel sind fetch-Requests, keine vollständigen Dokument-Ladevorgänge.
Turbo verlangt nur eines im Gegenzug: korrekte HTTP-Semantik für Formulare. Eine erfolgreiche Übermittlung muss weiterleiten (unsere tut das, dank redirectToRoute()), und eine fehlgeschlagene muss einen 4xx-Statuscode verwenden. Symfony erledigt Letzteres automatisch: Wenn ein übermitteltes Formular ungültig ist, antwortet render() mit dem Statuscode 422 Unprocessable Entity.
Einen Teil der Seite mit Turbo Frames aktualisieren
Turbo Drive vermeidet vollständige Seiten-Reloads. Turbo Frames gehen einen Schritt weiter: Sie lassen ein Fragment der Seite unabhängig vom Rest navigieren.
Die Kommentarliste ist eine perfekte Kandidatin. Beim Blättern durch die Kommentarseiten über die Links "Previous" und "Next" sollte sich nur die Liste ändern; den Header, das Formular und den Footer neu zu rendern ist verschwendete Arbeit. Umhülle die Kommentare mit einer Frame:
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">
Links innerhalb eines <turbo-frame>-Elements aktualisieren nur diese Frame: Beim Klick auf "Next" holt Turbo die Seite im Hintergrund und tauscht die passende Frame aus, während der Rest der Seite unberührt bleibt.
Das Attribut data-turbo-action="advance" stuft die Frame-Navigation zu einem vollständigen Seitenbesuch hoch: Die URL in der Adressleiste wird aktualisiert (einschließlich des Query-Parameters offset), sodass paginierte Kommentare teilbar bleiben und der Zurück-Button des Browsers wie erwartet funktioniert.
Das Kommentar-Foto mit Stimulus in der Vorschau anzeigen
Zeit, unsere ersten JavaScript-Zeilen des ganzen Buches zu schreiben. Teilnehmer*innen, die ein Foto übermitteln, können es vor dem Absenden des Kommentars nicht sehen. Zeigen wir eine Vorschau an, wenn sie eine Datei auswählen.
Erstelle einen Stimulus-Controller (der Dateiname bestimmt den Controller-Namen, 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);
}
}
Das ist das gesamte JavaScript, das wir brauchen. Stimulus entdeckt und registriert den Controller automatisch. Verbinde ihn über data--Attribute mit dem Foto-Feld des Kommentarformulars:
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')
],
Das data-controller-Attribut bindet den Controller an das Datei-Eingabefeld, und data-action ruft seine show()-Methode auf, wann immer sich der Wert des Feldes ändert. Wähle ein Foto auf einer Konferenzseite aus und freue Dich über die Vorschau.
Stimulus-Controller arbeiten Hand in Hand mit Turbo: Da Drive und Frames die Seite aktualisieren, ohne sie neu zu laden, werden Controller automatisch verbunden und getrennt, wenn Elemente in das DOM eintreten und es verlassen.
Was ist mit mobilen Anwendungen?
Wir haben das SPA-Erlebnis bekommen, ohne eine zweite Anwendung zu bauen: kein separater JavaScript-Stack, kein zweiter Webserver, keine CORS-Konfiguration, nichts Neues zu deployen.
Und falls Du jemals eine native mobile Anwendung brauchst, ist die im vorherigen Schritt erstellte API der richtige Einstiegspunkt: Sie ist öffentlich, dokumentiert und Framework-agnostisch. Die Website und die mobile Anwendung können sich unabhängig voneinander weiterentwickeln und sich dabei dasselbe Backend teilen.