
Blog
Hier erfahren Sie alle News und Insights. Wir stellen unser Wissen bereit, geben Tipps und informieren über alles, was intern und extern bei uns passiert.
Hier erfahren Sie alle News und Insights. Wir stellen unser Wissen bereit, geben Tipps und informieren über alles, was intern und extern bei uns passiert.
Die Erfahrung zeigt, dass Kunden und Entwickler bei einem Projekt einen unterschiedlichen Fokus haben (können): Kunden haben oft eher die Webseiteninhalte im Blick und möchten Landingpages erstellen, Impressumsdaten ändern oder ähnliche Inhaltsanpassungen vornehmen. Dagegen habe ich als Entwickler meinen Fokus eher auf den Daten, deren Strukturen und logischen Verbindungen, und wie diese in einem Domain-Model abgebildet werden können.
Genau diese Herausforderung gab es auch in einem Projekt für einen Kunden aus der Reisebranche. Dieser hat bereits ein System zur Pflege von Reisedaten, welches passgenau auf seine Anforderungen zugeschnitten ist. Jetzt möchte er jedoch das System bei der Ausgabe (Webseite) um die Flexibilität eines CMS erweitern, damit individuellere Reise-Angebote erstellt und im Frontend ausgeben werden können.
Das sind allerdings Inhalte, die keiner klaren Linie folgen, die sich nicht in Strukturen packen lassen. Mir stellte sich daher die Frage: Mache ich jetzt noch ein Page-Objekt auf, welches Any-Content aufnehmen kann?
Aber natürlich gibt es für so etwas Content-Management-Systeme. So ein System nachzubauen wäre aber Quatsch, da es inzwischen viele verschiedene Anbieter auf dem Markt gibt, die ausgefeilte Lösungen für diese Problematik bereitstellen - so wie zum Beispiel TYPO3.
Unser Kunde stand also vor folgender Frage: Sollte seine proprietäre Software dahingehend erweitert werden, dass sie auch CMS-Funktionalitäten beinhaltet? Oder sollte er auf ein CMS umsatteln, welches auf die eigenen Bedürfnisse zugeschnitten ist?
Anstatt direkt auf die Datenbank des Reisepflege-Systems zuzugreifen, habe ich mich dafür entschieden, die Daten über eine REST API bereitzustellen.
Warum? Das bringt folgende Vorteile mit sich:
Mit OpenAPI Specification ist eine API in überschaubarer Zeit sauber definiert. Durch Code-Generatoren lassen sich ein API-Server und API-Clients in der präferierten Programmiersprache generieren.
Ich habe mich bei der Auswahl der serverseitigen Sprache für PHP und bei den Clients für PHP und JavaScript entschieden. Dazu habe ich zunächst die vorhandene Datenbankstruktur genutzt, um daraus eine Konfigurationsdatei für OpenAPI zu generieren.
docker-compose exec apache-php bin/console api:openapi:export --spec-version=3 --yaml > openapi.yml
Beim Generieren des Frontends wird eine “lebendige” API-Dokumentation generiert, welche die verfügbaren Endpunkte mit ihren Parametern auflistet. Doch das ist noch nicht alles! In dieser Dokumentation können beispielhafte Anfragen an die API direkt ausgeführt und die Antwort ausgewertet werden.
Diese Dokumentation ist inzwischen auch automatisch in GitLab integriert:
Für wenig dynamische Ausgaben reicht eine serverseitige Generierung der Ausgabe. In den von uns erzeugten TYPO3-Plugins kommt der generierte PHP API-Client zum Einsatz. Durch hinzugefügte Parameter war es uns möglich, dem Redakteur Konfigurations- und Filteroptionen in den Plugins anzubieten. Zum Beispiel kann der Redakteur bei der Ausgabe von Reisen bei den Datensätzen nach Fluss- oder Meereskreuzfahrten filtern.
Die Geschwindigkeit von Reiseportalen ist für Nutzer ein wichtiger Punkt. Sie wollen schnell passende Angebote entdecken und buchen können. Ein ständiges Neuladen der Seite kann da den Buchungsspaß schnell trüben.
Für eine hohe Website-Geschwindigkeit und ein optimales Benutzererlebnis bedurfte es in diesem Projekt einer Business-Logik, die direkt im Browser des Nutzers ausgeführt wird. Frameworks wie Angular, React, Vue.js oder Svelte sind für solche Aufgaben prädestiniert. Durch das two way data binding sind Datenhaltung und deren Abbildung synchron. Bei der erweiterten Suche nach Reisen brachte mir das den Vorteil, dass ich die voneinander abhängigen Filteroptionen immer wieder anpassen konnte, ohne mich dabei um die Synchronität der Daten mit der DOM aktiv kümmern zu müssen.
Die generierte Vue.js-Anwendung habe ich in ein TYPO3-Inhaltselement gepackt. Das hat den Vorteil, dass Konfigurationen an die Vue.js-App durch TYPO3 übergeben werden können. Dies habe ich durch data-Attribute realisiert:
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:f="http://typo3.org/ns/TYPO3/Fluid/ViewHelpers"
xmlns:flux="http://typo3.org/ns/FluidTYPO3/Flux/ViewHelpers"
xmlns:v="http://typo3.org/ns/FluidTYPO3/Vhs/ViewHelpers"
data-namespace-typo3-fluid="true">
<f:layout name="Content" />
<f:section name="Configuration">
<flux:form id="Banner">
<flux:field.select
name="preselectCruiseType"
default="0"
items="{
0: {0: 'ocean', 1: 'ocean'},
1: {0: 'river', 1: 'river'}
}"/>
[…]
</flux:form>
</f:section>
<f:section name="Preview">
[…]
</f:section>
<f:section name="Main">
<f:format.raw>
<script type="text/javascript">
window.addEventListener('load', () => {
let element = document.createElement('script');
element.onload = function() {
window.finder();
}
element.src = '/path/to/vuejsapp/dist/app.js';
document.head.appendChild(element);
});
</script>
</f:format.raw>
<app id="app"
data-action="{f:uri.page(pageUid: settings.page.search)}"
data-type="Banner"
data-url-api="{settings.url.api}"
data-preselect-cruise-type="{preselectCruiseType}">
</app>
</f:section>
</html>
TYPO3 bietet durch GraphicsMagick die Möglichkeit, bequem Bilder zu komprimieren oder zurechtzuschneiden. Dies geschieht serverseitig, also bevor die Seite gerendert und zum Client geschickt wird.
Aber wie sollte mit clientseitigen Anwendungen umgegangen werden, die immer wieder neue Assets anfordern?
Die bisherige Lösung beim besagten Kunden war, all diese Bilder schon vorzurendern und unter bestimmten Pfaden abzulegen. Ich habe mich dagegen für einen Image-Crop-Service entschieden, um flexibler auf Projektanforderungen reagieren zu können. Dabei wird das originale Bild mit Parametern zu Breite, Höhe und Dateiformat an den Service gegeben. Das verkleinerte und optimierte, ggf. auch zugeschnittene Bild kommt als Antwort zurück.
Es gibt dazu einige SaaS-Anbieter und Open-Source-Projekte. Zum Zeitpunkt des Projektstarts war thumbor das Tool unserer Wahl. Ebenfalls empfehlenswert ist imgproxy.net. Vom Prinzip her funktionieren sie aber gleich:
Abschließend liefert der Service als Antwort das generierte Bild im gewünschten Dateiformat zurück.
Optional - aber dringend empfehlenswert - ist es noch zu verhindern, dass der Service durch Fremde verwendet werden kann. Im schlimmsten Fall werden eine zu große Last erzeugt und ggf. Copyrights verletzt. Das wird durch einen SALT erreicht. Damit ist es für unsere Client-Anwendung erlaubt, für bestimmte Bildgrößen zugeschnittene Bilder abzurufen, was die getrennte Entwicklung von Frontend und Backend ermöglicht.
Hier ein Beispiel:
<template>
<picture>
<source :media="cropSettings.sizes.lg.mediaBp" :srcset="`${cropSettings.server}/${cropObject[cropSettings.sizes.lg.pixels]}/${cropSettings.sizes.lg.pixels}/${imagePath}`">
<source :media="cropSettings.sizes.sm.mediaBp" :srcset="`${cropSettings.server}/${cropObject[cropSettings.sizes.sm.pixels]}/${cropSettings.sizes.sm.pixels}/${imagePath}`">
<img
:class="classes"
:id="id"
:alt="alt"
:src="`${cropSettings.server}/${cropObject[cropSettings.sizes[cropSettings.default].pixels]}/${cropSettings.sizes[cropSettings.default].pixels}/${imagePath}`"
>
</picture>
</template>
<script>
import { cropSettings } from '../../data/defaults'
export default {
props: {
id: {
type: String,
required: true
},
classes: {
type: String,
default: ''
},
alt: {
type: String,
required: true
},
imagePath: {
type: String,
required: true
},
cropObject: {
type: Object,
required: true
},
cropServer: {
type: String
}
},
data () {
return {
cropSettings: {
server: this.cropServer, // config from data attribute
sizes: {
lg: { pixels: '600x395', mediaBp: '(max-width: 768px)' },
sm: { pixels: '350x230', mediaBp: '(min-width: 769px)' }
},
default: 'sm'
}
}
}
}
</script>
Daraus wird dann folgendes HTML generiert:
<img src=”https://media.example.de/o_tAX0F0nsTgwUk9YL_T4VxxxxY=/350x230/rps.example.de/files/media_gallery/size_x/Liegestuhl_3_1.jpg” />
Steht ein leistungsfähiger Image-Crop-Service zur Verfügung, könnte dieser auch serverseitig verwendet werden. Hier ist meine erste Idee, den Fluid-ViewHelper f:image so zu erweitern, dass er die externen Bilder mit entsprechenden Parametern nicht an GraphicsMagick, sondern an den Crop-Service schickt.
Der Kunde kann weiterhin die Daten seiner Reisen in seinem Backend pflegen. Mit TYPO3 kann er die Ausgabe der Daten genau steuern und durch nicht-strukturierte Daten ergänzen.
Die gewonnene Flexibilität schlägt sich allerdings in einer höheren Komplexität der Infrastruktur nieder.
Unsere Erkenntnis bei diesem Kundenprojekt: Es muss nicht immer alles gleich neu gemacht werden. Durch OpenAPI ist es ein Leichtes, moderne Anwendungen über Schnittstellen an bestehende Systeme anzubinden. Mit reaktiven Frameworks wie Vue.js ist es möglich, ein besseres Nutzererlebnis zu schaffen als mit serverseitig gerenderten Webseiten.