Dare al sito web la sensazione di una SPA
Il sito web è veloce, ma ogni clic ricarica ancora una pagina HTML completa. La risposta tradizionale è costruire una Single-Page Application (SPA): un'applicazione JavaScript, scritta in un altro stack, che dialoga con l'API. Significa una seconda applicazione da sviluppare, mettere in sicurezza, distribuire e mantenere sincronizzata con quella principale.
Symfony ha una risposta diversa. Symfony UX è un'iniziativa che porta l'esperienza di una SPA nelle applicazioni renderizzate lato server: si continua a scrivere template Twig e controller Symfony, aggiungendo JavaScript solo dove porta valore.
Scoprire Symfony UX
La buona notizia? Usiamo Symfony UX fin dal primissimo passo di questo libro. Lo scheletro webapp include due dei suoi pacchetti: symfony/stimulus-bundle e symfony/ux-turbo. Dare un'occhiata a 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 è un piccolo framework JavaScript che collega controller JavaScript a elementi HTML tramite attributi data-. Il file assets/stimulus_bootstrap.js avvia l'applicazione Stimulus e registra automaticamente ogni controller presente nella cartella assets/controllers/.
Turbo si basa su queste fondamenta per rendere la navigazione istantanea.
Navigare istantaneamente con Turbo
Turbo Drive ha lavorato silenziosamente fin dall'inizio: intercetta ogni clic su un link e ogni invio di form, recupera la pagina in background e sostituisce il <body> senza un ricaricamento completo della pagina. Il browser mantiene attivi gli stessi JavaScript e CSS, quindi la navigazione sembra immediata.
Aprire gli strumenti per sviluppatori del browser, passare alla scheda "Network" e navigare nel sito: i cambi di pagina sono richieste fetch, non caricamenti completi di documento.
Turbo chiede solo una cosa in cambio: una semantica HTTP corretta per i form. Un invio riuscito deve reindirizzare (il nostro lo fa, grazie a redirectToRoute()), e uno fallito deve usare un codice di stato 4xx. Symfony gestisce quest'ultimo caso automaticamente: quando un form inviato non è valido, render() risponde con un codice di stato 422 Unprocessable Entity.
Aggiornare una parte di pagina con i Turbo Frames
Turbo Drive evita i ricaricamenti completi di pagina. I Turbo Frames fanno un passo in più: permettono a un frammento della pagina di navigare indipendentemente dal resto.
La lista dei commenti è una candidata perfetta. Quando si sfogliano le pagine dei commenti tramite i link "Previous" e "Next", solo la lista dovrebbe cambiare; rirenderizzare l'header, il form e il footer è lavoro sprecato. Avvolgere i commenti in un 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">
I link all'interno di un elemento <turbo-frame> aggiornano solo quel frame: cliccando su "Next", Turbo recupera la pagina in background e sostituisce il frame corrispondente, lasciando intatto il resto della pagina.
L'attributo data-turbo-action="advance" promuove la navigazione del frame a visita completa: l'URL nella barra degli indirizzi viene aggiornato (incluso il parametro di query offset), così i commenti paginati restano condivisibili e il pulsante indietro del browser funziona come previsto.
Anteprima della foto del commento con Stimulus
È il momento di scrivere le nostre prime righe di JavaScript di tutto il libro. I partecipanti che inviano una foto non possono vederla prima di pubblicare il commento. Mostriamo un'anteprima quando selezionano un file.
Creare un controller Stimulus (il nome del file determina il nome del controller, 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);
}
}
È tutto il JavaScript di cui abbiamo bisogno. Stimulus scopre e registra il controller automaticamente. Collegarlo al campo foto del form dei commenti tramite gli attributi 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')
],
L'attributo data-controller lega il controller al campo file, e data-action chiama il suo metodo show() ogni volta che il valore del campo cambia. Selezionare una foto sulla pagina di una conferenza e godersi l'anteprima.
I controller Stimulus lavorano mano nella mano con Turbo: poiché Drive e i Frames aggiornano la pagina senza ricaricarla, i controller vengono collegati e scollegati automaticamente man mano che gli elementi entrano ed escono dal DOM.
E le applicazioni mobili?
Abbiamo ottenuto l'esperienza di una SPA senza costruire una seconda applicazione: nessuno stack JavaScript separato, nessun secondo server web, nessuna configurazione CORS, niente di nuovo da distribuire.
E se un giorno servisse un'applicazione mobile nativa, l'API creata nel passo precedente è il punto d'ingresso giusto: è pubblica, documentata e indipendente dal framework. Il sito web e l'applicazione mobile possono evolversi indipendentemente, condividendo lo stesso backend.