Aufbau einer SPA
Die meisten Kommentare werden während der Konferenz eingereicht, wo einige Leute keinen Laptop mitbringen. Wahrscheinlich haben sie jedoch ein Smartphone. Wie wäre es mit der Erstellung einer mobilen App, um schnell die Kommentare zur Konferenz lesen zu können?
Eine Möglichkeit, eine solche mobile Anwendung zu erstellen, ist die Erstellung einer Javascript Single Page Application (SPA). Eine SPA läuft lokal, kann den Local-Storage verwenden, kann eine Remote-HTTP-API aufrufen und Service-Worker nutzen, um eine nahezu native Erfahrung zu schaffen.
Die Anwendung erstellen
Um die mobile Anwendung zu erstellen, werden wir Preact und Symfony Encore verwenden. Preact ist eine kleine und effiziente Basis, die sich gut für die Gästebuch SPA eignet.
Um sowohl die Website als auch die SPA konsistent zu machen, werden wir die Sass-Stylesheets der Website für die mobile Anwendung wiederverwenden.
Erstelle die SPA-Anwendung unterhalb des spa
-Verzeichnisses und kopiere die Stylesheets der Website:
1 2 3
$ mkdir -p spa/src spa/public spa/assets/styles
$ cp assets/styles/*.scss spa/assets/styles/
$ cd spa
Note
Wir haben ein public
-Verzeichnis erstellt, da wir hauptsächlich über einen Browser mit der SPA interagieren werden. Wir hätten es build
nennen können, wenn wir lediglich eine mobile Anwendung entwickeln wollten.
Füge sicherheitshalber eine .gitignore
-Datei hinzu:
Initialisiere die package.json
-Datei (entspricht der composer.json
-Datei für JavaScript):
1
$ npm init -y
Füge nun einige erforderliche Dependencies hinzu:
1
$ npm install @symfony/webpack-encore @babel/core @babel/preset-env babel-preset-preact preact html-webpack-plugin bootstrap
Der letzte Konfigurationsschritt besteht darin, die Webpack Encore-Konfiguration zu erstellen:
Das SPA Haupt-Template erstellen
Zeit, das initiale Template zu erstellen, in der Preact die Anwendung rendern wird:
Der <div>
-Tag ist der Ort, an dem die Anwendung per JavaScript dargestellt wird. Hier ist die erste Version des Codes, der die "Hello World"-Ansicht darstellt:
Die letzte Zeile registriert die App()
-Funktion auf dem #app
-Element der HTML-Seite.
Jetzt ist alles bereit!
Eine SPA im Browser ausführen
Da diese Anwendung unabhängig von der Haupt-Website ist, müssen wir einen anderen Webserver betreiben:
1
$ symfony server:start -d --passthru=index.html
Das --passthru
-Flag weist den Webserver an, alle HTTP-Requests an die public/index.html
-Datei zu übergeben (public/
ist das Standard-Web-Root-Verzeichnis des Webservers). Diese Seite wird von der Preact-Anwendung verwaltet und ermittelt die zu rendernde Seite über den Pfad im Browser.
Um die CSS und die JavaScript-Dateien zu kompilieren, führe npm
aus:
1
$ ./node_modules/.bin/encore dev
Öffne die SPA in einem Browser:
1
$ symfony open:local
Und schau Dir unsere Hallo-Welt SPA an:
Einen Router zur Behandlung von Zuständen hinzufügen
Die SPA ist derzeit nicht in der Lage, verschiedene Seiten zu verarbeiten. Um mehrere Seiten zu implementieren, benötigen wir einen Router, wie bei Symfony. Wir werden den preact-router verwenden. Er nimmt eine URL als Input und ordnet sie einer Preact-Komponente zu, die angezeigt werden soll.
Installiere den Preact-Router:
1
$ npm install preact-router
Erstelle eine Seite für die Homepage (eine Preact-Komponente):
Und noch eine für die Konferenzseite:
Ersetze das "Hello World"-div
mit der Router
-Komponente:
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>
)
}
Erstelle die Anwendung neu:
1
$ ./node_modules/.bin/encore dev
Wenn Du die Anwendung im Browser aktualisierst, kannst Du nun auf die Links "Home" und "Konferenz" klicken. Du siehst, dass die Browser-URL und die Vor- und Zurück-Buttons Deines Browsers so funktionieren, wie Du es erwarten würdest.
Die SPA gestalten
Lass uns den Sass-Loader zur Website hinzufügen:
1
$ npm install node-sass sass-loader
Aktiviere den Sass-Loader in Webpack und füge eine Referenz auf das Stylesheet hinzu:
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 }))
;
Wir können nun die Anwendung aktualisieren, um die Stylesheets zu verwenden:
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>
Erstelle die Anwendung noch einmal neu:
1
$ ./node_modules/.bin/encore dev
Du kommst nun in den Genuss einer komplett gestylten SPA:
Daten aus der API holen
Die Preact-Anwendungsstruktur ist nun fertig: Der Preact-Router verarbeitet die Seitenzustände – einschließlich des Platzhalters für den Konferenz-Slug – und das Stylesheet der Hauptanwendung wird zur Gestaltung des SPA verwendet.
Um die SPA dynamisch zu machen, müssen wir die Daten mittels HTTP-Requests aus der API holen.
Konfiguriere Webpack, um die Environment-Variable für die API-Endpunkte zu definieren:
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();
Die Environment-Variable API_ENDPOINT
sollte auf den Webserver der Website zeigen, wo wir unter /api
den API-Endpunkt haben. Wir werden sie ordnungsgemäß konfigurieren, sobald wir npm
ausführen.
Erstelle eine api.js
-Datei, die den Datenabruf aus der API abstrahiert:
Du kannst nun die Header- und Home-Komponenten anpassen:
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>
);
-};
+}
Schließlich übergibt der Preact Router den Platzhalter "slug" als Eigenschaft an die Konferenz-Komponente. Verwende ihn um die richtige Konferenz und ihre Kommentare darzustellen, wobei du wieder die API nutzt; passe außerdem das Rendering an, um die API-Daten zu verwenden:
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>
);
-};
+}
Die SPA muss nun die URL zu unserer API über die Environment-Variable API_ENDPOINT
kennen. Setze sie auf die API-Webserver-URL (die im ..
-Verzeichnis läuft):
1
$ API_ENDPOINT=`symfony var:export SYMFONY_PROJECT_DEFAULT_ROUTE_URL --dir=..` ./node_modules/.bin/encore dev
Du könntest es jetzt auch im Hintergrund laufen lassen:
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
Die Anwendung sollte nun im Browser einwandfrei funktionieren:
Wow! Wir haben jetzt eine voll funktionsfähige SPA mit Router und echten Daten. Wir könnten die Preact-App weiter organisieren, wenn wir wollen, aber sie funktioniert bereits hervorragend.
Die SPA zum Produktivsystem deployen
Platform.sh ermöglicht es, mehrere Anwendungen pro Projekt zu deployen. Das Hinzufügen einer weiteren Anwendung kann durch Erstellen einer .platform.app.yaml
-Datei in einem beliebigen Unterverzeichnis erfolgen. Erstelle eine unter spa/
namens spa
:
Bearbeite die .platform/routes.yaml
-Datei, um die spa.
-Subdomain an die im Projekt-Stammverzeichnis gespeicherte spa
-Anwendung weiterzuleiten:
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 für die SPA konfigurieren
Wenn Du den Code jetzt deployst, funktioniert er nicht, da ein Browser die API-Anfrage blockieren würde. Wir müssen der SPA explizit den Zugriff auf die API erlauben. Hole Dir den aktuellen Domainnamen, der mit Deiner Anwendung verknüpft ist:
1
$ symfony cloud:env:url --pipe --primary
Definiere die Environment-Variable CORS_ALLOW_ORIGIN
entsprechend:
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.#'`$"
Wenn Deine Domain https://master-5szvwec-hzhac461b3a6o.eu-5.platformsh.site/
ist, wird sie durch diesed
-Aufrufe zu https://spa.master-5szvwec-hzhac461b3a6o.eu-5.platformsh.site
umgewandelt.
Wir müssen auch die Environment-Variable API_ENDPOINT
setzen:
1
$ symfony cloud:variable:create --sensitive=1 --level=project -y --name=env:API_ENDPOINT --value=`symfony cloud:env:url --pipe --primary`
Committe und Deploye:
1 2 3
$ git add .
$ git commit -a -m'Add the SPA application'
$ symfony cloud:deploy
Greife in einem Browser auf die SPA zu, indem Du die Anwendung als Flag angibst:
1
$ symfony cloud:url -1 --app=spa
Eine Smartphone-Anwendung mit Cordova erstellen
Apache Cordova ist ein Tool, das plattformübergreifende Smartphone-Anwendungen erstellt. Eine gute Nachricht, es kann die SPA nutzen, die wir gerade erstellt haben.
Lass es uns installieren:
1 2
$ cd spa
$ npm install cordova
Note
Du musst auch das Android SDK installieren. Dieser Abschnitt erwähnt nur Android, aber Cordova funktioniert mit allen mobilen Plattformen, einschließlich iOS.
Erstelle die Verzeichnisstruktur der Anwendung:
1
$ ./node_modules/.bin/cordova create app
Und generiere die Android-Applikation:
1 2 3
$ cd app
$ ~/.npm/bin/cordova platform add android
$ cd ..
Das ist alles, was Du brauchst. Du kannst nun die Produktivdateien erstellen und zu Cordova verschieben:
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
Führe die Anwendung auf einem Smartphone oder einem Emulator aus:
1
$ ./node_modules/.bin/cordova run android