Iniziamo col dire che SPA non è un centro benessere ma un acronimo che sta per Single Page Application. In particolare, una SPA è un’implementazione di un’app Web che attraverso una singola pagina Web carica e aggiorna vari contenuti nel corpo del documento tramite delle API JavaScript come XMLHttpRequest. Ciò consente agli utenti di fruire di siti Web senza caricare pagine completamente nuove dal server, il che può comportare miglioramenti sulle prestazioni e un’esperienza più dinamica e fluida al pari dei software dei sistemi operativi tradizionali. In un’applicazione SPA tutto il codice necessario (HTML, JavaScript e CSS) è recuperato in un singolo caricamento della pagina o le risorse appropriate sono caricate dinamicamente e aggiunte alla pagina quando necessario, di solito in risposta ad azioni dell’utente.
Questa tecnologia viene sempre più utilizzata al giorno d’oggi, nonostante presenti alcuni evidenti come la difficoltà di indicizzazione della pagina (SEO), uno sforzo maggiore per mantenere lo stato, oltre al fatto che sarà necessario implementare la navigazione in maniera completamente diversa.
Il punto di partenza per implementare una SPA è senza dubbio l’utilizzo di un framework JS a scelta tra: AngularJS, Ember.js, ExtJS, Knockout.js , Meteor.js, React, Vue.js e Svelte. Ciascuno di essi, con caratteristiche uniche che li contraddistinguono, hanno adottato i principi SPA.
Oggi vedremo come sviluppare un piccolo sito che utilizzi questi concetti, utilizzando i framework Symfony 5 (framework PHP utilizzato per sviluppare applicazioni web, API, microservizi e servizi web) e React.
Step 1. Creiamo il progetto Symfony
Si farà riferimento alla versione 5.4 di Symfony. Per continuare è richiesta l’installazione dei seguenti applicativi:
- Composer (https://getcomposer.org/)
- PHP 7.4
- MySql
In alternativa, possiamo anche installare XAMPP o WAMP o qualsiasi altro sistema integrato in modo da aver già pronto all’uso PHP e MYSQL senza bisogno di dover perdersi tra i ile di configurazione.
Iniziamo. Apriamo una finestra del prompt dei comandi (se siamo su Windows) o una finestra di Shell (su Linux) e digitiamo i seguenti comandi:
composer create-project symfony/skeleton my_spa cd my_spa composer require webapp
In questo modo Composer si occuperà di creare la cartella my_spa e scaricare al suo interno tutte le librerie (vendors) di Symfony necessarie al suo funzionamento. La procedura è abbastanza veloce, anche se molto dipende anche dalla connessione Internet. Se il processo di creazione termina senza errori, potremmo già testare la nostra app (vuota), avviando una istanza web. Per fare questo avremo bisogno però dell’eseguibile symfony.exe che può essere scaricato da qui e che, una volta estratto, dovrà essere posizionato nella root della nostra webapp, cioè my_spa. A questo punto, sempre da riga di comando, digitiamo:
symfony serve
e come risultato ci verrà mostrato:
[OK] Web server listening The Web server is using PHP CGI 7.4.27 http://127.0.0.1:8000
A questo punto, con un qualsiasi browser, possiamo aprire l’URL http://127.0.0.1:8000 e ci troveremo di fronte a questo messaggio che ci avvisa che Symfony è stato correttamente installato e pronto all’uso:
Per terminare l’istanza del web server basta premere CTRL+C dalla finestra della riga di comando.
Step 2. Configuriamo il database
All’interno della cartella my_spa troveremo questa struttura di cartelle e file.
Apriamo con un editor IDE tipo Visual Studio Code (anche blocco note va bene…) il file .env. Questo particolare file contiene la configurazione del nostro ambiente Symfony, tra cui la stringa di connessione al database. Questo è quello che dovrebbe apparire:
###> symfony/framework-bundle ### APP_ENV=dev APP_SECRET=9810184bb35d26fbb0cad0d6454cb528 ###< symfony/framework-bundle ### ###> symfony/webapp-meta ### MESSENGER_TRANSPORT_DSN=doctrine://default?auto_setup=0 ###< symfony/webapp-meta ### ###> doctrine/doctrine-bundle ### # Format described at https://www.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/configuration.html#connecting-using-a-url # IMPORTANT: You MUST configure your server version, either here or in config/packages/doctrine.yaml # # DATABASE_URL="sqlite:///%kernel.project_dir%/var/data.db" # DATABASE_URL="mysql://db_user:db_password@127.0.0.1:3306/db_name?serverVersion=5.7&charset=utf8mb4" DATABASE_URL="postgresql://symfony:ChangeMe@127.0.0.1:5432/app?serverVersion=13&charset=utf8" ###< doctrine/doctrine-bundle ### ###> symfony/messenger ### # Choose one of the transports below # MESSENGER_TRANSPORT_DSN=doctrine://default # MESSENGER_TRANSPORT_DSN=amqp://guest:guest@localhost:5672/%2f/messages # MESSENGER_TRANSPORT_DSN=redis://localhost:6379/messages ###< symfony/messenger ### ###> symfony/mailer ### # MAILER_DSN=smtp://localhost ###< symfony/mailer ###
Per il momento concentriamoci sul parametro DATABASE_URL. Siccome utilizzeremo MySql (o MariaDB), dobbiamo commentare la riga relativa a PostgreSQL e decommentare la riga relativa a MySQL, facendo attenzione a inserire le credenziali corrette (io ho utilizzato root/password) e il nome di un database esistente (che ho chiamato spa). Quindi, il risultato sarebbe questo:
DATABASE_URL="mysql://root:password@127.0.0.1:3306/spa" # DATABASE_URL="postgresql://symfony:ChangeMe@127.0.0.1:5432/app?serverVersion=13&charset=utf8"
Da riga di comando, digitiamo questo comando che creerà il database MySQL.
php bin/console doctrine:database:create
Step 3. Creazione delle entità
Il contenuto delle nostre pagine verrà memorizzato all’interno di una tabella del database anziché in file HTML, in questo modo, successivamente, potremo implementare un’area di backend riservata che ci permetta di modificare tali contenuti senza bisogno di intervenire sui singoli file. Un’entità in Symfony rappresenta, in questo caso, una tabella di MySql. Per creare un’entità possiamo utilizzare la riga di comando. Delle semplici istruzioni ci guideranno passo passo per la creazione della struttura.
php bin/console make:entity
I campi della tabella Pagina saranno, per semplificare, solo due:
- body (di tipo text, nullable), in cui verrà memorizzato il contenuto HTML da recuperare
- slug (di tipo string 255, not null), che rappresenta l’id univoco della pagina in formato testuale
Adesso, possiamo eseguire:
php bin/console make:migration php bin/console doctrine:migrations:migrate
La prima istruzione crea un file PHP di migration nell’apposita cartella migrations, che contiene le istruzioni PHP e SQL per creare / modificare lo schema di database. I file di migration sono utili perchè permettono di condividere lo schema del database con il team ed avere un tracciato storico delle modifiche e delle integrazioni.
La seconda istruzione si occupa di eseguire le istruzioni SQL contenute all’interno dei file migration (non ancora elaborati) e creare e/o modificare fisicamente lo schema del database. Se tutto viene eseguito senza errori, il risultato nel prompt dei comandi dovrebbe essere simile a questo:
All’interno del database verranno create 3 tabelle:
- doctrine_migration_versions
- messenger_messages
- pagina (la nostra entità su Symfony)
Step 4: Creazione dei Controller
Creiamo adesso un primo controller su Symfony chiamato SpaController. Tale controller conterrà semplicemente un metodo che visualizzerà la pagina principale:
php bin/console make:controller SpaController
Al suo interno possiamo modificare il codice esistente in questo modo:
<?php namespace App\Controller; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Annotation\Route; class SpaController extends AbstractController { /** * @Route("/{reactRouting}", name="spa_app", defaults={"reactRouting": null}) */ public function index(): Response { return $this->render('spa/index.html.twig', [ 'controller_name' => 'SpaController', ]); } }
La riga da cambiare fondamentalmente è la riga di annotation con cui si definisce il nuovo pattern:
* @Route("/{reactRouting}", name="spa_app", defaults={"reactRouting": null})
Adesso dobbiamo modificare il template index.html.twig creato automaticamente con make:controller nella cartella templates/spa/ in questo modo:
{% extends 'base.html.twig' %} {% block body %} <div id="app"></div> {% endblock %}
Successivamente creiamo il controller relativo alla logica di gestione della pagina, in particolare a noi servirà un metodo per reperire i contenuti dal database:
php bin/console make:controller PaginaController
Al suo interno definiamo una funzione:
<?php namespace App\Controller; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Annotation\Route; /** * @Route("/api", name="api") */ class PaginaController extends AbstractController { /** * @Route("/loader/{slug}", name="loader", methods={"GET"}) */ public function loader(string $slug): Response { $pagina = $this->getDoctrine() ->getRepository(Pagina::class) ->findOneBy( array("slug" => $slug) ); if (!$pagina) { return $this->json('La pagina non esiste per lo slug ' . $slug, 404); } $data = [ 'id' => $pagina->getId(), 'body' => $pagina->getBody(), 'slug' => $pagina->getSlug(), ]; return $this->json($data); } }
Step 5. Installare le dipendenze React
Fatto ciò , possiamo concentrarci su React. Installiamo quindi tutte le dipendenze di Javascript, comprese le librerie React che ci serviranno. Possiamo usare Yarn o Npm in base a cosa abbiamo già installato sul nostro computer. Proviamo quindi con npm da riga di comando, sempre nella root del nostro progetto:
npm install
Il gestore npm si occuperà del “lavoro sporco” di scaricare tutte le dipendenze in una cartella chiamata node_modules. Sempre da riga di comando, installiamo questi moduli aggiuntivi:
npm install react-router-dom npm install react react-dom prop-types axios @babel/preset-react --dev npm install @babel/plugin-proposal-class-properties @babel/plugin-transform-runtime
Inoltre, nel file webpack.config.js presente nella root, de-commentiamo una riga per “istruire” il framework come analizzare la sintassi JSX. Tale riga contiene la seguente istruzione:
.enableReactPreset()
Iniziamo a creare i nostri file React. Ma prima eseguiamo questo comando per fare in modo che ogni modifica sui file JS venga automaticamente compilata.
npm run watch
In questo modo verrà creata una nuova cartella assets, nella root del progetto contenente questi file:
Creiamo un file chiamato Main.js che conterrà il routing della nostra SPA. Il file andrà creato nella cartella assets e conterrà questo codice:
import React from 'react'; import ReactDOM from 'react-dom'; import { BrowserRouter, Routes, Route, } from "react-router-dom"; import Reader from "./Reader"; function Main() { return ( <BrowserRouter> <Routes> <Route exact path="/" element={<Reader />} /> <Route path="/:slug" element={<Reader />} /> </Routes> </BrowserRouter> ); } export default Main; if (document.getElementById('app')) { ReactDOM.render(<Main />, document.getElementById('app')); }
Il path /:slug dirotterà tutte le richieste su un componente chiamato Reader, passando all”oggetto il parametro slug.
Nel file Reader.js, possiamo inserire questo codice
import React, { useState, useEffect } from 'react'; import { useParams, } from "react-router-dom"; import axios from "axios"; import Header from "./Header"; function Reader() { let {slug} = useParams(); const [page, setPage] = useState([]); useEffect(() => { fetchPage(); }, [slug]); const fetchPage = () => { if(!slug) slug = "home"; axios .get('/api/loader/' + slug) .then((res) => { setPage(res.data); }) .catch((err) => { alert("Errore caricamento dati. Guarda console.") console.log(err); }); }; return ( <div> <Header /> <h1> {page.slug} </h1> {page.body} </div> ); } export default Reader;
Analizziamo un blocco di questo codice:
useEffect(() => { fetchPage(); }, [slug]);
La funzione fetchPage() chiamata in useEffect verrà eseguita dopo che il rendering è stato eseguito sullo schermo.
Per impostazione predefinita, le istruzioni che stanno dentro useEffect vengono eseguiti dopo ogni rendering, e ciò sarebbe un problema in quanto il rendering della pagina viene eseguito solo una volta! Possiamo quindi scegliere di attivarli solo quando determinati valori sono cambiati, nel nostro caso quando cambia il parametro slug, che passiamo come secondo argomento della funzione useEffect.
Adesso creiamo il menu delle pagine, chiamato Header.js. In questa fase utilizzeremo un menu “statico”, nel senso che i vari URL saranno definiti su un array Js, anche se in una versione più articolata le voci di menu potrebbero essere presi dal database.
import React from 'react'; import {Link} from "react-router-dom"; function Header() { const pageLinks = [ { "name": "Home", "url" :"/", }, { "name": "Pagina 1", "url" :"/page1", }, { "name": "Pagina 2", "url" :"/page2", }, { "name": "Pagina 3", "url" :"/page3", }, ]; return ( <div> { pageLinks.map((page, key) => { return ( <div key={key}> <Link to={page.url} className={`nav-link ${location.pathname === page.url ? 'active' : ''}`}>{page.name}</Link> </div> ) }) } </div> ); } export default Header;
Questo componente non farà altro che creare una lista HTML di link cliccabili, ognuna delle quali deve corrispondere a un record all’interno del DB MySql. Quindi, se ancora non l’abbiamo fatto, apriamo il DB e iniziamo ad inserire i record come mostrato in figura.
Infine, modifichiamo leggermente il file styles/app.css in questo modo:
body { background-color: white; } .active { color: red; }
Avviamo adesso il nostro server con:
symfony serve
e apriamo con un browser l’URL http://127.0.0.1:8000 trovandoci di fronte a questa schermata (spartana, migliorabile sicuramente con l’uso di CSS).
Cliccando su ogni singolo link, il contenuto del titolo e del body verranno presi dal database tramite una richiesta XMLHttpRequest senza aggiornare la pagina.