Skip to content
  • About
    • What is Symfony?
    • Community
    • News
    • Contributing
    • Support
  • Documentation
    • Symfony Docs
    • Symfony Book
    • Screencasts
    • Symfony Bundles
    • Symfony Cloud
    • Training
  • Services
    • Platform.sh for Symfony Best platform to deploy Symfony apps
    • SymfonyInsight Automatic quality checks for your apps
    • Symfony Certification Prove your knowledge and boost your career
    • SensioLabs Professional services to help you with Symfony
    • Blackfire Profile and monitor performance of your apps
  • Other
  • Blog
  • Download
sponsored by
  1. Home
  2. Documentation
  3. Symfony: The Fast Track
  4. German
  5. Aufbau einer SPA

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:

.gitignore
1
2
3
4
5
6
/node_modules/
/public/
/npm-debug.log
/yarn-error.log
# used later by Cordova
/app/

Initialisiere die package.json-Datei (entspricht der composer.json-Datei für JavaScript):

1
$ yarn init -y

Füge nun einige erforderliche Dependencies hinzu:

1
$ yarn add @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:

webpack.config.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
const Encore = require('@symfony/webpack-encore');
const HtmlWebpackPlugin = require('html-webpack-plugin');

Encore
    .setOutputPath('public/')
    .setPublicPath('/')
    .cleanupOutputBeforeBuild()
    .addEntry('app', './src/app.js')
    .enablePreactPreset()
    .enableSingleRuntimeChunk()
    .addPlugin(new HtmlWebpackPlugin({ template: 'src/index.ejs', alwaysWriteToDisk: true }))
;

module.exports = Encore.getWebpackConfig();

Das SPA Haupt-Template erstellen

Zeit, das initiale Template zu erstellen, in der Preact die Anwendung rendern wird:

src/index.ejs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!DOCTYPE html>
<html>
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="msapplication-tap-highlight" content="no" />
    <meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width" />

    <title>Conference Guestbook application</title>
</head>
<body>
    <div id="app"></div>
</body>
</html>

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:

src/app.js
1
2
3
4
5
6
7
8
9
10
11
import {h, render} from 'preact';

function App() {
    return (
        <div>
            Hello world!
        </div>
    )
}

render(<App />, document.getElementById('app'));

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:stop
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 yarn aus:

1
$ yarn 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
$ yarn add preact-router

Erstelle eine Seite für die Homepage (eine Preact-Komponente):

src/pages/home.js
1
2
3
4
5
6
7
import {h} from 'preact';

export default function Home() {
    return (
        <div>Home</div>
    );
};

Und noch eine für die Konferenzseite:

src/pages/conference.js
1
2
3
4
5
6
7
import {h} from 'preact';

export default function Conference() {
    return (
        <div>Conference</div>
    );
};

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
$ yarn 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
$ yarn add 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="/">
+                            &#128217; 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
$ yarn 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 yarn ausführen.

Erstelle eine api.js-Datei, die den Datenabruf aus der API abstrahiert:

src/api/api.js
1
2
3
4
5
6
7
8
9
10
11
function fetchCollection(path) {
    return fetch(ENV_API_ENDPOINT + path).then(resp => resp.json()).then(json => json['hydra:member']);
}

export function findConferences() {
    return fetchCollection('api/conferences');
}

export function findComments(conference) {
    return fetchCollection('api/comments?conference='+conference.id);
}

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=..` yarn 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 yarn encore dev --watch

Die Anwendung sollte nun im Browser einwandfrei funktionieren:

/
/conference/amsterdam-2019

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:

.platform.app.yaml
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
name: spa

size: S

build:
    flavor: none

web:
    commands:
        start: sleep
    locations:
        "/":
            root: "public"
            index:
                - "index.html"
            scripts: false
            expires: 10m

hooks:
    build: |
        set -x -e

        curl -fs https://get.symfony.com/cloud/configurator | bash

        yarn-install
        unset NPM_CONFIG_PREFIX
        export NVM_DIR=${PLATFORM_APP_DIR}/.nvm
        set +x && . "${PLATFORM_APP_DIR}/.nvm/nvm.sh" && set -x
        yarn encore prod

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
$ yarn global add 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
$ ~/.yarn/bin/cordova create app

Und generiere die Android-Applikation:

1
2
3
$ cd app
$ ~/.yarn/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=..` yarn 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
$ ~/.yarn/bin/cordova run android

Weiterführendes

  • Die offizielle Preact Website;
  • Die offizielle Cordova-Website.
Previous page Eine API mit API Platform bereitstellen
Next page Eine Anwendung lokalisieren
This work, including the code samples, is licensed under a Creative Commons BY-NC-SA 4.0 license.
TOC
    Version

    Symfony 5.4 is backed by

    Online Symfony certification, take it now!

    Online Symfony certification, take it now!

    The life jacket for your team and your project

    The life jacket for your team and your project

    Version:
    Locale:
    ebook

    This book is backed by:

    see all backers

    Symfony footer

    Avatar of Simon Sargeant, a Symfony contributor

    Thanks Simon Sargeant for being a Symfony contributor

    1 commit • 8 lines changed

    View all contributors that help us make Symfony

    Become a Symfony contributor

    Be an active part of the community and contribute ideas, code and bug fixes. Both experts and newcomers are welcome.

    Learn how to contribute

    Symfony™ is a trademark of Symfony SAS. All rights reserved.

    • What is Symfony?

      • What is Symfony?
      • Symfony at a Glance
      • Symfony Components
      • Symfony Releases
      • Security Policy
      • Logo & Screenshots
      • Trademark & Licenses
      • symfony1 Legacy
    • Learn Symfony

      • Symfony Docs
      • Symfony Book
      • Reference
      • Bundles
      • Best Practices
      • Training
      • eLearning Platform
      • Certification
    • Screencasts

      • Learn Symfony
      • Learn PHP
      • Learn JavaScript
      • Learn Drupal
      • Learn RESTful APIs
    • Community

      • Symfony Community
      • SymfonyConnect
      • Events & Meetups
      • Projects using Symfony
      • Contributors
      • Symfony Jobs
      • Backers
      • Code of Conduct
      • Downloads Stats
      • Support
    • Blog

      • All Blog Posts
      • A Week of Symfony
      • Case Studies
      • Cloud
      • Community
      • Conferences
      • Diversity
      • Living on the edge
      • Releases
      • Security Advisories
      • Symfony Insight
      • Twig
      • SensioLabs Blog
    • Services

      • SensioLabs services
      • Train developers
      • Manage your project quality
      • Improve your project performance
      • Host Symfony projects

      Powered by

    Follow Symfony