Skip to content
Logo Theodo

A Comprehensive Introduction to Webpack, the Module Bundler

Stanislas Bernard10 min read

As a front-end Javascript developer, managing your source files can quickly get tricky. On the back-end side, Node has a built-in module resolver —require— whereas there is no built-in module resolver in the browsers.

In this article, I will get you started with Webpack, a very powerful tool to organize, compile and package your source files. Webpack is a module bundler that will let you compile, optimize and minify all your files: Javascript, CSS, pictures, etc.

You might have heard about Gulp as another module bundler. Compared to Webpack, Gulp can be seen as a scripting platform, that will let you chain different building tasks. Webpack is an all-in-one solution, capable of handling all your source files. Moreover, Webpack offers some very powerful features that will make your developments faster:

Instead of presenting a Webpack boilerplate or starter kit, I will show you how to build a web application step by step from scratch, so that you will end up with a fully working solution that you are comfortable coding with.

Let’s get started!

Basic project structure

First of all, we need to organize our project’s folders. Here is a common directory structure that has proven its worth for many people:

.
├── src                           // The main source folder, where all our source files will go (Webpack's input)
|   └── your-first-js-file.js
├── dist                          // Built files folder (Webpack's output)
├── package.json
└── webpack.config.js             // Webpack config file

We will go into more detail regarding each file later in the article.

Package installation

In order to use Webpack, we first need to fetch the corresponding module. We’ll assume you’re running Node v4+.

All commands will be run from your project’s root folder. If you don’t have a package.json in your project already, then run npm init.

Now let’s install Webpack:

npm install --save webpack

Minimum configuration

Now that we have Webpack installed, let’s configure it. This is done with a plain JS file, webpack.config.js.

The bare minimum to make Webpack compile our sources is to provide it with an entry point as well as an output.

var config = {
    entry: './src',               // entry point
    output: {                     // output folder
        path: './dist',           // folder path
        filename: 'my-app.js'     // file name
    }
}
module.exports = config;

We will now add an npm script to the package.json to call Webpack compilation:

// package.json

// ...

"scripts": {
    "build": "webpack"
}

// ...

Now, when running npm run build, Webpack will look the webpack.config.js up and compile our application.

As you set in the config, our application’s entry point is ./src. We therefore need to define this entry point:

// src/index.js
console.log('Hello Webpack!');

Now that we have our entry point, let’s build the application!

npm run build

As a result, Webpack will bundle up our src/index.js into dist/my-app.js. If you look at the produced file, you’ll notice that Webpack has added some code of its own. That is the module resolver. This provides a require function, exactly as in a NodeJS script!

Add an index.html to our project, so that we can see the effects of the JS files.

<!-- index.html -->
<html>
    <head>
        <script type="text/javascript" src="./dist/my-app.js"></script>
    </head>
    <body>
    </body>
</html>

Manually open this file with your favorite web browser, and see the Hello Webpack! in the console.

Modularity

Let’s recap what we have done so far. We have configured Webpack to bundle the entry point -src/index.js- into an output bundle -dist/my-app.js-. We then created an index.html that calls the bundle. This way, all the code inside src/index.js is executed in the browser when we manually open the index in a browser.

The next step is to separate our code into multiple files.

To build our source files, Webpack will start by compiling the entry point provided in the configuration file. It will then move from require to require (or import in ES6), and include every ’required’ file in the build pipe.

Let’s see how to include other files to the bundled output:

// src/greet.js
function greet(who) {
    console.log('Hello ' + who + '!');
};

module.exports = greet;       // Exposes the greet function to a require in another file

// src/index.js
var greet = require('./greet');   // Import the greet function

greet('Webpack');

As you see, we can require local files by providing a relative path. Notice that we don’t need to specify the file extension, since it’s a JS file. Webpack can manage a bunch of default extensions to know which file it will look for.

Another important thing to notice is that you might eventually end up with very long relative paths when your application grows. For example, it is quite common to depend on a file located in a different folder, and you will find yourself writing imports such as require('../../../../components/home/dashboard/title').

To avoid such messy paths, it is possible to tell Webpack which folder it can consider as the application’s root folder. In our case, src is the root folder:

// webpack.config.js

var path = require('path');
var SRC = path.join(__dirname, 'src/');
var NODE_MODULES = path.join(__dirname, 'node_modules/');

// ...
  resolve: {
    root: [SRC, NODE_MODULES],                  // root folders for Webpack resolving, so we can now call require('greet')
    alias: {
      'actions': path.join(SRC, 'actions/'),    // sample alias, calling require('actions/file') will resolve to ./src/actions/file.js
      // ...
    }   
  },
// ...

With the ability to use modules, we now have very clean code organization. This is because it has been split into several files and folders, depending on what your application is.

Moreover, we can manage our dependencies just as we would do in a Node application. Therefore, we can import external libraries by calling require('library-name').

This will ask Webpack to resolve the module library-name, by looking into the node_modules folder. (Webpack will look into other default folders too, see there).

Let’s modify our greet function to use an external library:

// src/greet.js
var moment = require('moment'); // Add momentjs

function greet(who) {
    console.log('Hello ' + who + ', it\'s ' + moment().format('h:mm:ss a') + '!');
};

module.exports = greet;

Compilation

So far we have our source files bundled up into a single output file, which is read by the index.html.

That’s a good starting point for building our web application, but unless we want to write it in plain old ES5 Javascript, we need to actually transform the files while they are bundled together.

Let’s say for instance we want to use the new Javascript specifications, EcmaScript2015 and EcmaScript2016.

Since we are building an application for the web, we need to maximize the browser compatibility of our code. Therefore, we need to transpile any ES6/ES7 code to ES5, which is supported by all modern browsers (do not hesitate to use the excellent caniuse website to check any browser-related compatibility questions).

We will use Babel to transpile ES6 and ES7.

Let’s install Babel and its presets for ES6 and ES7:

npm install --save-dev babel babel-preset-es2015 babel-preset-stage-0

We need to configure it to use these presets:

// package.json

// ...
    "babel": {
        "presets": [
            "es2015",      // ES6 compilation ability
            "stage-0"      // ES7 compilation ability
        ]
    },
// ...

Babel is now configured to transpile JS files, but it’s still not working together with Webpack.

Let me introduce the concept of Webpack loaders. Loaders are used by Webpack in order to handle a given file type.

Every time Webpack reads a require() or an import, it will handle the content of the required file with a loader.

By default, Webpack comes with a built-in JS handler, which tells it how to handle plain JS files and bundle them up. In order to handle different file types, or simply deal with JS files in a different manner, we must specify a loader configuration to Webpack.

As you might have guessed, the link between Webpack and Babel will be made by a loader.
It’s called… babel-loader!

First, we need to download it:

npm install --save-dev babel-loader

Now that we have it as a dependency, we must tell Webpack where to use it.

// webpack.config.js
var config = {
    entry: './src',
    output: {
        path: './dist',
        filename: 'my-app.js'
    },
    module: {
      loaders: [
        {
          test: /\.js$/,
          loaders: ['babel']      // note that specifying 'babel' or 'babel-loader' is equivalent for Webpack
        }
      ]
    }
}
module.exports = config;

This will add a new loader to Webpack.

Every time Webpack sees a require('xxx.yy'), it will loop through all its configured loaders and check if xxx.yy matches the provided test regexp (in our case, /\.js$/).

As you can infer now, the babel-loader will be used to compile every .js file. We can therefore rewrite our good old ES5 files into ES6/ES7 files!

Managing other file types

Now that we are clear on the loader concept, we can manage other file types. In this article, I’ll show you how to handle some common file types, such as fonts and CSS files; however, keep in mind that there are a LOT of loaders on npmjs for almost any file type.

Style files

Plain CSS files

There are two loaders for .css files, called style-loader and css-loader.

You can try to figure out by yourself how to add these loaders in order to handle .css files.

Here is a suggestion of configuration:

// webpack.config.js

// ...
  {
    test: /\.css$/,
    loaders: ['style', 'css'] // Note that the order is important here, it means that 'style-loader' will be applied to the ouput of 'css-loader'
  },
// ...


NOTE: the CSS will be added to the bundled file; in our case, my-app.js. The style will be loaded in your browser, but you might be used to having separate .css files. If you want Webpack to output separate style files, please use ExtractTextWebpackPlugin.

SASS/Less/PostCSS

In case you are using SASS, Less or PostCSS, simply add the corresponding loader:

Font files

If you use specific fonts in your project, Webpack can handle them too!

Since the fonts will be downloaded as such by your client application, use the file-loader:

// webpack.config.js

// ...
    {
      test: /\.(eot|svg|ttf|woff|woff2)$/,
      loader: 'file?name=public/fonts/[name].[ext]'
    }
// ...

This will output all font files to the public/fonts folder.

Images

Similarly to fonts, images are usually served directly by web servers and loaded on demand by the client. The loader configuration is very similar:

// webpack.config.js

// ...
    {
      test: /\.(jpg|png|svg)$/,
      loader: 'file?name=public/images/[name].[ext]'
    }
// ...

Other files

We could continue the list of all possible files Webpack loaders can handle, but this is not the aim here. If you want to load other file types, search for ‘yourFiletype loader’ on the Internet, and you will find the corresponding loader. For example, load .jade files with the jade loader, .cs files with the coffee loader, etc.

Going further

These are the basic usages of Webpack. You might now want to move to more advanced topics about Webpack.

Here is a non-exhaustive list of subjects you might want to read about:

In the meantime, do not hesitate to read the official documentation to discover new features and grab a deep understanding of Webpack’s multiple options.

Update 05/02/17

I had initially planned to write this article in two parts, putting the more advanced topics in the second parts. Unfortunately, I could not make it to write the second part, so I added two links to great articles dealing with these topics.

Liked this article?