Das folgende Projekt basiert auf dem Beispiel aus Webpack from Nothing. Die Applikation liest ein mit Markdown Syntax gefülltes Textfeld ein und gibt den formatierten Text auf der gleichen Seite aus. Um die Webpack Konfiguration zu testen, wird später für jeden Dateityp eine Beispieldatei hinterlegt.

Nachdem das Projekt erstellt wurde, wird die webpack-Konfiguration aufgeteilt, um für Produktion und Entwicklung unterschiedliche Bundles zu erzeugen. Warum sollte man dies tun? Zum Beispiel um in der Produktion das entgültige Applikationsbundle zu minifizierien. Dies spart Bandbreite und schützt den Code etwas vor neugierigen Blicken.

Also lasst uns beginnen...

Projektaufbau

Um eine saubere Umgebung zu erhalten, löschen wir als erstes den Inhalt des Minimalprojekts aus dem ersten Teil.

$ rm -rf dist
$ rm src/*

Um nicht selbst einen Markdown Parser schreiben zu müssen, bedienen wir uns einer vorhandenen Bibliothek. Diese installiert man wieder mit dem Paketmanager yarn bzw. npm und speichert sie als Projektabhängigkeit.

$ yarn add markdown
$ mkdir dist

Für den ersten Test wird eine einfache HTML Seite manuell angelegt um das fertige Applikationsbundle aufzurufen.

dist/index.html:
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8" />
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <title>WebApp</title>
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <script src="bundle.js"></script>
</head>
<body>
  <h1>Markdown Viewer</h1>
  <form id="editor">
    <textarea id="source" rows="10" cols="80"></textarea>
    <br>
    <input type="submit" value="Anzeigen">
  </form>
  <hr>
  <section id="preview"></section>
</body>
</html>

Das eigentliche Programm liest den Inhalt einer Textarea (id: source) und schreibt das Ergebnis des Markdown Parsers in ein Zielobjekt (id: preview).

src/index.js:
import markdownPreviewer from "./markdownPreviewer";

window.onload = function() {
  document.getElementById("editor").addEventListener("submit",
    markdownPreviewer.attachPreviewer(
      document,    // pass in document
      "source",    // id of source textarea
      "preview"    // id of preview DOM element
    )
  ); 
};

Hinweis: Es wird anstatt dem default-Export bei der Bibliothek der Member markdown importiert. Dadurch muss man nicht markdown.markdown.toHTML() aufrufen. Mehr zum Import von Modulen findet sich unter anderem im Mozilla Developer Network.

src/markdownPreviewer.js:
import { markdown } from "markdown";

var attachPreviewer = function($document,sourceId,previewId) {
  return function(event) {
    var text    = $document.getElementById(sourceId).value,
        preview = $document.getElementById(previewId);

    preview.innerHTML = markdown.toHTML(text);
    event.preventDefault();
  };
}

export default {
  attachPreviewer: attachPreviewer
}

Danach kann das Projekt gebaut und im Browser geöffnet werden.

$ yarn webpack

Das Ergebnis:

Minifizierung und bauen für die Produktion

Die bisher erzeugten Bundles funktionieren, sind aber noch ziemlich groß. Außerdem sollten sie so nicht auf einem CDN Server abgelegt werden, da der Inhalt lange gecached wird und damit neue Versionen der Applikation nicht sofort wirksam werden.

Das erste Problem lässt sich lösen, in dem das Applikationsbundle vor Produktionsdeployment minifiziert wird.

Nach dem Minifizieren soll das endgültige Bundle bei jeder Änderung des Inhalts einen neuen Namen bekommen, damit keine gecachte Version geladen wird, sondern immer die aktuellste Version (What is Cache Busting?).

Dies wird mit einem Hash im Dateinamen erreicht womit sich aber das nächste Problem ergibt. Man muss die HTML Datei jedes mal anpassen, wenn sich der Dateiname ändert. Daher soll diese automatisch aus einer Vorlage erstellt werden.

Aufteilen der Konfiguration in Produktion und Entwicklung

Da es sich bei der webpack-Konfiguration um eine JavaScript Datei handelt, die von NodeJS ausgeführt wird, kann man auch Umgebungsvariablen abfragen. Diese kann auch direkt als Kommandozeilenoption an webpack übergeben. Als Konvention hat sich NODE_ENV etabliert um eine Umgebung festzulegen. Für die Minifizierung installieren wir die aktuellste Version des uglifyjs-webpack-plugin Moduls, auch wenn webpack bereits eine Version des Moduls enthält, die bei Nutzung der -p Option verwendet wird. Die mitgelieferte Version ist aber zu alt und hat Probleme mit neuerem ECMAScript.

$ yarn add -D uglifyjs-webpack-plugin

Die folgende webpack Konfiguration macht das gleiche wie die alte Konfiguration. Nur wenn NODE_ENV den Wert production enthät, wird zusätzlich das Uglify Plugin verwendet und dem Dateinamen des Applikationsbundles ein Hashwert vorangestellt.

webpack.config.js:
// get environment variables from webpack command line
module.exports = env => {
    const path = require('path');
    const UglifyJsPlugin = require('uglifyjs-webpack-plugin');

    // application entry point
    const entryName = './src/index.js';
    const isProductionBuild = (env !== undefined && env.NODE_ENV === 'production') ? true : false;

    // prepare configuration (dev or prod)
    if (isProductionBuild) {
        return {
            entry: entryName,
            output: {
                path : path.resolve(__dirname, 'dist/prod'),
                filename : '[hash]-bundle.js'
            },
            plugins: [
                 new UglifyJsPlugin()
            ]
        };
    } else {
        return {
            entry: entryName,
            output: {
                path : path.resolve(__dirname, 'dist/dev'),
                filename : 'bundle.js'
            }
        };
    }
}

In der package.json Datei ersetzen wir den scripts Teil um zwei Alias-Kommandos anzulegen, die webpack mit der jeweiligen Konfiguration aufruft.

package.json:
  "scripts": {
    "build:dev": "webpack --config webpack.config.js --display-error-details",
    "build:prod": "webpack --config webpack.config.js --display-error-details --env.NODE_ENV=production"    
  },

Die beiden Varianten lassen sich wir folgt erstellen.

$ yarn build:dev
$ yarn build:prod

Ein Vergleich der Dateigrößen zeigt wie groß das Einsparpotential ist. Von ca. 77kb reduziert sich die Bundlegröße auf ca. 26kb.

  • Ohne Minifizierung:
$ wc -c dist/dev/bundle.js
   77841 dist/dev/bundle.js
  • Mit Minifizierung:
$ wc -c dist/prod/*-bundle.js
   25651 dist/prod/b18379d32183e6d8e31a-bundle.js

Generierung der HTML Datei aus einer Vorlage

Man erkennt an der produktiven Ausgabe, das das Applikationsbundle automatisch einen Hash-Wert im Dateinamen enthält. Daher ist es notwendig, das die benötigte HTML Datei als Vorlage angelegt und daraus die endgültige Datei im Ausgabeverzeichnis automatisch erzeugt wird.

Dazu installiert man das html-webpack-plugin und erstellt eine HTML-Vorlage.

$ yarn add -D html-webpack-plugin
$ mkdir -p src/assets
src/assets/index.template.html:
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8" />
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <title>WebApp</title>
  <meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
  <h1>Markdown Viewer</h1>
  <form id="editor">
    <textarea id="source" rows="10" cols="80"></textarea>
    <br>
    <input type="submit" value="Anzeigen">
  </form>
  <hr>
  <section id="preview"></section>
</body>
</html>

In der webpack-Konfiguration aktiviert man das neue Plugin und beim erneuten bauen der Anwendung wird das SCRIPT-Tag vor dem schließenden BODY-Tag eingefügt.

webpack.config.js:
// get environment variables from webpack command line
module.exports = env => {
    const path = require('path');
    const webpack = require('webpack');
    const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
/* begin neuer code */
    const HtmlPlugin = require('html-webpack-plugin');
/* ende neuer code */

    // application entry point
    const entryName = './src/index.js';
    const isProductionBuild = (env !== undefined && env.NODE_ENV === 'production') ? true : false;

    // prepare configuration (dev or prod)
    if (isProductionBuild) {
        return {
            entry: entryName,
            output: {
                path : path.resolve(__dirname, 'dist/prod'),
                filename : '[hash]-bundle.js'
            },
/* begin neuer code */
            plugins: [
                new UglifyJsPlugin(),
                new HtmlPlugin({
                    template: './src/assets/index.template.html'
                })
            ]
/* ende neuer code */                
        };
    } else {
        return {
            entry: entryName,
            output: {
                path : path.resolve(__dirname, 'dist/dev'),
                filename : 'bundle.js'
/* begin neuer code */
            },
            plugins: [
                new HtmlPlugin({
                    template: './src/assets/index.template.html'
                })
            ]
/* ende neuer code */                
        };
    }
}

Bevor wir diesen Abschnitt abschliessen gibt es noch eine Erweiterung, die einem das Leben erleichtert. Es soll vor jedem Build-Vorgang das Ausgabeverzeichnis automatisch gelöscht werden. Dazu werden die Skripte rimraf (emuliert rm -rf) und yarn-run-all bzw. npm-run-all (führt mehrere Kommandos aus der package.json Konfiguration aus) benötigt.

$ yarn add -D rimraf
$ yarn add -D yarn-run-all

Zu den neuen Skripten werden wieder Alias-Kommandos in die package.json Datei eingefügt.

package.json:
  "main": "src/index.js",
/* begin neuer code */      
  "scripts": {
    "clean:dev"  : "rimraf dist/dev",
    "clean:prod" : "rimraf dist/prod",
    "build:dev"  : "webpack --config webpack.config.js --display-error-details",
    "build:prod" : "webpack --config webpack.config.js --display-error-details --env.NODE_ENV=production",
    "clean"      : "rimraf dist",
    "dev"        : "npm-run-all clean:dev build:dev",
    "prod"       : "npm-run-all clean:prod build:prod"
  },     
/* ende neuer code */

Mit den neuen Befehlen lässt sich das Projekt viel einfacher bauen. Das Ausgabeverzeichnis, dessen Inhalt sich jederzeit neu erzeugen lässt, soll nicht in die Versionskontrolle übernommen werden, daher fügen wir es der .gitignore Datei hinzu.

$ yarn dev
$ yarn prod

$ echo dist >> .gitignore
$ yarn clean

Den bisherigen Stand des Projekts findet Ihr in meinem gitlab-Projekt unter https://gitlab.com/svenpaass/webpack-template/tree/chapter2

Im dritten Teil geht es um folgende Themen:

  • Das Tool Babel wird eingeführt, mit dem andere Sprachen bzw. Sprachvarianten in JavaScript übersetzt wird, das der Browser ausführen kann.
  • Außerdem wird die Behandlung von CSS-Dateien, Bildern, Schriftarten und anderen Dateien hinzugefügt. Dadurch lassen sich alle Dateien wie Module behandeln und im Code importieren.
  • Am Schluß wird die webpack-Konfiguration in zwei Dateien aufgeteilt. Einen allgemeinen Teil, und einen speziellen Teil der die unterschiede der beiden Umgebungen (Produktion und Entwicklung) beinhaltet.

Nächster Beitrag Vorheriger Beitrag