Getting Started with Webpack

You've heard great things about Webpack - "Webpack is the future!" You know it's somehow related to npm and packages, you've heard about people talk about plugins and loaders, you nod like you know what they're talking about.

But in all honesty, you just feel lost. Well...this article is for you! Let's start at the beginning...

What is Webpack?

Webpack is a module bundler.

When you write a complex application, most of the code you use would be from packages downloaded from npm or Bower; these packages can also be called modules.

On the server, this is fine, because you only have to download these packages once, and they'll be available forever on your server. On the client, however, to download hundreds of packages (and thus making hundreds of requests) would take an unacceptably long time. Thus, Webpack bundles (concatenates, minifies etc.) all your application code, all its dependencies and all assets into a few files, which are sent to the client.

This is better because:

  • The browser no longer need to make hundreds of requests, download hundreds of modules
  • The browser no longer need to analyse the dependency graphs to resolve deeper dependencies
  • Webpack can make use of minifying steps to ensure whatever is sent to the client is as slim as possible

Much of the confusion surrounding Webpack is that people think Webpack only does one thing - this is not the case. As a module bundler, it's function is similar to Browserify; at the same time, the build steps like minification crosses over to the Grunt/Gulp realm.

Webpack is not a single tool. View it as toolbox.

Getting Started

There are two versions of Webpack 1 and 2. Webpack 2 is still in beta (and has been for the last year), is still unstable and the documentation is sparse. However, the core concepts remains the same, so let's go through Webpack 1 for now, and we will supplement this article with an update once Webpack 2 is officially released.

It's hard to understand something without trying it, so let's do it! We'll be making a very simple npm package that prints hello Daniel! whenever it's ran. It is a meaningless program which highlights the amount of boilerplate required to start a simple project using Webpack. But hopefully, at the end of the article, you'll see the value of using Webpack.

You can find the completed code on Github at brewhk/greeter

First, let's install Webpack.

# Using npm client
$ npm install webpack -g

# Using yarn
$ yarn global add webpack

Next, let's create two files - greeter.js and main.js

greeter.js

var helloWorld = function (name) { console.log("hello " + name + "!") };  
exports.sayHello = helloWorld;  

And in main.js, we are importing the greeter.js module, and using its exported function sayHello to print out a message into the console.

main.js

var greeter = require('./greeter.js');  
greeter.sayHello("Daniel");  

Now, if we run webpack ./main.js bundle.js, you'll find that Webpack has now generated a bundle.js file.

$ webpack ./main.js bundle.js
Hash: 11269b96fa9a67055333  
Version: webpack 1.13.3  
Time: 51ms  
    Asset     Size  Chunks             Chunk Names
bundle.js  1.63 kB       0  [emitted]  main  
   [0] ./main.js 67 bytes {0} [built]
   [1] ./greeter.js 99 bytes {0} [built]

Let's look inside this bundle.js. You'll find a lot of bootstrap code at the top (which we have excluded below), but most importantly, you'll see that our two modules - greeter.js and main.js have been bundled into the same file. The /* 0 */ and /* 1 */ corresponds to the [0] and [1] in the terminal output.

/******/ (function(modules) { // webpackBootstrap
/******/ ...
/******/ })
/************************************************************************/
/******/ ([
/* 0 */
/***/ function(module, exports, __webpack_require__) {

    var greeter = __webpack_require__(1);
    greeter.sayHello("Daniel");


/***/ },
/* 1 */
/***/ function(module, exports) {

    var helloWorld = function (name) { console.log("hello " + name + "!") };
    exports.sayHello = helloWorld;


/***/ }
/******/ ]);

Webpack has seen the require statement inside main.js, and know that it depends on greeter.js, so it's gone and grabbed the greeter.js code and added it the bundle.

This demonstrates two of the core concepts of Webpack - Entry and Output.

Entry

Just like a Node application, which has a single entry point, Webpack follows dependencies from a single starting point - the entry point, and creates a dependency graph from there.

In our example above, it's ./main.js

Output

The output specifies where the bundled files are to be emitted (a fancy term for produced/located). You can specify the file name(s) and path, and many other options. For all options, please see the API Reference.

Configuration File

Instead of specifying the entry and output each time we run webpack, we can, instead, create a configuration file called webpack.config.js to house this information.

Webpack's power comes in its configurability, and so we will add more to this config file further down the line, when we're using plugins and loaders.

webpack.config.js

module.exports = {  
  entry: "./main.js",
  output: {
    filename: "bundle.js"
  }
}

Now, we can simply run webpack in the terminal.

Note that the configuration format for Webpack 2 is significantly different from Webpack 1. You can find the latest format here.

Let's just do some cleanup here, and put the source files inside src, and the output files in dist. We should update our webpack.config.js file.

module.exports = {  
  entry: "./src/app.js",
  output: {
    filename: "dist/bundle.js"
  }
}

Using ES6 syntax with Webpack

You'd noticed that we used the ES5 var instead of ES6 const. This is because those ES6 syntax are not supported in older versions of Node.

We also used the CommonJS syntax of require-ing files, because Node does not currently (as of 7.1.0) support ES6 modules.

ES6 Modules needs to be loaded, and the Loader spec is not yet finalized by the WHATWG. This blocks the V8 engine from implementing support for modules. Since Node.js is ran on the V8 engine, Node is unlikely to natively support modules any time soon (the next few months).

But ECMAScript 2015 (the proper name for ES6) is the way forward, so let's just see what happens when we switch to ES6 syntax.

src/greeter.js

const helloWorld = function (name) { console.log(`hello ${name}!`) };  
exports.sayHello = helloWorld;  

src/main.js

import greeter from './greeter.js';  
greeter.sayHello("Daniel");  

When we run webpack (with Node v7.1.0), we can see that it is no longer following the import statements, as it is not identifying greeter.js as a dependency.

$ webpack
Hash: acbe008686123ac22d94  
Version: webpack 1.13.3  
Time: 42ms  
    Asset     Size  Chunks             Chunk Names
bundle.js  1.45 kB       0  [emitted]  main  
   [0] ./src/main.js 64 bytes {0} [built]

Furthermore, running bundle.js leads to an error.

$ node bundle.js 
/home/administrator/sandbox/tempwebpack/bundle.js:47
    import greeter from './greeter.js';
    ^^^^^^

SyntaxError: Unexpected reserved word  
    at exports.runInThisContext (vm.js:53:16)
    at Module._compile (module.js:373:25)
    at Object.Module._extensions..js (module.js:416:10)
    at Module.load (module.js:343:32)
    at Function.Module._load (module.js:300:12)
    at Function.Module.runMain (module.js:441:10)
    at startup (node.js:139:18)
    at node.js:974:3

So how can we:

  1. Add support for ES6 modules syntax
  2. Ensures support for ES6 features on older versions of Node

The answer is Babel.

Traceur is Google's version of Babel, but since Babel is more popular at the moment, we will go with Babel for now.

Babel

Babel is, like Webpack, a multitude of tools. Relevant to us is that it is a transpiler. Babel allows us to write in ES6 syntax, and it will transpile it to ES5 syntax, which means our code can run on older browsers.

Likewise, it can also transpile our ES6 modules to fit with the CommonJS module definition specification, which Node uses.

We can include Babel into our project by using loaders.

Loaders

Webpack aims to treat everything inside your application as modules. This includes stylesheets, images as well as JavaScript files.

But Webpack only understands ES5 JavaScript. It doesn't understand non-ES5 files - stylesheets (CSS/Sass/LESS/Stylus/Compass), images, and other flavours/derivatives of JavaScript (ES6, TypeScript, CoffeeScript).

However, we can dictate to Webpack how it should be loading these files / modules, by using loaders.

Loaders allows Webpack to include non-JS dependencies into your bundle.

For example, to use Babel, we would use the babel-loader, and ask it to be applied on all *.js files.

We must first add the babel-loader from npm. So let's create a package.json file to make this an npm package, and add babel-loader as a dev dependency (as it is not required in the outputed file, but we need it in development to build the distribution files).

$ npm init

Fill out the details, which will give you a package.json file like the one below:

package.json

{
  "name": "greeter",
  "version": "1.0.0",
  "description": "Says hello to everyone",
  "main": "dist/bundle.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "Daniel Li <dan@danyll.com>",
  "license": "MIT"
}

Now we can add babel-core, babel-loader and babel-preset-es2015 as dev dependencies.

# Using npm client
$ npm install --save-dev babel-core babel-loader babel-preset-es2015

# Using yarn
$ yarn add --dev babel-core babel-loader babel-preset-es2015

Lastly, add the loader to the configuration file.

module.exports = {  
  entry: "./src/main.js",
  output: {
    filename: "dist/bundle.js"
  },
  module: {
    loaders: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        loader: "babel-loader"
      }
    ]
  }
}

The module.loaders.$.test property specifies a RegEx of which files it should be applied to, and the loader property dictates which loader to apply.

For a complete list of loaders, see List of Loaders

Babel also uses plugins (different than the Webpack plugins), and there are different plugins for different ES6 features. For example, to use spread and rest, we need the babel-plugin-transform-object-rest-spread plugin. To save us having to include individual plugins for common features, Babel has set up presets, which are like a pre-configured grouping of plugins. Most ES6 features have been included in the es2015 preset.

We can load those plugins (and presets) using a .babelrc file.

.babelrc

{
  "presets": ["es2015"]
}

Now when we run webpack, everything works!

$ webpack
Hash: 3481a4d8e247a5cfad81  
Version: webpack 1.13.3  
Time: 458ms  
         Asset     Size  Chunks             Chunk Names
dist/bundle.js  1.84 kB       0  [emitted]  main  
    + 2 hidden modules

Minifying using Plugins

Right now, bundle.js is 1843 bytes long.

$ stat -c %s bundle.js
1843  

But for such a simple feature, we can do better. Let's minify/uglify the files. For this we would use another feature of Webpack - plugins.

Plugins are similar to loaders, in that they transform files; but whereas loaders act on individual files, plugins work on the bundled files.

For uglifying files, we'll use the UglifyJsPlugin.

Usually, we must download the plugin from npm like we do with loaders, but some of the common plugin, like our UglifyJsPlugin, comes with the webpack npm module.

So far, we've been using webpack from the terminal, and it works because webpack is installed globally. To use our UglifyJsPlugin, which is part of the webpack object, we must require the webpack module in our config.

webpack.config.js is not passed through babel-loader, so we cannot use ES6 in the config file. However, when we downloaded babel-core, we also downloaded babel-register package, which will process all files ending with .babel.js. So all we need to do is change the filename of the config file to webpack.config.babel.js

webpack.config.babel.js

import webpack from 'webpack';

module.exports = {  
  entry: "./src/main.js",
  output: {
    filename: "dist/bundle.js"
  },
  module: {
    loaders: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        loader: "babel-loader"
      }
    ]
  },
  plugins: [
      new webpack.optimize.UglifyJsPlugin({
        compress: {
          warnings: false
        }
      })
  ]
}

Let's run webpack again to get the minified bundle.

$ webpack
Hash: 3481a4d8e247a5cfad81  
Version: webpack 1.13.3  
Time: 162ms  
         Asset       Size  Chunks             Chunk Names
dist/bundle.js  418 bytes       0  [emitted]  main  
    + 2 hidden modules
$ stat -c %s bundle.js 
418  

This is much better. The filesize went from 1843 to 418 bytes.

You can find the completed code on Github at brewhk/greeter

Summary

Webpack should be viewed as a toolbox, rather than a single tool. It takes a bunch of files, and bundles them into fewer files. It uses loaders to transform individual files (e.g. CoffeeScript to JavaScript), and uses plugins to transform the bundled files (e.g. minifying files).

This article covers the basics of Webpack. Webpack is very powerful because of its loaders and plugin ecosystem, making it extremely configurable and extensible. We have not covered using Webpack with images, stylesheets and HTML, generating bundles with multiple entry points and outputs, using Webpack with React etc. But I hope this post acts as a good introduction for you moving forward.

The next step is to explore the different loaders and plugins available and think about you can incorporate them in your next projects!

Daniel Li

Full-stack Web Developer in Hong Kong. Founder of Brew.

Hong Kong http://danyll.com