Gulp: Die Geheimwaffe eines Webentwicklers zur Maximierung der Website-Geschwindigkeit
Veröffentlicht: 2022-03-11Viele von uns müssen webbasierte Projekte bearbeiten, die in der Produktion verwendet werden und verschiedene Dienste für die Öffentlichkeit bereitstellen. Bei solchen Projekten ist es wichtig, unseren Code schnell erstellen und bereitstellen zu können. Etwas schnell zu tun führt oft zu Fehlern, besonders wenn sich ein Prozess wiederholt, daher ist es eine gute Praxis, einen solchen Prozess so weit wie möglich zu automatisieren.
In diesem Beitrag werden wir uns ein Tool ansehen, das ein Teil dessen sein kann, was es uns ermöglicht, eine solche Automatisierung zu erreichen. Dieses Tool ist ein npm-Paket namens Gulp.js. Um sich mit der in diesem Beitrag verwendeten grundlegenden Gulp.js-Terminologie vertraut zu machen, lesen Sie bitte „An Introduction to JavaScript Automation with Gulp“, das zuvor im Blog von Antonios Minas, einem unserer Toptal-Entwicklerkollegen, veröffentlicht wurde. Wir gehen davon aus, dass Sie mit der npm-Umgebung vertraut sind, da sie in diesem Beitrag ausgiebig zum Installieren von Paketen verwendet wird.
Bereitstellen von Front-End-Assets
Bevor wir fortfahren, gehen wir ein paar Schritte zurück, um uns einen Überblick über das Problem zu verschaffen, das Gulp.js für uns lösen kann. Viele webbasierte Projekte verfügen über Front-End-JavaScript-Dateien, die dem Client bereitgestellt werden, um der Webseite verschiedene Funktionalitäten bereitzustellen. Normalerweise gibt es auch eine Reihe von CSS-Stylesheets, die dem Client ebenfalls bereitgestellt werden. Wenn wir uns den Quellcode einer Website oder einer Webanwendung ansehen, sehen wir manchmal Code wie diesen:
<link href="css/main.css" rel="stylesheet"> <link href="css/custom.css" rel="stylesheet"> <script src="js/jquery.min.js"></script> <script src="js/site.js"></script> <script src="js/module1.js"></script> <script src="js/module2.js"></script>
Es gibt ein paar Probleme mit diesem Code. Es enthält Verweise auf zwei separate CSS-Stylesheets und vier separate JavaScript-Dateien. Das bedeutet, dass der Server insgesamt sechs Anfragen an den Server stellen muss und jede Anfrage separat eine Ressource laden muss, bevor die Seite fertig ist. Dies ist bei HTTP/2 weniger ein Problem, da HTTP/2 Parallelität und Header-Komprimierung einführt, aber es ist immer noch ein Problem. Es erhöht das Gesamtverkehrsvolumen, das zum Laden dieser Seite erforderlich ist, und verringert die Qualität der Benutzererfahrung, da das Laden der Dateien länger dauert. Im Fall von HTTP 1.1 belastet es außerdem das Netzwerk und reduziert die Anzahl der verfügbaren Anfragekanäle. Es wäre viel besser gewesen, die CSS- und JavaScript-Dateien jeweils in einem einzigen Paket zusammenzufassen. Auf diese Weise gäbe es insgesamt nur zwei Anfragen. Es wäre auch schön gewesen, verkleinerte Versionen dieser Dateien bereitzustellen, die normalerweise viel kleiner als die Originale sind. Unsere Webanwendung kann auch kaputt gehen, wenn eines der Assets zwischengespeichert wird, und der Client würde eine veraltete Version erhalten.
Ein primitiver Ansatz zur Lösung einiger dieser Probleme besteht darin, jeden Asset-Typ manuell mit einem Texteditor zu einem Bündel zusammenzufassen und das Ergebnis dann über einen Minifier-Dienst wie http://jscompress.com/ laufen zu lassen. Dies erweist sich als sehr mühsam, dies kontinuierlich während des Entwicklungsprozesses zu tun. Eine leichte, aber fragwürdige Verbesserung wäre, unseren eigenen Minifier-Server mit einem der auf GitHub verfügbaren Pakete zu hosten. Dann könnten wir Dinge tun, die ungefähr so aussehen würden wie die folgenden:
<script src="min/f=js/site.js,js/module1.js"></script>
Dies würde unserem Client minimierte Dateien liefern, aber es würde das Caching-Problem nicht lösen. Es würde auch eine zusätzliche Last auf dem Server verursachen, da unser Server im Wesentlichen alle Quelldateien bei jeder Anfrage wiederholt verketten und minimieren müsste.
Automatisierung mit Gulp.js
Sicherlich können wir es besser machen als jeder dieser beiden Ansätze. Was wir wirklich wollen, ist die Bündelung zu automatisieren und in die Build-Phase unseres Projekts einzubeziehen. Wir möchten am Ende vorgefertigte Asset-Bundles haben, die bereits minifiziert und einsatzbereit sind. Wir möchten den Kunden auch zwingen, bei jeder Anfrage die aktuellsten Versionen unserer gebündelten Assets zu erhalten, aber wir möchten nach Möglichkeit immer noch Caching nutzen. Zum Glück kann Gulp.js damit umgehen. Im Rest des Artikels werden wir eine Lösung entwickeln, die die Leistungsfähigkeit von Gulp.js nutzt, um die Dateien zu verketten und zu minimieren. Wir werden auch ein Plugin verwenden, um den Cache zu sprengen, wenn es Updates gibt.
In unserem Beispiel erstellen wir die folgende Verzeichnis- und Dateistruktur:
public/ |- build/ |- js/ |- bundle-{hash}.js |- css/ |- stylesheet-{hash}.css assets/ |- js/ |- vendor/ |- jquery.js |- site.js |- module1.js |- module2.js |- css/ |- main.css |- custom.css gulpfile.js package.json
In der Datei gulpfile.js definieren wir die Aufgaben, die Gulp für uns ausführen wird. Die package.json
wird von npm verwendet, um das Paket unserer Anwendung zu definieren und die Abhängigkeiten zu verfolgen, die wir installieren werden. Das öffentliche Verzeichnis sollte für das Web konfiguriert werden. Im Assets-Verzeichnis speichern wir unsere Quelldateien. Um Gulp im Projekt zu verwenden, müssen wir es über npm installieren und als Entwicklerabhängigkeit für das Projekt speichern. Wir werden auch mit dem concat
Plugin für Gulp beginnen wollen, mit dem wir mehrere Dateien zu einer verketten können.
Um diese beiden Elemente zu installieren, führen wir den folgenden Befehl aus:
npm install --save-dev gulp gulp-concat
Als nächstes wollen wir damit beginnen, den Inhalt von gulpfile.js zu schreiben.
var gulp = require('gulp'); var concat = require('gulp-concat'); gulp.task('pack-js', function () { return gulp.src(['assets/js/vendor/*.js', 'assets/js/main.js', 'assets/js/module*.js']) .pipe(concat('bundle.js')) .pipe(gulp.dest('public/build/js')); }); gulp.task('pack-css', function () { return gulp.src(['assets/css/main.css', 'assets/css/custom.css']) .pipe(concat('stylesheet.css')) .pipe(gulp.dest('public/build/css')); }); gulp.task('default', ['pack-js', 'pack-css']);
Hier laden wir die gulp-Bibliothek und ihr concat-Plugin. Anschließend definieren wir drei Aufgaben.
Die erste Aufgabe ( pack-js
) definiert ein Verfahren zum Komprimieren mehrerer JavaScript-Quelldateien in ein Bündel. Wir listen die Quelldateien auf, die in der angegebenen Reihenfolge globbed, gelesen und verkettet werden. Wir leiten das in das concat-Plugin, um eine letzte Datei namens bundle.js
zu erhalten. Schließlich weisen wir gulp an, die Datei nach public/build/js
zu schreiben.
Die zweite Aufgabe ( pack-css
) macht dasselbe wie oben, aber für die CSS-Stylesheets. Es weist Gulp an, die verkettete Ausgabe als stylesheet.css
in public/build/css
zu speichern.
Die dritte Aufgabe ( default
) ist diejenige, die Gulp ausführt, wenn wir sie ohne Argumente aufrufen. Im zweiten Parameter übergeben wir die Liste anderer Aufgaben, die ausgeführt werden sollen, wenn die Standardaufgabe ausgeführt wird.
Fügen wir diesen Code mit einem beliebigen Quellcode-Editor, den wir normalerweise verwenden, in gulpfile.js ein und speichern die Datei dann im Stammverzeichnis der Anwendung.
Als nächstes öffnen wir die Befehlszeile und führen Folgendes aus:
gulp
Wenn wir uns unsere Dateien ansehen, nachdem wir diesen Befehl ausgeführt haben, finden wir zwei neue Dateien: public/build/js/bundle.js
und public/build/css/stylesheet.css
. Sie sind Verkettungen unserer Quelldateien, was einen Teil des ursprünglichen Problems löst. Sie sind jedoch nicht minifiziert, und es gibt noch kein Cache-Busting. Lassen Sie uns die automatische Minimierung hinzufügen.
Optimieren gebauter Assets
Wir benötigen zwei neue Plugins. Um sie hinzuzufügen, führen wir den folgenden Befehl aus:
npm install --save-dev gulp-clean-css gulp-minify
Das erste Plugin dient zum Minimieren von CSS und das zweite zum Minimieren von JavaScript. Der erste verwendet das Paket clean-css und der zweite das Paket UglifyJS2. Wir werden diese beiden Pakete zuerst in unsere gulpfile.js laden:
var minify = require('gulp-minify'); var cleanCss = require('gulp-clean-css');
Wir müssen sie dann in unseren Aufgaben verwenden, kurz bevor wir die Ausgabe auf die Festplatte schreiben:
.pipe(minify()) .pipe(cleanCss())
Die gulpfile.js sollte nun so aussehen:
var gulp = require('gulp'); var concat = require('gulp-concat'); var minify = require('gulp-minify'); var cleanCss = require('gulp-clean-css'); gulp.task('pack-js', function () { return gulp.src(['assets/js/vendor/*.js', 'assets/js/main.js', 'assets/js/module*.js']) .pipe(concat('bundle.js')) .pipe(minify()) .pipe(gulp.dest('public/build/js')); }); gulp.task('pack-css', function () { return gulp.src(['assets/css/main.css', 'assets/css/custom.css']) .pipe(concat('stylesheet.css')) .pipe(cleanCss()) .pipe(gulp.dest('public/build/css')); }); gulp.task('default', ['pack-js', 'pack-css']);
Lassen Sie uns noch einmal schlucken. Wir werden sehen, dass die Datei stylesheet.css
im verkleinerten Format gespeichert wird und die Datei bundle.js
weiterhin so gespeichert wird, wie sie ist. Wir werden feststellen, dass wir jetzt auch Bundle-min.js haben, das minimiert ist. Wir wollen nur die minimierte Datei, und wir wollen, dass sie als bundle.js
gespeichert wird, also werden wir unseren Code mit zusätzlichen Parametern ändern:

.pipe(minify({ ext:{ min:'.js' }, noSource: true }))
Gemäß der gulp-minify-Plugin-Dokumentation (https://www.npmjs.com/package/gulp-minify) wird dadurch der gewünschte Name für die minimierte Version festgelegt und das Plugin angewiesen, die Version mit der Originalquelle nicht zu erstellen. Wenn wir den Inhalt des Build-Verzeichnisses löschen und gulp erneut über die Befehlszeile ausführen, erhalten wir am Ende nur zwei minimierte Dateien. Wir haben gerade die Implementierung der Minimierungsphase unseres Build-Prozesses abgeschlossen.
Cache-Busting
Als nächstes wollen wir Cache-Busting hinzufügen, und dafür müssen wir ein Plugin installieren:
npm install --save-dev gulp-rev
Und verlangen Sie es in unserer Gulp-Datei:
var rev = require('gulp-rev');
Die Verwendung des Plugins ist etwas schwierig. Wir müssen die minimierte Ausgabe zuerst durch das Plugin leiten. Dann müssen wir das Plugin erneut aufrufen, nachdem wir die Ergebnisse auf die Festplatte geschrieben haben. Das Plugin benennt die Dateien um, sodass sie mit einem eindeutigen Hash gekennzeichnet sind, und erstellt außerdem eine Manifestdatei. Die Manifestdatei ist eine Karte, die von unserer Anwendung verwendet werden kann, um die neuesten Dateinamen zu ermitteln, auf die wir in unserem HTML-Code verweisen sollten. Nachdem wir die Gulp-Datei geändert haben, sollte sie am Ende so aussehen:
var gulp = require('gulp'); var concat = require('gulp-concat'); var minify = require('gulp-minify'); var cleanCss = require('gulp-clean-css'); var rev = require('gulp-rev'); gulp.task('pack-js', function () { return gulp.src(['assets/js/vendor/*.js', 'assets/js/main.js', 'assets/js/module*.js']) .pipe(concat('bundle.js')) .pipe(minify({ ext:{ min:'.js' }, noSource: true })) .pipe(rev()) .pipe(gulp.dest('public/build/js')) .pipe(rev.manifest()) .pipe(gulp.dest('public/build')); }); gulp.task('pack-css', function () { return gulp.src(['assets/css/main.css', 'assets/css/custom.css']) .pipe(concat('stylesheet.css')) .pipe(cleanCss()) .pipe(rev()) .pipe(gulp.dest('public/build/css')) .pipe(rev.manifest()) .pipe(gulp.dest('public/build')); }); gulp.task('default', ['pack-js', 'pack-css']);
Lassen Sie uns den Inhalt unseres Build-Verzeichnisses löschen und gulp erneut ausführen. Wir werden feststellen, dass wir jetzt zwei Dateien mit Hashtags haben, die an jeden der Dateinamen angehängt sind, und eine manifest.json, die in public/build
gespeichert ist. Wenn wir die Manifestdatei öffnen, sehen wir, dass sie nur einen Verweis auf eine unserer minimierten und markierten Dateien enthält. Was passiert ist, dass jede Aufgabe eine separate Manifestdatei schreibt und eine von ihnen die andere überschreibt. Wir müssen die Aufgaben mit zusätzlichen Parametern ändern, die sie anweisen, nach der vorhandenen Manifestdatei zu suchen und die neuen Daten darin zusammenzuführen, falls vorhanden. Die Syntax dafür ist etwas kompliziert, also schauen wir uns an, wie der Code aussehen sollte, und gehen ihn dann durch:
var gulp = require('gulp'); var concat = require('gulp-concat'); var minify = require('gulp-minify'); var cleanCss = require('gulp-clean-css'); var rev = require('gulp-rev'); gulp.task('pack-js', function () { return gulp.src(['assets/js/vendor/*.js', 'assets/js/main.js', 'assets/js/module*.js']) .pipe(concat('bundle.js')) .pipe(minify({ ext:{ min:'.js' }, noSource: true })) .pipe(rev()) .pipe(gulp.dest('public/build/js')) .pipe(rev.manifest('public/build/rev-manifest.json', { merge: true })) .pipe(gulp.dest('')); }); gulp.task('pack-css', function () { return gulp.src(['assets/css/main.css', 'assets/css/custom.css']) .pipe(concat('stylesheet.css')) .pipe(cleanCss()) .pipe(rev()) .pipe(gulp.dest('public/build/css')) .pipe(rev.manifest('public/build/rev-manifest.json', { merge: true })) .pipe(gulp.dest('')); }); gulp.task('default', ['pack-js', 'pack-css']);
Wir leiten die Ausgabe zuerst an rev.manifest()
. Dadurch werden markierte Dateien anstelle der Dateien erstellt, die wir zuvor hatten. Wir geben den gewünschten Pfad unserer rev-manifest.json
an und rev.manifest()
an, in die vorhandene Datei einzufügen, falls vorhanden. Dann weisen wir gulp an, das Manifest in das aktuelle Verzeichnis zu schreiben, das zu diesem Zeitpunkt public/build sein wird. Das Pfadproblem ist auf einen Fehler zurückzuführen, der auf GitHub ausführlicher besprochen wird.
Wir haben jetzt eine automatisierte Minifizierung, markierte Dateien und eine Manifestdatei. All dies ermöglicht es uns, die Dateien schneller an den Benutzer zu liefern und seinen Cache zu sprengen, wenn wir unsere Änderungen vornehmen. Es bleiben jedoch nur noch zwei Probleme.
Das erste Problem ist, dass wir, wenn wir Änderungen an unseren Quelldateien vornehmen, neu markierte Dateien erhalten, aber die alten auch dort bleiben. Wir brauchen eine Möglichkeit, alte minimierte Dateien automatisch zu löschen. Lösen wir dieses Problem mit einem Plugin, mit dem wir Dateien löschen können:
npm install --save-dev del
Wir werden es in unserem Code benötigen und zwei neue Aufgaben definieren, eine für jeden Quelldateityp:
var del = require('del'); gulp.task('clean-js', function () { return del([ 'public/build/js/*.js' ]); }); gulp.task('clean-css', function () { return del([ 'public/build/css/*.css' ]); });
Wir werden dann sicherstellen, dass die neue Aufgabe vor unseren beiden Hauptaufgaben beendet wird:
gulp.task('pack-js', ['clean-js'], function () { gulp.task('pack-css', ['clean-css'], function () {
Wenn wir nach dieser Änderung erneut gulp
ausführen, haben wir nur die neuesten minimierten Dateien.
Das zweite Problem ist, dass wir nicht jedes Mal Schlucken laufen wollen, wenn wir eine Änderung vornehmen. Um dies zu lösen, müssen wir eine Watcher-Aufgabe definieren:
gulp.task('watch', function() { gulp.watch('assets/js/**/*.js', ['pack-js']); gulp.watch('assets/css/**/*.css', ['pack-css']); });
Wir werden auch die Definition unserer Standardaufgabe ändern:
gulp.task('default', ['watch']);
Wenn wir jetzt gulp von der Kommandozeile aus ausführen, werden wir feststellen, dass es beim Aufruf nichts mehr aufbaut. Dies liegt daran, dass jetzt die Watcher-Task aufgerufen wird, die unsere Quelldateien auf Änderungen überwacht und nur erstellt, wenn sie eine Änderung erkennt. Wenn wir versuchen, eine unserer Quelldateien zu ändern und dann erneut auf unsere Konsole schauen, werden wir sehen, dass die Aufgaben pack-js
und pack-css
automatisch zusammen mit ihren Abhängigkeiten ausgeführt werden.
Jetzt müssen wir nur noch die manifest.json-Datei in unsere Anwendung laden und daraus die getaggten Dateinamen abrufen. Wie wir das machen, hängt von unserer speziellen Back-End-Sprache und unserem Technologie-Stack ab und wäre ziemlich trivial zu implementieren, also gehen wir nicht im Detail darauf ein. Die allgemeine Idee ist jedoch, dass wir das Manifest in ein Array oder ein Objekt laden und dann eine Hilfsfunktion definieren können, die es uns ermöglicht, versionierte Assets aus unseren Vorlagen auf ähnliche Weise wie folgt aufzurufen:
gulp('bundle.js')
Sobald wir das getan haben, müssen wir uns nie wieder Gedanken über geänderte Tags in unseren Dateinamen machen und können uns darauf konzentrieren, qualitativ hochwertigen Code zu schreiben.
Den endgültigen Quellcode für diesen Artikel finden Sie zusammen mit einigen Beispielassets in diesem GitHub-Repository.
Fazit
In diesem Artikel haben wir uns mit der Implementierung der Gulp-basierten Automatisierung für unseren Build-Prozess befasst. Ich hoffe, dass sich dies als hilfreich für Sie erweist und es Ihnen ermöglicht, anspruchsvollere Build-Prozesse in Ihren eigenen Anwendungen zu entwickeln.
Bitte denken Sie daran, dass Gulp nur eines der Tools ist, die für diesen Zweck verwendet werden können, und es gibt viele andere wie Grunt, Browserify und Webpack. Sie unterscheiden sich in ihrem Zweck und im Umfang der Probleme, die sie lösen können. Einige können Probleme lösen, die Gulp nicht kann, wie das Bündeln von JavaScript-Modulen mit Abhängigkeiten, die bei Bedarf geladen werden können. Dies wird als „Code-Splitting“ bezeichnet und ist eine Verbesserung gegenüber der Idee, eine große Datei mit allen Teilen unseres Programms auf jeder Seite bereitzustellen. Diese Tools sind ziemlich ausgefeilt, könnten aber in Zukunft abgedeckt werden. In einem folgenden Beitrag werden wir erläutern, wie Sie die Bereitstellung unserer Anwendung automatisieren können.