Webpack oder Browserify & Gulp: Was ist besser?
Veröffentlicht: 2022-03-11Da Webanwendungen immer komplexer werden, ist es von größter Bedeutung, Ihre Webanwendung skalierbar zu machen. Während in der Vergangenheit das Schreiben von Ad-hoc-JavaScript und jQuery ausreichte, erfordert das Erstellen einer Web-App heutzutage ein viel höheres Maß an Disziplin und formelle Softwareentwicklungspraktiken, wie zum Beispiel:
- Komponententests, um sicherzustellen, dass Änderungen an Ihrem Code die vorhandene Funktionalität nicht beeinträchtigen
- Linting, um einen konsistenten Codierungsstil ohne Fehler zu gewährleisten
- Produktions-Builds, die sich von Entwicklungs-Builds unterscheiden
Das Web bietet auch einige seiner eigenen einzigartigen Entwicklungsherausforderungen. Da beispielsweise Webseiten viele asynchrone Anforderungen stellen, kann die Leistung Ihrer Web-App erheblich beeinträchtigt werden, wenn Hunderte von JS- und CSS-Dateien angefordert werden müssen, von denen jede ihren eigenen kleinen Overhead (Header, Handshakes usw.) hat. Dieses spezielle Problem kann häufig durch Bündeln der Dateien behoben werden, sodass Sie nur eine einzelne gebündelte JS- und CSS-Datei anfordern und nicht Hunderte von einzelnen.
Es ist auch üblich, Sprachpräprozessoren wie SASS und JSX zu verwenden, die zu nativem JS und CSS kompilieren, sowie JS-Transpiler wie Babel, um vom ES6-Code zu profitieren und gleichzeitig die ES5-Kompatibilität aufrechtzuerhalten.
Dies führt zu einer beträchtlichen Anzahl von Aufgaben, die nichts mit dem Schreiben der Logik der Web-App selbst zu tun haben. Hier kommen Task-Runner ins Spiel. Der Zweck eines Task-Runners besteht darin, all diese Aufgaben zu automatisieren, sodass Sie von einer verbesserten Entwicklungsumgebung profitieren und sich gleichzeitig auf das Schreiben Ihrer App konzentrieren können. Sobald der Task-Runner konfiguriert ist, müssen Sie nur noch einen einzigen Befehl in einem Terminal aufrufen.
Ich werde Gulp als Task Runner verwenden, da es sehr entwicklerfreundlich, leicht zu erlernen und leicht verständlich ist.
Eine kurze Einführung in Gulp
Die API von Gulp besteht aus vier Funktionen:
-
gulp.src -
gulp.dest -
gulp.task -
gulp.watch
Hier ist zum Beispiel eine Beispielaufgabe, die drei dieser vier Funktionen nutzt:
gulp.task('my-first-task', function() { gulp.src('/public/js/**/*.js') .pipe(concat()) .pipe(minify()) .pipe(gulp.dest('build')) }); Wenn my-first-task ausgeführt wird, werden alle Dateien, die dem Glob-Muster /public/js/**/*.js entsprechen, minimiert und dann in einen build -Ordner übertragen.
Das Schöne daran ist die .pipe() Verkettung. Sie nehmen eine Reihe von Eingabedateien, durchlaufen sie durch eine Reihe von Transformationen und geben dann die Ausgabedateien zurück. Um die Dinge noch bequemer zu machen, werden die eigentlichen Piping-Transformationen, wie z. B. minify() , oft von NPM-Bibliotheken durchgeführt. Infolgedessen ist es in der Praxis sehr selten, dass Sie Ihre eigenen Transformationen schreiben müssen, die über das Umbenennen von Dateien in der Pipe hinausgehen.
Der nächste Schritt zum Verständnis von Gulp besteht darin, die Reihe von Aufgabenabhängigkeiten zu verstehen.
gulp.task('my-second-task', ['lint', 'bundle'], function() { ... }); Hier führt my-second-task die Rückruffunktion erst aus, nachdem die lint und bundle -Aufgaben abgeschlossen sind. Dies ermöglicht die Trennung von Anliegen: Sie erstellen eine Reihe kleiner Aufgaben mit einer einzigen Verantwortung, z. B. die Konvertierung von LESS in CSS , und erstellen eine Art Hauptaufgabe, die einfach alle anderen Aufgaben über die Reihe von Aufgabenabhängigkeiten aufruft.
Schließlich haben wir gulp.watch , das ein Glob-Dateimuster auf Änderungen überwacht und eine Reihe von Aufgaben ausführt, wenn eine Änderung erkannt wird.
gulp.task('my-third-task', function() { gulp.watch('/public/js/**/*.js', ['lint', 'reload']) }) Im obigen Beispiel würden alle Änderungen an einer Datei, die mit lint übereinstimmt, die /public/js/**/*.js und reload -Aufgabe auslösen. Eine häufige Verwendung von gulp.watch ist das Auslösen von Live-Neuladevorgängen im Browser, eine Funktion, die für die Entwicklung so nützlich ist, dass Sie nicht mehr darauf verzichten können, wenn Sie sie einmal erlebt haben.
Und schon verstehst du alles, was du wirklich über gulp wissen musst.
Wo passt Webpack hinein?
Bei Verwendung des CommonJS-Musters ist das Bündeln von JavaScript-Dateien nicht so einfach wie das Verketten. Stattdessen haben Sie einen Einstiegspunkt (normalerweise index.js oder app.js genannt) mit einer Reihe von require oder import -Anweisungen am Anfang der Datei:
ES5
var Component1 = require('./components/Component1'); var Component2 = require('./components/Component2');ES6
import Component1 from './components/Component1'; import Component2 from './components/Component2'; Die Abhängigkeiten müssen vor dem verbleibenden Code in app.js aufgelöst werden, und diese Abhängigkeiten können selbst weitere Abhängigkeiten aufweisen, die aufgelöst werden müssen. Darüber hinaus require Sie möglicherweise dieselbe Abhängigkeit an mehreren Stellen in Ihrer Anwendung, aber Sie möchten diese Abhängigkeit nur einmal auflösen. Wie Sie sich vorstellen können, wird der Prozess der Bündelung Ihres JavaScripts ziemlich komplex, sobald Sie einen Abhängigkeitsbaum mit einigen Ebenen Tiefe haben. Hier kommen Bundler wie Browserify und Webpack ins Spiel.
Warum verwenden Entwickler Webpack anstelle von Gulp?
Webpack ist ein Bundler, während Gulp ein Task Runner ist, also würden Sie erwarten, dass diese beiden Tools häufig zusammen verwendet werden. Stattdessen gibt es einen wachsenden Trend, insbesondere in der React-Community, Webpack anstelle von Gulp zu verwenden. Warum ist das?
Einfach ausgedrückt, Webpack ist ein so leistungsstarkes Tool, dass es bereits die überwiegende Mehrheit der Aufgaben ausführen kann, die Sie sonst über einen Task Runner erledigen würden. Beispielsweise bietet Webpack bereits Optionen für die Minimierung und Sourcemaps für Ihr Bundle. Darüber hinaus kann Webpack als Middleware über einen benutzerdefinierten Server namens webpack-dev-server , der sowohl Live-Neuladen als auch Hot-Neuladen unterstützt (wir werden später über diese Funktionen sprechen). Durch die Verwendung von Ladeprogrammen können Sie auch ES6 zu ES5-Transpilation sowie CSS-Prä- und -Postprozessoren hinzufügen. Damit bleiben Unit-Tests und Linting als Hauptaufgaben übrig, die Webpack nicht unabhängig bewältigen kann. Angesichts der Tatsache, dass wir mindestens ein halbes Dutzend potenzieller Gulp-Aufgaben auf zwei reduziert haben, entscheiden sich viele Entwickler dafür, stattdessen NPM-Skripte direkt zu verwenden, da dies den Aufwand für das Hinzufügen von Gulp zum Projekt vermeidet (worüber wir später sprechen werden). .
Der größte Nachteil bei der Verwendung von Webpack ist, dass es ziemlich schwierig zu konfigurieren ist, was unattraktiv ist, wenn Sie versuchen, ein Projekt schnell zum Laufen zu bringen.
Unsere 3 Task Runner Setups
Ich werde ein Projekt mit drei verschiedenen Task-Runner-Setups einrichten. Jedes Setup führt die folgenden Aufgaben aus:
- Richten Sie einen Entwicklungsserver mit Live-Neuladen bei beobachteten Dateiänderungen ein
- Bündeln Sie unsere JS- und CSS-Dateien (einschließlich ES6-zu-ES5-Transpilation, SASS-zu-CSS-Konvertierung und Sourcemaps) auf skalierbare Weise bei beobachteten Dateiänderungen
- Führen Sie Komponententests entweder als eigenständige Aufgabe oder im Überwachungsmodus aus
- Führen Sie Linting entweder als eigenständige Aufgabe oder im Überwachungsmodus aus
- Bieten Sie die Möglichkeit, alle oben genannten Funktionen über einen einzigen Befehl im Terminal auszuführen
- Haben Sie einen weiteren Befehl zum Erstellen eines Produktionspakets mit Verkleinerung und anderen Optimierungen
Unsere drei Setups werden sein:
- Schluck + Browserify
- Schluck + Webpack
- Webpack + NPM-Skripte
Die Anwendung verwendet React für das Frontend. Ursprünglich wollte ich einen Framework-agnostischen Ansatz verwenden, aber die Verwendung von React vereinfacht tatsächlich die Verantwortlichkeiten des Task-Runners, da nur eine HTML-Datei benötigt wird und React sehr gut mit dem CommonJS-Muster funktioniert.
Wir werden die Vor- und Nachteile jedes Setups behandeln, damit Sie eine fundierte Entscheidung darüber treffen können, welche Art von Setup am besten zu Ihren Projektanforderungen passt.
Ich habe ein Git-Repository mit drei Zweigen eingerichtet, einen für jeden Ansatz (Link). Das Testen jedes Setups ist so einfach wie:
git checkout <branch name> npm prune (optional) npm install gulp (or npm start, depending on the setup)Lassen Sie uns den Code in jedem Zweig im Detail untersuchen …
Gemeinsamer Kodex
Ordnerstruktur
- app - components - fonts - styles - index.html - index.js - index.test.js - routes.jsindex.html
Eine einfache HTML-Datei. Die React-Anwendung wird in <div></div> geladen und wir verwenden nur eine einzige gebündelte JS- und CSS-Datei. Tatsächlich benötigen wir in unserem Webpack-Entwicklungssetup nicht einmal bundle.css .
index.js
Dies fungiert als JS-Einstiegspunkt unserer App. Im Wesentlichen laden wir React Router nur in die zuvor erwähnte div with id- app .
routen.js
Diese Datei definiert unsere Routen. Die URLs / , /about und /contact werden jeweils den Komponenten HomePage , AboutPage und ContactPage .
index.test.js
Dies ist eine Reihe von Komponententests, die das native JavaScript-Verhalten testen. In einer echten App in Produktionsqualität würden Sie einen Komponententest pro React-Komponente schreiben (mindestens eine, die den Zustand manipuliert), um das React-spezifische Verhalten zu testen. Für die Zwecke dieses Beitrags reicht es jedoch aus, einfach einen funktionalen Komponententest zu haben, der im Überwachungsmodus ausgeführt werden kann.
Komponenten/App.js
Dies kann als Container für alle unsere Seitenaufrufe betrachtet werden. Jede Seite enthält eine <Header/> -Komponente sowie this.props.children , die den Seitenaufruf selbst auswerten (z. B. ContactPage , wenn im Browser /contact ).
components/home/HomePage.js
Dies ist unsere Heimatansicht. Ich habe mich für react-bootstrap entschieden, da sich das Grid-System von Bootstrap hervorragend zum Erstellen von responsiven Seiten eignet. Bei richtiger Verwendung von Bootstrap wird die Anzahl der Medienabfragen, die Sie für kleinere Darstellungsbereiche schreiben müssen, drastisch reduziert.
Die restlichen Komponenten ( Header , AboutPage , ContactPage ) sind ähnlich aufgebaut (React react-bootstrap Markup, keine Zustandsmanipulation).
Lassen Sie uns jetzt mehr über das Styling sprechen.
CSS-Styling-Ansatz
Mein bevorzugter Ansatz zum Stylen von React-Komponenten besteht darin, ein Stylesheet pro Komponente zu haben, dessen Stile nur für diese bestimmte Komponente gelten. Sie werden feststellen, dass in jeder meiner React-Komponenten das div der obersten Ebene einen Klassennamen hat, der mit dem Namen der Komponente übereinstimmt. So hat beispielsweise HomePage.js sein Markup umschlossen von:
<div className="HomePage"> ... </div> Es gibt auch eine zugehörige HomePage.scss -Datei, die wie folgt aufgebaut ist:
@import '../../styles/variables'; .HomePage { // Content here }Warum ist dieser Ansatz so nützlich? Es führt zu einem hochgradig modularen CSS, das das Problem des unerwünschten Kaskadierungsverhaltens weitgehend eliminiert.
Angenommen, wir haben zwei React-Komponenten, Component1 und Component2 . In beiden Fällen möchten wir die h2 Schriftgröße überschreiben.
/* Component1.scss */ .Component1 { h2 { font-size: 30px; } } /* Component2.scss */ .Component2 { h2 { font-size: 60px; } } Die h2 Schriftgröße von Component1 und Component2 ist unabhängig davon, ob die Komponenten benachbart sind oder eine Komponente in der anderen verschachtelt ist. Im Idealfall bedeutet dies, dass das Styling einer Komponente vollständig in sich geschlossen ist, was bedeutet, dass die Komponente genau gleich aussieht, egal wo sie in Ihrem Markup platziert wird. In Wirklichkeit ist es nicht immer so einfach, aber es ist sicherlich ein großer Schritt in die richtige Richtung.
Zusätzlich zu styles pro Komponente möchte ich einen Stilordner haben, der ein globales Stylesheet global.scss , zusammen mit SASS-Partials, die eine bestimmte Verantwortung übernehmen (in diesem Fall _fonts.scss und _variables.scss für Schriftarten bzw. Variablen ). Das globale Stylesheet ermöglicht es uns, das allgemeine Erscheinungsbild der gesamten App zu definieren, während die Hilfspartials bei Bedarf von den komponentenspezifischen Stylesheets importiert werden können.
Nachdem nun der gemeinsame Code in jedem Zweig eingehend untersucht wurde, richten wir unseren Fokus auf den ersten Task-Runner/Bundler-Ansatz.
Gulp + Browserify-Setup
gulpfile.js
Das ergibt ein überraschend großes Gulpfile mit 22 Importen und 150 Codezeilen. Der Kürze halber werde ich daher nur die js , css , server , watch und default im Detail betrachten.
JS-Paket
// Browserify specific configuration const b = browserify({ entries: [config.paths.entry], debug: true, plugin: PROD ? [] : [hmr, watchify], cache: {}, packageCache: {} }) .transform('babelify'); b.on('update', bundle); b.on('log', gutil.log); (...) gulp.task('js', bundle); (...) // Bundles our JS using Browserify. Sourcemaps are used in development, while minification is used in production. function bundle() { return b.bundle() .on('error', gutil.log.bind(gutil, 'Browserify Error')) .pipe(source('bundle.js')) .pipe(buffer()) .pipe(cond(PROD, minifyJS())) .pipe(cond(!PROD, sourcemaps.init({loadMaps: true}))) .pipe(cond(!PROD, sourcemaps.write())) .pipe(gulp.dest(config.paths.baseDir)); } Dieser Ansatz ist aus mehreren Gründen ziemlich hässlich. Zum einen ist die Aufgabe in drei separate Teile aufgeteilt. Zuerst erstellen Sie Ihr Browserify-Bundle-Objekt b , übergeben einige Optionen und definieren einige Event-Handler. Dann haben Sie die Gulp-Aufgabe selbst, die eine benannte Funktion als Callback übergeben muss, anstatt sie einzufügen (da b.on('update') genau denselben Callback verwendet). Dies hat kaum die Eleganz einer Gulp-Aufgabe, bei der Sie einfach eine gulp.src und einige Änderungen leiten.
Ein weiteres Problem ist, dass uns dies zwingt, unterschiedliche Ansätze zum erneuten Laden von html , css und js im Browser zu verfolgen. Betrachten wir unsere Gulp watch Aufgabe:
gulp.task('watch', () => { livereload.listen({basePath: 'dist'}); gulp.watch(config.paths.html, ['html']); gulp.watch(config.paths.css, ['css']); gulp.watch(config.paths.js, () => { runSequence('lint', 'test'); }); }); Wenn eine HTML-Datei geändert wird, wird die html -Aufgabe erneut ausgeführt.
gulp.task('html', () => { return gulp.src(config.paths.html) .pipe(gulp.dest(config.paths.baseDir)) .pipe(cond(!PROD, livereload())); }); Die letzte Pipe ruft livereload() auf, wenn NODE_ENV nicht production ist, was eine Aktualisierung im Browser auslöst.
Dieselbe Logik wird für die CSS-Überwachung verwendet. Wenn eine CSS-Datei geändert wird, wird die CSS-Aufgabe erneut ausgeführt, und die letzte Pipe in der css -Aufgabe löst css livereload() aus und aktualisiert den Browser.
Die js Uhr ruft die js Aufgabe jedoch überhaupt nicht auf. Stattdessen behandelt der Event-Handler b.on('update', bundle) von Browserify das Neuladen mit einem völlig anderen Ansatz (nämlich Hot Module Replacement). Die Inkonsistenz in diesem Ansatz ist irritierend, aber leider notwendig, um inkrementelle Builds zu haben. Wenn wir am Ende der bundle -Funktion naiverweise livereload() würden, würde dies das gesamte JS-Bundle bei jeder Änderung einer einzelnen JS-Datei neu erstellen. Ein solcher Ansatz ist offensichtlich nicht skalierbar. Je mehr JS-Dateien Sie haben, desto länger dauert jede Neubündelung. Plötzlich dauern Ihre 500-ms-Neubündelungen 30 Sekunden, was die agile Entwicklung wirklich hemmt.
CSS-Paket
gulp.task('css', () => { return gulp.src( [ 'node_modules/bootstrap/dist/css/bootstrap.css', 'node_modules/font-awesome/css/font-awesome.css', config.paths.css ] ) .pipe(cond(!PROD, sourcemaps.init())) .pipe(sass().on('error', sass.logError)) .pipe(concat('bundle.css')) .pipe(cond(PROD, minifyCSS())) .pipe(cond(!PROD, sourcemaps.write())) .pipe(gulp.dest(config.paths.baseDir)) .pipe(cond(!PROD, livereload())); }); Das erste Problem hier ist die umständliche CSS-Einbindung des Anbieters. Immer wenn dem Projekt eine CSS-Datei eines neuen Anbieters hinzugefügt wird, müssen wir daran denken, unsere gulpfile zu ändern, um ein Element zum Array gulp.src hinzuzufügen, anstatt den Import an einer relevanten Stelle in unserem tatsächlichen Quellcode hinzuzufügen.
Das andere Hauptproblem ist die verworrene Logik in jeder Röhre. Ich musste eine NPM-Bibliothek namens gulp-cond hinzufügen, nur um bedingte Logik in meinen Pipes einzurichten, und das Endergebnis ist nicht allzu lesbar (überall drei Klammern!).
Server-Task
gulp.task('server', () => { nodemon({ script: 'server.js' }); }); Diese Aufgabe ist sehr einfach. Es ist im Wesentlichen ein Wrapper um den Befehlszeilenaufruf nodemon server.js , der server.js in einer Knotenumgebung ausführt. nodemon wird anstelle von node verwendet, sodass Änderungen an der Datei zu einem Neustart führen. Standardmäßig würde nodemon den laufenden Prozess bei jeder Änderung der JS-Datei neu starten, weshalb es wichtig ist, eine nodemon.json -Datei einzuschließen, um ihren Umfang einzuschränken:
{ "watch": "server.js" }Sehen wir uns unseren Servercode an.
server.js
const baseDir = process.env.NODE_ENV === 'production' ? 'build' : 'dist'; const port = process.env.NODE_ENV === 'production' ? 8080: 3000; const app = express();Dies legt das Basisverzeichnis des Servers und den Port basierend auf der Knotenumgebung fest und erstellt eine Instanz von express.
app.use(require('connect-livereload')({port: 35729})); app.use(express.static(path.join(__dirname, baseDir))); Dies fügt connect-livereload Middleware (notwendig für unser Live-Reloading-Setup) und statische Middleware (notwendig für den Umgang mit unseren statischen Assets) hinzu.

app.get('/api/sample-route', (req, res) => { res.send({ website: 'Toptal', blogPost: true }); }); Dies ist nur eine einfache API-Route. Wenn Sie im Browser zu localhost:3000/api/sample-route navigieren, sehen Sie Folgendes:
{ website: "Toptal", blogPost: true }In einem echten Backend hätten Sie einen ganzen Ordner für API-Routen, separate Dateien zum Herstellen von DB-Verbindungen und so weiter. Diese Beispielroute wurde lediglich eingefügt, um zu zeigen, dass wir problemlos ein Backend auf das von uns eingerichtete Frontend aufbauen können.
app.get('*', (req, res) => { res.sendFile(path.join(__dirname, './', baseDir ,'/index.html')); }); Dies ist eine Catch-All-Route, was bedeutet, dass der Server unabhängig davon, welche URL Sie in den Browser eingeben, unsere einzige index.html -Seite zurückgibt. Es liegt dann in der Verantwortung des React-Routers, unsere Routen auf der Client-Seite aufzulösen.
app.listen(port, () => { open(`http://localhost:${port}`); });Dies weist unsere Express-Instanz an, den von uns angegebenen Port abzuhören und den Browser in einem neuen Tab unter der angegebenen URL zu öffnen.
Das einzige, was mir bisher am Server-Setup nicht gefällt, ist:
app.use(require('connect-livereload')({port: 35729})); Da wir bereits gulp-livereload in unserem Gulpfile verwenden, entstehen zwei getrennte Stellen, an denen Livereload verwendet werden muss.
Nun zu guter Letzt:
Standardaufgabe
gulp.task('default', (cb) => { runSequence('clean', 'lint', 'test', 'html', 'css', 'js', 'fonts', 'server', 'watch', cb); }); Dies ist die Aufgabe, die ausgeführt wird, wenn Sie einfach gulp in das Terminal eingeben. Eine Kuriosität ist die Notwendigkeit, runSequence zu verwenden, damit die Aufgaben nacheinander ausgeführt werden. Normalerweise wird eine Reihe von Aufgaben parallel ausgeführt, aber das ist nicht immer das gewünschte Verhalten. Zum Beispiel müssen wir die clean vor html ausführen lassen, um sicherzustellen, dass unsere Zielordner leer sind, bevor Dateien in sie verschoben werden. Wenn gulp 4 veröffentlicht wird, wird es die gulp.series und gulp.parallel Methoden nativ unterstützen, aber vorerst müssen wir mit dieser kleinen Eigenart in unserem Setup aufhören.
Darüber hinaus ist dies eigentlich ziemlich elegant. Die gesamte Erstellung und das Hosting unserer App wird in einem einzigen Befehl ausgeführt, und das Verständnis eines Teils des Workflows ist so einfach wie das Untersuchen einer einzelnen Aufgabe in der Ausführungssequenz. Darüber hinaus können wir die gesamte Sequenz in kleinere Teile aufteilen, um einen granulareren Ansatz zum Erstellen und Hosten der App zu erhalten. Beispielsweise könnten wir eine separate Aufgabe namens validate einrichten, die die lint und test Aufgaben ausführt. Oder wir könnten einen host -Task haben, der server und watch . Diese Fähigkeit zur Orchestrierung von Aufgaben ist sehr leistungsfähig, insbesondere wenn Ihre Anwendung skaliert und stärker automatisierte Aufgaben erfordert.
Entwicklungs- vs. Produktions-Builds
if (argv.prod) { process.env.NODE_ENV = 'production'; } let PROD = process.env.NODE_ENV === 'production'; Mit der NPM-Bibliothek yargs können wir Befehlszeilen-Flags an Gulp liefern. Hier weise ich das gulpfile an, die Node-Umgebung auf Produktion einzustellen, wenn --prod im Terminal an gulp übergeben wird. Unsere PROD Variable wird dann als Bedingung verwendet, um Entwicklungs- und Produktionsverhalten in unserem Gulpfile zu unterscheiden. Eine der Optionen, die wir an unsere browserify Konfiguration übergeben, ist beispielsweise:
plugin: PROD ? [] : [hmr, watchify] Dies weist browserify an, keine Plugins im Produktionsmodus zu verwenden und hmr und watchify Plugins in anderen Umgebungen zu verwenden.
Diese PROD Bedingung ist sehr nützlich, da sie uns das Schreiben eines separaten Gulpfiles für Produktion und Entwicklung erspart, das letztendlich viele Codewiederholungen enthalten würde. Stattdessen können wir Dinge wie gulp --prod , um die Standardaufgabe in der Produktion auszuführen, oder gulp html --prod , um nur die html -Aufgabe in der Produktion auszuführen. Andererseits haben wir bereits gesehen, dass das Vermüllen unserer Gulp-Pipelines mit Anweisungen wie .pipe(cond(!PROD, livereload())) nicht besonders lesbar ist. Letztendlich ist es eine Frage der Präferenz, ob Sie den Ansatz mit booleschen Variablen verwenden oder zwei separate Gulpfiles erstellen möchten.
Sehen wir uns nun an, was passiert, wenn wir Gulp weiterhin als Task-Runner verwenden, aber Browserify durch Webpack ersetzen.
Gulp + Webpack-Setup
Plötzlich ist unser Gulpfile nur noch 99 Zeilen lang mit 12 Importen, eine ziemliche Reduzierung gegenüber unserem vorherigen Setup! Wenn wir die Standardaufgabe überprüfen:
gulp.task('default', (cb) => { runSequence('lint', 'test', 'build', 'server', 'watch', cb); });Jetzt erfordert unser vollständiges Web-App-Setup nur noch fünf Aufgaben statt neun, eine dramatische Verbesserung.
Darüber hinaus haben wir die Notwendigkeit von livereload . Unsere watch ist jetzt einfach:
gulp.task('watch', () => { gulp.watch(config.paths.js, () => { runSequence('lint', 'test'); }); }); Das bedeutet, dass unser Schluckbeobachter keinerlei Rebündelungsverhalten auslöst. Als zusätzlichen Bonus müssen wir index.html nicht mehr von der app auf dist oder build übertragen.
Um unseren Fokus wieder auf die Aufgabenreduzierung zu richten, wurden unsere Aufgaben html , css , js und fonts alle durch eine einzige build -Aufgabe ersetzt:
gulp.task('build', () => { runSequence('clean', 'html'); return gulp.src(config.paths.entry) .pipe(webpack(require('./webpack.config'))) .pipe(gulp.dest(config.paths.baseDir)); }); Einfach genug. Führen Sie die clean und html -Tasks nacheinander aus. Sobald diese abgeschlossen sind, schnappen Sie sich unseren Einstiegspunkt, leiten Sie ihn durch Webpack, übergeben Sie eine webpack.config.js -Datei, um ihn zu konfigurieren, und senden Sie das resultierende Bundle an unser baseDir (entweder dist oder build , je nach Node env).
Werfen wir einen Blick auf die Webpack-Konfigurationsdatei:
webpack.config.js
Dies ist eine ziemlich große und einschüchternde Konfigurationsdatei, also erklären wir einige der wichtigen Eigenschaften, die für unser module.exports Objekt festgelegt werden.
devtool: PROD ? 'source-map' : 'eval-source-map',Dies legt den Typ der Sourcemaps fest, die Webpack verwenden wird. Webpack unterstützt nicht nur Sourcemaps von Haus aus, es unterstützt tatsächlich eine breite Palette von Sourcemap-Optionen. Jede Option bietet ein anderes Gleichgewicht zwischen Sourcemap-Details und Rebuild-Geschwindigkeit (die Zeit, die zum Rebundlen bei Änderungen benötigt wird). Das bedeutet, dass wir eine „günstige“ Sourcemap-Option für die Entwicklung verwenden können, um schnelles Neuladen zu erreichen, und eine teurere Sourcemap-Option in der Produktion.
entry: PROD ? './app/index' : [ 'webpack-hot-middleware/client?reload=true', // reloads the page if hot module reloading fails. './app/index' ] Dies ist unser Bundle-Einstiegspunkt. Beachten Sie, dass ein Array übergeben wird, was bedeutet, dass es möglich ist, mehrere Einstiegspunkte zu haben. In diesem Fall haben wir unseren erwarteten Einstiegspunkt app/index.js sowie den webpack-hot-middleware , der als Teil unseres Hot-Modul-Neulade-Setups verwendet wird.
output: { path: PROD ? __dirname + '/build' : __dirname + '/dist', publicPath: '/', filename: 'bundle.js' }, Hier wird das zusammengestellte Bundle ausgegeben. Die verwirrendste Option ist publicPath . Es legt die Basis-URL dafür fest, wo Ihr Bundle auf dem Server gehostet wird. Wenn Ihr publicPath beispielsweise /public/assets ist, dann erscheint das Bundle unter /public/assets/bundle.js auf dem Server.
devServer: { contentBase: PROD ? './build' : './app' }Dadurch wird dem Server mitgeteilt, welcher Ordner in Ihrem Projekt als Stammverzeichnis des Servers verwendet werden soll.
Wenn Sie jemals verwirrt darüber sind, wie Webpack das erstellte Bundle in Ihrem Projekt dem Bundle auf dem Server zuordnet, denken Sie einfach an Folgendes:
-
path+filename: Der genaue Speicherort des Bundles in Ihrem Projektquellcode -
contentBase(als Root,/) +publicPath: Der Speicherort des Bundles auf dem Server
plugins: PROD ? [ new webpack.optimize.OccurenceOrderPlugin(), new webpack.DefinePlugin(GLOBALS), new ExtractTextPlugin('bundle.css'), new webpack.optimize.DedupePlugin(), new webpack.optimize.UglifyJsPlugin({compress: {warnings: false}}) ] : [ new webpack.HotModuleReplacementPlugin(), new webpack.NoErrorsPlugin() ], Dies sind Plugins, die die Funktionalität von Webpack in gewisser Weise verbessern. Beispielsweise ist webpack.optimize.UglifyJsPlugin für die Minimierung verantwortlich.
loaders: [ {test: /\.js$/, include: path.join(__dirname, 'app'), loaders: ['babel']}, { test: /\.css$/, loader: PROD ? ExtractTextPlugin.extract('style', 'css?sourceMap'): 'style!css?sourceMap' }, { test: /\.scss$/, loader: PROD ? ExtractTextPlugin.extract('style', 'css?sourceMap!resolve-url!sass?sourceMap') : 'style!css?sourceMap!resolve-url!sass?sourceMap' }, {test: /\.(svg|png|jpe?g|gif)(\?\S*)?$/, loader: 'url?limit=100000&name=img/[name].[ext]'}, {test: /\.(eot|woff|woff2|ttf)(\?\S*)?$/, loader: 'url?limit=100000&name=fonts/[name].[ext]'} ] Das sind Lader. Im Wesentlichen verarbeiten sie Dateien vor, die durch require() Anweisungen geladen werden. Sie ähneln in gewisser Weise den Gulp-Rohren, da Sie Lader miteinander verketten können.
Lassen Sie uns eines unserer Loader-Objekte untersuchen:
{test: /\.scss$/, loader: 'style!css?sourceMap!resolve-url!sass?sourceMap'} Die Eigenschaft test teilt Webpack mit, dass der angegebene Loader angewendet wird, wenn eine Datei mit dem bereitgestellten Regex-Muster übereinstimmt, in diesem Fall /\.scss$/ . Die loader -Eigenschaft entspricht der Aktion, die der Loader ausführt. Hier verketten wir die Ladeprogramme style , css , resolve-url und sass , die in umgekehrter Reihenfolge ausgeführt werden.
Ich muss zugeben, dass ich die Syntax loader3!loader2!loader1 nicht sehr elegant finde. Denn wann muss man schon mal etwas in einer Sendung von rechts nach links lesen? Trotzdem sind Loader ein sehr mächtiges Feature von Webpack. Tatsächlich erlaubt uns der gerade erwähnte Loader, SASS-Dateien direkt in unser JavaScript zu importieren! Beispielsweise können wir unsere Hersteller- und globalen Stylesheets in unsere Einstiegspunktdatei importieren:
index.js
import React from 'react'; import {render} from 'react-dom'; import {Router, browserHistory} from 'react-router'; import routes from './routes'; // CSS imports import '../node_modules/bootstrap/dist/css/bootstrap.css'; import '../node_modules/font-awesome/css/font-awesome.css'; import './styles/global.scss'; render(<Router history={browserHistory} routes={routes} />, document.getElementById('app')); In ähnlicher Weise können wir in unserer Header-Komponente import './Header.scss' um das zugehörige Stylesheet der Komponente zu importieren. Dies gilt auch für alle unsere anderen Komponenten.
Meiner Meinung nach kann dies fast als eine revolutionäre Veränderung in der Welt der JavaScript-Entwicklung angesehen werden. Sie müssen sich keine Gedanken über CSS-Bündelung, Minifizierung oder Sourcemaps machen, da unser Loader all dies für uns erledigt. Sogar das Neuladen von Hot-Modulen funktioniert für unsere CSS-Dateien. Die Möglichkeit, JS- und CSS-Importe in derselben Datei zu handhaben, macht die Entwicklung konzeptionell einfacher: Mehr Konsistenz, weniger Kontextwechsel und einfachere Argumentation.
Um eine kurze Zusammenfassung zu geben, wie diese Funktion funktioniert: Webpack fügt das CSS in unser JS-Bundle ein. Tatsächlich kann Webpack dies auch für Bilder und Schriftarten tun:
{test: /\.(svg|png|jpe?g|gif)(\?\S*)?$/, loader: 'url?limit=100000&name=img/[name].[ext]'}, {test: /\.(eot|woff|woff2|ttf)(\?\S*)?$/, loader: 'url?limit=100000&name=fonts/[name].[ext]'}Der URL-Loader weist Webpack an, unsere Bilder und Schriftarten als Daten-URLs einzubetten, wenn sie weniger als 100 KB groß sind, andernfalls dienen sie als separate Dateien. Natürlich können wir die Cutoff-Größe auch auf einen anderen Wert konfigurieren, z. B. 10 KB.
Und das ist die Webpack-Konfiguration auf den Punkt gebracht. Ich gebe zu, es gibt eine Menge Einrichtung, aber die Vorteile der Verwendung sind einfach phänomenal. Obwohl Browserify über Plugins und Transformationen verfügt, können sie sich in Bezug auf zusätzliche Funktionen einfach nicht mit Webpack-Loadern vergleichen.
Einrichtung von Webpack + NPM-Skripten
In diesem Setup verwenden wir direkt npm-Skripte, anstatt uns auf ein Gulpfile zu verlassen, um unsere Aufgaben zu automatisieren.
Paket.json
"scripts": { "start": "npm-run-all --parallel lint:watch test:watch build", "start:prod": "npm-run-all --parallel lint test build:prod", "clean-dist": "rimraf ./dist && mkdir dist", "clean-build": "rimraf ./build && mkdir build", "clean": "npm-run-all clean-dist clean-build", "test": "mocha ./app/**/*.test.js --compilers js:babel-core/register", "test:watch": "npm run test -- --watch", "lint": "esw ./app/**/*.js", "lint:watch": "npm run lint -- --watch", "server": "nodemon server.js", "server:prod": "cross-env NODE_ENV=production nodemon server.js", "build-html": "node tools/buildHtml.js", "build-html:prod": "cross-env NODE_ENV=production node tools/buildHtml.js", "prebuild": "npm-run-all clean-dist build-html", "build": "webpack", "postbuild": "npm run server", "prebuild:prod": "npm-run-all clean-build build-html:prod", "build:prod": "cross-env NODE_ENV=production webpack", "postbuild:prod": "npm run server:prod" } Um Entwicklungs- und Produktions-Builds auszuführen, geben npm start bzw. npm run start:prod ein.
Dies ist sicherlich kompakter als unser Gulpfile, da wir 99 bis 150 Codezeilen auf 19 NPM-Skripte reduziert haben, oder 12, wenn wir die Produktionsskripts ausschließen (von denen die meisten nur die Entwicklungsskripts spiegeln, wobei die Knotenumgebung auf Produktion eingestellt ist ). Der Nachteil ist, dass diese Befehle im Vergleich zu unseren Gulp-Task-Gegenstücken etwas kryptisch und nicht ganz so ausdrucksstark sind. Beispielsweise gibt es keine Möglichkeit (zumindest die mir bekannte), ein einzelnes npm-Skript bestimmte Befehle nacheinander und andere parallel ausführen zu lassen. Es ist entweder das eine oder das andere.
Dieser Ansatz hat jedoch einen großen Vorteil. Indem Sie NPM-Bibliotheken wie mocha direkt von der Befehlszeile aus verwenden, müssen Sie nicht für jede einen entsprechenden Gulp-Wrapper installieren (in diesem Fall gulp-mocha ).
Anstatt NPM zu installieren
- schluck-eslint
- Schluck-Mokka
- Schluck-Knoten
- etc
Wir installieren folgende Pakete:
- eslint
- Mokka
- Knotenmon
- etc
Zitat von Cory Houses Post, Why I Left Gulp and Grunt for NPM Scripts :
Ich war ein großer Fan von Gulp. Aber bei meinem letzten Projekt hatte ich Hunderte von Zeilen in meinem Gulpfile und etwa ein Dutzend Gulp-Plugins. Ich hatte Mühe, Webpack, Browsersync, Hot Reloading, Mocha und vieles mehr mit Gulp zu integrieren. Warum? Nun, einige Plugins hatten eine unzureichende Dokumentation für meinen Anwendungsfall. Einige Plugins haben nur einen Teil der API verfügbar gemacht, die ich benötigte. Einer hatte einen seltsamen Fehler, bei dem nur eine kleine Anzahl von Dateien angezeigt wurde. Ein anderer entfernte Farben bei der Ausgabe an die Befehlszeile.
Er spezifiziert drei Kernprobleme mit Gulp:
- Abhängigkeit von Plugin-Autoren
- Frustrierend zu debuggen
- Unzusammenhängende Dokumentation
Allen würde ich tendenziell zustimmen.
1. Abhängigkeit von Plugin-Autoren
Immer wenn eine Bibliothek wie eslint aktualisiert wird, benötigt die zugehörige gulp-eslint Bibliothek eine entsprechende Aktualisierung. Wenn der Betreuer der Bibliothek das Interesse verliert, ist die gulp-Version der Bibliothek nicht mehr synchron mit der nativen. Dasselbe gilt, wenn eine neue Bibliothek erstellt wird. Wenn jemand eine Bibliothek xyz erstellt und sie sich durchsetzt, brauchen Sie plötzlich eine entsprechende gulp-xyz Bibliothek, um sie in Ihren Gulp-Aufgaben zu verwenden.
In gewisser Weise lässt sich dieser Ansatz einfach nicht skalieren. Idealerweise möchten wir einen Ansatz wie Gulp, der die nativen Bibliotheken verwenden kann.
2. Frustrierend beim Debuggen
Obwohl Bibliotheken wie gulp-plumber helfen, dieses Problem erheblich zu lindern, ist es dennoch wahr, dass die Fehlerberichterstattung in gulp einfach nicht sehr hilfreich ist. Wenn auch nur eine Pipe eine unbehandelte Ausnahme auslöst, erhalten Sie einen Stack-Trace für ein Problem, das anscheinend völlig unabhängig von der Ursache des Problems in Ihrem Quellcode ist. Dies kann das Debuggen in einigen Fällen zu einem Alptraum machen. Keine noch so lange Suche bei Google oder Stack Overflow kann Ihnen wirklich helfen, wenn der Fehler kryptisch oder irreführend genug ist.
3. Unzusammenhängende Dokumentation
Oft stelle ich fest, dass kleine gulp Bibliotheken dazu neigen, eine sehr begrenzte Dokumentation zu haben. Ich vermute, das liegt daran, dass der Autor die Bibliothek normalerweise hauptsächlich für seinen eigenen Gebrauch erstellt. Darüber hinaus ist es üblich, sich die Dokumentation sowohl für das Gulp-Plugin als auch für die native Bibliothek selbst anzusehen, was viele Kontextwechsel und doppelt so viel Leseaufwand bedeutet.
Fazit
Es scheint mir ziemlich klar zu sein, dass Webpack Browserify vorzuziehen ist und NPM-Skripte Gulp vorzuziehen sind, obwohl jede Option ihre Vor- und Nachteile hat. Gulp ist sicherlich ausdrucksstärker und bequemer zu verwenden als NPM-Skripte, aber Sie zahlen den Preis für die zusätzliche Abstraktion.
Nicht jede Kombination ist möglicherweise perfekt für Ihre App, aber wenn Sie eine überwältigende Anzahl von Entwicklungsabhängigkeiten und ein frustrierendes Debugging-Erlebnis vermeiden möchten, ist Webpack mit NPM-Skripten der richtige Weg. Ich hoffe, Sie finden diesen Artikel hilfreich bei der Auswahl der richtigen Tools für Ihr nächstes Projekt.
- Behalten Sie die Kontrolle: Ein Leitfaden für Webpack und React, Pt. 1
- Gulp Under the Hood: Erstellen eines Stream-basierten Tools zur Aufgabenautomatisierung
