We are the Dev Teams of
  • brands
  • ebay_main
  • ebay
  • mobile
<
>
BLOG

Packing the Web Like a Boss

by Patrick Hund
in Tutorials

webpack-logowebpack is an awesome new tool for bundling and optimizing your JavaScript modules and other frontend resources. If you've been using RequireJS or Browserify, there's a great chance you'll love webpack, like I do. This article will give you a hands-on introduction.

To show you the magic of webpack, I'm going to use a very simple, very contrived code example throughout the tutorial. You can clone the code examples from GitHub here.

Imagine that, for some strange reason, you want some JavaScript code that can make a text block pink, and some other JavaScript code that can make a text block bold when you click on a button. Extremely sophisticated stuff, for sure, how would you go about it?

The Naïve Way of Delivering JavaScript to the Client

In the good old, carefree days of JavaScript hacking, you would create these files:

function Pinkyfier(id) {
    this.element = document.getElementById(id);
}

Pinkyfier.prototype.pink = function () {
    this.element.style.backgroundColor = "mistyrose";
    this.element.style.color = "hotpink";
}

js/Pinkyfier.js

function Fattyfier(id) {
    this.element = document.getElementById(id);
}

Fattyfier.prototype.fat = function () {
    this.element.style.fontWeight = "bold";
}

js/Fattyfier.js

var pinkyfier = new Pinkyfier("text"),
    fattyfier = new Fattyfier("text");

pinkyfier.pink();

document.getElementById("fat").onclick = function () {
    fattyfier.fat();
}

js/main.js

You'd include these 3 JS files in an HTML document using script tags like this:

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="utf-8">
    </head>
    <body>
        <p id="text">
            Nonne vides quid sit? Tu es ... Jesse me respice. Tu ... blowfish sunt. A blowfish! Cogitare. Statura
            pusillus, nec sapientium panem, nec artificum. Sed predators facile prædam blowfish secretum telum non se
            habet. Non ille? Quid faciam blowfish, Isai. Blowfish quid faciat? In blowfish inflat, purus?
        </p>
        <button id="fat" type="button">Make it fat</button>

        <script src="js/Fattyfier.js"></script>
        <script src="js/Pinkyfier.js"></script>
        <script src="js/main.js"></script>
    </body>
</html>

index.html

You load your HTML file in your browser (or look at the demo here), you marvel at the pink colorz, you click the button to make if fat, you're happy. What's not to like?

Well, all the functions and variables you define in those three files are in the global scope. As every programmer knows, global variables suck. What if you had a more complex application with, say, a hundred JS files instead of just three? What if the code in those JS files was dependent on the code in other files in a more complex way, so that changing the order of your script tags in the HTML code will break your page?

If you keep going down this road, you will end up with a big, fat, unmaintainable code mess. You need a module system.

A Better Way: AMD Modules

Before we look at the magic of webpack and how it makes everything better, let's do a quick review of the most commonly used module systems out there: Asynchronous Module Definition (AMD), CommonJS and, in the near future, ES6.

The most widely used implementation of AMD modules is RequireJS. Let's have a look how we can do our pink fat text page using RequireJS.

Instead of throwing our Pinkyfier and Fattyfier classes out there in the global scope, we wrap them in nice define statements:

define(function () {
    function Pinkyfier(id) {
        this.element = document.getElementById(id);
    }

    Pinkyfier.prototype.pink = function () {
        this.element.style.backgroundColor = "mistyrose";
        this.element.style.color = "hotpink";
    }

    return Pinkyfier;
});

js/Pinkyfier.js

define(function () {
    function Fattyfier(id) {
        this.element = document.getElementById(id);
    }

    Fattyfier.prototype.fat = function () {
        this.element.style.fontWeight = "bold";
    }

    return Fattyfier;
});

js/Fattyfier.js

In your main.js, you can now require those two modules you defined like so:

require([ "Fattyfier", "Pinkyfier" ], function (Fattyfier, Pinkyfier) {

    var pinkyfier = new Pinkyfier("text"),
        fattyfier = new Fattyfier("text");

    pinkyfier.pink();

    document.getElementById("fat").onclick = function () {
        fattyfier.fat();
    }
});

js/main.js

In your HTML code, instead of the three script tags in the above example, you write just one script tag that loads RequireJS, with a data attribute that points it to your application entry point main.js:

<script data-main="js/main.js" src="https://cdnjs.cloudflare.com/ajax/libs/require.js/2.1.17/require.min.js"></script>

index.html

You can easily build much more complex applications this way, and the nice part about it is that the modules are loaded asynchronously — only loading the RequireJS lib blocks the loading of the page, but none of your JavaScript code.

Look a the example here to see it in action.

The Node.js Way: CommonJS Modules

With the advent of server-side JavaScript using Node.js or io.js, another system of JavaScript modules became popular: CommonJS.

While this is primarily used for npm modules and node applications running on web servers, you can use Browserify to use it for client-side code, as well. This is very cool, because it puts a wealth of npm modules at your disposal and allows you to use the same code on the backend and on the frontend.

Here's our lil' example in a CommonJS version:

function Pinkyfier(id) {
    this.element = document.getElementById(id);
}

Pinkyfier.prototype.pink = function () {
    this.element.style.backgroundColor = "mistyrose";
    this.element.style.color = "hotpink";
}

module.exports = Pinkyfier;

js/Pinkyfier.js

function Fattyfier(id) {
    this.element = document.getElementById(id);
}

Fattyfier.prototype.fat = function () {
    this.element.style.fontWeight = "bold";
}

module.exports = Fattyfier;

js/Fattyfier.js

As you can see, the only difference to the "naïve" examples at the beginning are the last lines on each file, which state which parts of the code should be exposed to clients using the modules — the classes Pinkyfier and Fattyfier. In that respect, CommonJS modules are simpler and less intrusive than RequireJS.

To use the modules, you add require statements to your main.js:

var Pinkyfier = require("./Pinkyfier"),
    Fattyfier = require("./Fattyfier"),

    pinkyfier = new Pinkyfier("text"),
    fattyfier = new Fattyfier("text");

pinkyfier.pink();

document.getElementById("fat").onclick = function () {
    fattyfier.fat();
}

js/main.js

How do you use this in the browser? Browserify works differently than RequireJS. Instead of loading a library that does the trick, you use a node tool to create a JavaScript bundle file that contains all of your code.

To do so, install Browserify with npm (I'm assuming you have node and npm installed):

npm install -g browserify

You can then go to the directory that contains your JS files and create a bundle with this command:

browserify main.js > bundle.js

You can then simply include this script bundle in your HTML code with a script tag:

<script src="js/bundle.js"></script>

index.html

Look at the example to see it in action here.

The Future: ES6 Modules

At the time of writing this, the new JavaScript standard ECMAScript 6 is just around the corner, due to be released in June 2015.

ES6 brings along native support for JavaScript modules, rendering AMD and CommonJS obsolete.

Let's have a look how to pinkyfy and fatten up our lorem ipsum text with ES6:

class Pinkyfier {

    constructor(id) {
        this.element = document.getElementById(id);
    }

    pink() {
        this.element.style.backgroundColor = "mistyrose";
        this.element.style.color = "hotpink";
    }
}

export default Pinkyfier;

js/Pinkyfier.js

class Fattyfier {

    constructor(id) {
        this.element = document.getElementById(id);
    }

    fat() {
        this.element.style.fontWeight = "bold";
    }
}

export default Fattyfier;

js/Fattyfier.js

Whoa, what happened here? Well, in ES6, as you can see, classes are defined in a very different way than in the trusty old JavaScript we've grown to love. But that's actually not the point here, the interesting part are the last lines, with the export statements that expose the classes defined in the modules to the client modules. Looks similar to the CommonJS example, doesn't it?

Here's how to use modules in ES6:

import Pinkyfier from "./Pinkyfier";
import Fattyfier from "./Fattyfier";

let pinkyfier = new Pinkyfier("text"),
    fattyfier = new Fattyfier("text");

pinkyfier.pink();

document.getElementById("fat").onclick = function () {
    fattyfier.fat();
}

js/main.js

In ES6, export and import are part of the language and understood by the (future) browser without the help of a library like RequireJS or a build tool like Browserify, so you can simply include the main.js file in your script tag.

You can look at the example and see it in action here. Or no, you can't, unless you're from the future. Currently, my browser still complains: "modules are not implemented yet".

But webpack can fix that, as we'll see later in this article.

Enter webpack

So, finally, let's get started with webpack. We'll start with the AMD/RequireJS example and "webpackify" it. webpack supports AMD modules without further fiddling, so we can simply use the AMD modules from the example above: Pinkyfier.js, Fattyfier.js and main.js.

Using webpack is similar to using Browserify: you install a node tool with npm and use it to compile one or more bundles.

Install webpack:

npm install -g webpack

To configure webpack, you create a configuration file named webpack.config.js. In this simple version, it only contains config code that tells webpack the paths where to find its stuff (modulesDirectories), what the application's main file is (entry) and what bundle file to create where (output).

var webpack = require("webpack");

module.exports = {
    entry: "./main",
    resolve: {
        modulesDirectories: [
            "."
        ]
    },
    output: {
        publicPath: "js/",
        filename: "bundle.js"
    }
};

js/webpack.config.js

With the configuration file in place, we can simply enter "webpack" on the command line:

webpack

This creates two files, bundle.js and 1.bundle.js.

"Why two?" you ask. Well, because we're using AMD modules, which are loaded asynchronously. bundle.js contains the code from main.js, 1.bundle.js contains the code from Pinkyfier.js and Fattyfier.js and is loaded asynchronously. If we were using CommonJS-style modules, which are always loaded synchronously, we would get only one bundle file. This gives you a first glimpse at how fiendishly clever this tool is.

We include bundle.js in the script tag in our HTML code (no need to include the other bundle).

Look at the example here to see it in action.

The Fun Part

That's all very well, but what's the advantage over just using RequireJS? RequireJS comes with an optimizer (r.js) that can bundle files just as well...

That's where the fun part begins: remember what I said about Browserify, how it allows you to use npm modules both in the backend and frontend, while the advantage of RequireJS is asynchronous loading? Well, with webpack, you can have the best of both worlds. webpack can handle both AMD modules and CommonJS modules at the same time.

Try it out — you can simply swap the AMD Pinkifier.js with the CommonJS version. Run the webpack command again.

Look at the example and see it in action — it works just the same way.

Note that there's no additional configuration neccessary, you don't need to tell webpack: "Hey, I'm using both styles of module." webpack is smart enough to figure this out by itself.

Back to the Future

Let's go back to our ES6 example, which, sadly, I can't run in my April-2015-browser. How can webpack help? Easily. webpack has the concept of loaders, additional modules that you add to the webpack config to load files with that match a certain pattern. There are many, many loaders out there for all kinds of things, not only loading JavaScript, but even CSS or images.

We'll configure the Babel loader for all JavaScript files by adding this block to our webpack.config.js:

module: {
        loaders: [
            {
                test: /\.js$/,
                loader: "babel-loader"
            }
        ]
    }

js/webpack.config.js

This will run all the JS files through a loader that uses Babel to transpile the ES6 code to plain old JavaScript code that current browsers understand.

Since the Babel loader is not a part of webpack by default but rather an addon, you have to install it to your project with npm.

I added a package.json to my code example so I can just run "npm install" on the command line (in the js directory) to do that.

After running the webpack command, we get a single bundle (nothing asynchronous going on here) that we include in our script tag.

Look at the example to see it in action.

Good, Clean, Asynchronous Fun

Back to the AMD+webpack example — I wrote earlier that webpack automatically creates multiple bundles when it encounters AMD modules. That's nice, but is it really useful? We are loading the fattyfier and pinkyfier asynchronously, but the asynchronous loading happens immediately after the page was loaded. There's not a very big sitespeed advantage over just having one big bundle file with everything in it, if we include the script tag before the closing body tag, which is commonly accepted best practice.

But do we really need the fattyfier module? Actually, we only need it when the user clicks the "make it fat" button. Wouldn't it be cool if we could use asynchronous loading to our advantage and only load the farryfier code when we actually need it, i.e. when the button is clicked?

With webpack, this is very easy to do. We change our main.js code like so:

var Pinkyfier = require("./Pinkyfier"),
    pinkyfier = new Pinkyfier("text");

pinkyfier.pink();

document.getElementById("fat").onclick = function () {
    require(["./Fattyfier"], function (Fattyfier) {
        var fattyfier = new Fattyfier("text");
        fattyfier.fat();
    });
}

js/main.js

What's going on here? I'm actually mixing up CommonJS module style and AMD module style in the same file: The CommonJS style require statement in line 1 is for loading the pinkyfier module synchronously. The AMD style require statement in line 7 is for loading the fattyfier module asynchronously.

When I run the webpack command, I get a bundle.js file that contains the code from main.js and Pinkyfier.js and a 1.bundle.js file that contains the code from Fattyfier.js.

When I load my page in the browser, only bundle.js is loaded. Only after I click the button, the other bundle is loaded.

Look at the example to see it in action.

This is great for reducing page loading time and increasing site speed. We use this for mobile.de's search page — when I click on the "Detailed Search" button on the upper left, a big old search form box pops up. All the JavaScript code that drives this form, even the template code, which is a client-side rendered Soy template, is loaded asynchronously, only after the button has been clicked.

Conclusion

So that's about it, you should now have a good idea about what webpack is all about and how it works.

Obviousy, this is just the beginning.

As I wrote above, there's a vast selection of loader modules and plugins at your disposable to do things like minification or compilation of SASS or Less to CSS. You can have webpack generate source maps to easily debug your JavaScript in your browser. You can have webpack run a dev server that listens for changes to your code and instantly updates the generated files. You can integrate webpack in your Grunt or Gulp build. You can make webpack create file names with chunk hashes (a.k.a. fingerprints) to optimize browser caching. You can have different bundles for different pages of your application and let webpack organize the libs shared by these pages in shared bundles automatically. You can write your own loader modules, which is actually quite easy.

Have fun exploring the possibilities!

javascript, es6, webpack, frontend, webdev

?>