Costruire una SPA
La maggior parte dei commenti saranno inviati durante la conferenza, i cui partecipanti, probabilmente, avranno un telefono e non un computer. Perché non creare un'applicazione mobile per controllare rapidamente i commenti della conferenza?
Un modo per creare un'applicazione mobile di questo tipo è quello di creare un'applicazione Javascript a pagina singola (SPA). Una SPA viene eseguita localmente, può utilizzare archiviazione locale, può chiamare una API HTTP remota e può sfruttare service workers per creare un'esperienza quasi nativa.
Creare l'applicazione
Per creare l'applicazione mobile useremo Preact e Symfony Encore. Preact è una piccola ed efficiente libreria che semplificherà la creazione della SPA del Guestbook.
Per rendere il sito e la SPA coerenti, utilizzeremo i fogli di stile sass del sito anche per l'applicazione mobile.
Creare l'applicazione SPA nella cartella spa
e copiare i fogli di stile del sito:
1 2 3
$ mkdir -p spa/src spa/public spa/assets/styles
$ cp assets/styles/*.scss spa/assets/styles/
$ cd spa
Note
Abbiamo creato una cartella public
in quanto interagiremo principalmente con la SPA tramite un browser. Avremmo potuto chiamarla build
se avessimo voluto solo costruire un'applicazione mobile.
Per sicurezza, aggiungere un file .gitignore
:
Inizializzare il file package.json
(equivalente JavaScript del file composer.json
):
1
$ npm init -y
Ora, aggiungere alcune delle dipendenze richieste:
1
$ npm install @symfony/webpack-encore @babel/core @babel/preset-env babel-preset-preact preact html-webpack-plugin bootstrap
L'ultimo passo è quello di creare la configurazione per Webpack Encore:
Creare il template principale della SPA
È ora di creare il template iniziale che Preact utilizzerà per eseguire il render dell'applicazione:
Il tag <div>
è il punto in cui JavaScript eseguirà il render dell'applicazione. Ecco la prima versione del codice che esegue il render della view "Hello World":
L'ultima riga registra la funzione App()
con l'elemento #app
della pagina HTML.
Ora è tutto pronto!
Eseguire una SPA nel browser
Poiché questa applicazione è indipendente dal sito principale, abbiamo bisogno di eseguire un altro server web:
1
$ symfony server:stop
1
$ symfony server:start -d --passthru=index.html
Il parametro --passthru
comunica al server web di passare tutte le richieste HTTP al file public/index.html
(public/
è la cartella web principale predefinita per il server web). Questa pagina è gestita dall'applicazione Preact e recupera la pagina di cui eseguire il render tramite la cronologia del "browser".
Per compilare i CSS e i file JavaScript, eseguire il comando npm
:
1
$ ./node_modules/.bin/encore dev
Aprire la SPA in un browser:
1
$ symfony open:local
Diamo uno sguardo alla nostra SPA "Hello world":
Aggiungere un router per gestire gli stati
Attualmente la SPA non è in grado di gestire pagine diverse. Per implementare diverse pagine, abbiamo bisogno di un router, così come per Symfony. Useremo preact-router. Prende un URL come input e trova la corrispondenza con un componente Preact da visualizzare.
Installare preact-router:
1
$ npm install preact-router
Creare una pagina per la homepage (un componente Preact):
E un altro per la pagina della conferenza:
Sostituire il div
"Hello World" con il componente Router
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
--- a/src/app.js
+++ b/src/app.js
@@ -1,9 +1,22 @@
import {h, render} from 'preact';
+import {Router, Link} from 'preact-router';
+
+import Home from './pages/home';
+import Conference from './pages/conference';
function App() {
return (
<div>
- Hello world!
+ <header>
+ <Link href="/">Home</Link>
+ <br />
+ <Link href="/conference/amsterdam2019">Amsterdam 2019</Link>
+ </header>
+
+ <Router>
+ <Home path="/" />
+ <Conference path="/conference/:slug" />
+ </Router>
</div>
)
}
Eseguire nuovamente il build dell'applicazione:
1
$ ./node_modules/.bin/encore dev
Aggiornando l'applicazione nel browser, sarà ora possibile fare click sul link "Home" e sui link della conferenza. Si noti che l'URL e i pulsanti avanti/indietro del browser funzionano come ci si aspetterebbe.
Aggiungere lo stile alla SPA
Per quanto riguarda il sito, aggiungiamo il sass loader:
1
$ npm install node-sass sass-loader
Attivare il sass loader in Webpack e aggiungere un riferimento al foglio di stile:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
--- a/src/app.js
+++ b/src/app.js
@@ -1,3 +1,5 @@
+import '../assets/styles/app.scss';
+
import {h, render} from 'preact';
import {Router, Link} from 'preact-router';
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -7,6 +7,7 @@ Encore
.cleanupOutputBeforeBuild()
.addEntry('app', './src/app.js')
.enablePreactPreset()
+ .enableSassLoader()
.enableSingleRuntimeChunk()
.addPlugin(new HtmlWebpackPlugin({ template: 'src/index.ejs', alwaysWriteToDisk: true }))
;
Ora possiamo aggiornare l'applicazione affinché utilizzi i fogli di stile:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
--- a/src/app.js
+++ b/src/app.js
@@ -9,10 +9,20 @@ import Conference from './pages/conference';
function App() {
return (
<div>
- <header>
- <Link href="/">Home</Link>
- <br />
- <Link href="/conference/amsterdam2019">Amsterdam 2019</Link>
+ <header className="header">
+ <nav className="navbar navbar-light bg-light">
+ <div className="container">
+ <Link className="navbar-brand mr-4 pr-2" href="/">
+ 📙 Guestbook
+ </Link>
+ </div>
+ </nav>
+
+ <nav className="bg-light border-bottom text-center">
+ <Link className="nav-conference" href="/conference/amsterdam2019">
+ Amsterdam 2019
+ </Link>
+ </nav>
</header>
<Router>
Eseguire nuovamente il build dell'applicazione:
1
$ ./node_modules/.bin/encore dev
Ora abbiamo a disposizione una SPA con uno stile completo:
Recupero dei dati dall'API
La struttura dell'applicazione Preact è ora completa: Preact Router gestisce gli stati della pagina, incluso il segnaposto slug per le conferenze, e il foglio di stile dell'applicazione principale viene utilizzato per il design della SPA.
Per rendere dinamica la SPA, abbiamo bisogno di recuperare i dati dall'API tramite chiamate HTTP.
Configurare Webpack per esporre la variabile di ambiente per l'endpoint dell'API:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -1,3 +1,4 @@
+const webpack = require('webpack');
const Encore = require('@symfony/webpack-encore');
const HtmlWebpackPlugin = require('html-webpack-plugin');
@@ -10,6 +11,9 @@ Encore
.enableSassLoader()
.enableSingleRuntimeChunk()
.addPlugin(new HtmlWebpackPlugin({ template: 'src/index.ejs', alwaysWriteToDisk: true }))
+ .addPlugin(new webpack.DefinePlugin({
+ 'ENV_API_ENDPOINT': JSON.stringify(process.env.API_ENDPOINT),
+ }))
;
module.exports = Encore.getWebpackConfig();
La variabile d'ambiente API_ENDPOINT
dovrebbe puntare al server del sito dove abbiamo l'API endpoint all'indirizzo relativo /api
. La configureremo correttamente quando eseguiremo npm
.
Creare un file api.js
che astragga il recupero dei dati dall'API:
Ora è possibile adattare i componenti "header" e "home":
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83
--- a/src/app.js
+++ b/src/app.js
@@ -2,11 +2,23 @@ import '../assets/styles/app.scss';
import {h, render} from 'preact';
import {Router, Link} from 'preact-router';
+import {useState, useEffect} from 'preact/hooks';
+import {findConferences} from './api/api';
import Home from './pages/home';
import Conference from './pages/conference';
function App() {
+ const [conferences, setConferences] = useState(null);
+
+ useEffect(() => {
+ findConferences().then((conferences) => setConferences(conferences));
+ }, []);
+
+ if (conferences === null) {
+ return <div className="text-center pt-5">Loading...</div>;
+ }
+
return (
<div>
<header className="header">
@@ -19,15 +31,17 @@ function App() {
</nav>
<nav className="bg-light border-bottom text-center">
- <Link className="nav-conference" href="/conference/amsterdam2019">
- Amsterdam 2019
- </Link>
+ {conferences.map((conference) => (
+ <Link className="nav-conference" href={'/conference/'+conference.slug}>
+ {conference.city} {conference.year}
+ </Link>
+ ))}
</nav>
</header>
<Router>
- <Home path="/" />
- <Conference path="/conference/:slug" />
+ <Home path="/" conferences={conferences} />
+ <Conference path="/conference/:slug" conferences={conferences} />
</Router>
</div>
)
--- a/src/pages/home.js
+++ b/src/pages/home.js
@@ -1,7 +1,28 @@
import {h} from 'preact';
+import {Link} from 'preact-router';
+
+export default function Home({conferences}) {
+ if (!conferences) {
+ return <div className="p-3 text-center">No conferences yet</div>;
+ }
-export default function Home() {
return (
- <div>Home</div>
+ <div className="p-3">
+ {conferences.map((conference)=> (
+ <div className="card border shadow-sm lift mb-3">
+ <div className="card-body">
+ <div className="card-title">
+ <h4 className="font-weight-light">
+ {conference.city} {conference.year}
+ </h4>
+ </div>
+
+ <Link className="btn btn-sm btn-primary stretched-link" href={'/conference/'+conference.slug}>
+ View
+ </Link>
+ </div>
+ </div>
+ ))}
+ </div>
);
-};
+}
Infine, Preact Router passa la variabile "slug" al componente Conference come proprietà. Usiamola per visualizzare la conferenza corretta e i suoi commenti, sfruttando nuovamente l'API. Inoltre adattiamo il rendering affinché utilizzi i dati dell'API:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54
--- a/src/pages/conference.js
+++ b/src/pages/conference.js
@@ -1,7 +1,48 @@
import {h} from 'preact';
+import {findComments} from '../api/api';
+import {useState, useEffect} from 'preact/hooks';
+
+function Comment({comments}) {
+ if (comments !== null && comments.length === 0) {
+ return <div className="text-center pt-4">No comments yet</div>;
+ }
+
+ if (!comments) {
+ return <div className="text-center pt-4">Loading...</div>;
+ }
+
+ return (
+ <div className="pt-4">
+ {comments.map(comment => (
+ <div className="shadow border rounded-3 p-3 mb-4">
+ <div className="comment-img mr-3">
+ {!comment.photoFilename ? '' : (
+ <a href={ENV_API_ENDPOINT+'uploads/photos/'+comment.photoFilename} target="_blank">
+ <img src={ENV_API_ENDPOINT+'uploads/photos/'+comment.photoFilename} />
+ </a>
+ )}
+ </div>
+
+ <h5 className="font-weight-light mt-3 mb-0">{comment.author}</h5>
+ <div className="comment-text">{comment.text}</div>
+ </div>
+ ))}
+ </div>
+ );
+}
+
+export default function Conference({conferences, slug}) {
+ const conference = conferences.find(conference => conference.slug === slug);
+ const [comments, setComments] = useState(null);
+
+ useEffect(() => {
+ findComments(conference).then(comments => setComments(comments));
+ }, [slug]);
-export default function Conference() {
return (
- <div>Conference</div>
+ <div className="p-3">
+ <h4>{conference.city} {conference.year}</h4>
+ <Comment comments={comments} />
+ </div>
);
-};
+}
La SPA ora deve conoscere l'URL della nostra API: Questo avviene tramite la variabile d'ambiente API_ENDPOINT
: impostiamola con l'URL del server web API (in esecuzione nella cartella ..
):
1
$ API_ENDPOINT=`symfony var:export SYMFONY_PROJECT_DEFAULT_ROUTE_URL --dir=..` ./node_modules/.bin/encore dev
Ora si potrebbe eseguire anche in background:
1
$ API_ENDPOINT=`symfony var:export SYMFONY_PROJECT_DEFAULT_ROUTE_URL --dir=..` symfony run -d --watch=webpack.config.js ./node_modules/.bin/encore dev --watch
E l'applicazione nel browser ora dovrebbe funzionare correttamente:
Fantastico! Ora abbiamo una SPA completamente funzionale, con router e dati reali. Potremmo organizzare ulteriormente l'app Preact se vogliamo, ma sta già funzionando alla grande.
Deploy della SPA in produzione
Platform.sh permette il deploy di più applicazioni per progetto. L'aggiunta di un'altra applicazione può essere fatta creando un file .platform.app.yaml
in qualsiasi sottocartella. Crearne uno nella cartella spa/
con il nome spa
:
Modificare il file .platform/routes.yaml
affinché al sottodominio spa.
risponda l'applicazione spa
contenuta nella cartella principale del progetto:
1
$ cd ../
1 2 3 4 5 6 7 8
--- a/.platform/routes.yaml
+++ b/.platform/routes.yaml
@@ -1,2 +1,5 @@
"https://{all}/": { type: upstream, upstream: "varnish:http", cache: { enabled: false } }
"http://{all}/": { type: redirect, to: "https://{all}/" }
+
+"https://spa.{all}/": { type: upstream, upstream: "spa:http" }
+"http://spa.{all}/": { type: redirect, to: "https://spa.{all}/" }
Configurare CORS per la SPA
Se si facesse il deploy del codice ora, questo non funzionerebbe poiché un browser bloccherebbe la richiesta API. Dobbiamo permettere esplicitamente alla SPA di accedere all'API. Prendere il nome a dominio corrente legato all'applicazione:
1
$ symfony cloud:env:url --pipe --primary
Definire, di conseguenza, la variabile d'ambiente CORS_ALLOW_ORIGIN
:
1
$ symfony cloud:variable:create --sensitive=1 --level=project -y --name=env:CORS_ALLOW_ORIGIN --value="^`symfony cloud:env:url --pipe --primary | sed 's#/$##' | sed 's#https://#https://spa.#'`$"
Se il dominio è https://master-5szvwec-hzhac461b3a6o.eu-5.platformsh.site/
, il comando sed
lo convertirà in https://spa.master-5szvwec-hzhac461b3a6o.eu-5.platformsh.site
.
Dobbiamo anche impostare la variabile d'ambiente API_ENDPOINT
:
1
$ symfony cloud:variable:create --sensitive=1 --level=project -y --name=env:API_ENDPOINT --value=`symfony cloud:env:url --pipe --primary`
Commit e deploy:
1 2 3
$ git add .
$ git commit -a -m'Add the SPA application'
$ symfony cloud:deploy
Accedere la SPA in un browser specificando l'applicazione come parametro:
1
$ symfony cloud:url -1 --app=spa
Usare Cordova per costruire un'applicazione per smartphone
Apache Cordova è uno strumento per costruire applicazioni multipiattaforma per smartphone. La buona notizia è che può utilizzare la SPA che abbiamo appena creato.
Installiamolo:
1 2
$ cd spa
$ npm install cordova
Note
È inoltre necessario installare l'SDK Android. Questa sezione cita solo Android, ma Cordova funziona con tutte le piattaforme mobili, incluso iOS.
Creare la struttura di cartelle dell'applicazione:
1
$ ./node_modules/.bin/cordova create app
E generare l'applicazione Android:
1 2 3
$ cd app
$ ~/.npm/bin/cordova platform add android
$ cd ..
Questo è tutto ciò di cui abbiamo bisogno. Ora è possibile eseguire la "build" dei file di produzione e spostarli in Cordova:
1 2 3 4
$ API_ENDPOINT=`symfony var:export SYMFONY_PROJECT_DEFAULT_ROUTE_URL --dir=..` ./node_modules/.bin/encore production
$ rm -rf app/www
$ mkdir -p app/www
$ cp -R public/ app/www
Eseguire l'applicazione su uno smartphone o un emulatore:
1
$ ./node_modules/.bin/cordova run android
Andare oltre