Task Runner und die Geburt des modernen Bundlings
Als Grunt die Build-Automatisierung transformierte und Webpack revolutionierte, wie wir über Dependencies denken. Der schmerzhafte Übergang von manuellen Prozessen zu ausgeklügeltem Bundling, der Frontend-Entwicklung für immer veränderte.
Bis 2012 waren die Probleme mit manuellen Build-Prozessen unerträglich geworden. Ich erinnere mich an ein Projekt, bei dem unser "Deployment-Skript" eine 200-zeilige Bash-Datei war, die ein Entwickler geschrieben hatte und die niemand sonst verstand. Als dieser Entwickler das Unternehmen verließ, hatten wir Angst, etwas zu ändern. Der Build schlug mysteriös fehl, wenn man ihn an einem Dienstag ausführte, und unsere Lösung war... dienstags nicht deployen.
Das war die Umgebung, in die Grunt hineinwuchs, und es fühlte sich revolutionär an. Zum ersten Mal hatten wir ein Tool, das die langweiligen, fehleranfälligen Sachen automatisieren konnte, während es konfigurierbar genug war, um komplexe Projekte zu handhaben. Aber wie bei jedem Tool in dieser Serie löste Grunt ein Set von Problemen, während es völlig neue offenbarte.
Die Grunt Revolution (2012-2015)#
Als Ben Alman 2012 Grunt veröffentlichte, adressierte es etwas Fundamentales: Build-Prozesse mussten deklarativ, nicht imperativ sein. Anstatt Shell-Skripte zu schreiben, die auf verschiedenen Maschinen unterschiedlich funktionieren könnten, beschriebst du, was passieren sollte.
Konfiguration über Skripting#
So sah eine typische Gruntfile aus:
module.exports = function(grunt) {
grunt.initConfig({
concat: {
options: {
separator: ';'
},
dist: {
src: ['src/**/*.js'],
dest: 'dist/built.js'
}
},
uglify: {
options: {
banner: '/*! <%= pkg.name %> <%= grunt.template.today("dd-mm-yyyy") %> */\n'
},
dist: {
files: {
'dist/built.min.js': ['<%= concat.dist.dest %>']
}
}
},
jshint: {
files: ['Gruntfile.js', 'src/**/*.js', 'test/**/*.js'],
options: {
globals: {
jQuery: true,
console: true,
module: true
}
}
},
watch: {
files: ['<%= jshint.files %>'],
tasks: ['jshint']
}
});
grunt.loadNpmTasks('grunt-contrib-uglify');
grunt.loadNpmTasks('grunt-contrib-jshint');
grunt.loadNpmTasks('grunt-contrib-watch');
grunt.loadNpmTasks('grunt-contrib-concat');
grunt.registerTask('default', ['jshint', 'concat', 'uglify']);
};
Das war riesig. Zum ersten Mal konntest du dir ein Projekt ansehen und genau verstehen, was während des Build-Prozesses passierte. Keine mysteriösen Shell-Skripte mehr, kein Beten mehr, dass die Person, die den Build geschrieben hatte, ihn ordentlich dokumentiert hatte.
Die Plugin-Ökosystem-Explosion#
Grunts Genie war zu erkennen, dass Build-Tasks Mustern folgen. Sass kompilieren? Da ist grunt-contrib-sass
. Bilder optimieren? grunt-contrib-imagemin
. Zu S3 deployen? grunt-aws-s3
.
Bis 2013 gab es Hunderte von Grunt-Plugins. Du konntest fast alles automatisieren:
- CSS-Preprocessing (Sass, Less, Stylus)
- JavaScript-Linting und Minifizierung
- Bildoptimierung
- Dateikopieren und -überwachung
- Template-Kompilierung
- Testing-Frameworks
- Deployment-Prozesse
Auswirkungen in der Praxis: Das erste Mal, dass Builds wirklich funktionierten#
Ich erinnere mich an das erste Projekt, bei dem wir Grunt erfolgreich eingerichtet hatten. Unser Deployment-Prozess ging von "Daumen drücken und hoffen" zu "führe grunt build
aus und hol dir einen Kaffee". Die psychologische Wirkung war tiefgreifend—wir hörten auf, Angst vor unserem eigenen Tooling zu haben.
Performance-Gewinne waren sofort:
- Build-Zeit: Von 15 Minuten manueller Arbeit zu 2 Minuten automatisiertem Prozess
- Fehlerrate: Build-Fehler sanken um 80%, weil menschliche Fehler eliminiert wurden
- Deployment-Vertrauen: Wir konnten mehrmals täglich deployen statt einmal pro Woche
Aber noch wichtiger war, dass Grunt das Muster etablierte, dem moderne Tools immer noch folgen: Konfiguration über Code, Plugin-basierte Architektur und klare Trennung zwischen Development- und Production-Builds.
Wo Grunt kämpfte#
Als Projekte größer wurden, wurden Grunts Limitationen offensichtlich:
Konfigurations-Hölle: Komplexe Gruntfiles wurden unwartbar. Hier ist ein echtes Beispiel aus einem Produktionsprojekt, an dem ich gearbeitet habe:
// Das war nur der CSS-Bereich einer 400-Zeilen-Gruntfile
sass: {
options: {
sourceMap: true,
outputStyle: 'compressed'
},
dev: {
files: {
'dist/css/main.css': 'src/scss/main.scss',
'dist/css/admin.css': 'src/scss/admin.scss',
'dist/css/mobile.css': 'src/scss/mobile.scss'
}
},
prod: {
options: {
sourceMap: false,
outputStyle: 'compressed'
},
files: {
'dist/css/main.min.css': 'src/scss/main.scss',
'dist/css/admin.min.css': 'src/scss/admin.scss',
'dist/css/mobile.min.css': 'src/scss/mobile.scss'
}
}
},
autoprefixer: {
options: {
browsers: ['last 3 versions', 'ie 8', 'ie 9']
},
dev: {
src: 'dist/css/*.css'
},
prod: {
src: 'dist/css/*.min.css'
}
},
cssmin: {
options: {
advanced: false,
keepSpecialComments: 0
},
prod: {
files: [{
expand: true,
cwd: 'dist/css/',
src: ['*.css', '!*.min.css'],
dest: 'dist/css/',
ext: '.min.css'
}]
}
}
Temporäre Dateien überall: Grunts Task-basierter Ansatz bedeutete, dass jeder Schritt auf die Festplatte schrieb. Ein typischer Build könnte Dutzende von temporären Dateien erstellen, was ihn langsam und schwer zu debuggen machte.
Keine inkrementelle Verarbeitung: Ändere eine Datei, erstelle alles neu. Das war nicht nachhaltig, als Projekte Hunderte von Dateien erreichten.
Die Gulp-Antwort: Streams und Geschwindigkeit (2013-2016)#
Gulp, von Eric Schoffstall erstellt, nahm einen fundamental anderen Ansatz. Anstatt Konfiguration betonte es Code. Anstatt Dateien nutzte es Streams.
Die Stream-Revolution#
const gulp = require('gulp');
const sass = require('gulp-sass');
const concat = require('gulp-concat');
const uglify = require('gulp-uglify');
const autoprefixer = require('gulp-autoprefixer');
gulp.task('styles', function() {
return gulp.src('src/scss/**/*.scss')
.pipe(sass())
.pipe(autoprefixer('last 3 versions'))
.pipe(gulp.dest('dist/css'));
});
gulp.task('scripts', function() {
return gulp.src('src/js/**/*.js')
.pipe(concat('app.js'))
.pipe(uglify())
.pipe(gulp.dest('dist/js'));
});
gulp.task('watch', function() {
gulp.watch('src/scss/**/*.scss', ['styles']);
gulp.watch('src/js/**/*.js', ['scripts']);
});
gulp.task('default', ['styles', 'scripts', 'watch']);
Die Vorteile waren sofort:
- Schnellere Builds: Keine temporären Dateien bedeutete, dass alles im Speicher passierte
- Intuitiver: Die Pipe-Metapher passte zu dem, wie Entwickler über Datentransformation denken
- Bessere Fehlerbehandlung: Streams machten es einfacher, Fehler zu handhaben und zu melden
- Inkrementelle Verarbeitung: Nur geänderte Dateien wurden verarbeitet
Warum Gulp gewann (vorübergehend)#
Gulp gewann massive Akzeptanz, weil es sich mehr wie Programmieren und weniger wie Konfiguration anfühlte. Entwickler konnten JavaScript-Logik verwenden, um komplexe Build-Szenarien zu handhaben:
gulp.task('scripts', function() {
const isProduction = process.env.NODE_ENV === 'production';
let stream = gulp.src('src/js/**/*.js')
.pipe(concat('app.js'));
if (isProduction) {
stream = stream.pipe(uglify());
}
return stream.pipe(gulp.dest('dist/js'));
});
Performance-Verbesserungen in der Praxis:
- Build-Zeit: 40-60% schneller als äquivalente Grunt-Tasks
- Speicherverbrauch: 50% Reduktion durch Stream-Verarbeitung
- Watch-Modus: Nahezu sofortige Rebuilds für inkrementelle Änderungen
Das Modul-Problem entsteht#
Sowohl Grunt als auch Gulp lösten das Build-Automatisierungsproblem, aber sie offenbarten ein tieferes Problem: JavaScript hatte kein natives Modulsystem. Du konntest Dateien verketten, aber musstest immer noch Abhängigkeiten manuell verwalten.
Betrachte dieses häufige Muster von 2013:
// In utils.js
var Utils = {
formatDate: function(date) { /* ... */ },
parseJSON: function(str) { /* ... */ }
};
// In models.js (abhängig von utils.js)
var User = {
create: function(data) {
var parsed = Utils.parseJSON(data);
// ...
}
};
// In views.js (abhängig von models.js und utils.js)
var UserView = {
render: function(user) {
var date = Utils.formatDate(user.createdAt);
// ...
}
};
Die Abhängigkeitsreihenfolge war immer noch manuell:
<script src="js/utils.js"></script>
<script src="js/models.js"></script>
<script src="js/views.js"></script>
<script src="js/app.js"></script>
Ändere die Reihenfolge, brich die Anwendung. Dieses Problem sollte viel schlimmer werden, als Anwendungen größer wurden.
Die Modulsystem-Kriege (2009-2014)#
Während Grunt und Gulp die Build-Automatisierung lösten, passierte eine parallele Evolution: JavaScript bekam endlich Modulsysteme. Das Problem war, dass drei verschiedene Ansätze entstanden, jeder mit unterschiedlichen Philosophien.
CommonJS: Server-Side-Denken#
CommonJS, popularisiert durch Node.js, verwendete synchrone require()
-Aufrufe:
// math.js
function add(a, b) {
return a + b;
}
function multiply(a, b) {
return a * b;
}
module.exports = {
add: add,
multiply: multiply
};
// app.js
var math = require('./math');
console.log(math.add(1, 2)); // 3
Das funktionierte perfekt für Node.js, wo Dateien lokal waren, aber Browser konnten Module nicht synchron laden, ohne die UI zu blockieren.
AMD: Asynchronous Module Definition#
RequireJS führte AMD ein, um asynchrones Laden zu handhaben:
// math.js
define(function() {
function add(a, b) {
return a + b;
}
function multiply(a, b) {
return a * b;
}
return {
add: add,
multiply: multiply
};
});
// app.js
require(['./math'], function(math) {
console.log(math.add(1, 2)); // 3
});
AMD löste das Browser-Ladeproblem, führte aber zu ausführlichem, Callback-lastigem Code, den viele Entwickler unnatürlich fanden.
UMD: Universal Module Definition#
UMD versuchte, Module zu erstellen, die überall funktionierten:
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD
define(['exports'], factory);
} else if (typeof exports === 'object' && typeof exports.nodeName !== 'string') {
// CommonJS
factory(exports);
} else {
// Browser globals
factory((root.myModule = {}));
}
}(typeof self !== 'undefined' ? self : this, function (exports) {
function add(a, b) {
return a + b;
}
exports.add = add;
}));
UMD funktionierte überall, war aber so ausführlich, dass es normalerweise von Tools generiert wurde, anstatt von Hand geschrieben zu werden.
Das Chaos in der Praxis#
In der Praxis endeten die meisten Projekte mit einer Mischung aus Modulformaten. Ein typisches Projekt könnte haben:
- Drittanbieter-Bibliotheken mit AMD (RequireJS-Ökosystem)
- Server-seitigen Code mit CommonJS (Node.js-Module)
- Legacy-Code mit globalen Variablen
- Neuen Code, der versuchte, was auch immer das Team als "Standard" entschieden hatte, zu verwenden
Ich arbeitete 2013 an einem Projekt, bei dem wir RequireJS für Anwendungscode hatten, aber jQuery-Plugins, die globales $
erwarteten, und Node.js-Module für Build-Skripte. Die Konfigurationsdatei, um das zum Laufen zu bringen, war 150 Zeilen lang und niemand verstand sie vollständig.
Browserify: Node.js-Module im Browser (2011-2016)#
James Halliday (substack) nahm mit Browserify einen radikalen Ansatz: Anstatt ein neues Modulformat zu erstellen, lass CommonJS einfach im Browser funktionieren.
Die Transform-Revolution#
# Abhängigkeiten wie Node.js installieren
npm install underscore jquery
# Code wie Node.js schreiben
# app.js
var _ = require('underscore');
var $ = require('jquery');
$('#app').html(_.template('<h1>Hello <%= name %>!</h1>')({ name: 'World' }));
# Für den Browser bündeln
browserify app.js -o bundle.js
Das war revolutionär, weil:
- Ein Modulformat: Keine AMD vs CommonJS vs UMD-Entscheidungen mehr
- npm-Ökosystem: Zugang zu Tausenden von Node.js-Modulen im Browser
- Vertraute Syntax: Entwickler kannten CommonJS bereits von Node.js
- Transform-Pipeline: Plugins konnten Code während des Bundlings modifizieren
Transforms: Die erste Bundle-Verarbeitungspipeline#
Browserifys Transform-System war der Vorläufer moderner webpack-Loader:
# ES6 zu ES5 transformieren
browserify app.js -t babelify -o bundle.js
# CoffeeScript transformieren
browserify app.coffee -t coffeeify -o bundle.js
# Templates transformieren
browserify app.js -t hbsfy -o bundle.js
Du konntest Transforms verketten, um ausgeklügelte Verarbeitungspipelines zu erstellen:
browserify app.js \
-t [ babelify --presets es2015 ] \
-t envify \
-t uglifyify \
-o bundle.js
Das npm + Browserify-Ökosystem#
Zum ersten Mal konnte Frontend-Entwicklung dasselbe Paket-Ökosystem wie Backend-Entwicklung verwenden. Datum-Manipulation? npm install moment
. HTTP-Anfragen? npm install axios
.
Das schuf einen positiven Kreislauf:
- Mehr Pakete wurden "isomorph" (funktionierten sowohl in Node.js als auch in Browsern)
- Frontend-Projekte konnten kampferprobte Server-seitige Bibliotheken nutzen
- Das JavaScript-Ökosystem wurde um npm vereint
Wo Browserify an Grenzen stieß#
Als Anwendungen größer wurden, wurde Browserifys Einfachheit zu einer Begrenzung:
Bundle-Größe-Probleme: Browserify enthielt ganze Module, auch wenn du nur eine Funktion verwendetest. Die vollständige Lodash-Bibliothek zu laden, um _.map
zu verwenden, führte zu massiven Bundles.
Kein Code-Splitting: Alles ging in eine bundle.js-Datei. Große Anwendungen resultierten in Multi-Megabyte-Bundles.
Kein Asset-Management: Browserify handhabte JavaScript, aber CSS, Bilder und andere Assets brauchten immer noch separate Tools.
Build-Performance: Große Projekte konnten Minuten zum Bündeln brauchen, ohne inkrementelle Kompilierung.
Webpack: Der Game Changer (2012-Gegenwart)#
Tobias Koppers erstellte webpack mit einer fundamental anderen Philosophie: behandle alles als Modul. Nicht nur JavaScript—CSS, Bilder, Schriftarten, alles.
Alles ist ein Modul#
// JavaScript-Module (vertraut)
import utils from './utils.js';
// CSS-Module (revolutionär)
import './styles.css';
// Bild-Module (umwerfend)
import logo from './logo.png';
// JSON-Module
import config from './config.json';
// Sogar HTML-Templates
import template from './template.html';
Dieser Ansatz löste mehrere Probleme auf einmal:
- Dependency-Tracking: webpack wusste genau, welche Dateien benötigt wurden
- Dead-Code-Elimination: Nicht verwendete Dateien wurden nicht im Bundle enthalten
- Cache-Busting: Datei-Hashes wurden automatisch generiert
- Asset-Optimierung: Bilder konnten automatisch optimiert, eingebettet oder konvertiert werden
Das Loader-System#
webpacks Loader-System war von Browserify-Transforms inspiriert, aber viel mächtiger:
module.exports = {
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
}
},
{
test: /\.css$/,
use: ['style-loader', 'css-loader']
},
{
test: /\.(png|svg|jpg|gif)$/,
use: ['file-loader']
}
]
}
};
Code-Splitting: Der Performance-Durchbruch#
webpack führte automatisches Code-Splitting basierend auf dynamischen Imports ein:
// Dynamischer Import erstellt ein separates Bundle
import('./heavy-feature.js').then(module => {
module.initialize();
});
// Mehrere Entry-Points erstellen mehrere Bundles
module.exports = {
entry: {
app: './src/app.js',
admin: './src/admin.js'
}
};
Das löste das Bundle-Größe-Problem, das Browserify nicht handhaben konnte. Anwendungen konnten minimalen Code vorab laden und zusätzliche Features bei Bedarf abrufen.
Die Development-Experience-Revolution#
webpack-dev-server führte Hot Module Replacement (HMR) ein:
// Änderungen an dieser Datei aktualisieren den Browser ohne Refresh
if (module.hot) {
module.hot.accept('./component.js', function() {
// Komponente an Ort und Stelle aktualisieren
updateComponent();
});
}
Die Produktivitätswirkung war enorm:
- CSS-Änderungen waren sofort (keine Seitenaktualisierung)
- JavaScript-Änderungen bewahrten den Anwendungszustand
- Debugging wurde viel einfacher mit Source Maps
- Development-Builds waren schnell mit inkrementeller Kompilierung
Konfigurations-Komplexität: Der Preis der Macht#
webpacks Macht kam mit Komplexität. Eine typische webpack-Konfiguration 2015:
const path = require('path');
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
module.exports = {
entry: {
app: './src/app.js',
vendor: ['react', 'react-dom', 'lodash']
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].[chunkhash].js'
},
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: 'babel-loader'
},
{
test: /\.css$/,
use: ExtractTextPlugin.extract({
fallback: 'style-loader',
use: 'css-loader'
})
},
{
test: /\.(png|svg|jpg|gif)$/,
use: {
loader: 'file-loader',
options: {
name: '[path][name].[hash].[ext]'
}
}
}
]
},
plugins: [
new HtmlWebpackPlugin({
template: './src/index.html'
}),
new ExtractTextPlugin('[name].[contenthash].css'),
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor'
}),
new webpack.optimize.CommonsChunkPlugin({
name: 'runtime'
})
],
resolve: {
modules: [
path.resolve(__dirname, 'src'),
'node_modules'
]
}
};
Diese Konfiguration war notwendig, aber einschüchternd. Viele Entwickler mieden webpack wegen seiner Komplexität, was zum Aufstieg "zero-config"-Tools wie Create React App führte.
Die Ökosystem-Konvergenz (2015-2018)#
Bis 2015 war das Frontend-Tooling-Ökosystem um einige Schlüsselprinzipien konvergiert:
npm als universeller Package Manager#
Bower war im Wesentlichen tot. npm hatte den Package-Management-Krieg gewonnen durch:
- Unterstützung sowohl für Frontend- als auch Backend-Pakete
- Ordnungsgemäße Handhabung verschachtelter Abhängigkeiten
- Bessere Versionsauflösung
- Integration mit Build-Tools
ES6-Module als Standard#
ES6 (ES2015) gab JavaScript endlich ein natives Modulsystem:
// math.js
export function add(a, b) {
return a + b;
}
export function multiply(a, b) {
return a * b;
}
// app.js
import { add, multiply } from './math.js';
Das bot die saubere Syntax von CommonJS mit den statischen Analyse-Vorteilen von AMD.
Babel als Übersetzungsschicht#
Babel wurde essentiell für die Verwendung von modernem JavaScript in älteren Browsern:
// Modernen Code schreiben
const users = await fetch('/api/users').then(r => r.json());
const admins = users.filter(u => u.role === 'admin');
// Babel transformiert zu kompatiblem Code
var users = fetch('/api/users').then(function(r) { return r.json(); });
var admins = users.filter(function(u) { return u.role === 'admin'; });
webpack als Build-Standard#
Trotz seiner Komplexität wurde webpack zum de facto Standard, weil es Probleme löste, die kein anderes Tool konnte:
- Universelles Modulsystem (CommonJS, AMD, ES6)
- Asset-Management (CSS, Bilder, Schriftarten)
- Code-Splitting und Lazy Loading
- Hot Module Replacement
- Produktionsoptimierungen (Tree Shaking, Minifizierung)
Die Schmerzpunkte, die weitere Innovation antrieben#
Bis 2016 war der moderne Frontend-Tooling-Stack etabliert, aber mehrere Schmerzpunkte blieben:
Konfigurations-Müdigkeit#
Ein neues Projekt einzurichten erforderte das Verständnis mehrerer Tools:
- webpack für Bundling
- Babel für Transpilation
- ESLint für Linting
- Jest für Testing
- PostCSS für CSS-Verarbeitung
Ein typisches Projekt hatte 6-8 Konfigurationsdateien und Hunderte von Zeilen Setup-Code.
Build-Performance#
Große webpack-Builds konnten 30+ Sekunden dauern, was die Entwicklung verlangsamte. Hot Reloading half während der Entwicklung, aber Production-Builds waren schmerzhaft langsam.
Bundle-Größen-Optimierung#
Bundle-Größen zu optimieren erforderte tiefes Wissen über webpack-Interna. Konzepte wie Tree Shaking, Code-Splitting und Chunk-Optimierung waren komplex und schlecht dokumentiert.
Tool-Interoperabilität#
Verschiedene Tools zusammenzubringen war oft fragil. Änderungen an der Konfiguration eines Tools konnten die Annahmen eines anderen Tools brechen.
Diese Probleme legten den Grundstein für die nächste Innovationswelle: Zero-Config-Tools, Performance-fokussierte Bundler und Framework-integrierte Tools, die 2017-2020 entstehen würden.
Vorausschauend: Das Fundament ist gelegt#
Bis 2016 war die Frontend-Entwicklung transformiert worden. Wir waren von manueller Dateiverwaltung zu ausgeklügelten Build-Pipelines übergegangen, die:
- Abhängigkeiten automatisch verwalten
- Modernen Code für Browser-Kompatibilität transformieren
- Assets für die Produktion optimieren
- Nahezu sofortiges Feedback während der Entwicklung bieten
- Code für optimale Ladeperformance aufteilen
Die Tools waren mächtig, aber komplex. Die nächste Evolution würde sich darauf konzentrieren, diese Komplexität zu verbergen und gleichzeitig noch bessere Performance und Entwicklererfahrung zu bieten.
Im nächsten Teil dieser Serie werden wir erkunden, wie Tools wie Parcel, Vite und esbuild die Performance- und Komplexitätsprobleme angingen, wie Frameworks wie Next.js und Vue CLI opinionierte Alternativen zur manuellen Konfiguration boten und wie das Aufkommen nativer ES-Module und HTTP/2 die grundlegenden Annahmen über Bundling veränderte.
Die Revolution hatte gerade erst begonnen.
Die Evolution des Frontend-Toolings: Ein Senior Engineer Rückblick
Von jQuery-Dateien-Verkettung zu Rust-betriebenen Bundlern - die unerzählte Geschichte, wie sich Frontend-Tooling entwickelt hat, um echte Produktionsprobleme zu lösen, erzählt durch Kriegsgeschichten und praktische Einsichten.
Alle Beiträge in dieser Serie
Kommentare (0)
An der Unterhaltung teilnehmen
Melde dich an, um deine Gedanken zu teilen und mit der Community zu interagieren
Noch keine Kommentare
Sei der erste, der deine Gedanken zu diesem Beitrag teilt!
Kommentare (0)
An der Unterhaltung teilnehmen
Melde dich an, um deine Gedanken zu teilen und mit der Community zu interagieren
Noch keine Kommentare
Sei der erste, der deine Gedanken zu diesem Beitrag teilt!