Chris Sainty

A technical blog covering full-stack web development.

twitter | github | stackoverflow

Webpack

Webpack has become my standard tooling for front-end assets in recent times. Though for all the in-depth documentation I found the learning curve steep and the number of options confusing. So I'd like to share some of what I learnt.

Getting started

Webpack is an npm module. You can install it globally npm install webpack -g or bring it in to your existing tooling - grunt or gulp.
For the purposes of this blog I will use a globally installed copy from the command line. In this scenario you need to create a webpack.config.js file to store you setup.

Javascript modules and concatenation

Webpack can do a lot, but let's focus on the first thing you are likely to use it for first, handling javascript modules and building concatenated production assets.

Consider the following two .js files, first the main file we want to run on our page, second a service providing some functionality.

// index.js
var greeter = require('./greeter');
greeter.greet('world');

// greeter.js
module.exports = {
    greet: function (name) {
        console.log('Hello ' + name);
    }
};

Getting webpack to handle the process of resolving this dependency is actually really easy. You simply provide the entry point, the file that runs first, and the output location.

// webpack.config.js
module.exports = {
    entry: './index.js',
    output: {
        filename: 'bundle.js'
    }
};

Webpack will resolve the require() statement at the top of index.js and include it in the output for us.

Multi-page websites

The truth is not many of us build single page apps like that example above tends towards. So let's define multiple entry points, one for each page that requires javascript.

// home.js
var greeter = require('./greeter');
greeter.greet('home world');

// about.js
var greeter = require('./greeter');
greeter.greet('about world');
// webpack.config.js
module.exports = {
    entry: {
        home: './home.js',
        about: './about.js'
    },
    output: {
        path: './dist',
        filename: '[name].js'
    }
};

With this set up, webpack will process each entry point, resolve its dependencies and create multiple output files. We use [name] in the output file name to give the bundles different names. In this case home and about.

Shared modules

But now we have greeter.js being duplicated in to both the home and about bundles. What we want is to extract greeter.js and other shared modules out in to their own file which we can include on every page. Of course webpack makes this simple with some additional config.

// webpack.config.js
var webpack = require('webpack');

module.exports = {
    entry: {
        home: './home.js',
        about: './about.js'
    },
    output: {
        path: './dist',
        filename: '[name].js'
    },
    plugins: [
        new webpack.optimize.CommonsChunkPlugin({
            name: 'common',
            minChunks: 2
        })
    ]
};

The CommonsChunkPlugin will automatically find common modules and extract them in to a shared file for you. Then you simple have two script references in your page.

<script src="common.js"></script>
<script src="home.js"></script>

Minification

By default webpack emits unminified source. The pieces it adds for module resolution are also quite verbose with comments and whitespace. For production we need to add minification and scriptmaps. Let's add that config now.

// webpack.config.js
var webpack = require('webpack');

module.exports = {
    entry: {
        home: './home.js',
        about: './about.js'
    },
    output: {
        path: './dist',
        filename: '[name].js'
    },
    plugins: [
        new webpack.optimize.CommonsChunkPlugin({
            name: 'common',
            minChunks: 2
        }),
        new webpack.optimize.UglifyJsPlugin(),
        new webpack.SourceMapDevToolPlugin({
            filename: '[name].js.map'
        })
    ]
};

ES6

Finally let's introduce ES6, we want all that goodness now. To add ES6 support via babel we need to use a webpack loader. Loaders act as pre-processors over input files. A lot of webpack's power is added with various loaders.
So let's npm install --save babel-loader and update our webpack.config.js.

// webpack.config.js
var webpack = require('webpack');

module.exports = {
    entry: {
        home: './home.js',
        about: './about.js'
    },
    output: {
        path: './dist',
        filename: '[name].js'
    },
    plugins: [
        new webpack.optimize.CommonsChunkPlugin({
            name: 'common',
            minChunks: 2
        }),
        new webpack.optimize.UglifyJsPlugin(),
        new webpack.SourceMapDevToolPlugin({
            filename: '[name].js.map'
        })
    ],
    module: {
        loaders: [
            { test: /\.js$/, loader: 'babel' }
        ]
    }
};

Now all .js files will be run through babel. So we can update our two pages and our shared module.

// home.js
import * as greeter from './greeter';
greeter.greet('home world');

// about.js
import * as greeter from './greeter';
greeter.greet('about world');

// greeter.js
export function greet(name) {
    console.log(`Hello ${name}`);
}

Summary

Wrapping it up, we have quickly created a pipeline for our javascript that allows us to structure our code around modules, write them in ES6, and deliver them minified and concatenated to the client.
We have only scratched the surface of webpack (just check this list of loaders!) but I hope this article helps introduce you to some of the core uses.