Zur Übersicht

Symfony & Vue.js: der Stack der Zukunft

Michael Scharl
Michael Scharl Aktualisiert am 25. Apr. 2022
Php Magazin - Symfony & Vue.js

Jedes Webprojekt ist einzigartig: in Design, Funktionalität, User Experience und Gamification. Die Anforderungen an diese Punkte haben sich in den letzten Jahren stark verändert. Bei jedem Projekt werden andere Prioritäten gesetzt und es ist nicht einfach, sich für das richtige Set-up zu entscheiden. Abgesehen von den nahezu unendlichen Möglichkeiten wird gefühlt jeden Tag ein neues Tool oder Framework vorgestellt, das verspricht, die Welt zu verändern.

Diese Umstände sorgen oft dafür, dass man beim Set-up etwas Neues ausprobiert. Das behalte ich mir (aus Erfahrung) lieber für kleine Nebenprojekte auf. Damit ich bei wichtigen Projekten nicht Samstagmittag einen Hofix einspielen muss (auch hier spreche ich leider aus Erfahrung), habe ich viel Zeit und Aufwand in ein grundlegendes Set-up investiert, welches es mir erlaubt, ohne viel Initialisierungs-Overhead mit der Entwicklung zu starten. Gleichzeitig bleibt dabei trotzdem die Flexibilität hoch genug, um jede Website oder Web-App individuell aufbauen zu können. Ich habe mich dabei für zwei starke Frameworks entschieden, die beide in ihrem Segment Grossartiges leisten und meiner Meinung nach sehr gute Partner sind, wenn man sie richtig verwendet: Symfony und Vue.js.

Deshalb Symfony

Symfony selbst hat dazu sechs gute Gründe verfasst, warum man sich für das Framework entscheiden sollte. Für mich persönlich zählt vor allem, dass es ein unvoreingenommenes Framework ist, das mir erlaubt, das zu bauen, was ich möchte und wie ich es möchte. Man bekommt mit Symfony eine leere Vorlage, die mächtige Features und starke Helferlein enthält. Die kann man ohne Aufwand einfach verwenden, wenn man sie braucht. Es gibt nahezu für jeden Anwendungsfall ein Bundle, welches bereits alles kann, was man braucht. So muss man “nur” noch die Business-Logik bauen und alles zusammenführen, um zum gewünschten Ergebnis zu kommen.

Darum Vue.js

Im Gegensatz zu vielen anderen sehr populären JS-Frameworks, lässt sich Vue.js sehr einfach auch in bestehende Projekte integrieren. Vue bietet zum Beispiel die Möglichkeit, mit “Custom Directives” die Funktionalität von HTML-Elementen zu erweitern, ohne dazu eine ganze Komponente schreiben zu müssen. Eine meiner Lieblingsstärken von Vue ist aber, dass man Templates, die als HTML vom Server gerendert wurden, on-demand kompilieren und verwenden kann. Das erlaubt einen sehr flexiblen Ansatz an Funktionalität, die durch Javascript erweitert wird. Gleichzeitig erfordert es nicht zwingend, den Inhalt anzuzeigen. Ausserdem lassen sich auch grosse Single-Page-Applications bauen, wenn erforderlich. Vue liefert dafür den Router und das Store-Management als optionale Core-Erweiterung dazu.

Der Kiel des Eisbergs

Leider ist die Auswahl der Frameworks meist nur die Spitze des offensichtlichen Aufwandes. Die Zusammenführung der dazugehörigen sowie meist notwendigen Build-Tools ist oftmals abschreckend und überwältigend, wenn man sich nicht regelmässig damit auseinandersetzt. Dazu zählen vor allem der Aufbau der Dateistruktur, des Frontend-Source-Codes und der Assets, sowie die Auswahl und Notwendigkeitsabschätzung diverser Prä- und Postprozessoren.

Zuallererst gehört geklärt, wo nun die Dateien abgelegt und geordnet werden, denn im public-Verzeichnis haben Source-Files natürlich nichts zu suchen. Deshalb gibt es in meinen Projekten im Root immer einen assets-Ordner, in dem die Sass (scss), TypeScript und Bilddateien abgelegt werden. Das Ganze könnte dann etwa folgendermassen aussehen:

├── assets
│   ├── images
│   │   └── logo.svg
│   ├── scss
│   │   ├── _atoms
│   │   ├── _blocks
│   │   ├── _components
│   │   └── main.scss
│   └── typescript
│       ├── components
│       ├── services
│       ├── store
│       ├── utility
│       └── main.ts
├── bin
├── config
├── public
├── src
├── …

Damit liegen alle relevanten Dateien, egal ob Frontend oder Backend, schön beieinander und fügen sich nahtlos in die vorhandene Symfony-Struktur ein. Für mich ist dabei sehr wichtig, dass ich nicht im Symfony src nach irgendwelchen Style- oder Scriptdateien suchen muss, wie ich es leider schon bei vielen – hauptsächlich älteren – Projekten gesehen habe.

Als nächstes geht es darum, die Asstes für den Browser aufzubereiten. Dafür gibt es allerhand Build-Tools, wobei jedes natürlich seine Vor- und Nachteile hat. Ich verwende dazu Webpack. Es begleitet mich jetzt schon seit einigen Jahren und ich hatte bisher noch keinen Fall, bei dem ich ein anderes Tool gebraucht hätte. Ausserdem kenne ich mich damit gut genug aus, dass ich es bedenkenlos in grossen und langfristigen Projekten einsetzen kann. Niemand hat Spass dabei, wenn man in solch einem Projekt zu experimentieren beginnt und dann etwas schiefläuft.

Genauso wie das NPM package.json kommt die Datei für die Webpack-Konfiguration in das root-Verzeichnis des Projekts. Denn nach diesen Dateien möchte man erfahrungsgemäss nicht suchen müssen. Der Code für die Konfiguration von Webpack kann auf den ersten Blick etwas abschreckend wirken und hat ein bisschen was von “schwarzer Magie”. Aber im Grunde ist es relativ einfach.

Damit Webpack weiss, welche Dateien kompiliert werden sollen, wird unter entry ein Array mit der main.js und main.scss Datei aus der oben angeführten Dateistruktur befüllt. Im output wird der path nach public definiert und der Dateiname mit dem Template assets/[name].[chunkhash].js befüllt. Dasselbe Template, abgesehen von der Javascript-Dateiendung, wird auch für den Dateinamen im mini-css-extract-plugin verwendet. Der Chunkhash stellt dabei sicher, dass der Browser keine veralteten zwischengespeicherten Dateien verwendet.

Da beim Kompilieren der Dateien immer ein neuer Hash entsteht, ist ein statisches Verlinken im Twig/HTML natürlich nicht möglich. Dabei helfen uns Symfony und das webpack-manifest-plugin. Das Plugin erstellt eine manifest.json, Datei, die den original Dateinamen auf den generierten Namen mappt. In den Plugin-Optionen stellen wir noch den fileName auf assets/manifest.json, damit die Datei auch bei den dazugehörigen Assets liegt. Anschliessend wird der Pfad zur Manifest-Datei in den Symfony-Einstellungen unter framework.assets.json_manifest_path mit '%kernel.project_dir%/public/assets/manifest.json' hinterlegt. Dadurch kann Symfony automatisch alle Dateireferenzen, die mit dem asset-Twig-Helper gesetzt werden (z.B. {{ asset('main.js') }}), auf die kompilierten Dateien in unserem public Verzeichnis mappen.

Ein kleiner Tipp am Rande: Symfony unterstützt mit dem preload-Twig-Helper den Link-Header, der von kompatiblen Webservern verwendet wird, um wichtige assets mit HTTP/2 an den Client zu pushen – und zwar noch bevor dieser den Header oder das Markup eingelesen hat und weiss, was noch alles geladen werden muss. Voraussetzung dafür ist allerdings eine verschlüsselte Verbindung, die – wie ich finde – mittlerweile auch lokal verwendet werden sollte.

Um Sass (mit der scss-Syntax) zu verwenden, wird eine Regel für alle *.scss Dateien erstellt. Der sass-loader und node-sass kümmern sich dann darum, dass die Dateien in css umgewandelt werden. Bevor der css-loader die Daten an das mini-css-extract-plugin weitergibt, kümmert sich der postcss-loader darum, dass das CSS durch den autoprefixer alle noch notwendigen vendor-prefixes erhält und mit cssnano minimiert wird. Wenn alle Loader die sourceMap Option aktiviert haben, lassen sich alle kompilierten Zeilen schön in die Quelldateien zurückverfolgen (selbst bis in die Vue Single-File-Components)

Damit der babel-loader sich um die Generierung von Browser-Kompatiblem Javascript kümmern kann, muss auch der Typescript-Compiler (typescript) installiert und mit der tsconfig.json konfiguriert sein. Die JSON-Datei kann initial mit tsc --init erstellt werden und enthält praktischerweise bereits alle empfohlenen Einstellungen sowie allen weiteren Optionen inklusive einer kurzen Beschreibung. Ich empfehle ausserdem, in der ersten Ebene das exclude Array hinzuzufügen und die Ordner node_modules sowie vendor zu exkludieren, um beim Kompilieren unnötige Fehler zu vermeiden.

Spätestens, wenn ein Hintergrundbild über CSS referenziert wird, wird sich Webpack beschweren, da es nicht mit einer Bilddatei umzugehen weiss. Um das zu vermeiden, gibt es den file-loader, der einfach alle gängigen Web-Bild-Typen in Empfang nimmt. Die emitFile Option wird deaktiviert und der context wird mit assets hinterlegt. Der name bekommt das Template '[path][name].[hash].[ext]'. Dieses Template verwendet auch das copy-webpack-plugin mit der Option to und toType: ‘template’, um alle Bilddateien zu kopieren. So sind sie damit sowohl im CSS als auch im Twig/HTML verwendbar. So wie bei JS und CSS stellen wir mit dem Hash sicher, dass der Browser bei einer Dateiänderung keine zwischengespeicherte Version verwenden kann.

Um die Single-File-Components von Vue zu verwenden – was ich absolut empfehle – gibt es ein paar weitere kleine Anpassungen. Zuerst muss der vue-loader für alle *.vue Dateien konfiguriert werden und das vue-template-compiler package installiert sein. Zusätzlich benötigt der loader das VueLoaderPlugin, welches Teil des loader packages ist, um richtig zu funktionieren. Weiters muss in den resolve Einstellungen der alias 'vue$': 'vue/dist/vue.esm.js' hinterlegt und der Typescript Loader um die Option appendTsSuffixTo: [/\.vue$/] erweitert werden. Damit Typescript auch die Single-File-Components von Vue importieren kann, muss ein kleines Typescript-Declaration-File angelegt werden.

declare module "*.vue" {
    import Vue from "vue";
    export default Vue;
}
single-file-components.d.ts

Zugegeben: Das klingt etwas erschlagend. Die diversen Loader und Plugins sind aber alle sehr gut dokumentiert und es lassen sich alle hier angeführten Punkte dort finden. Damit ist dann alles vorbereitet, um im Frontend richtig durchstarten zu können.

Teilen der Konfiguration

Mit “Teilen” ist nicht gemeint, dass diverse Werte, die im Frontend gebraucht werden, woanders hinterlegt sind als die Werte fürs Backend. Ganz im Gegenteil. Es sollte nur einen Punkt geben, an dem die Applikation konfiguriert wird. Das ist meiner Meinung nach Symfony. Das Framework bietet uns schon alle notwendigen Tools und Einstiegspunkte und kann auch Environment-abhängig verschiedene Werte annehmen. Mit Teilen ist vielmehr gemeint, dass man Konfigurationen, die man unter Umständen auf beiden Enden des Systems braucht, verwenden kann, ohne sie an zwei Punkten hinterlegen und warten zu müssen. Ein gängiges Beispiel sind Social Media App IDs: Benutzer müssen sich im Frontend einloggen, um im Backend Daten abrufen und speichern zu können. Oder ein SaaS API Key, um Daten anzuzeigen oder abfragen durchzuführen. Für gewöhnlich finden sich solche Konfigurationen bereits unter den Symfony-Parameters.

Um die Werte schlussendlich ins Frontend zu übergeben, bevorzuge ich einen Script-Tag im Master-Layout, wenn es etwas Seitenweites ist oder in dem Template, in dem es benötigt wird, wenn es sich um sehr spezifische Daten handelt. Darin wird dann eine const Variable, die bereits in allen aktuellen Browsern nativ unterstützt wird (ja, sogar im Internet Explorer 11 falls man nicht aktuelle Browser unterstützen muss), angelegt und mithilfe von Twig wird das Javascript-Objekt erzeugt. Das Objekt kann dann von einem Store oder Service eingelesen und weiterverarbeitet werden, oder man verwendet es einfach dort, wo es benötigt wird.

Mit einem Twig-Helper, der hilft, Parameter auszulesen, könnte das dann in etwa so aussehen.

<?php

namespace App\Services;

use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use Twig\Extension\AbstractExtension;
use Twig\TwigFunction;

class TwigExtension extends AbstractExtension
{
    /**
     * @var ParameterBagInterface
     */
    private $parameterBag;

    public function __construct(ParameterBagInterface $parameterBag)
    {
        $this->parameterBag = $parameterBag;
    }

    public function getFunctions()
    {
        return [
            new TwigFunction('parameter', function ($key) {
                return $this->parameterBag->get($key);
            }),
        ];
    }
}
App\Services\TwigExtension
<script>
    const __CONFIG__ = {{ {
        facebook: {
            app_id: parameter('facebook.app_id'),
            api_version: parameter('facebook.api_version'),
        },
        google: {
            maps_key: parameter('google.maps_key'),
        }
    }|json_encode|raw }};
</script>
Übergabe im Twig

Übersetzungen im Frontend

Die meisten Textausgaben erfolgen vermutlich statisch über Twig. Hier können wie gewohnt Symfony Translations und die dazugehörige Twig-Funktion verwendet werden. Selbst wenn man initial nicht vorhat, mehr als eine Sprache anzubieten, bin ich der Meinung, dass Texte mit Übersetzungen ausgegeben werden sollten. Es hält die Templates kompakter und aufgeräumter. Auch Textänderungen lassen sich einfacher und übersichtlicher durchführen. Ausserdem ist es eine Qual, Mehrsprachigkeit zu integrieren, wenn man es nicht von Anfang an vorgesehen hat.

Damit auch bei den Übersetzungen, so wie schon bei der Config, nicht an zwei Stellen Texte verwaltet werden muss, liegt die Datenhoheit auch hier wieder bei Symfony. Solange man nur die ein oder andere Übersetzung in ein paar Komponenten benötigt, könnte man einen ähnlich Ansatz wie bei der Übergabe der Konfiguration wählen. Sollte man mehrere Texte benötigen oder auch Platzhalter bzw. unterschiedliche Schreibweisen bei Einzahl oder Mehrzahl, kann das BazingaJsTranslationBundle verwendet werden. Es ist sehr einfach zu konfigurieren, gut dokumentiert und bringt all die tollen Funktionen, die einem mit dem Twig-Translation-Helper zur Verfügung stehen, ins Frontend. Derzeit setze ich sehr erfolgreich auf die Variante, die YAML-Translation Files mit Webpack direkt in das Javascript kompiliert. Diese sind dort in einer Objekt-Struktur, wie man es erwarten würde, verfügbar und können mit Frameworks wie zum Beispiel i18next mit nur wenig Aufwand verwendet werden. Das funktioniert auch mit dem von Symfony unterstütztem ICU Format und i18next-icu absolut problemlos.

Vorbefüllung des Stores

Die Möglichkeiten für die Befüllung des Stores sind stark davon abhängig, was das eigentliche Ziel ist. Abhängig von der Menge, Wichtigkeit und Aktualisierungsfrequenz der Daten nutze ich verschiedene Ansätze, um Daten in die Stores zu laden. Natürlich ist nichts schneller als wenn Symfony das fertige HTML sofort ausliefert. Das ist absolut richtig und ich würde es immer bevorzugen. Sollten diese Daten dann aber zum Beispiel im Frontend gefiltert werden können, ist es oftmals notwendig, den kompletten Datensatz zur Verfügung zu haben.

Für kleine Datenmengen, wie zum Beispiel Benutzerinformationen, nutze ich die gleiche Technik wie bei den Konfigurationen. Es ist die einfachste und effektivste Möglichkeit, Daten vom Backend an die Frontend-Logik zu übergeben. Für grössere Datenmengen greife ich aber auf asynchrone JSON-Requests zurück. Das hält die initiale Ladezeit gering und bietet dem User ein schnelles Erlebnis. Falls es sich dabei um Daten handelt, die sofort angezeigt werden sollen, können dann zumindest so viele Datensätze, wie nötig sind, um einen ganzen Bildschirm zu füllen, vom Backend im Vorhinein gerendert werden. Dabei kann man auf die Vue-Funktionalität zurückgreifen und On-Demand Templates verwenden. Damit lässt sich dasselbe Template für Twig und Vue verwenden und man spart sich eine Menge doppelten Code. Sobald der Request im Frontend die vollständigen Daten erhält, werden diese von der entsprechenden Vue-Komponente ersetzt.

Daten, die sich nicht oft aktualisieren und schnell im Frontend zur Verfügung stehen müssen, könnten beim ersten Besuch im LocalStorage gespeichert werden. Damit spart man sich zukünftige Requests und braucht nur prüfen, ob die Daten aktualisiert werden müssen.

Fazit

Dieses Set-up verwende ich nun seit einiger Zeit und bin jedes Mal wieder aufgeregt, wenn ich einem neuen Projekt damit Leben einhauchen kann. Allerdings hat es sich im Laufe der Zeit auch oft verändert und weiterentwickelt – und das wird es auch in Zukunft tun. Fürs Erste denke ich, dass man mit Symfony einen sehr standfesten Partner im Backend hat und mit Vue.js einen ebenso stabilen und treuen Begleiter im Frontend. Beide Frameworks erfreuen sich grosser Beliebtheit und werden in vielen Grossprojekten für verschiedenste Anwendungen verwendet. Ich kann nur jedem empfehlen, es einmal auszuprobieren – wenn man es noch nicht hat – und für sich selbst zu entscheiden, ob ein Set-up wie dieses für einen funktionieren kann.

MASSIVE-ART_Michael_Scharl_640x640_circle
Michael Scharl
Technischer Projektmanager & Senior Web Developer
Michael hat als Senior Web Developer bei MASSIVE ART begonnen und ist jetzt technischer Projektmanager. Er ist Experte für Frontend-Entwicklung und dem dazugehörigen Toolset. Michael ist der Meister der Automatisierung, denn er mag es nicht, wenn er immer dieselben Abläufe wiederholen muss und vereinfacht so nicht nur seinen Alltag, sondern auch den seiner Kollegen. Nach Feierabend geht Michael zum Kampfsport oder raus in die Natur – Hauptsache es kommt keine Langeweile auf!