< Back to articles

Webpack – Modern Web Development

Webpack became quite a popular technology and is often compared to utilities like Gulp or Grunt. In this article I will explain the fundamentals and philosophy behind Webpack as well as mention when and how to use (or not to use) this technology.

The blog post is based on text which I wrote for Webpack example configurations on Github to support our educational meetings at Ackee ❤.

Since the article in 2017, there have been several changes in Webpack. The original blog post was written for Webpack version 2 and Webpack is now in stable version 4 with version 5 coming soon.

Webpack has dropped support of Node version 4 so now you need to use at least version 8. The project has split into two packages `webpack` and `webpack-cli`. If you want to use Webpack from CLI, be sure you have the second one installed. 

The configuration of Webpack has changed too. Webpack now supports modes (`production` and `development`), which makes it possible to use Webpack with almost no configuration. The modes are sets of battle tested configurations you can rely on. Using features like code-splitting and tree-shaking is now easier than ever before. The upcoming version of Webpack will bring even better developer experience with fully supported caching and parallelism. These features will speed up build time.

Webpack had been known as hard to use for its complicated configuration. With the changes in version 4, Webpack is now easy to use and thanks to the new documentation even easier to understand.

Find out more in my blog post about Webpack updates in 2019.

What is Webpack?

What are modules?

From the beginning of it’s existence, Node.js has supported _CommonJS_code splitting used for splitting the program code into dependency modules that can be loaded and used when needed:

var $ = require('jquery');— jQuery is loaded as a dependency in some module and ready to be used
module.export = jquery; — jQuery is exported from a module and can be used somewhere as a dependency

Code splitting and splitting into modules are often used to establish better readability and easier code management. It is a common practice known for languages such as C, C++, PHP, Ruby, Python, Java … as well as Node.js as the JavaScript terminal interpreter.

JavaScript interpreted from any browser doesn’t support code splitting natively. Each script is executed within one context in the browser sequentially as the HTML is parsed. This approach causes problems for global variables, the existence of variables within the context, the dependency origin, and for code functionality.

Preparing modules for the browser

This is where Webpack comes into play. Webpack supports code splitting in the browser by writing modules as if they were for a Node.js interpreter (i.e. CommonJS, but since v2.0 natively also ES modules and AMD). Suppose we have a Javascript code written with CommonJS modules like we have now with Node.js. How do we tell the browser to work with it?

Webpack processes the code written as a module and creates a package out of it (for simplicity let’s assume one js file which is not always necessarily true). Furthermore, it allows us to use npm packages as modules like React and jQuery which don’t have to be loaded as separate scripts in the HTML document. This means: no more global variables, outscoping, or unpredictable codes.

Some might say that the tool called Browserify is used for the very same purpose, in which case they would be right. However, Webpack goes even farther. The primary purpose of Webpack is working with JavaScript modules and creating packages for browsers, though it also functions to work with various other types of assets. With the proper configuration it is able to process these assets and create a package(s) which is/are uploaded onto a webserver where everything will run smoothly. This means that we are able to load SASS or CSS as a dependency in any js module.

What are Webpack modules?

A Webpack module is anything that can be loaded with require (import) in the code processed by Webpack. It can be CommonJS module, ES module, AMD (these three have native support by Webpack) as well as Sass @import in Sass code (not natively, but thanks to extensions), and Webpack will be able to process and analyze them when configured to do so.

Non-javascript assets supported by Webpack that can be imported to js:

  • jsx, coffee
  • css, sass, less, stylus, postcss
  • png, jpeg, svg
  • json, yaml, xml and others

It is important to keep in mind that Webpack’s output is mainly js package(s) for browsers. This means processing and converting assets to js or extracting them from the source modules to individual files. Webpack uses loaders and plugins for this specific preprocessing of modules and packages them right before the output is saved onto the filesystem.

Webpack summary

  • main goal = creating js packages out of modular js code to be used in browsers
  • allows transforming, processing, modifying, and packaging for almost any type of asset
  • uses loaders and plugins for assets’ preprocessing
  • input is a modular Webpack code (i.e. modules of different types), output is a js package (and other files if preprocessing is configured to do so)

Webpack configuration

The Webpack config file is a Node.js module that exports a config object. It can contain any Node.js code which itself gives us a lot of options. For instance, it can give us dynamic loading of the input files for Webpack from the filesystem.

The default config file name is webpack.config.js. Run the webpack with webpack --config ./configs/webpack.config.js

The configuration object contains 4 important keys:

  • entry – input settings
  • output – output settings
  • module – module settings (mainly loader settings)
  • plugins – plugins and their settings

Entry

Entry sets Webpack’s input. It tells Webpack where to start with the creation of a package. Every single file mentioned in the entry is a root in a dependency graph used for creating the final package.

Entry comes in the following types:

  • string — path to file => one entry point, one package
  • array— array of paths to files => more entry points, one package
  • object — object of named paths to files => more entry points, more packages

Within a configuration we can use the key context, making the relative paths in Entry related to that context.

Rule: one entry point to a HTML page, SPA = one global entry point, MPA = more entry points

Example entry config:

module.exports = {  
  context: path.resolve(__dirname),  
  entry: './index.js',  
  // entry: ['./index.js', './login.js'],  
  // entry: {  
  //   index: './index.js',  
  //   login: './login.js',  
  // },  
}

Output

Output sets the output of Webpack. It is an object with keys:

path

Path to the output folder. Relative to context.

filename

Output name:

  • if the input is a string or an array, the value is a single string used as a package name saved to path
  • if the input is an object, the value is a template string for generating the package names. For instance, [name].js means using the keys from an input object. Alternatively, we could use [chunkhash].js for saving the package as it’s hash generated by Webpack.

publicPath

The public path to outputted files (important when outputting other assets like css files) is used by loaders (e.g. url-loader) and plugins. It is a path exposed by the webserver. If this webserver’s root is identical with the path, the publicPath should be / .

Example output config:

module.exports = {  
  context: path.resolve(__dirname),  
  entry: './index.js',  
  // entry: ['./index.js', './login.js'],  
  // entry: {  
  //   index: './index.js',  
  //   login: './login.js',  
  // },  
  output: {  
    path: path.join(__dirname, 'build'),  
    publicPath: '/',  
    filename: 'bundle.js',  
    // filename: '[name].js',  
    // filename: '[chunkhash].js',  
  }  
}

When using Webpack only to build packages from a modular ES5 Javascript code,  specifying entry an output is enough.

Loaders

Loaders are transformations applied per module. They are applied in distinct types of modules before the package is created. In the module key of config there are specific rules in which cases loaders should be applied.
Every rule consists of a regular expression to test if the loader should be used. The loader has it’s own name and options:

module.exports = {  
module: {  
  rules: [  
    {  
      test: /.js$/  
      use: [  
        {  
          loader: 'babel-loader',  
          options: {  
            presets: ['react']  
          }  
        }  
      ]  
    },  
    {  
      test: /.sass$/,  
      use: [  
        {  
          loader: 'style-loader',  
        },  
        {  
          loader: 'css-loader',  
        },  
        {  
          loader: 'sass-loader',  
        }  
      ]  
    }  
  ]  
}  
}

When there are more loaders, they are applied on the module with a bottom-up strategy. In the exhibit above, sass, css, and a style loader are applied respectively for files with .sass extension.

Loaders set in config are applied automatically when there’s a regexp match for module import. For the exhibit above the loader would be applied in case of  require('../sass/main.sass').

Loaders don’t have to necessarily be set in configuration. An alternative way is the require clause require('style-loader!css-loader!sass-loader!../sass/main.sass') . This loader usage is preferred to the config way and the loaders are applied right-to-left. The loader configuration is done via query string require('babel-loader?presets=['react']!./component.jsx').

Using loaders, we can transpile JSX to ES5 js, ES6 js to ES5 js, Sass to CSS, images to base64, json files to js objects, etc.

The last loader in the pipe has to be Javascript (because Webpack is a js module bundler). It is thus necessary to convert all non-browser-js modules to ES5 Javascript, because Webpack mainly creates JS packages. Therefore, style-loader takes CSS and converts it to Javascript which then inserts the original CSS dynamically into the HTML head document. Something similar must be done with the other non-js modules as well.

Plugins

Plugins are transformations per bundle (package). They are used for functionality not supported by the loaders. Such functionalities can be style extractions, separating out vendors into standalone packages, obfuscation (minification, uglification) of Javascript, or setting global variables.
Plugins are configured in the plugins key of config. It is an array of instances of plugins (a single plugin can be used several times).

Example plugin usage:

module.exports = {  
  plugins: [  
    new ExtractTextPlugin({  
      name: 'bundle.css'  
    }),  
    new webpack.optimize.UglifyJsPlugin({  
      minimize: true  
    }),  
 ]  
}

As mentioned earlier, Webpack is not limited to Javascript package creation. If we don’t want to add CSS to the js package as a code (with style-loader) but want to extract them as a standalone CSS bundle, we can use ExtractTextPlugin. Webpack then generates via the plugin to output.path the file bundle.css containing all CSS from js package due to the plugin applied on the whole js package.

How it works

  1. Webpack creates dependency graphs whose roots are described in entry.
  2. It goes deep in modules which are required at entry points, and applies loaders according to rules to transpile or transform them.
  3. When Webpack completes the whole dependency graph for a chosen entry point, it is ready to create the package. First, however, the specified plugins are applied, and things like minification, uglification (obfuscation), and extraction of vendor packages are also completed.
  4. Finally, Webpack saves the package to output.

Example configuration

const webpack = require('webpack');  
const path = require('path');  
const Extract = require('extract-text-webpack-plugin');

module.exports = {  
  context: path.resolve(__dirname),  
  entry: './index.js',  
  output: {  
    path: path.join(__dirname, 'build'),  
    filename: 'index.js',  
  },  
  module: {  
    rules: [  
      {  
        test: /**.**js$/,  
        use: [  
          {  
            loader: 'babel-loader',  
            options: {  
              presets: ['latest', 'react']  
            }  
          }  
        ]  
      }, {  
        test: /**.**sass$/,  
        loader: Extract.extract({  
          fallback: 'style-loader',  
          use: 'css-loader!sass-loader'  
        })  
      },  
    ]  
  },  
  plugins: [  
    new Extract({  
      filename: 'bundled-sass.css',  
    }),  
    new webpack.optimize.UglifyJsPlugin({  
      minimize: true,  
    }),  
    new webpack.EnvironmentPlugin([  
      'NODE_ENV',  
    ]),  
  ]  
};

Module bundler versus task runner

Gulp and Grunt are task runners. They are used to task automation, which the user defines in programming language or configuration. Some of the tasks might be compilation, transpilation, linting, testing, etc. More importantly, every task works with one asset (CSS, Javascript, etc) and it is up to the programmer to ensure the correct result, e.g. having the right path to images.

As we mentioned earlier, Webpack is mainly a utility working with JavaScript and allows writing modular codes for the browser bundled into one package. From this point of view, running Webpack is one single task.****

Neither Gulp nor Grunt examine the code being passed to them, only executing certain tasks on that code. On the contrary, Webpack analyzes the passed code, and based on the configuration edits and processes it.

This is the main difference. Webpack is one single task being run on a Javascript code. It is similar to running one task for sass compiler, only a bit more complex, considering there are more complex transformations on the code.

When to use Webpack

  • When writing a modular JavaScript code for the browser.
  • When using JS standards that are not yet implemented (ES6+).
  • When writing SPA in React.
  • When writing MPA and you want to write a modern code, etc.

I only mentioned JavaScript because other assets should be, in my opinion, considered only if you want to use Webpack for JavaScript, as you need to import them to JavaScript. It doesn’t make any sense to use Webpack for Sass when there is only Sass and css. You are better off just running  sass --watch sass:css.

I personally use Webpack all the time. When writing small js snippets or a bigger project it allows me to write a clean, readable, and modern code which can be obfuscated without any further hassle.
As I use Webpack often I have added loaders for SASS and PostCSS and hence can write modern StyleSheets.
Added value is placed on optimizing other assets like images and fonts.

Does it make any sense to use task runners with Webpack?

Yes it does. If you are using Gulp and you need features provided by Webpack, there is a plugin for Gulp allowing you to run Webpack as a task.
If you don’t like to import SASS to JavaScript when using Webpack with Gulp, separate the SASS transpilation into it’s own task.

But

As mentioned above, task runners can run minification, transpilation, linting, and testing. Webpack can also do that with a little bit of configuration, so there is no need to use task runners when already using Webpack.
You can run export NODE_ENV=production; webpack --progress --config webpack.config.js or add this command to package.json and run yarn run deploy.prod and the result can be exactly the same as with the task runner. It is clearly up to you what solution you find best or which suits your company’s dev flow better. 

Use Yarn

Every single one of you has probably been in a situation where dev instance works smoothly but an app deployed to production with CI doesn’t work at all. In most cases, the npm packages are to blame.

The reason is simple: during deployment, the packages in package.json are installed, which can cause the installation of a newer version of a package which is different from the one you are using locally, making everything break. Solution? Yarn.

Yarn is a packaging system for Node.js. We are already using one: npm. Yarn, same as npm, works with NPM repository and uses package.json. This means that you can install the same packages as you would with npm. Why use it then? It has a different philosophy, different commands, but more importantly – it has a lockfile.

Running yarn install when there is package.json and yarn.lock at the root of the project causes yarn to ignore package.json and installs the locked versions from the yarn.lock file. The CI then installs the same package versions as you’d have. This is useful not only with CI/CD but when collaborating with more developers on the same project.
This functionality is known from things like composer for PHP, but npm still lacks this feature.

Yarn also claims to be faster than npm and should work in offline mode, but I personally find the lockfile mechanism to be more useful.

Use yarn, it will make your life easier!

Marek Janča
Marek Janča
Frontend Developer

Are you interested in working together? Let’s discuss it in person!