Een SPA bouwen
De meeste reacties zullen tijdens de conferentie worden gepost. Sommige mensen zullen daar geen laptop bij zich hebben, maar waarschijnlijk wel een smartphone. Laten we een mobiele app maken om snel de reacties van een conferentie te kunnen checken.
Een manier om zo'n mobiele applicatie te maken is het bouwen van een Javascript Single Page Application (SPA). Een SPA draait lokaal, kan lokale opslag gebruiken, kan een externe HTTP-API aanroepen en kan service workers gebruiken om een bijna native ervaring te creëren.
De applicatie creëren
Om de mobiele applicatie te creëren, gaan we Preact en Symfony Encore gebruiken. Preact is een kleine en efficiënte basis die zeer geschikt is voor de Gastenboek-SPA.
Om de website en de SPA consistent te maken, gaan we de Sass-stylesheets van de website hergebruiken voor de mobiele applicatie.
Maak de SPA-applicatie aan onder de spa
-map en kopieer de stylesheets van de website:
1 2 3
$ mkdir -p spa/src spa/public spa/assets/styles
$ cp assets/styles/*.scss spa/assets/styles/
$ cd spa
Note
We hebben een public
-map aangemaakt omdat we voornamelijk via een browser met de SPA zullen communiceren. We hadden het build
kunnen noemen als we alleen een mobiele applicatie zouden bouwen.
Voeg alvast een .gitignore
bestand toe:
Maak het package.json
-bestand aan (het Javascript-equivalent van het composer.json
-bestand):
1
$ yarn init -y
Voeg nu wat vereiste dependencies toe:
1
$ yarn add @symfony/webpack-encore @babel/core @babel/preset-env babel-preset-preact preact html-webpack-plugin bootstrap
De laatste configuratiestap is het maken van de Webpack Encore-configuratie:
De basistemplate van de SPA aanmaken
Tijd om de initiële template te maken waarin Preact de applicatie zal renderen:
De <div>
-tag is waar de toepassing wordt weergegeven door JavaScript. Hier is de eerste versie van de code die de "Hello World"-weergave weergeeft:
De laatste regel registreert de App()
-functie op het #app
-element van de HTML-pagina.
Nu is alles klaar!
Een SPA uitvoeren in de browser
Omdat deze applicatie onafhankelijk is van de hoofdwebsite, moeten we een andere webserver draaien:
1
$ symfony server:start -d --passthru=index.html
De --passthru
-vlag vertelt de webserver om alle HTTP-requests door te geven aan het public/index.html
-bestand ( public/
is de standaard root-map van de webserver). Deze pagina wordt beheerd door de Preact-toepassing en die laat de pagina gerenderd worden via de browsergeschiedenis.
Voer yarn
uit om de CSS- en de JavaScript-bestanden te compileren:
1
$ yarn encore dev
Open de SPA in een browser:
1
$ symfony open:local
En kijk eens naar onze "hello world"-SPA:
Een router toevoegen om states te beheren
De SPA is momenteel niet in staat om verschillende pagina's te verwerken. Om meerdere pagina's te implementeren hebben we een router nodig, zoals voor Symfony. We gaan gebruik maken van de preact-router. Het neemt een URL als invoer en koppelt met een Preact component om weer te geven.
Installeer de preact-router:
1
$ yarn add preact-router
Maak een pagina aan voor de homepage (een Preact component ):
En nog een voor de conferentiepagina:
Vervang de "Hello World" div
door de Router
component:
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>
)
}
Bouw de applicatie opnieuw:
1
$ yarn encore dev
Als je de applicatie in de browser vernieuwt, kan je op de "Home" en conferentie links klikken. Merk op dat de browser-URL en de terug/vooruit-knoppen van jouw browser werken zoals je dat zou verwachten.
De SPA van styling voorzien
Zoals bij de website, gaan we de Sass loader toevoegen:
1
$ yarn add node-sass sass-loader
Activeer de Sass loader in Webpack en voeg een verwijzing naar de stylesheet toe:
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 }))
;
We kunnen de applicatie nu bijwerken om de stylesheets te gebruiken:
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>
Bouw de applicatie opnieuw:
1
$ yarn encore dev
Je kan nu genieten van een SPA die van styling voorzien is:
Gegevens uit de API halen
De Preact applicatiestructuur is nu klaar: Preact router behandelt de pagina states - inclusief de conference slug placeholder - en de belangrijkste stylesheet van de toepassing wordt gebruikt om de SPA van styling te voorzien.
Om de SPA dynamisch te maken, moeten we gegevens via de API ophalen met HTTP-calls.
Configureer Webpack om de API-endpoint-omgevingsvariabele beschikbaar te maken:
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();
De API_ENDPOINT
-omgevingsvariabele moet verwijzen naar de webserver van de website waar het API-endpoint /api
staat. We zullen dit binnenkort beter configureren wanneer we yarn
draaien.
Maak een api.js
bestand aan dat het ophalen van gegevens uit de API verzorgt:
Je kan nu de header- en home componenten aanpassen:
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>
);
-};
+}
Eindelijk geeft Preact Router de "slug" placeholder netjes door aan het Conference component. Gebruik deze om de juiste conferentie en bijhorende reacties te tonen, wederom met behulp van de API; en pas de rendering aan om de API-gegevens te gebruiken:
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>
);
-};
+}
De SPA moet de URL naar onze API weten via de API_ENDPOINT
-omgevingsvariabele. Stel deze in op de URL van de API-webserver (die in de ..
directory wordt uitgevoerd):
1
$ API_ENDPOINT=`symfony var:export SYMFONY_PROJECT_DEFAULT_ROUTE_URL --dir=..` yarn encore dev
Je kunt dit nu ook op de achtergrond laten lopen:
1
$ API_ENDPOINT=`symfony var:export SYMFONY_PROJECT_DEFAULT_ROUTE_URL --dir=..` symfony run -d --watch=webpack.config.js yarn encore dev --watch
En de toepassing in de browser zou nu goed moeten werken:
Wow! We hebben nu een volledig functionele SPA, met router en echte data. We zouden de Preact app verder kunnen organiseren als we dat willen, maar het werkt nu al geweldig.
De SPA naar productie deployen
Platform.sh maakt het mogelijk om meerdere applicaties per project te deployen. Het toevoegen van een andere toepassing kan gedaan worden door een .platform.app.yaml
bestand aan te maken in een subdirectory. Maak er een aan onder de spa/
directory met de naam spa
:
Bewerk het .platform/routes.yaml
bestand om het spa.
subdomein te routeren naar de spa
applicatie die is opgeslagen in de hoofdmap van het project:
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}/" }
CORS configureren voor de SPA
Als je de code nu deployed, zal deze niet werken omdat een browser het API-verzoek zal blokkeren. We moeten de SPA expliciet toegang geven tot de API. Gebruik de huidige domeinnaam die aan de applicatie gekoppeld is:
1
$ symfony cloud:env:url --pipe --primary
Definieer de CORS_ALLOW_ORIGIN
omgevingsvariabele net zo:
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.#'`$"
Als jouw domein gelijk is aan https://master-5szvwec-hzhac461b3a6o.eu-5.platformsh.site/
, zullen de sed
calls deze omzetten naar https://spa.master-5szvwec-hzhac461b3a6o.eu-5.platformsh.site
.
We moeten ook de API_ENDPOINT
omgevingsvariabele instellen:
1
$ symfony cloud:variable:create --sensitive=1 --level=project -y --name=env:API_ENDPOINT --value=`symfony cloud:env:url --pipe --primary`
Commit en deploy:
1 2 3
$ git add .
$ git commit -a -m'Add the SPA application'
$ symfony cloud:deploy
Bezoek de SPA in een browser door de applicatie als een parameter mee te geven:
1
$ symfony cloud:url -1 --app=spa
Cordova gebruiken voor het bouwen van een smartphone-applicatie
Apache Cordova is een tool die cross-platform smartphone-toepassingen bouwt. En goed nieuws, het kan gebruik maken van de SPA die we zojuist hebben gecreëerd.
Laten we dit installeren:
1 2
$ cd spa
$ yarn global add cordova
Note
Je moet ook de Android SDK installeren. Deze sectie vermeldt alleen Android, maar Cordova werkt met alle mobiele platforms, inclusief iOS.
Maak de mapstructuur van de toepassing aan:
1
$ ~/.yarn/bin/cordova create app
En genereer de Android-applicatie:
1 2 3
$ cd app
$ ~/.yarn/bin/cordova platform add android
$ cd ..
Dat is alles wat je nodig hebt. Je kan nu de productiebestanden bouwen en deze naar Cordova verplaatsen:
1 2 3 4
$ API_ENDPOINT=`symfony var:export SYMFONY_PROJECT_DEFAULT_ROUTE_URL --dir=..` yarn encore production
$ rm -rf app/www
$ mkdir -p app/www
$ cp -R public/ app/www
Start de applicatie op een smartphone of emulator:
1
$ ~/.yarn/bin/cordova run android