Haciendo que el sitio web parezca una SPA
El sitio web es rápido, pero cada clic sigue recargando una página HTML completa. La respuesta tradicional es construir una Single-Page Application (SPA): una aplicación JavaScript, escrita en otra pila tecnológica, que se comunica con la API. Eso significa una segunda aplicación que desarrollar, asegurar, desplegar y mantener sincronizada con la principal.
Symfony tiene una respuesta diferente. Symfony UX es una iniciativa que lleva la experiencia SPA a las aplicaciones renderizadas en el servidor: sigue escribiendo plantillas Twig y controladores Symfony, y añade JavaScript solo donde aporta valor.
Descubriendo Symfony UX
¿La buena noticia? Hemos estado usando Symfony UX desde el primer paso de este libro. El esqueleto webapp incluye dos de sus paquetes: symfony/stimulus-bundle y symfony/ux-turbo. Echa un vistazo 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 es un pequeño framework de JavaScript que conecta controladores JavaScript con elementos HTML a través de atributos data-. El archivo assets/stimulus_bootstrap.js inicia la aplicación Stimulus y registra automáticamente cualquier controlador almacenado en el directorio assets/controllers/.
Turbo se apoya en esta base para hacer que la navegación sea instantánea.
Navegando instantáneamente con Turbo
Turbo Drive ha estado trabajando silenciosamente todo este tiempo: intercepta cada clic en un enlace y cada envío de formulario, obtiene la página en segundo plano e intercambia el <body> sin recargar la página completa. El navegador mantiene vivos el mismo JavaScript y CSS, por lo que la navegación se siente inmediata.
Abre las herramientas de desarrollo del navegador, cambia a la pestaña "Network" y navega por el sitio web: los cambios de página son peticiones fetch, no cargas completas del documento.
Turbo solo pide una cosa a cambio: una semántica HTTP correcta para los formularios. Un envío correcto debe redirigir (el nuestro lo hace, gracias a redirectToRoute()), y uno fallido debe usar un código de estado 4xx. Symfony se encarga de esto último automáticamente: cuando un formulario enviado no es válido, render() responde con un código de estado 422 Unprocessable Entity.
Actualizando parte de una página con Turbo Frames
Turbo Drive evita las recargas completas de la página. Los Frames de Turbo van un paso más allá: permiten que un fragmento de la página navegue de forma independiente del resto.
La lista de comentarios es un candidato perfecto. Al navegar por las páginas de comentarios con los enlaces "Anterior" y "Siguiente", solo debería cambiar la lista; volver a renderizar el encabezado, el formulario y el pie de página es trabajo desperdiciado. Envuelve los comentarios en 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">
Los enlaces dentro de un elemento <turbo-frame> solo actualizan ese frame: al hacer clic en "Siguiente", Turbo obtiene la página en segundo plano e intercambia el frame coincidente, dejando el resto de la página intacto.
El atributo data-turbo-action="advance" promueve la navegación del frame a una visita completa: la URL de la barra de direcciones se actualiza (incluyendo el parámetro de consulta offset), de modo que los comentarios paginados siguen siendo compartibles y el botón de retroceso del navegador funciona como se espera.
Previsualizando la foto del comentario con Stimulus
Es hora de escribir nuestras primeras líneas de JavaScript de todo el libro. Los asistentes que envían una foto no pueden verla antes de publicar el comentario. Vamos a mostrar una previsualización cuando seleccionen un archivo.
Crea un controlador Stimulus (el nombre del archivo determina el nombre del controlador, 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);
}
}
Eso es todo el JavaScript que necesitamos. Stimulus descubre y registra el controlador automáticamente. Conéctalo al campo de la foto del formulario de comentarios a través de atributos 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')
],
El atributo data-controller vincula el controlador al input de archivo, y data-action llama a su método show() cada vez que cambia el valor del input. Selecciona una foto en una página de conferencia y disfruta de la previsualización.
Los controladores Stimulus trabajan codo con codo con Turbo: como Drive y Frames actualizan la página sin recargarla, los controladores se conectan y desconectan automáticamente a medida que los elementos entran y salen del DOM.
¿Y las aplicaciones móviles?
Hemos conseguido la experiencia SPA sin construir una segunda aplicación: sin una pila JavaScript separada, sin un segundo servidor web, sin configuración de CORS, nada nuevo que desplegar.
Y si alguna vez necesitas una aplicación móvil nativa, la API creada en el paso anterior es el punto de entrada adecuado: es pública, está documentada y es independiente del framework. El sitio web y la aplicación móvil pueden evolucionar de forma independiente mientras comparten el mismo backend.