Créer une SPA (Single Page Application)
La plupart des commentaires seront soumis pendant les conférences, et certaines personnes n'y apportent pas leur ordinateur portable. Mais ils auront probablement leur téléphone. Pourquoi ne pas créer une application mobile permettant de lire rapidement les commentaires de la conférence ?
Une façon de créer une telle application mobile est de créer une Single Page Application (SPA) Javascript. Une SPA s'exécute localement, a accès au stockage local, peut faire des appels à des API HTTP distantes et peut s'appuyer sur les service workers pour créer une expérience presque native.
Créer l'application
Pour créer l'application mobile, nous allons utiliser Preact et Symfony Encore. Preact est une petite base efficace convenant parfaitement à la SPA du livre d'or.
Afin de rendre le site web et la SPA uniforme, nous allons réutiliser les feuilles de style Sass du site web pour l'application mobile.
Créez la SPA dans le répertoire spa
et copiez les feuilles de style du site web :
1 2 3
$ mkdir -p spa/src spa/public spa/assets/styles
$ cp assets/styles/*.scss spa/assets/styles/
$ cd spa
Note
Nous avons créé un répertoire public
car nous allons principalement interagir avec la SPA via un navigateur. Nous aurions pu le nommer build
si nous avions voulu nous limiter à une application mobile.
Pour bien faire les choses, créez un fichier .gitignore
:
Initialisez le fichier package.json
(équivalent au fichier composer.json
pour JavaScript) :
1
$ npm init -y
Maintenant, ajoutez quelques dépendances requises :
1
$ npm install @symfony/webpack-encore @babel/core @babel/preset-env babel-preset-preact preact html-webpack-plugin bootstrap
La dernière étape de configuration consiste à créer la configuration Webpack Encore :
Créer le template principal de la SPA
Il est temps de créer le template initial dans lequel Preact fera le rendu de l'application :
JavaScript générera le rendu de l'application dans la balise <div>
. Voici la première version du code qui affichera la vue "Hello World" :
La dernière ligne enregistre la fonction App()
sur l'élément #app
de la page HTML.
Maintenant, tout est prêt !
Exécuter la SPA dans le navigateur web
Comme cette application est indépendante du site web principal, nous avons besoin d'un autre serveur web :
1
$ symfony server:start -d --passthru=index.html
L'option --passthru
indique au serveur web de transmettre toutes les requêtes HTTP au fichier public/index.html
(public/
est le répertoire racine par défaut du serveur web). Cette page est gérée par l'application Preact et récupère la page à afficher via l'historique du "navigateur".
Pour compiler les fichiers CSS et JavaScript, exécutez npm
:
1
$ ./node_modules/.bin/encore dev
Ouvrez la SPA dans un navigateur :
1
$ symfony open:local
Et contemplez notre SPA hello world :
Ajouter un routeur pour gérer les états
La SPA n'est actuellement pas en mesure de traiter plusieurs pages. Pour pouvoir les implémenter, nous avons besoin d'un routeur, comme pour Symfony. Nous allons utiliser preact-router. Il prend une URL en entrée et la fait correspondre à un composant Preact à afficher.
Installez preact-router :
1
$ npm install preact-router
Créez une page pour l'accueil (un composant Preact) :
Et une autre pour la page d'une conférence :
Remplacez le div
"Hello World" par le composant 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>
)
}
Rebuildez l'application :
1
$ ./node_modules/.bin/encore dev
Si vous rafraîchissez l'application dans le navigateur, vous pouvez maintenant cliquer sur les liens "Accueil" et conférence. Notez que l'URL du navigateur et les boutons précédent/suivant de votre navigateur fonctionnent normalement.
Styliser la SPA
Comme pour le site web, ajoutons le loader Sass :
1
$ npm install node-sass sass-loader
Activez le loader Sass dans Webpack et ajoutez une référence à la feuille de style :
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 }))
;
Nous pouvons désormais mettre à jour l'application pour utiliser les feuilles de style :
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>
Rebuildez encore l'application :
1
$ ./node_modules/.bin/encore dev
Vous pouvez à présent profiter d'une SPA entièrement stylisée :
Récupérer les données depuis l'API
La structure de l'application Preact est maintenant terminée : Preact Router gère les états de la page, incluant le slug des conférences dans l'URL, et la feuille de style principale de l'application est utilisée pour styliser la SPA.
Pour rendre la SPA dynamique, nous avons besoin de récupérer les données de l'API via des appels HTTP.
Configurez Webpack pour exposer la variable d'environnement contenant le point d'entrée de l'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 variable d'environnement API_ENDPOINT
doit pointer vers le serveur du site web où nous hébergeons le point d'entrée de l'API, /api
. Nous le configurerons correctement plus tard quand nous exécuterons npm
.
Créez un fichier api.js
qui abstrait la récupération des données de l'API :
Vous pouvez maintenant adapter l'en-tête et les composants de l'accueil :
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>
);
-};
+}
Enfin, Preact Router passe le paramètre "slug" au composant Conference en tant que propriété. Utilisez-le pour afficher la conférence appropriée et ses commentaires, toujours en utilisant l'API ; et adaptez le rendu pour utiliser les données de l'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 a maintenant besoin de connaître l'URL de notre API grâce à la variable d'environnement API_ENDPOINT
. Définissez-la avec l'URL du serveur web de l'API (tournant dans le répertoire ..
) :
1
$ API_ENDPOINT=`symfony var:export SYMFONY_PROJECT_DEFAULT_ROUTE_URL --dir=..` ./node_modules/.bin/encore dev
Vous pourriez aussi exécuter maintenant en arrière-plan :
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
Et l'application devrait maintenant fonctionner correctement dans le navigateur :
Wow ! Nous avons à présent une SPA entièrement fonctionnelle, avec routeur et données réelles. Nous pourrions organiser l'application Preact davantage si nous le voulions, mais elle fonctionne déjà très bien.
Déployer la SPA en production
Platform.sh permet de déployer plusieurs applications par projet. L'ajout d'une autre application peut se faire en créant un fichier .platform.app.yaml
dans n'importe quel sous-répertoire. Créez-en un sous spa/
nommé spa
:
Modifiez le fichier .platform/routes.yaml
pour faire pointer le sous-domaine spa.
vers l'application spa
stockée dans le répertoire racine du projet :
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}/" }
Configurer CORS pour la SPA
Si vous déployez le code maintenant, il ne fonctionnera pas car les navigateurs bloqueraient la requête à l'API. Nous devons explicitement autoriser la SPA à accéder à l'API. Récupérez le nom de domaine correspondant à votre application :
1
$ symfony cloud:env:url --pipe --primary
Définissez la variable d'environnement CORS_ALLOW_ORIGIN
en conséquence :
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.#'`$"
Si votre domaine est https://master-5szvwec-hzhac461b3a6o.eu-5.platformsh.site/
, les appels sed
le convertiront en https://spa.master-5szvwec-hzhac461b3a6o.eu-5.platformsh.site
.
Nous devons également définir la variable d'environnement API_ENDPOINT
:
1
$ symfony cloud:variable:create --sensitive=1 --level=project -y --name=env:API_ENDPOINT --value=`symfony cloud:env:url --pipe --primary`
Commitez et déployez :
1 2 3
$ git add .
$ git commit -a -m'Add the SPA application'
$ symfony cloud:deploy
Accédez à la SPA dans un navigateur en spécifiant l'application comme option :
1
$ symfony cloud:url -1 --app=spa
Utiliser Cordova pour construire une application mobile
Apache Cordova est un outil qui génère des applications mobiles multiplateformes. Et bonne nouvelle, il peut utiliser la SPA que nous venons de créer.
Installons-le :
1 2
$ cd spa
$ npm install cordova
Note
Vous devez également installer le SDK Android. Cette section ne mentionne qu'Android, mais Cordova fonctionne avec toutes les plateformes mobiles, y compris iOS.
Créez la structure des répertoires de l'application :
1
$ ./node_modules/.bin/cordova create app
Et générez l'application Android :
1 2 3
$ cd app
$ ~/.npm/bin/cordova platform add android
$ cd ..
C'est tout ce dont vous avez besoin. Vous pouvez maintenant builder les fichiers de production et les déplacer vers 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
Exécutez l'application sur un smartphone ou un émulateur :
1
$ ./node_modules/.bin/cordova run android