Einblicke in den Frontend-Build-Prozess bei myposter
Update 12.04.2019: In der Frontend-Entwicklung gibt es keinen Stillstand und so ist das auch bei uns. Was gestern noch Best Practise war, ist morgen schon wieder veraltet.
In den letzten Jahren ist auch unsere Anwendung gewachsen: wir haben von einen Monolithen auf einen Multi-Repository-Ansatz umgestellt und verwenden Webpack für unsere FE-seitigen Build-Prozesse.
Web-Applikationen, vor allem große E-Commerce-Anwendungen wie bei myposter, werden immer komplexer. Damit steigen auch die Anforderungen an den Frontend-seitigen Entwicklungs- und Build-Prozess. Abhängigkeiten müssen verwaltet werden und immer wiederkehrende Aufgaben sollten automatisiert werden. JavaScript-Code befindet sich nicht mehr in einer monolithischen Datei, sondern in vielen kleinen Modulen, die untereinander Abhängigkeiten besitzen und für die Ausführung im Browser zusammengefasst werden müssen. Statt einfachem CSS werden Präprozessoren wie Sass und less eingesetzt und JavaScript wird immer öfter in ECMAScript2015 (ES6) geschrieben. Diese Technologien werden jedoch noch nicht von allen Browsern unterstützt, weshalb Code heutzutage oftmals aufbereitet, gebundelt und transpiliert werden muss, bevor er im Live-Betrieb eingesetzt werden kann. Kurz gesagt: Die Zeiten, in denen zur Frontend-Entwicklung ein Texteditor ausgereicht hat, sind schon lange vorbei.
Zur Automatisierung und Optimierung dieser Aufgaben steht uns jedoch eine Vielzahl an Tools zur Verfügung, die wir auch bei myposter einsetzen, um den wachsenden Anforderungen in der Web-Entwicklung gerecht zu werden. Dieser Blog-Artikel gibt einen kurzen Überblick über den Frontend-Build-Prozess bei myposter.
Unser Setup im Überblick:
- Package Management: NPM
- Task Runner: Gulp
- Module Bundler: Browserify
- CSS-Präprozessor: Sass
- ES6-zu-ES5-Transpilation: Babel
- Error-Logging: Rollbar
Frontend: genereller Aufbau und verwendete Technologien
Bevor wir zu den eingesetzten Tools und dem eigentlichen Build-Prozess kommen, möchte ich einen kurzen Überblick über den generellen Aufbau im Frontend geben. Die Verzeichnisstruktur ist exemplarisch in der folgenden Abbildung dargestellt. Wie hier zu sehen ist, unterteilt sich die Codebasis im Frontend in die vier Bereiche
- admin (CMS),
- common (bereichsübergreifender Code),
- production (Steuerung der Produktion) und
- web (Online-Shop).
Diese Unterscheidung spiegelt sich auch in den Gulp-Tasks wieder, die wiederum in Development- und Release-Tasks unterteilt sind.
frontend ├── admin │ ├── css │ ├── img │ └── js ├── common │ └── js ├── production │ ├── css │ ├── img │ └── js ├── web │ ├── css │ │ ├── modules │ │ ├── util │ │ ├── vendor │ │ ├── configurator.scss │ │ ├── customer.scss │ │ └── default.scss │ ├── img │ └── js │ ├── lib │ ├── modules │ ├── util │ ├── vendor │ ├── configurator.entry.js │ ├── customer.entry.js │ └── default.entry.js ├── gulp-tasks │ ├── dev │ └── release ├── gulpfile.js └── package.json |
Für den Bereich „web“ sind beispielhaft einige unserer Einstiegspunkte aufgelistet. Jeder Bereich der Webseite, wie der Konfigurator oder das Kundenkonto, haben eigene CSS- und JavaScript-Einstiegspunkte, die direkt in den jeweiligen CSS- und JavaScript-Ordnern liegen.
Wir verwenden die CSS-Erweiterung Sass (Syntactically Awesome Stylesheets), genauer gesagt die SCSS-Syntax (Sassy CSS), weshalb die .scss-Quelldateien erst zu CSS kompiliert werden müssen, bevor sie in die Webseite eingebunden werden können. Zur Strukturierung des CSS verwenden wir die Methodologie BEM (kurz für Block Element Modifier), welche rein auf Klassen-Selektoren basiert.
CSS-Einstiegspunkte enthalten ausschließlich @import-Statements, wobei alle für einen bestimmten Bereich notwendigen CSS-Dateien, sowohl modulspezifische, als auch wiederverwendbare Komponenten, importiert werden. Der Vorteil bei der Verwendung der Sass @import-Regel ist, dass diese nicht erst im Browser aufgelöst werden. Dadurch werden die einzelnen CSS-Dateien bereits während dem Build-Prozess zu einer zusammengefasst.
Die JavaScript-Einstiegspunkte importieren und initialisieren alle benötigten Module und dienen als Input für Browerify, den von uns verwendeten Module Bundler. Da wir die JavaScript-Version ES6 benutzen, wird der Code außerdem mit Hilfe von Babel transpiliert.
Abhängigkeitsverwaltung und Task-Automatisierung mit NPM und Gulp
Gulp. Gulp ist ein JavaScript Task Runner, der Node.js-Streams zur Dateimanipulation nutzt. Für die einzelnen Aufgaben, die im Entwicklungs- und Build-Prozess anstehen, nutzen wir diverse Gulp-Plugins. Die package.json-Datei listet alle benötigten Abhängigkeiten auf, die wir mit dem Node Package Manager (NPM) verwalten.
Wie bereits angesprochen nutzen wir in der Entwicklung und im Release unterschiedliche Gulp-Tasks. Die eigentliche Task-Logik ist dabei in eigene Module ausgelagert, die im Gulpfile nur noch geladen werden. Das Gulpfile kümmert sich zudem um die Verkettung von Tasks nach Bereichen und Anwendungsbereichen (Development und Release). Demnach ergibt sich folgende Struktur bei der Benennung unserer Gulp-Tasks:
[[<type>:]<group>:]<scope> |
Dabei ist <scope> entweder „dev“ oder „release“, <group> bezieht sich auf die vier unterschiedlichen Bereiche und <type> spezifiziert die eigentliche Aufgabe (zum Beispiel „js“, „css“ oder „qa“). Entwicklungs- und Release-Tasks unterscheiden sich zudem in den folgenden zwei Punkten:
- Tasks im Entwicklungsprozess verwenden Beobachter (gulp.watch), um Änderungen an Dateien zu erkennen und entsprechende Tasks erneut auszuführen.
- Jedem Task im Releaseprozess ist ein „clean“-Task vorangestellt. Dieser löscht vor jedem neuen Build alle Dateien im Zielverzeichnis.
In der folgenden Abbildung ist als Beispiel unserer CSS-Task dargestellt. Dieser nutzt die Gulp-Plugins „gulp-filter„, „gulp-sass„, „gulp-autoprefixer“ und „gulp-cssnano„. Mit Hilfe dieser Plugins werden zuerst SCSS-Dateien in CSS umgewandelt, Vendor-Präfixe ergänzt und Dateien minimiert (nur im Release-Task). Diese Logik befindet sich in einem eigenen Modul und wird im Gulpfile mit Angabe der relevanten Quell- und Zielpfade geladen. Das heißt, dieser Task existiert in allen acht möglichen css:<group>:<scope>-Verkettungen.
Weitere Tasks umfassen:
- Konkatenation und Minimierung von Vendor-Dateien,
- Optimierung und Versionierung von Bildern (jeweils eigene Tasks),
- Linting mit jshint,
- Bundling und Transpilation von JavaScript.
Module Bundling mit Browserify
Für die Zusammenfassung der JavaScript-Dateien zu mehreren Bundles setzen wir den Module Bundler Browserify ein, ebenfalls als Plugin für einen Gulp-Task. Dabei wird für jeden Einstiegspunkt ein separates Bundle erstellt, welches dann vom Browser geladen werden kann. Zudem werden ES6-Module, die in mehreren Einstiegspunkten vorkommen, in ein eigenes Bundle ausgeklammert, um denselben Code nicht mehrmals übertragen zu müssen. Während des Bundling-Prozesses findet auch die Transpilation von ES6 zu ES5 statt, wozu wir den JavaScript-Compiler Babel einsetzen. Der Workflow dieses Gulp-Tasks ist vereinfacht in der nachfolgenden Abbildung dargestellt.
Da die mit Browserify erstellten Bundles von außen nicht zugänglich sind, erstellen wir zusätzlich ein Standalone-Bundle, welches dem Datenaustausch zwischen der Webseite und den einzelnen Modulen dient. Hierfür werden Getter und Setter für die Daten an das Window-Objekt gebunden. Dieses Bundle wird mit Hilfe der „standalone“-Option von Browserify generiert.
Source Maps und Error Logging mit Rollbar
Die Release-Tasks unterscheiden sich noch in einem weiteren Punkt von den Tasks im Entwicklungsprozess. Da konkatenierter und minimierter Code sehr schwierig zu debuggen ist, werden Source Maps erstellt. Source Maps sind Objekt-Literale, die Informationen über die Quelldateien des zusammengefassten und komprimierten Codes enthalten und ein Mapping auf die ursprüngliche Position im Quellcode erlauben. In Tasks, die Browserify nutzen, wird das über die „debug“-Option realisiert. Alle anderen Tasks erstellen Source Maps mit Hilfe eines Gulp-Plugins (gulp-sourcemaps). In der nachfolgenden Abbildung ist beispielhaft dargestellt, wie die Gulp-Tasks um die Erstellung von Source Maps erweitert werden.
Beim Deployment werden die Source Maps automatisiert zu Rollbar übertragen und die aktuelle Revision mit einer fortlaufenden Build-Nummer getaggt. Rollbar ist ein Fehlerüberwachungstool mit dessen Hilfe wir JavaScript-Fehler im Live-Betrieb tracken. Das Tool ist so konfiguriert, dass Fehlermeldungen sowohl per E-Mail gesendet, als auch an einen eigenen Slack-Channel geschickt werden.
Zusammenfassung
All diese Tools sind aus unserem Entwicklungs- und Releaseprozess nicht mehr wegzudenken. Sie helfen uns bei der Automatisierung und Optimierung von Aufgaben, erhöhen die Effizienz und erlauben uns auch Technologien einzusetzen, die nicht in jedem Browser unterstützt werden. Dieser Blog-Artikel gibt nur einen kurzen Überblick über die von uns genutzten Frontend-Tools, weshalb nicht alle Tasks vollständig aufgeführt beziehungsweise vereinfacht dargestellt wurden. Mehr zu diesem Thema wird in weiteren Blog-Artikeln vorgestellt werden.
Links und Referenzen
- NPM: https://www.npmjs.com/
- Gulp
- Offizielle Webseite: gulpjs.com
- Dokumentation: https://github.com/gulpjs/gulp/tree/master/docs
- API Dokumentation: https://github.com/gulpjs/gulp/blob/master/docs/API.md
- Plugin Registry: gulpjs.com/plugins/
- Browserify: http://browserify.org/
- Babel: https://babeljs.io/
- Sass: http://sass-lang.com/
- BEM: https://en.bem.info/methodology/
- Rollbar: https://rollbar.com/