Was haben wir bisher erreicht? Im ersten Teil haben wir mittels webpack unser erstes Applikationsbundle erzeugt und haben im zweiten Teil damit eine kleine Beispielanwendung erstellt.

Hierfür wurde eine Unterscheidung zwischen Entwicklung und Produktion eingeführt, um die Ausgabe für das Produktionsdeployment zu optimieren.

Im diesem Teil führen wir Babel ein, um andere Sprachvarianten in reines JavaScript zu übersetzen. Außerdem wird die Verarbeitung von weiteren Dateitypen behandelt, damit diese wie alle anderen Abhängigkeiten im Code als Modul importiert werden können. Da die webpack-Konfiguration sehr groß wird, wird diese in zwei Dateien aufgeteilt. Einen allgemeinen Teil, und einen speziellen Teil der die unterschiede der beiden Umgebungen (Produktion und Entwicklung) beinhaltet.

Einführung von Babel

Der Markdown Previewer wird auf die neuere ECMAScript Syntax umgestellt (hier: Pfeilfunktionen, const).

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

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

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

export default {
  attachPreviewer: attachPreviewer
}

Die fertige Applikation wird trotz der neuen Syntax auch mit einem aktuellen Browser funktionieren, da die beiden Konstrukte in modernen Browsern implementiert sind, aber um sicher zu stellen, das auch ältere Browser den Code ausführen können wird Babel als Übersetzer installiert.

Dazu installiert man das babel Grundpaket, den zugehörigen webpack-Loader und die gewünschten Sprachpresets.

$ yarn add -D babel-core babel-loader
$ yarn add -D babel-preset-env

Damit Babel für alle JavaScript Dateien verwendet wird, muss dies in der webpack.config.js mittels einer Regel konfiguriert werden, die die betroffenen Dateien selektiert. Der zusätzliche resolve-Abschnitt sorgt dafür, das die angegebenen Dateiendungen bei Importen automatisch aufgelöst werden.

webpack.config.js (für dev & prod)

            },
/* begin neuer code */
            resolve: {
                extensions: [ '.jsx', '.js' ]
            },
            module: {
                rules: [
                    {
                        test: /\.jsx?$/,
                            exclude: /(node_modules)/,
                        use: {
                            loader: 'babel-loader'
                        }
                    }
                ]
            },
/* ende neuer code */          
            plugins: [

Die Babel-Konfiguration befindet sich in der Datei .babelrc im Projekthauptverzeichnis.

.babelrc:
{
  "presets" : [
    "env"
  ]
}

Nun kann man die Anwendung neu erstellen. Der resultierende Code in dem Applikationsbundle ist reiner JavaScript Code, der auch von älteren Browsern ausgeführt werden kann. Für die Übersetzung von Typescript ergibt sich ein ähnliches Vorgehen. Man benötigt einen webpack-Loader (ts-loader), eine Regel für die Dateinamen (test: /.tsx?$/,) und eine Konfiguration für den Typescript Compiler (tsconfig.json). Dies wird in einer späteren Folge hinzugefügt.

Für unser Beispielprojekt werden aber erst einige andere Dateitypen hinzugefügt.

Behandlung von CSS-Dateien, Bildern, Schriftarten und anderen Dateien

Damit die Beispielanwendung ein angepasstes Aussehen erhält, werden als nächstes eine zugehörigen css-Datei behandelt.

Diese wird initial angelegt und für den ersten Test manuell der html-Vorlage hinzugefügt.

$ mkdir src/assets/stylesheets
src/assets/stylesheets/index.css:
html {
  color: #111111;
  background-color: #EEEEEE;
  font-family: avenir next, avenir, helvetica, sans-serif;
}

textarea {
  color: black;
  background-color: white;
}
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">
<!-- begin neuer code -->
  <link rel="stylesheet" href="index.css">
<!-- ende neuer code -->
</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 Projekt wird erstellt und die css-Datei in den dist-Ordner kopiert.

$ yarn dev
$ cp src/assets/stylesheets/index.css dist/dev

Bei dem Stylesheet haben wir das gleiche Problem wie bei dem eigentlichen Applikationsbundle. Die verwendeten Stylesheets sollen automatisch zu einem Bundle zusammengefasst, im Ausgabeverzeichnis plaziert und im HTML Template hinterlegt werden.

Dafür wird das css-loader Modul und als Fallback das style-loader Modul verwendet. Diese Module fügen die verwendeten CSS-Dateien in das Applikationsbundle als Module ein. Im Anschluß an die Verarbeitung, werden die CSS-Teile in eine einzelne CSS-Datei extrahiert.

Die verwendeten Bilder sollen, bis zu einer definierten Größe, mit dem url-loader Modul als Data-URI direkt in das Applikations- bzw. das CSS-bundle integriert werden (Grafik/Grafiken mit Data-URI). Dies reduziert die Anzahl der kleinen Anfragen an den Webserver.

Optional können die Bilder voher noch mit dem image-webpack-loader optimiert werden, damit die Ladezeiten durch die verringerten Größen verkürzt werden.

Alle anderen Dateiarten wie z.B. Fonts, Icons und sonstige Dateien werden per file-loader direkt im Zielverzeichnis abgelegt. Diese sollen aber beim Produktionsdeployment auch einen Hashwert im Dateinamen erhalten.

Als erstes installieren wir die benötigten Module und erstellen Verzeichnisse, in denen die jeweiligen Dateitypen abgelegt werden.

$ yarn add -D extract-text-webpack-plugin
$ yarn add -D css-loader style-loader
$ yarn add -D url-loader file-loader
$ mkdir -p src/assets/fonts src/assets/icons src/assets/images src/assets/media

Aufteilung der webpack-Konfiguration

Damit die webpack-Konfiguration nicht zu groß und unübersichtlich wird, wird der gleichbleibende Teil in eine neue Datei ausgelagert und von der zentralen Konfiguration importiert.

Zuerst der Teil, der sich in beiden Umgebungen unterscheidet.

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');
/* beginn neuer code */
    const ExtractTextWebPackPlugin = require('extract-text-webpack-plugin');
/* ende neuer code */

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

/* beginn neuer code */
    const common = require('./config/webpack.common');
    let webpackConfig;

    // prepare configuration (dev or prod)
    if (isProductionBuild) {
        webpackConfig = common(entryName, '[hash]-');

        webpackConfig.output =  {
            path : path.resolve(__dirname, 'dist/prod'),
            filename : '[hash]-bundle.js'
        };

        webpackConfig.plugins.push(
            new ExtractTextWebPackPlugin({ filename: '[hash]-styles.css', disable: false, allChunks: true }),            
            new UglifyJsPlugin()
        );
    } else {
        webpackConfig = common(entryName, '');

        webpackConfig.output =  {
            path : path.resolve(__dirname, 'dist/dev'),
            filename : 'bundle.js'
        };

        webpackConfig.plugins.push(
            new ExtractTextWebPackPlugin({ filename: 'styles.css', disable: false, allChunks: true })
        );        
    }

    // console.log("Webpack-Config: " + JSON.stringify(webpackConfig, null, 4));
    return webpackConfig;
}
/* ende neuer code */

Als erstes wird der gemeinsame Code als Funktion mittels require eingebunden. Danach wird im jeweiligen Umgebungsteil diese Funktion mit der Start-Datei (index.js) und den Dateinamensprefix (mit oder ohne Hash) als Parameter aufgerufen und das Ergebnis in der Variablen webpackConfig gespeichert. Alle weiteren Konfigurationsteile werden der Variablen hinzugefügt und am Ende an webpack übergeben. Die auskommentierte Konsolenausgabe kann dazu genutzt werden, um den gesamten Inhalt der Konfiguration vor der Ausführung auszugeben.

Der gemeinsame Konfigurationsteil enthält alle Regeln und Parameter, die sich nicht mehr ändern bzw. die man über die beiden Übergabeparameter steuern kann.

config/webpack.common.js:
const path = require('path');
const webpack = require('webpack');
const HtmlPlugin = require('html-webpack-plugin');
const ExtractTextWebPackPlugin = require('extract-text-webpack-plugin');

module.exports = (entry, prefix) => {
    return {
        entry : entry,
        resolve: {
            // Automatisches erkennen von bestimmten Dateitypen und Verzeichnisnamen bei den Importen
            extensions: [ '.js', '.jsx', '.json', '.css', '.jpeg', '.jpg', 'png', '.gif', '.svg' ], // Automatically resolve certain extensions
            alias : {
                images: path.resolve(__dirname, '../src/assets/images'),
                icons: path.resolve(__dirname, '../src/assets/icons'),
                stylesheets: path.resolve(__dirname, '../src/assets/stylesheets'),
                media: path.resolve(__dirname, '../src/assets/media')
            }
        },
        module: {
            rules: [
                // JavaScript Dateien
                {
                    test: /\.jsx?$/,
                    exclude: /node_modules/,
                    loader: "babel-loader"
                },

                // Cascading Style Sheets
                {
                    test: /\.(css|scss|sass)$/,
                    use: ExtractTextWebPackPlugin.extract({
                        use: [
                            { 
                                loader : 'css-loader',
                                options : {
                                    importLoaders: 0
                                    // 0 => no loaders (default); 1 => postcss-loader; 2 => postcss-loader, sass-loader
                                }
                            }
                        ],
                        fallback: 'style-loader'
                    })
                },

                // Bilder
                {
                    test: /\.(jpe?g|png|gif|svg)$/i,
                    use: [
                        {
                            loader: 'url-loader',
                            options: {
                                limit: 8192, // Convert images < 8kb to base64 strings
                                context : 'src/assets/images',
                                name : 'images/' + prefix + '[name].[ext]'
                            }
                        }
                    ]
                },

                // Schriftarten 
                //
                // Hinweis: Es wird davon ausgegangen, das sie sich in einem 
                // Verzeichnis mit dem Namen fonts befinde, da sonst font-svg Dateien 
                // in images/ (siehe Bilder) abgelegt würden.
                {
                    test: /fonts\/.*\.(svg|eot|ttf|woff|woff2)$/i,
                    use: [
                        {
                            loader : 'file-loader',
                            options : {
                                name : 'fonts/' + prefix + '[name].[ext]'
                            }
                        }
                    ]
                },

                // Icons
                {
                    test: /\.(ico|icns)$/,
                    exclude: /node_modules/,
                    use: [
                        {
                            loader : 'file-loader',
                            options : {
                                name : '[name].[ext]'
                            }
                        }
                    ]
                },

                // sonstige Dateien im Verzeichnis media
                {
                    test: /\.(txt|raw|bin|json)$/,
                    exclude: /node_modules/,
                    use: [
                        {
                            loader : 'file-loader',
                            options : {
                                name : 'media/' + prefix + '[name].[ext]'
                            }
                        }
                    ]
                }            
            ],
        },
        plugins : [
            new HtmlPlugin({
                template: path.resolve(__dirname, '../src/assets/index.template.html')
            })
        ]
    }
}

Zum testen der Konfiguration legen wir einige Dateien in den jeweiligen Verzeichnissen ab:

Datei Ordner
Artifika-Regular.ttf fonts
SIL Open Font License.txt media
favicon.ico icons
markdown-logo.svg images
DARPABigData.jpg images
spinner.css stylesheets

Damit die Dateien benutzt werden, werden sie wie ein Modul in der jeweiligen JavaScript- / CSS-Datei hinzugefügt. Die Dateiendungen, die im resolve-Block definiert sind, können optional weggelassen werden. Innerhalb der JavaScript Dateien müssen keine relativen Pfade angegeben werden, da die Verzeichnisaliase diese auflösen. Somit muss man sich keine Gedanken um den Import machen, wenn man eine Quelldatei in einen anderen Ordner verschiebt. Einzig die für die url-Statements in den CSS-Dateien habe ich noch keine saubere Lösung gefunden. Dort muss der Pfad relativ zur css-Datei angegeben werden.

Somit haben wir für die gängigsten Dateitypen jeweils eine Regel definiert und Webpack kümmert sich durch die Loader um die weitere Verarbeitung.

In der HTML Vorlage kann die Zeile wieder entfernt werden.

src/index.js:
import 'icons/favicon.ico';
import 'media/SIL Open Font License.txt';
import 'stylesheets/index.css';

// import spinner to display while loading logo
import spinner from './spinner';
import markdownPreviewer from './markdownPreviewer';

// import logo image resource
import logo from 'images/markdown-mark-solid.svg';

// import only to demonstrate that big images are written to 
// the dist directory. bigImage is not used.
import bigImage from 'images/DARPABigData.jpg';

const rootElement = document.getElementById('root');

/**
 * load an asset and call the function when finished
 *
 * name     : filename string
 * callback : function to call when finished
 */
function loadAsset(name, callback) {
  spinner.showSpinner();

  fetch(logo)
    .then((response) => {
        if (response.status !== 200) {
            console.log('Looks like there was a problem. Status Code: ' + response.status);
            return;
        }
        return response.blob();
    }).then( imageBlob => {
        // hide spinner and call main application
        spinner.hideSpinner();
        mainApp(imageBlob);
     }, error => {
        document.write('<h2>Error loading assets.<br>' + error + '</h2>');
        return;
     });
}

/**
 * main application
 *
 * imageBlob: takes the loaded asset and runs main app
 */
function mainApp(imageBlob) {
    rootElement.hidden = false;

    // display image
    let img = document.createElement('img');
    img.src = URL.createObjectURL(imageBlob);
    img.width=100;
    img.height=100;
    document.getElementById('logo').appendChild(img);

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

/**
 * hide root element and preload the asset
 * then run the main application
 */
window.onload = function() {
  // hide mainApp Element
  rootElement.hidden = true;

  // preload Assets and then call mainApp
  loadAsset(logo, mainApp);
};
assets/stylesheets/index.css:
@font-face {
    font-family: artifika; 
    src: url(../fonts/Artifika-Regular.ttf);
}

html {
    color: #111111;
    background-color: #EEEEEE;
    font-family: artifika, avenir next, avenir, helvetica, sans-serif;
}
assets/index.template.html:
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8" />
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <link rel="icon" type="image/x-icon" href="favicon.ico" />
  <title>WebApp</title>
  <meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
  <div id="logo"></div>
  <div id="root">
    <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>
  </div>
</body>
</html>

Jetzt kann man das fertige Projekt erstellen. Alle eingebundenen Dateien werden automatisch in die Ausgabe übernommen. Das Logo befindet sich im Bundle, da es unter 8kb groß ist.

Im nächsten Teil wird der Webpack Entwicklungsserver hinzugefügt, der bei Änderung einer Quelldatei automatisch ein neues Bundle erzeugt und die Änderungen mittels Hot Module Replacement direkt an den Browser überträgt. Durch das direkte Feedback bei einer Änderung wird der Entwicklungsprozess erheblich beschleunigt.

Außerdem wird die webpack Loader-Konfiguration erweitert. Dazu gehört

  • die Erstellung von Source-Maps.
  • die Optimierung von Bildern vor dem Produktionsdeployment,
  • die Verarbeitung von Sass: Syntactically Awesome Style Sheets,
  • die automatische Vergabe von Vendor-Prefixen in den Stylesheets
  • und die Minifizierung der finalen Stylesheet-Datei.

Bis dahin befindet sich der bisherige Stand in meinem gitlab-Projekt unter https://gitlab.com/svenpaass/webpack-template/tree/chapter3

Nächster Beitrag Vorheriger Beitrag