Nachdem in den vorherigen Beiträgen eine grundlegende Struktur aufgebaut wurde, mit der man sein JavaScript-Projekt starten kann, wird dies noch um einige Funktionen erweitert, die die Entwicklung und die Fehlersuche erleichtern.

Dazu gehört der webpack-dev-server, der das Webprojekt zur Verfügung stellt und bei Änderung einer Quelldatei automatisch mittels Hot Module Replacement die Änderungen an den Browser überträgt.

Die Anzeige der Log-Ausgaben und der Status des Übersetzungsvorgangs wird im webpack-dashboard angezeigt.

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

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

Los geht's ...

Installation des Webpack Development Servers

Der Webserver und das Dashboard Modul werden mit dem Paketmanager installiert.

$ yarn add -D webpack-dev-server webpack-dashboard

Danach fügt man die Alias-Kommandos in der package.json Konfiguration ein.

package.json:
  "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",
/* begin neuer code */  
    "prod": "npm-run-all clean:prod build:prod",
    "run:dev": "webpack-dashboard --minimal -- webpack-dev-server --hot --config webpack.config.js --watch",
    "run:prod": "webpack-dev-server --hot --config webpack.config.js --watch --env.NODE_ENV=production",
    "start": "npm run run:dev"    
/* ende neuer code */  
  },

In der webpack-Konfiguration werden noch einige default-Einstellungen für das Dashboard und den Webserver hinterlegt.

Hinweis: Da das Dashboard nur mit unminifiziertem Code funktioniert, wird es nur für den Entwicklungswebserver aktiviert.

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

module.exports = (entry, prefix) => {

[...]

        plugins : [
            new HtmlPlugin({
                template: path.resolve(__dirname, '../src/assets/index.template.html')
            })
/* begin neuer code */  
        ],
        devServer: {
            contentBase : path.resolve(__dirname, '../dist'), // A directory or URL to serve HTML content from.
            historyApiFallback: true, // fallback to /index.html for Single Page Applications.
            inline: true, // inline mode (set to false to disable including client scripts (like livereload)
            open: true, // open default browser while launching
            compress: true, // Enable gzip compression for everything served:
            hot: true // Enable webpack's Hot Module Replacement feature        
        }
/* ende neuer code */
    }
}        
webpack.config.js:
    const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
    const ExtractTextWebPackPlugin = require('extract-text-webpack-plugin');
/* begin neuer code */  
    const DashboardPlugin = require('webpack-dashboard/plugin');  
/* ende neuer code */  

[...]

    } else {
        webpackConfig = common(entryName, '');

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

        webpackConfig.plugins.push(
/* begin neuer code */
            new DashboardPlugin(),
/* ende neuer code */
            new ExtractTextWebPackPlugin({ filename: 'styles.css', disable: false, allChunks: true })
        );        
    }

Mittels yarn start wird die Webanwendung im Browser geöffnet und man erhält bei jeder Anpassung sofortiges Feedback. Das Dashboard gibt einem noch einen übersichtlichen Überblick über den Build-Status und die Log Meldungen.

Das Produktionsbundle kann man mittels yarn run:prod testen.

Erstellung von Source-Maps

Ein einfache Möglichkeit um Performance-Gewinn zu erzeugen besteht darin, die JavaScript- und CSS-Dateien zu kombinieren und zu komprimieren. Dies haben wir bereits durch den Bundler und die Minifizierung erreicht.

Aber was passiert, wenn der Code in diesen komprimierten Dateien debuggt werden muss? Das ist ein Albtraum, da keine Fehlermeldung mit Zeilenangabe zum Originalcode passt. Dafür gibt es aber eine Lösung, die Source-Maps.

Dazu wird eine Zuordnungsdatei erzeugt, mit der man den minifizierten Code zurück zum Originalcode mappen kann. Für die Produktion wird die Datei extern erstellt, damit man diese nicht ausliefern muss. Das bedeutet, dass die Anwendung problemlos debuggt werden kann nachdem alle Teile optimiert wurden. Die Chrome- und Firefox-Entwicklertools bieten eine integrierte Unterstützung für Source-Maps.

Beispiel (ohne Source-Map):

Beispiel (mit Source-Map):

Die Konfiguration ist sehr einfach. Man muss nur an einigen Stellen die Erstellung der Source-Maps aktiviert werden und einmalig der Typ definiert werden, welche Art von Source-Map erzeugt werden soll.

webpack.config.js:
    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 }),            
/* begin neuer code */
            new UglifyJsPlugin({ sourceMap: true })
        );

        webpackConfig.devtool = 'source-map';  
/* ende neuer code */        
    } else {
        webpackConfig = common(entryName, '');

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

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

/* begin neuer code */
        webpackConfig.devtool = 'inline-source-map';  
/* ende neuer code */        
    }
config/webpack.common.js:
                            { 
                                loader : 'css-loader',
                                options : {
/* begin neuer code */
                                    sourceMap : true,
/* ende neuer code */
                                    importLoaders: 0
                                    // 0 => no loaders (default); 1 => postcss-loader; 2 => postcss-loader, sass-loader
                                }
                            }

Als nächstes werden wir eine Sprachvariante von CSS unterstützen und auch dafür die Source-Maps aktivieren.

Verarbeitung von Sass: Syntactically Awesome Style Sheets

Sass (Syntactically Awesome Stylesheets) ist eine Stylesheet-Sprache, die als Präprozessor die Erzeugung von Cascading Style Sheets erleichtert.

Die Regeln werden anstatt in .css Dateien in .scss Dateien geschrieben. Aus diesen Dateien werden dann .css Dateien generiert. Dazu gibt es das Übersetztermodul node-sass, das von Webpack mit dem sass-loader ausgeführt wird.

Der Vorteil von SASS liegt in den zusätzlichen Features die es mit sich bringt, unter anderem Variablen und Mixins. Diese verkleinern den selbst geschrieben Code und ermöglichen vor allem eine saubere und einfache Verwaltung.

Neben der Interpretation von Sass wird noch postcss eingefügt, das die finalen CSS-Dateien optimiert. Dazu gehört automatisch Herstellter-Prefixe zu erstellen und die finale CSS-Datei zu minifizieren.

$ yarn add -D postcss-loader 
$ yarn add -D sass-loader node-sass 
$ yarn add -D autoprefixer cssnano css-hot-loader

Die allgemeine Konfiguration wird um diese Module erweitert. Wichtig dabei ist die Anzahl der Import-Loader beim Modul css-loader. Sie gibt die Anzahl der Loader an, die vor dem css-Loader auf importierte Resourcen angewendet werden sollen (in der Konfiguration von hinten nach vorne lesen).

config/webpack.common.js:
            extensions: [ '.js', '.jsx', '.json', '.scss', '.css', '.jpeg', '.jpg', '.gif', '.png', '.svg' ], // Automatically resolve certain extensions

[...]

                // Cascading Style Sheets
                {
                    test: /\.(css|scss|sass)$/,
                    use: ['css-hot-loader'].concat(ExtractTextWebPackPlugin.extract({  // HMR for styles
                        use: [
                            { 
                                loader : 'css-loader',
                                options : {
                                    sourceMap: true,
                                    importLoaders: 2
                                    // 0 => no loaders (default); 1 => postcss-loader; 2 => postcss-loader, sass-loader
                                }
                            },
                            {
                                loader : 'postcss-loader',
                                    options: { 
                                        config: {
                                            path: path.resolve(__dirname, '../config/postcss.config.js')
                                        },
                                        sourceMap: true
                                    }
                            },                    
                            { 
                                loader : 'sass-loader',
                                options : {
                                    sourceMap: true
                                }
                            }
                        ],
                        fallback: 'style-loader'                            
                    }))
                },

In der postcss Konfiguration wird festgelegt, welche Plugins verwendet werden, um die finale CSS-Datei zu optimieren. Eine Auswahl von Plugins findet man unter https://www.postcss.parts/.

config/postcss.config.js:
module.exports = {
    plugins: [
        require('autoprefixer'),
        require('cssnano')
    ]
}

Optimierung von Bildern vor dem Produktionsdeployment

Um die Ladezeiten von Bildern zu optimieren, wird optional ein Modul installiert, das alle Bilder komprimiert und somit einiges an Platz und damit benötigte Bandbreite spart.

$ yarn add -D image-webpack-loader

In der webpack-Konfiguration wird das Modul hinter den url-loader angefügt, wodurch das image-webpack-loader vorher ausgeführt wird.

config/webpack.common.js:
                        {
                            loader: 'url-loader',
                            options: {
                                limit: 8192, // Convert images < 8kb to base64 strings
                                context : 'src/assets/images',
                                name : 'images/' + prefix + '[name].[ext]'
                            }
/* begin neuer code */
                        },
                        { 
                            loader : 'image-webpack-loader',
                            options: {
                                mozjpeg: {
                                    progressive: true
                                },
                                gifsicle: {
                                    interlaced: false,
                                },
                                optipng: {
                                    optimizationLevel: 4,
                                },
                                pngquant: {
                                    quality: '75-90',
                                    speed: 3
                                },
                                // Specifying webp here will create a WEBP version of your JPG/PNG images
                                webp: {
                                    quality: 75
                                }
                              }
                        }
/* ende neuer code */

Wenn man das Projekt nun neu erstellt, werden die Bilder komprimiert, die CSS-Datei minifiziert. Trotzdem kann man im Browser dank den Source-Maps den Code ordentlich debuggen.

Wie immer befindet sich der Code in meinem gitlab-Projekt unter https://gitlab.com/svenpaass/webpack-template/tree/chapter4

Im vorerst letzten Teil der Serie geht es noch um diese Themen:

  • Einbindung von Typescript und React in den Entwicklungsprozess
  • Laden einer Bibliothek in den globalen Kontext am Beispiel von jQuery
  • Bibliotheken von einem CDN laden

Abschließend wird die Vorlage bereinigt, damit man eine saubere Basis für neue Projekte hat.

Nächster Beitrag Vorheriger Beitrag