< Zurück zu den Artikeln

Webpack – moderne Webentwicklung

Webpack ist in letzter Zeit ziemlich beliebt geworden und wird oft verglichen mit Tools wie Gulp oder Grunt. In diesem Artikel erkläre ich die Grundprinzipien und die Philosophie von Webpack. Außerdem gebe ich Beispiele, wann und wie man das Tool einsetzt (und wann und wie nicht). 

Der Artikel basiert auf Probekonfigurationen Webpack sowie Github und einem Text, den ich für unsere internen Fortbildungskurse in der App Agentur Ackee ❤ geschrieben habe.

Seit dem Artikel im Jahr 2017 gab es mehrere Änderungen in Webpack. Der ursprüngliche Artikel wurde für Webpack Version 2 geschrieben. Webpack gibt es aktuell nun in der stabilen Version 4. Version 5 kommt demnächst.

Webpack hat die Unterstützung von Node Version 4 eingestellt, deshalb musst du nun mindestens Version 8 verwenden. Das Projekt wurde in zwei Pakete aufgeteilt: 'webpack' und 'webpack.cli'. Wenn du Webpack aus der CLI verwenden willst, stelle erst sicher, dass du das zweite Paket installiert hast.

Die Konfiguration von Webpack hat sich ebenfalls geändert. Webpack unterstützt nun Modi (‚Produktion‘ und ‚Entwicklung‘) mit denen du Webpack fast ohne Konfiguration verwenden kannst. Die Modi sind Sätze von kampferprobten Konfigurationen, auf die du dich blind verlassen kannst. Die Verwendung von Features wie Code-Splitting und Tree-Shaking ist noch einfacher geworden. Die kommende Version von Webpack soll  eine noch bessere Entwickler-Erfahrung durch voll unterstütztes Caching und Parallelität bringen. Diese Funktionen werden die Build-Time deutlich beschleunigen.

Webpack ist berüchtigt wegen seiner komplizierten Konfiguration schwer benutzbar zu sein. Mit den Änderungen in der Version 4 ist Webpack nun einfach zu bedienen und durch die neue Dokumentation  auch leicht zu verstehen.

Was ist Webpack

Was sind Module

Von Anfang an hat Node.js CommonJS Code-Splitting unterstützt. Das kann man zum Splitten von Code in abhängige Module nutzen, die geladen und angewendet werden können, wann immer sie gebraucht werden.

var $ = require('jquery'); – jQuery wird als Dependency in einem beliebigen Modul geladen und ist einsatzbereit.module.export = jquery;  – jQuery wird aus einem Modul exportiert und kann irgendwo in Dependency benutzt werden.

Code-Splitting und die Aufteilung in einzelne Module wird vor allem wegen Überschaubarkeit und einer einfachen Code-Verwaltung  gemacht. Es ist eine klassische Ingenieurpraxis, bekannt aus verschiedenen Programmiersprachen wie z. B. C, C++, PHP, Ruby, Python, Java. Deswegen wurde sie auch von Node.js als Terminal-Interpret des JavaScripts eingeführt.

Egal von welchem Browser es verarbeitet wird, Javascript unterstützt nativ kein Code-Splitting. Jedes Script wird innerhalb eines Kontexts ausgeführt in der Reihenfolge während HTML geparsed wird. Alle Scripts werden im Browser im Rahmen eines Kontexts  bearbeitet, und zwar in der Reihenfolge, in der sie aus einem HTML-Format geparst werden. Diese Vorgehensweise verursacht Probleme bei den globalen Variablen, der Existenz von Variablen innerhalb des Kontexts, dem Ursprung der Dependency und für das Funktionieren des Codes. 

Module für den Browser vorbereiten

Hier kommt Webpack ins Spiel. Webpack unterstützt das Code-Splitting im Browser, indem es Module schreibt als wären sie für einen Node.js Interpreter (z.B. CommonJS, aber seit v2.0 nativ auch ES Module und AMD). Angenommen wir haben einen Javascript Code, der mit CommonJS Modulen geschrieben wurde so wie wir es jetzt mit Node.js gemacht haben. Wie erklären wir dem Browser jetzt, dass er damit arbeiten soll?

Webpack verarbeitet den als Modul geschriebenen Code und kreiert ein Paket daraus (Lasst uns der Einfachheit halber annehmen, dass es 1 JS File ist, was nicht immer stimmt). Des weiteren erlaubt uns Webpack npm Pakete als Module zu nutzen wie z. B. React oder jQuery. Diese müssen dann nicht als separate Scripts in das HTML Dokument geladen werden. Das bedeutet: keine globalen Variablen, kein Outscoping und kein unberechenbarer Code mehr.

Manche werden sagen, dass es dafür schon ein Tool namens Browserify gibt. Das ist richtig. Allerdings hat Webpack noch viel mehr auf Lager. Der Hauptzweck von Webpack ist es, mit JavaScript Modulen zu arbeiten und Packages für Browser zu kreieren. Es funktioniert aber auch mit allen möglichen anderen Arten von Assets. Richtig konfiguriert kann es diese Assets verarbeiten und ein oder mehrere Packages kreieren, die dann auf einen Webserver hochgeladen werden, wo dann alles läuft wie geölt. Das bedeutet, wir können SASS oder CSS als Dependency in jedem JS Modul laden.

Was sind Webpack-Module

Alles, was mit dem Befehl “require” (import) in den Code, der von Webpack verarbeitet wird, importiert werden kann, ist ein Webpack-Modul. Das können CommonJS-Module, ES-Module oder AMD sein (diese drei werden nativ von Webpack unterstützt) genauso wie Sass @import in Sass Code (wird nicht nativ unterstützt, aber dank Extension funktioniert es auch). Webpack ist in der Lage sie zu verarbeiten und zu analysieren, wenn es entsprechend konfiguriert wurde. 

Nicht-Javascript Assets, die von Webpack unterstützt werden und die in JS importiert werden können:

  • JSX, Coffee
  • CSS, SASS, Less, Stylus, PostCSS,
  • png, jpeg, svg
  • JSON, YAML, XLM und weitere

Bitte nicht vergessen, dass das Ergebnis von Webpack immer primär ein oder mehrere JS Packages für Browser sind. Das bedeutet, Assets müssen verarbeitet und zu JS konvertiert oder von den Quellmodulen zu individuellen Files extrahiert werden. Webpack nutzt Loaders und Plugins für dieses spezifische Preprocessing von Modulen und verpackt sie unmittelbar bevor das Ergebnis auf dem Filesystem gespeichert wird.

Webpack Zusammenfassung

  • Der Hauptzweck ist es aus modularem JS Code JS Packages zu kreieren, die in Browsern verwendet werden sollen. 
  • Webpack ermöglicht es, so gut wie jede Art von Assets umzuwandeln, zu verarbeiten, zu modifizieren und zu verpacken. 
  • Webpack verwendet Loaders und Plugins um Assets vorzuverarbeiten.
  • Der Input ist ein modularer Webpack Code (d.h. verschiedene Modultypen), der Output ist ein JS Package (sowie weitere Files, sofern das Preprocessing so konfiguriert wurde).

Die Konfiguration von Webpack

Die config Datei von Webpack ist ein Node.js-Modul, das ein config Objekt exportiert. Es kann jeden beliebigen Node.js-Code enthalten, was uns eine Menge Möglichkeiten eröffnet. Zum Beispiel können wir damit Input-Dateien für Webpack dynamisch vom Filesystem laden.

Die Ausgangsbezeichnung der Config Datei lautet webpack.config.js. Webpack wird durch webpack --config ./configs/webpack.config.js ausgelöst.

Das Konfigurationsobjekt enthält 4 wichtige Stichwörter:

  • entry: Das sind die Input Settings
  • output: Das sind die Output Settings
  • module: Das sind die Modul Settings (hauptsächlich Loader Settings)
  • plugins: Die Plugins und deren Settings

Entry

Entry veranlasst den Input von Webpack. Damit erhält Webpack genaue Anweisungen, wo mit dem Erstellen von Packages begonnen werden soll. Jedes einzelne in Entry erwähnte File ist ein Root in einem Dependency Graph (Abhängigkeitsgraph), das zum Erstellen des finalen Packages dient. 

(Falls wir eine Parallele zu Node.js ziehen, dann können wir uns jede Datei am Input des Webpacks als eine Datei vorstellen, die wir Node.js zur Interpretation überlassen. Das Ergebnisbundle Webpack können wir dem Browser übergeben und es wird als ein valider modularer Code funktionieren.)

Entry wird auf folgende Arten verwendet:

  • string  –  path to file => ein Entry Eintrag, ein Package
  • array – array of paths to files => mehrere Entry Einträge, ein Package
  • object — object of named paths to files => mehrere Entry Einträge, mehrere Packages

Innerhalb einer Konfiguration können wir Key context verwenden und damit die relativen Pfade in Entry mit diesem Kontext verbinden.

Regel: ein Entry-Eintrag zu einer HTML-Seite, SPA = ein globaler Entry-Eintrag, MPA = mehrere Entry-Einträge
Beispiel Entry Config:

module.exports = {  
  context: path.resolve(__dirname),  
  entry: './index.js',  
  // entry: ['./index.js', './login.js'],  
  // entry: {  
  //   index: './index.js',  
  //   login: './login.js',  
  // },  
}

Output

Output regelt den Output von Webpack. Es ist ein Objekt mit Keys:

path

Der Pfad zum Output-Folder. Relativ zu context.

filename

Output Name:

  • Wenn der Input ein String ist oder ein Array ist, ist der Wert ein einzelner String, der als Package-Name genutzt und im path gespeichert wird. 
  • Wenn der Input ein Objekt ist, ist der Wert ein Template String um die Package Namen zu generieren. Zum Beispiel bedeutet [name].js, dass die Keys von einem Input Objekt benutzt werden. Wir könnten alternativ auch [chunkhash].js nutzen, um das Package zu speichern, denn sein Hash wurde von Webpack generiert.

publicPath

Der public Path zu den Ausgabedateien (und das ist wichtig, wenn andere Werte als CSS Files ausgegeben werden) wird von Loadern genutzt (z.B. von URL-Loadern) und von Plugins. Dieser Pfad wird vom Webserver aufgerufen.

Wenn das Root des Webservers identisch ist mit dem Pfad, dann sollte der publicPath / sein.

Beispiel Output Config:

module.exports = {  
  context: path.resolve(__dirname),  
  entry: './index.js',  
  // entry: ['./index.js', './login.js'],  
  // entry: {  
  //   index: './index.js',  
  //   login: './login.js',  
  // },  
  output: {  
    path: path.join(__dirname, 'build'),  
    publicPath: '/',  
    filename: 'bundle.js',  
    // filename: '[name].js',  
    // filename: '[chunkhash].js',  
  }  
}

**Wenn wir Webpack nur dazu benutzen, um Packages aus einem modularen ES5 Javascript Code zu bauen, dann genügt es, Entry und Output festzulegen.**

Loader

Loaders sind Transformationen, die je Modul angewendet werden. Sie werden bei bestimmten Modultypen angewendet, bevor das Package zusammengestellt wird. Im Modul Key of Config herrschen bestimmte Regeln, in welchen Fällen Loaders angewendet werden sollen.

Jede Regel basiert auf einem regulären Ausdruck, um zu testen, ob der Loader benutzt werden sollte. Der Loader hat seinen eigenen Namen und seine eigenen Optionen:

module.exports = {  
module: {  
  rules: [  
    {  
      test: /.js$/  
      use: [  
        {  
          loader: 'babel-loader',  
          options: {  
            presets: ['react']  
          }  
        }  
      ]  
    },  
    {  
      test: /.sass$/,  
      use: [  
        {  
          loader: 'style-loader',  
        },  
        {  
          loader: 'css-loader',  
        },  
        {  
          loader: 'sass-loader',  
        }  
      ]  
    }  
  ]  
}  
}

Gibt es mehrere Loaders, werden sie beim Modul mittels einer Von-unten-nach-oben-Strategie eingesetzt. Im Beispiel oben haben wir Sass, CSS und einen Style Loader speziell für Files mit .sass-Endung angewendet.

Loaders, die in config definiert sind, werden automatisch angewendet, wenn es ein regexp Match für den Modulimport gibt. Beim obigen Beispiel würde der Loader zur Anwendung kommen bei require('../sass/main.sass').

Loaders müssen nicht unbedingt in der Konfiguration definiert sein. Alternativ kann das auch in der Reqire Clause geschehen: require('style-loader!css-loader!sass-loader!../sass/main.sass'). Diese Art der Loader-Nutzung hat Vorrang vor dem Weg über Config und die Loaders werden von rechts nach links angewandt. Die Loader Konfiguration erfolgt via Query String require('babel-loader?presets=['react']!./component.jsx').

Wenn wir Loaders nutzen, dann können wir JSX zu ES5 js, ES6 js zu ES5 Js, Sass to CSS, Images zu Base64, json Files zu js Objekten, etc. transpilieren.

Der letzte Loader in der Pipeline muss Javascript sein (weil Webpack ein JS Modulbündler ist). Deswegen ist es notwendig alle Nicht-Browser-JS-Module in ES5 JS zu konvertieren, weil Webpack hauptsächlich JS Packages erzeugt. Deswegen nimmt der Style-Loader CSS und konvertiert es zu JS, welches dann das Original-CSS dynamisch in das HTML-Head Dokument einfügt. Ähnlich muss auch mit den anderen Nicht-JS-Modulen verfahren werden.

Plug-Ins

Plugins sind Transformationen pro Bündel bzw. Packet. Sie werden für Funktionalitäten verwendet, die nicht von den Loaders unterstützt werden. Das können bspw. Style Extractions sein oder das Separieren von Vendors in standalone Packages oder Obfuscation (Verschleierung, Minifizierung, Verhässlichung) von Javascript sowie das Setzen von globalen Variablen. 

Plugins werden im plugins Key von Config konfiguriert. Das ist eine Reihe von Plugin-Instanzen (ein einzelnes Plugin kann mehrfach verwendet werden).

Beispiel für die Verwendung eines Plugins:

module.exports = {  
  plugins: [  
    new ExtractTextPlugin({  
      name: 'bundle.css'  
    }),  
    new webpack.optimize.UglifyJsPlugin({  
      minimize: true  
    }),  
 ]  
}

Wie schon zuvor erwähnt, kann Webpack mehr als nur Javascript Packages erzeugen. Wenn wir CSS nicht als Code zu den JS Packages hinzufügen (mit Style-Loader), sondern es als standalone CSS Bundle extrahieren wollen, können wir ExtractTextPlugin verwenden. Webpack erzeugt dann mittels Plugin das File bundle.css in output.path. Dieses enthält aufgrund des Plugins, das auf das gesamte JS Package angewendet wurde, das komplette CSS des JS Packages.

Und so funktioniert es

  1. Webpack erzeugt Dependency-Diagramme, deren Roots in entry beschrieben werden. 
  2. Es geht tief in die Module hinein, die an den Entry-Points notwendig sind und wendet Loaders nach Regeln an, sie zu transpilieren oder transformieren. 
  3. Sobald Webpack das gesamte Dependency-Diagramm für einen gewählten Entry-Point abgeschlossen hat, kann es das Package erzeugen. Zuerst werden aber die spezifizierten Plugins angewendet und Dinge wie Minifizierung, Verhässlichung (Verschleierung) und die Extraktion von Vendor Packages werden ebenfalls durchgeführt. 
  4. Am Ende speichert Webpack das Package in output.

Beispiel Konfiguration

const webpack = require('webpack');  
const path = require('path');  
const Extract = require('extract-text-webpack-plugin');

module.exports = {  
  context: path.resolve(__dirname),  
  entry: './index.js',  
  output: {  
    path: path.join(__dirname, 'build'),  
    filename: 'index.js',  
  },  
  module: {  
    rules: [  
      {  
        test: /**.**js$/,  
        use: [  
          {  
            loader: 'babel-loader',  
            options: {  
              presets: ['latest', 'react']  
            }  
          }  
        ]  
      }, {  
        test: /**.**sass$/,  
        loader: Extract.extract({  
          fallback: 'style-loader',  
          use: 'css-loader!sass-loader'  
        })  
      },  
    ]  
  },  
  plugins: [  
    new Extract({  
      filename: 'bundled-sass.css',  
    }),  
    new webpack.optimize.UglifyJsPlugin({  
      minimize: true,  
    }),  
    new webpack.EnvironmentPlugin([  
      'NODE_ENV',  
    ]),  
  ]  
};

Module Bundler vs. Task Runner

Gulp and Grunt sind Task Runner. Sie werden zur Automatisierung von Tasks verwendet, die der User vorher in der Programmiersprache oder in der Konfiguration festlegt. Solche Tasks können Compilations, Transpilations, Linting, Testing, etc. sein. Wichtiger ist, dass jedes Task mit einem Asset arbeitet (CSS, Javascript, etc.) und es liegt am Programmierer, ob das Ergebnis korrekt ist, z.B. der Pfad zu den Images stimmt.

Wie wir vorher schon besprochen haben, ist Webpack eine Utility, die hauptsächlich mit Javascript arbeitet und es uns ermöglicht, modulare Codes gebündelt in ein Package für den Browser zu schreiben. So gesehen ist es ein einziges Task, Webpack laufen zu lassen.

Weder Gulp noch Grunt untersuchen den Code, der ihnen geschickt wird. Sie führen nur bestimmte Aufgaben mit dem Code aus. Im Gegensatz dazu analysiert Webpack den zugespielten Code und editiert und verarbeitet ihn, je nach Konfiguration.

Das ist der Hauptunterschied. Webpack ist ein einziges Task auf Basis von Javascript Code. Es ist so ähnlich wie ein Task für den Sass Compiler laufen zu lassen nur ein bisschen komplexer, wenn man bedenkt, dass da auch komplexere Transformationen im Code enthalten sind.

Wann kommt Webpack zum Einsatz bei App Entwicklung?

  • Wenn du einen modularen JavaScript Code für den Browser schreibst.
  • Wenn du JS Standards nutzt, die noch nicht implementiert sind (ES6+).
  • Wenn du SPA in React schreibst. 
  • Wenn du MPA schreibst und einen modernen Code, etc. haben willst.

Ich habe JavaScript nur erwähnt, weil du andere Assets meiner Meinung nach nur erwägen solltest, wenn du Webpack für JavaScript nutzen willst, da du sie in Javascript importieren musst. Es ergibt keinen Sinn Webpack für Sass zu nutzen, wenn da nur Sass und CSS sind. Dann bist du besser bedient mit sass --watch sass:css.

Ich persönlich nutze Webpack ständig. Wenn ich kleine JS Snippets schreibe oder auch an größeren Projekten arbeite, gestattet es mir, einen sauberen, lesbaren und modernen Code zu schreiben, der ohne größere Umstände verschleiert werden kann.

Da ich Webpack häufig nutze, habe ich Loaders für Sass und PostCSS hinzugefügt und kann jetzt auch moderne StyleSheets schreiben.

Ein zusätzlicher Nutzen ist, dass andere Assets wie Images und Fonts damit verbessert werden können.

Ist es sinnvoll, Task Runners bei Webpack einzusetzen?

Ja, das ist es! Wenn du Gulp nutzt und Features von Webpack brauchst, dann gibt es ein Plugin für Gulp, mit dem du Webpack als Task laufen lassen kannst. 

Wenn du Sass nicht in JavaScript importieren willst, während du Webpack mit Gulp nutzt, dann separiere die Sass Transpilation in ein eigenes Task.

Aber

Wie ich oben schon erwähnt habe, können Task Runners Minifizierung, Transpilation, Linting und Testing laufen lassen. Webpack kriegt das auch mit ein bisschen Konfiguration hin. Deshalb gibt es keinen Grund Task Runners einzusetzen, wenn du schon Webpack nutzt.

Du kannst export NODE_ENV=production; webpack --progress --config webpack.config.js laufen lassen oder diesen Befehl zu package.json hinzufügen und yarn run deploy.prod laufen lassen. Das Ergebnis wird das gleiche sein wie mit dem Task Runner. Du hast die Wahl, welche Lösung dir besser gefällt oder welche besser in den Dev Flow deines Unternehmens passt.

Nutze Yarn

Bestimmt kennst auch du die Situation, dass Dev Instance läuft wie geschmiert, aber eine App die per CI an die Produktion verteilt wurde, funktioniert überhaupt nicht. In den meisten Fällen sind daran die npm Packages schuld.

Aus einem einfachen Grund: Während der Verteilung werden die Packages in package.json installiert. Dies kann dazu führen, dass eine neuere Version eines Packages installiert wird. Also eine andere Version, als die, die du lokal nutzt. Das führt dann zum Stillstand. Die Lösung: Yarn.

Yarn ist ein Packaging-System für Node.js. Wir nutzten bereits ein solches System: npm. Yarn arbeitet, genauso wie npm mit NPM Repository und nutzt package.json. Das heißt, du kannst damit die gleichen Pakete installieren wie du es mit npm tun würdest. Warum solltest du es dann benutzen? Nun, es hat eine andere Philosophie, andere Befehle und am allerwichtigsten: Es hat ein Lockfile.

Läuft yarn install, wenn package.json und yarn.lock im Root des Projekts vorhanden sind, dann wird Yarn angewiesen package.json zu ignorieren und die gesperrte Version des yarn.lock Files zu installieren. Die CI installiert daraufhin die gleichen Package-Versionen, die du gewählt hättest. Das ist nicht nur bei CI/CD nützlich, sondern auch, wenn du mit mehreren Entwicklern am gleichen Projekt arbeitest. 

Diese Funktionalität kennt man schon von z.B. Composer für PHP, aber npm fehlt dieses Feature leider.

Yarn behauptet auch schneller zu sein als npm und soll auch offline funktionieren. Ich persönlich finde den Lockfile Mechanismus nützlicher.

Nutze Yarn, es wird dein Leben leichter machen!

Nachtrag 6. 6. 2017: Zusätzlich würde ich gerne erwähnen, dass NPM zwar shrinkwrap besitzt, dieser aber bis vor kurzem nicht als Lockfile des höher erwähnten Composers funktioniert hat.

Marek Janča
Marek Janča
Front-end Web Developer

Beratungsbedarf? Lassen Sie uns über Ihr Projekt sprechen.