Webpack

Eine Snippet-Sammlung für Webpack.

Quellen:

TODO Einheitliche Terminologie: Main-Bundle (statt Haupt-Datei, bundle.js)

Was ist Webpack?

Webpack ist ein Module-Bundler und ein Build-System. Das bedeutet, dass es nicht nur beides tut, es kombiniert beides. Webpack bundelt nicht nur die JavaScript-Abhängigkeiten und baut dann die Assets dazu, es sieht die Assets ebenfalls als Abhängigkeiten.

Alle Abhängigkeiten - in Webpack "Module" genannt - können importiert, manipuliert und am Ende ins finale Bundle gepackt werden. Egal ob es nun JavaScript, CSS, HTML-Snippets oder Bilder sind.

Webpack in Projekt einbinden

npm init -y
npm install --save-dev webpack

Script in package.json definieren:

{
  "scripts": {
    "clean": "rm -rf assets",
    "build": "node_modules/.bin/webpack --progress --colors",
    "watch": "node_modules/.bin/webpack --progress --colors --watch",
    "production": "npm run clean && NODE_ENV=production npm run build --"
  }
}

Hinweise:

Webpack-Konfiguration als webpack.config.js anlegen:

var path = require('path');

module.exports = {
    // Entry point(s) to the application (the root of the dependency tree)
    entry: {
        app: './src/app/index.js',
        someOtherModule: [ './src/bla.js', './src/blubb.js' ] 
    },
    output: {
        path:       path.resolve(__dirname, 'assets'),  // The path where to build
        publicPath: 'assets/',   // The path where to build seen from the browser
        filename:   '[name].js'  // The script to build (e.g. 'app.js')
    },
    resolve: {
        modules: [
            // directories where to look for modules
            path.resolve('./src'),
            'node_modules'
        ]
    }
};

TODO Wofür genau nochmal der publicPath? Allgemein für Dev-Server oder für HMR oder sonstwas?

Gebautes Skript in index.html einbinden:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta http-equiv="x-ua-compatible" content="ie=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">

    <title>My project</title>
</head>
<body>
    <script src="assets/app.js"></script>
</body>
</html>

Projekt einmal bauen:

npm run build

Projekt bei jeder Änderung automatisch bauen (Watch-Mode):

npm run watch

Im Watch-Mode behält Webpack bereits gebaute Module im Arbeitsspeicher und baut nur die geänderten Module neu.

Abhängigkeiten definieren

Durch Webpack kann man im JavaScript-Code Abhängigkeiten anfordern. Dabei werden zwei Stile unterstützt: require (ES5 / CommonJS) und import (ES6).

// ES5
require('WantedModule.js')
var Button = require('./Components/Button').default;   // Man beachte das `.default` am Ende

// ES6
import Button from './Components/Button';

Hinweis: Bei require muss man den Default-Export manuell mit .default auswählen. Das liegt daran, dass require nicht zwischen dem Default-Export und anderen Exports unterscheidet, so dass man selbst den gewünschten Export wählen muss.
Bei import dagegen kann man über die Syntax festlegen, ob man den Default-Export (z.B. import foo from 'bar') oder einen anderen Export meint (z.B. import {baz} from 'bar').

Production Build

Es gibt verschiedene Schalter und Plugins, die man in der Entwicklung nicht braucht, jedoch für den produktiven Build sinnvoll sind.

Plugins installieren:

npm i --save-dev circular-dependency-plugin

In webpack.config.js Production Build einrichten:

const path = require('path')
const webpack = require('webpack')
const CircularDependencyPlugin = require('circular-dependency-plugin')

const production = process.env.NODE_ENV === 'production'

console.log('Building in ' + (production ? 'production' : 'dev') + ' mode')

let plugins = [
    // Plugins for both dev and production go here

    // Detect circular dependencies
    new CircularDependencyPlugin({
        //exclude: /a\.js|node_modules/,  // This is a Regex
        failOnError: true
    })
]

if (production) {
    plugins = plugins.concat([
        // Production plugins go here

        // This plugin minifies all the Javascript code of the final bundle
        new webpack.optimize.UglifyJsPlugin({
            mangle:   { screw_ie8 : true },
            compress: { screw_ie8: true, warnings: false },
            comments: false
        }),

        // This plugins defines various variables that we can set to false
        // in production to avoid code related to them from being compiled
        // in our final bundle
        new webpack.DefinePlugin({
            __SERVER__:      !production,
            __DEVELOPMENT__: !production,
            __DEVTOOLS__:    !production,
            'process.env':   {
                NODE_ENV:  JSON.stringify(process.env.NODE_ENV),
                BABEL_ENV: JSON.stringify(process.env.NODE_ENV)
            }
        })

    ])
}

module.exports = {
    ...

    // Use sourcemaps only in dev build
    // If using less, this must be 'source-map' or 'inline-source-map'
    // (see: https://webpack.js.org/loaders/less-loader/#sourcemaps)
    devtool: production ? false : 'source-map',

    plugins: plugins,
    module: {
        ...
    }
}

Hinweis: Im Attribut devtool kann man neben source-map auch andere Optionen für die Sourcemap-Generierung angeben.

Beispiele von Plugins für den Production-Build:

TODO Beispiel ist Webpack 1

if (production) {
    plugins = plugins.concat([

        // This plugin prevents Webpack from creating chunks
        // that would be too small to be worth loading separately
        new webpack.optimize.MinChunkSizePlugin({
            minChunkSize: 51200 // ~50kb
        })

    ]);
}

Hinweis: HTML und CSS werden automatisch von html-loader und css-loader minimiert, sobald die Option debug auf false gesetzt ist (das ist Default). Hierzu ist also kein Plugin wie das UglifyJsPlugin nötig.

ES6-Code einbinden

Hinweis: Wenn auch React verwendet werden soll, dann Abschnitt "React einbinden" folgen.

Man kann auch sein JavaScript in ES6 (aka ES2015) schreiben und automatisch nach ES5 transpilieren lassen.

Babel-Loader, Babel selbst und das ES2015-Preset lokal installieren:

npm install --save-dev babel-loader babel-core babel-preset-es2015

Regel in webpack.config.js definieren:

module.exports = {
    // ...
    module: {
        rules: [
            {
                test:   /\.js$/,
                include: path.resolve(__dirname, 'src'), // Only pass our code to babel (no libs)
                loader: 'babel-loader',
                options: {
                    presets: [ ['es2015', { modules: false }] ]
                }
            }
        ]
    }
}

Hinweise:

React einbinden

React lokal installieren:

npm install --save react react-dom prop-types

Babel-Loader, Babel selbst und die Presets "ES2015" und React lokal installieren:

npm install --save-dev babel-loader babel-core babel-preset-es2015 babel-preset-stage-0 babel-preset-react babel-plugin-autobind-class-methods

Regel in webpack.config.js definieren:

module.exports = {
    // ...
    module: {
        rules: [
            {
                test:   /\.jsx?$/,
                include: path.resolve(__dirname, 'src'),
                exclude: path.resolve(__dirname, 'src/lib'),
                loader: 'babel-loader',
                options: {
                    presets: [ ['es2015', { modules: false }], 'stage-0', 'react'],
                    plugins: [ 'autobind-class-methods' ].concat(production ? [] : [ 'react-hot-loader/babel' ])
                }
            }
        ]
    }
}

Hinweis: Zu include-Regel und Option modules: false siehe Abschnitt "ES6-Code einbinden".

Jetzt kann React mit ES6 verwendet werden:

import React, { Component } from 'react'
import ReactDOM from 'react-dom'

class MyView extends Component {
    render() {
        return <div>Hallo</div>
    }
}

ReactDOM.render(<MyView />, document.body);

Hinweis: Man kann auch die ES6-Schreibweise zur Definition von React-Klassen verwenden (also class MyView extends Component { ... }). Dann hat man jedoch kein automatisches Function-Binding mehr (d.h. Listener werden mit falschem this aufgerufen). Ein automatisches Function-Binding kann man jedoch auch per Babel-Preset stage-0 bekommen.

CSS einbinden

Hinweis: Wenn auch Less oder Scss verwendet werden soll, dann Abschnitt "Less einbinden" bzw. "Scss einbinden" folgen.

Benötigte Plugins und Loader lokal installieren:

npm install --save-dev extract-text-webpack-plugin css-loader style-loader

webpack.config.js erweitern:

TODO Beispiel ist Webpack 1

var ExtractPlugin = require('extract-text-webpack-plugin');

module.exports = {
    // ...

    plugins: [
        new ExtractPlugin('[name].css')      // Target CSS file
    ],
    module: {
        loaders: [
            {
                test: /\.css$/,
                loader: ExtractPlugin.extract('style', 'css?sourceMap')
            }
        ]
    }
};

CSS in HTML anfordern:

<!DOCTYPE html>
<html>
<head>
    <link rel="stylesheet" href="assets/app.css">
</head>
<body>
    // Static markup goes here

    <script src="assets/app.js"></script>
</body>
</html>

Im JavaScript-Code benötigtes CSS anfordern:

// ES5
require('./MyComponent.css');

// ES6
import './MyComponent.css';

Hinweise:

TODO devtool

TODO autoprefixer

TODO Beispiel ist Webpack 1

{
  // When you encounter SCSS files, parse them with node-sass, then pass autoprefixer on them
  // then return the results as a string of CSS
  test: /\.scss/,
  loaders: ['css', 'autoprefixer', 'sass']
}

TODO autoprefixer-loader ist deprecated. Statt dessen postcss-loader verwenden.

TODO Link: autoprefixer

TODO index.html erweitern

less einbinden

Hinweis: Einbindung von SCSS funktioniert analog. Allerdings wird sass-loader node-sass statt less-loader less installiert, der Test wird auf /\.scss$/ gesetzt und dann der sass-loader statt dem less-loader verwendet.

Benötigte Plugins und Loader lokal installieren:

npm install --save-dev extract-text-webpack-plugin@beta css-loader style-loader less-loader less

Hinweis: Aktuell (Stand 13.02.17) ist die Stable-Version vom extract-text-webpack-plugin noch eine 1.x-Version und nicht kompatibel zu Webpack 2. Daher muss mit @beta installiert werden, damit eine 2.x-Version gezogen wird.

webpack.config.js erweitern:

var ExtractPlugin = require('extract-text-webpack-plugin')

var plugins = [
    // Plugins for both dev and production go here

    // Extract CSS into separate file
    new ExtractPlugin({
        filename: '[name].css'   // Target CSS file
    })

]

module.exports = {
    ...

    // Use sourcemaps only in dev build
    // Must be 'source-map' or 'inline-source-map'
    // (see: https://webpack.js.org/loaders/less-loader/#sourcemaps)
    devtool: production ? false : 'source-map',

    plugins: plugins,
    module: {
        rules: [
            {
                test: /\.(less|css)$/,
                // We don't extract our CSS in dev mode in order to make hot-reload work
                use: production ?
                    ExtractPlugin.extract({
                        fallback: 'style-loader',
                        use: ['css-loader', 'less-loader']
                    }) :
                    [
                        { loader: 'style-loader', options: { sourceMap: true } },
                        { loader: 'css-loader',   options: { sourceMap: true, importLoaders: 1 } },
                        { loader: 'less-loader',  options: { sourceMap: true } }
                    ]
            }
        ]
    }
};

Hinweis: Laut Doku vom less-loader muss devtool entweder 'source-map' oder 'inline-source-map' sein, damit Sourcemaps funktionieren.

index.html erweitern:

...
<head>
    ...
    <link rel="stylesheet" href="assets/app.css">
</head>
...

Bootstrap einbinden

Wenn man less eingebunden hat, kann man recht einfach Bootstrap verwenden - am besten mit einem Custom-Build. So werden nur die Teile eingebunden, die man auch verwendet und man kann die Variablen umdefinieren.

Bootstrap lokal installieren:

npm install --save bootstrap

Bootstrap-Haupt-Datei kopieren:

mkdir src/app/style
cp node_modules/bootstrap/less/bootstrap.less src/app/style/custom-bootstrap.less

custom-bootstrap.less anpassen, nicht benötigte Teile auskommentieren:

// Copied and adjusted from: node_modules/bootstrap/less/bootstrap.less

@bootstrap-src: '../../../node_modules/bootstrap/less';

/*!
 * Bootstrap v...
 */

// Core variables and mixins
@import "@{bootstrap-src}/variables.less";
...
//@import "@{bootstrap-src}/print.less";
...

custom-bootstrap.less in index.js importieren:

import 'app/style/custom-bootstrap.less'

Bilder einbinden

Loader lokal installieren:

npm install --save-dev url-loader file-loader

Loader-Pipeline in webpack.config.js definieren:

TODO Beispiel ist Webpack 1

module.exports = {
    // ...
    module: {
        loaders: [
            {
                test:   /\.(png|gif|jpe?g|ico|svg)$/i,
                // If the asset is smaller than 10kb inline it,
                // else, fallback to the file-loader and reference it
                loader: 'url?limit=10000'
            }
        ]
    }
};

Hinweise:

TODO Woher weiß Webpack, dass file-loader der Fallback ist?

Statischen Kram kopieren

Plugin lokal installieren:

npm install --save-dev copy-webpack-plugin

webpack.config.js erweitern:

const CopyPlugin = require('copy-webpack-plugin');

module.exports = {
    // ...
    plugins: [
        new CopyPlugin([
            // Copy everything from the directory `static` to the output
            { from: 'static' }
        ])      
    ]
};

Hinweis: Weitere Details siehe Doku vom copy-webpack-plugin.

HTML-Snippets einbinden

Loader lokal installieren:

npm install --save-dev html-loader

Loader-Pipeline in webpack.config.js definieren:

TODO Beispiel ist Webpack 1

module.exports = {
    // ...
    module: {
        loaders: [
            {
                test: /\.html$/,
                loader: 'html'
            }
        ]
    }
};

Im JavaScript-Code benötigtes Snippet anfordern:

// ES5
var template = require('./MyComponent.html').default;

// ES6
import template from './MyComponent.html';

Automatische Imports

Wenn man z.B. Komponenten baut und neben jeder Komponente MyComponent.js auch ein MyComponent.css und ein MyComponent.html liegen hat, dann kann WebPack diese Module automatisch importieren.

TODO Scheint aktuell (Stand 22.02.17) noch nicht mit Webpack 2 zu gehen. Siehe auch: https://github.com/deepsweet/baggage-loader/issues/9

Loader lokal installieren:

npm install --save-dev baggage-loader

webpack.config.js erweitern:

TODO Beispiel ist Webpack 1

module.exports = {
    // ...
    module: {
        loaders: [
            {
                test: /\.js$/,
                loader: 'baggage?[file].html=template&[file].css'
            }
        ]
    }
};

Das sagt Webpack: Wenn Du eine HTML-Datei mit dem gleichen Namen findest, dann importiere sie als template. Und wenn Du eine CSS-Datei mit dem gleichen Namen findest, dann importiere diese auch.

Damit kann man den Import von:

import $ from 'jquery';
import Mustache from 'mustache';
import template from './MyComponent.html';
import './MyComponent.css';

Zu diesem Import ändern:

import $ from 'jquery';
import Mustache from 'mustache';

Wevpack Dev-Server

Der webpack-dev-server ist ein kleiner express-Server, der alle statischen Assets und das Bundle ausliefert. Der Dev-Server verwendet Webpacks Watch-Mode um bei Änderungen neu zu bauen. Das Ergebnis wird jedoch nicht auf die Platte geschrieben, sondern existiert nur im Arbeitsspeicher. Die geöffnete Seite wird dann automatisch per Sock.js neu geladen.

Development-Server lokal im Projekt installieren:

npm install --save-dev webpack-dev-server

Script in package.json definieren:

{
  "scripts": {
    "start": "node_modules/.bin/webpack-dev-server --progress --colors"
  }
}

Dev-Server in webpack.config.js konfigurieren:

module.exports = {
    // ...
    devServer: {
        host: '0.0.0.0', // Make dev-server accessible from local network
        port: 3000
    }
}

Proxy definieren:

TODO Beispiel ist Webpack 1

module.exports = {
    // ...
    devServer: {
        // Proxy to another server
        proxy: {
            // Proxy a certain path to another server
            '/myservice': {
                target: 'http://other-server.example.com/myservice',
                secure: false,
                pathRewrite: {'^/myservice' : ''}    // Optional: rewrite the request path
            },
            // Proxy everything but a certain path to another server
            '**': {
                target: 'http://localhost:8000/myappname',
                secure: false,
                bypass: function(req, res, proxyOptions) {
                    if (req.path.match('^/assets/')) {
                        return req.path;
                    }
                }
            }
        }
    }
};

Hinweise:

Development-Server starten:

npm start

Projekt im Browser öffnen: http://localhost:8080/webpack-dev-server/bundle

Hot Module Replacement (HMR)

Man kann statt einem automatischen Reload auch HMR (Hot Module Replacement) verwenden. Dabei wird bei einer Änderung die Seite nicht neu geladen. Statt dessen wird das geänderte Modul in der laufenden Seite ersetzt.

Ändert sich ein Modul, dann wird dieses Modul und alle Module die davon abhängen neu initialisiert. Ein Modul kann auf ein solches Update reagieren, um bestehende Objekt-Instanzen in der laufenden App zu aktualisieren (Details siehe Doku zu Hot Module Replacement). Manche Bibliotheken (z.B. Redux) unterstützen dies bereits out-of-the-box, für andere gibt es entsprechende Bibliotheken (z.B. react-hot-loader für React).

Um CSS HMR-fähig zu machen, muss der style-Loader (statt dem css-Loader) verwendet werden.

Verwendung

Webpack mit webpack-dev-server --inline --hot aufrufen.

Statt als Parameter kann man HMR auch in der Konfiguration anschalten:

devServer: {
    hot: true
},

React Hot Loader

react-hot-loader lokal installieren:

npm install --save-dev react-hot-loader@next

Hinweis: Wir verwenden die Version 3 von react-hot-loader, welche aktuell (Stand 22.02.17) nur als Beta verfügbar ist (daher Installation mit @next).

In package.json beim Start von webpack-dev-server HMR (--hot) verwenden (Entsprechende Option in webpack.config.js reicht nicht):

{
  "scripts": {
    "server": "node_modules/.bin/webpack-dev-server --progress --colors --hot"
  }
}

webpack.config.js erweitern ('react-hot-loader' als Babel-Plugin eintragen):

var plugins = ...

if (production) {
    ...
} else {
    plugins = plugins.concat([

        // Prints more readable module names in the browser console on HMR updates
        new webpack.NamedModulesPlugin()

    ])
}

module.exports = {
    ...
    plugins: plugins,
    module: {
        rules: [
            {
                test:   /\.jsx?$/,
                include: path.resolve(__dirname, 'src'),
                exclude: path.resolve(__dirname, 'src/lib'),
                loader: 'babel-loader',
                options: {
                    plugins: production ? [] : [ 'react-hot-loader/babel' ],
                    presets: [ ['es2015', { modules: false }], 'react']
                }
            },
            ...
        ]
    }
}

Nun muss noch ganz oben in der UI-Hierarchie (z.B. index.jsx) ein Aufruf von module.hot.accept rein:

if (module.hot) {
    module.hot.accept('app/ui/main/Main', () => {
        const NextMain = require('app/ui/main/Main').default;
        render(
            <Provider store={store}>
                <NextMain />
            </Provider>,
            rootElem)
    });
}

Links:

Code-Splitting (Chunks)

Normalerweise erzeugt Webpack ein einziges monolitisches Bundle pro Entry. Wenn man nun große Teile im Code hat, die nur selten benötigt werden, dann kann man diese in einen Chunk - also ein separates Bundle - auslagern. Ein Chunk wird nur dann geladen, sobald er benötigt wird. Man definiert einen Chunk, indem man im Code einen Split-Point vorsieht.

Split-Point im JavaScript-Code:

require.ensure([], function () {
    // Dieser Code landet mit seinen Abhängigkeiten in einer separaten Datei
    var library = require('some-big-library');
    library.doSomething();
});

TODO Chunks benennen mit require.ensure([], function () { ... }, 'myChunkName') geht nicht.

In webpack.config.js definieren, unter welchem Pfad die Chunks geladen werden können:

TODO Beispiel ist Webpack 1

module.exports = {
    // ...
    output: {
        // ...
        publicPath: 'build/'
    },
};

Hinweis: Das Attribut output.publicPath gibt den Pfad zu den Chunks an - aus der Sicht der Seite zur Laufzeit im Browser.

Tipp: Wenn man beim Webpack-Aufruf die Parameter --display-modules --display-chunks mitgibt, dann sieht man genau, welches Modul in welchem Chunk landet.

Chunks automatisch erzeugen

Wenn man mehrere Entries definiert hat oder wenn man manuelle Chunks verwendet, dann kann es passieren, dass mehrere Bundles oder Chunks die gleichen Abhängigkeiten haben. Diese werden dann unter Umständen doppelt geladen.

Dies kann man mit dem CommonChunksPlugin vermeiden. Das CommonChunksPlugin findet doppelte Abhängigkeiten in Bundles und Chunks und kann diese in eine separate Datei (z.B. vendor.js) oder auch in das Haupt-Bundle auslagern.

CommonChunksPlugin in webpack.config.js hinzufügen:

TODO Beispiel ist Webpack 1

var webpack = require('webpack');

module.exports = {
    // ...
    plugins: [
        new webpack.optimize.CommonsChunkPlugin({
            name:      'main', // Füge die gemeinsamen Abhängigkeiten zum Haupt-Bundle hinzu
            children:  true,   // Prüfe alle Kinder auf gemeinsame Abhängigkeiten
            minChunks: 2       // Wie oft eine Abhängigkeit auftauchen muss, damit sie verschoben wird
        })
    ],
    module: {
        // ...
    }
};

Hinweis: Gibt man im Attribut name einen andern Namen an, dann wird ein separates Bundle für die gemeinsamen Abhängigkeiten generiert. Ein name: 'common' erzeugt beispielsweise ein common.js. Dieses Bundle müsste man dann manuell z.B. im HTML-Code einbinden.

Hinweis: Statt einem name-Attribut, kann man auch async: true angeben. Dann wird der Chunk mit den gemeinsamen Abhängigkeiten bei Bedarf automatisch geladen.

Dependency-Tree analysieren

TODO Siehe Webpack your bags, Abschnitt "Would you like to know more?".

Hintergrund: Loader-Pipelines definieren

Webpack kann von sich aus nur JavaScript verarbeiten. Für alle anderen Formate muss man Loader oder Pipelines von Loadern definieren.

TODO Loaders are small plugins that basically say “When you encounter this kind of file, do this with it”. Ultimately at the end of the food chain all loaders return strings. This allows Webpack to wrap them into Javascript modules.

Loader-Pipeline im require-Statement definieren

Man kann die Loader-Pipeline direkt im require-Statement definieren.

Im JavaScript-Code benötigtes CSS anfordern:

require('!style!css!./style.css');

Mit den ! definiert man eine Loader-Pipeline - diese muss man von rechts nach links lesen. Im Beispiel: ./style.css laden, an css-Loader weitergeben und dessen Ergebnis dem style-Loader geben.

Loader-Pipeline im Aufruf von Webpack definieren

Damit man nicht bei jeder CSS-Abhängigkeit immer wieder die gleiche Pipeline definieren muss, kann man sie auch einmalig im Aufruf von Webpack definieren.

webpack ./entry.js bundle.js --module-bind 'css=style!css'

Das reqire-Statement sieht dann so aus:

require('./style.css');

Loader-Pipeline in Config-Datei definieren

Noch besser ist jedoch, Loader-Pipelines in der Config-Datei zu definieren. Dann muss man sich beim Aufruf von Webpack nicht mehr darum kümmern.

Loader-Pipeline in webpack.config.js definieren:

TODO Beispiel ist Webpack 1

module.exports = {
    // ...
    module: {
        loaders: [
            {
                test: /\.css$/,
                loader: "style!css"
                // Oder als Array:
                loaders: ['style', 'css'],
            }
        ]
    }
};

Die Regel besagt also: Webpack soll alles mit der Dateiendung css erst durch den css-Loader und dann durch den style-Loader jagen.

Das reqire-Statement bleibt wie gehabt:

require('./style.css');

Hintergrund: Plugins

TODO Plugins differ from loaders in the sense that instead of only executing on a certain set of files, and being more of a “pipe”, they execute on all files and perform more advanced actions, that aren’t necessarily related to transformations.