Key Concepts

Modern front-end projects become very complex and include multiple modules of different kind. Such tools as webpack called module bundlers help developers to make process of consuming modules manageable.

Configuration file webpack.config.js

webpack uses file webpack.config.js that holds configuration for the project. The configuration itself is stored into configuration object that should be assigned to module.exports property:

Sample starting point for webpack.config.js:

module.exports = {};

Entry

Entry point is the starting point for graph of dependencies that webpack builds while creating bundle.

Sample entry in webpack.config.js:

module.exports = {
  entry: './app/app.js'
};

Output

Output tells webpack where to bundle application.

Sample output in webpack.config.js:

const path = require('path');

module.exports = {
  entry: './app/app.js',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'app.bundle.js'
  }
};

Loaders

Loaders are used to transform all files in a project (.js, .html, .css, .jpg, etc.) as they are added to dependency graph. Each file is treated as module and test property allows to chose proper loader for the file.

Sample loaders configuration in webpack.config.js:

const path = require('path');

module.exports = {
  entry: './app/app.js',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'app.bundle.js'
  },
  module: {
    rules: [
      { test: /\.txt$/, use: 'raw-loader' }
    ]
  }
};

Plugins

Unlike loaders that execute transforms on per-file basis, plugins can perform actions on compilations or chunks of bundled modules, they are much more flexible and configurable.

Sample configuration with plugins in webpack.config.js:

const path = require('path');

module.exports = {
  entry: './app/app.js',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'app.bundle.js'
  },
  module: {
    rules: [
      { test: /\.txt$/, use: 'raw-loader' }
    ]
  },
  plugins: [
    new webpack.optimize.UglifyJsPlugin(),
    new HtmlWebpackPlugin({template: './src/index.html'})
  ]
};

Installation and Initial Configuration

It is recommended to install webpack locally

npm install webpack --save-dev 

To run it with npm we need to create entry in scripts section of package.json file:

"bundle": "webpack --config webpack.config.js" 

Let’s create config file webpack.config.js:

var path = require('path');

module.exports = {
    entry: './app/index.js',
    output: {
        filename: 'bundle.js',
        path: path.resolve(__dirname, 'dist')
    }
};

It will take index.js file in app folder as entry point and produce output to the file named bundle.js in dist folder.

We need to reference bundled file to include it in html code:

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
</head>
<body>
    <script src="dist/bundle.js"></script>
</body>
</html>

Bundling with External Module

Let’s create index.js file that relies on npm module lodash.

We need to install lodash module with the command:

npm install lodash --save

Then we modify our index.js:

import _ from 'lodash';
function component () {
    var element = document.createElement('div');

    /* lodash is required for the next line to work */
    element.innerHTML = _.join(['Hello','webpack'], ' ');

    return element;
}

document.body.appendChild(component());

Then we can build our bundle with webpack:

npm run bundle

Bundling Vendor Libraries Separately

In most cases it is preferrable to have vendor code bundled separately To implement such scenario we need to make following changes to webpack.config.js from the previous sample:

  • need to add webpack module with require(‘webpack’)
  • convert entry to object with two keys: index and vendor, first is used to reference actual entry point, second will hold array of vendor libraries
  • modify filename in the output to include name of the entry
  • at last we need to use built-in webpack plugin CommonChunkPlugin to optimize output and remove libraries bundled to vendor.bundle.js file from index.bundle.js, otherwise vendor libraries will be duplicated in both bundles

webpack.config.js:

var path = require('path');
const webpack = require('webpack');

module.exports = {
    entry: {
        index: './app/index.js',
        vendor: ['lodash']
    },
    output: {
        filename: '[name].bundle.js',
        path: path.resolve(__dirname, 'dist')
    },
    plugins: [
        new webpack.optimize.CommonsChunkPlugin({
          name: 'vendor', 
          filename: 'vendor.bundle.js'
        })
    ]
};

To reference separate bundle with vendor code we need to make changes to index.html and reference vendor.bundle.js before index.bundle.js:

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
</head>
<body>
    <script src="dist/vendor.bundle.js"></script>
    <script src="dist/index.bundle.js"></script>
</body>
</html>

Then we can run webpack the same way as before with npm run bundle command.

Adding EcmaScript 2015 and Above Support with Babel Loader

It is possible to configure webpack to support EcmaScript 2015 and above with Babel loader First, we need to install Babel loader and required npm modules locally:

npm install --save-dev babel-loader babel-core babel-preset-es2015

Then we need to make changes to webpack.config.js and add module section where we describe rules how loaders are applied:

  • we pass array or rules to rules property
  • array entry has test, exclude and use properties
  • test property defines RegEx used to test for file name extensions that will be treated by this rule
  • exclude property defines RegEx that we use to exclude node_modules folder
  • use property defines the rule, it tells that we use babel-loader, pass options where define list of presets with one value 'es2015'
var path = require('path');
const webpack = require('webpack');

module.exports = {
    entry: {
        index: './app/index.js',
        vendor: ['lodash']
    },
    output: {
        filename: '[name].bundle.js',
        path: path.resolve(__dirname, 'dist')
    },
    plugins: [
        new webpack.optimize.CommonsChunkPlugin({
           name: 'vendor', 
           filename: 'vendor.bundle.js'
        })
    ],
    module: {
        rules: [
            {
                test: /\.js$/,
                exclude: [/node_modules/],
                use: [{
                    loader: 'babel-loader',
                    options: { presets: ['es2015'] }                    
                }]
            }
        ]
    }
};

To show that Babel loader works, we will make few small changes to our app/index.js file - replace var with const and replace ‘div’ with div:

import _ from 'lodash';

function component () {
    const element = document.createElement(`div`);

    /* lodash is required for the next line to work */
    element.innerHTML = _.join(['Hello','webpack'], ' ');

    return element;
}

document.body.appendChild(component());

Then we run webpack with npm run bundle command and output will be almost same as before. The difference will be that file index.bundle.js will be produced in ES5 format:

Fragment from dist/index.bundle.js:

//…
function component() {
    var element = document.createElement('div');

    /* lodash is required for the next line to work */
    element.innerHTML = _lodash2.default.join(['Hello', 'webpack'], ' ');

    return element;
}
//…

Bundling CSS Styles with webpack

CSS Styles can be bundled with webpack too, we need two loaders for this:

  • css-loader - reads CSS file and converts it into a module available with import statement;
  • style-loader - dynamically injects styles with <style> tag into <head> section of an HTML page.

To install them issue the following command:

npm install style-loader css-loader --save-dev

These loaders should be chained together to allow webpack first read and transform into module and then insert styles into a document.

Please note that we pass an array of loaders to use property of the rule and webpack applies loaders reading array in reversed order, from left to right, so in our sample css-loader will be called first and style-loader will be called second to process output

Important note: we need to import styles with import keyword into one of the JavaScript files that are bundled by webpack

Fragment from webpack.config.js:

module.exports = {
    //... 
    module: {
        rules: [
            //...
            {
                test: /\.css$/,
                use: [
                  'style-loader', 
                  'css-loader'
                ]
            }
            //...
        ]
    }
};

Let’s add CSS styles and load them into our document with webpack.

We need to complete following steps:

  • configure rules in webpack.config.js with code shown on a previous screen
  • create file app.css in a folder app/assets/styles with sample style that sets red color for a <div> element
  • reference style with import keyword in app/index.js file
  • run webpack, open index.html and see that styles have applied successfully

Bundling Styles with SASS

Using SASS is simple with webpack:

  • Install two additional modules for SASS support with the following command (assume we already installed css-loader and style-loader):

    npm install sass-loader node-sass --save-dev
    
  • Add new rule to webpack.config.js for .scss files (on the screen).

The rule is very similar to previous one, the difference is that we test for .scss file extension and run sass-loader first, then two other loaders from the previous sample - css-loader and style-loader. Remember, that loaders run in reversed order.

Fragment from webpack.config.js:

module.exports = {
    //... 
    module: {
        rules: [
            //...
            {
                test: /\.css$/,
                use: [
                  'style-loader', 
                  'css-loader',
                  'sass-loader'
                ]
            }
            //...
        ]
    }
};

To demonstrate how webpack processes SASS files, we will rename our file with styles in folder app/assets/styles from app.css to app.scss and create variable $highlight-color that will hold color code to show that this file is using SASS syntax.

$highlight-color: red;

div {
    color: $highlight-color;
}

Also we need to modify line in file app/index.js where we import styles and reference new file instead of old one: import styles from './assets/styles/app.scss';

Next, we run webpack with npm run bundle command, open index.html and see that styles applied successfully, output will be same as before as well as contents of the <style> element dynamically injected into head.

Extracting Styles to a File

When styles become too large it is better to have them in separate file, so browser can download them in parallel

Let’s configure webpack to bundle styles to separate file

We need to install plugin ExtractTextPlugin with the following command:

npm install extract-text-webpack-plugin --save-dev

Then we will make changes to webpack.config.js file:

  • load ExtractTextPlugin with require
  • add new instance of the plugin to plugins array and pass configuration string that defines how to name files
  • modify rule for .scss files to use the plugin

Modifications to webpack.config.js:

//...
const ExtractTextPlugin = 
    require('extract-text-webpack-plugin');

module.exports = {
    //...
    plugins: [
        //..
        new ExtractTextPlugin('[name].bundle.css')
    ],
    module: {
        rules: [
            //...
            {
                test: /\.scss$/,
                use: ExtractTextPlugin.extract([
                  'css-loader', 
                  'sass-loader'
                ])
            }
        ]
    }
};

To load extracted styles into html we need to modify index.html and add link element that targets file with CSS styles:

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
    <link rel="stylesheet" href="dist/index.bundle.css">
</head>
<body>
    <script src="dist/vendor.bundle.js"></script>
    <script src="dist/index.bundle.js"></script>
</body>
</html>

File app/index.js will be not modified, we still need to import styles otherwise webpack will ignore them.

To run webpack will execute command npm run bundle.

Lazy Loading with webpack

In some cases it is preferable to load code on demand. webpack allows to load chunks of bundles lazily only when they are needed. To achieve this we can use System.import() or import() statements that return promises and perform actual load of bundles only when we need them.

Let’s refactor our example and display greeting only when user clicks on a link.

First, we will create file `async-load.js that will be loaded dynamically, it will have one line of code exporting constant with message ‘Async’.

export const msg = 'Async';

Next, we will refactor app.js: when app starts we will create a link that will call addComponent() function that will load dynamically file async-load.js and show greetings.

import _ from 'lodash';

function addComponent () {
    const element = document.createElement('div');
    System.import('./async-load').then(asyncLoad => {
        element.innerHTML = _.join(['Hello','webpack', asyncLoad.msg], ' ');
        document.body.appendChild(element);
    });
}

document.body.innerHTML += '<a href="#">Click to show greetings</a>';
document.querySelector('a').addEventListener('click', addComponent);

Please note how we load file asynchronously with System.import()

Next, we should modify webpack.config.js and add the following key-value pair to output section, it will tell webpack an address from where to load bundles dynamically: publicPath: 'dist/'

Then we can run webpack with npm run bundle command and see in the output that there is additional bundle produced with name 0.bundle.js:

webpack.config.js:

var path = require('path');
const webpack = require('webpack');

module.exports = {
    entry: {
        index: './app/index.js',
        vendor: ['lodash']
    },
    output: {
        filename: '[name].bundle.js',
        path: path.resolve(__dirname, 'dist'),
        publicPath: 'dist/'
    },
    plugins: [
        new webpack.optimize.CommonsChunkPlugin({name: 'vendor', 
               filename: 'vendor.bundle.js'})
    ],
    module: {
        rules: [
            {
                test: /\.js$/,
                exclude: [/node_modules/],
                use: [{
                    loader: 'babel-loader',
                    options: {presets: ['es2015']}
                }]
            }
        ]
    }
};

Let’s open index.html and see how lazy loading works (we should use HTTP server). First, when page opens only two bundles vendor.bundle.js and index.bundle.js are loaded. When we click on a link, we see that additional request is issued and bundle 0.bundle.js is loaded and executed.

Using webpack development server

To simplify development and debugging processes webpack has development server.

Install it with the command:

npm install webpack-dev-server --save-dev

To run development server let’s add this line to scripts section of package.json file:

"start": "webpack-dev-server --inline"

Then run command:

npm start

It will start development server that will monitor file changes and rebuild project automatically.

To open website in a browser we need to navigate to http://localhost:8080

Switching to Production Mode

Finally, when we are ready to deploy our project to a production, we need to run webpack in production mode with key -p. To do so, let’s add a line to scripts section of a package.json file:

"build": "webpack -p --config webpack.config.js"

Then we run webpack in production mode with this command:

npm run build

It will produce minificated output ready for production.