An Intro To Webpack

What is webpack?

Webpack takes the modules and dependencies in our web applications and assembles them into an asset that is optimized for browsers. It’s a static module bundler. This means that when we write our web application, and we set up the many folders, file extensions, and dependencies that make up our app, we can depend on webpack to take that information and assemble it into a neat bundle that the browser can retrieve in as little as one API call. Webpack is a static module bundler because it expects the dependencies to be defined at compile time.

When we add webpack to our application, we won't have to worry about organizing or ordering our dependencies in the code bundle served to the client, and we even have additional options for further configuring this bundle to suit our application's needs.

Configuring webpack

We use webpack.config files to customize the behavior of webpack. A webpack.config file exports an object containing our desired settings.

We have quite a few options we can choose from to customize our config, so I'll mention a few very useful ones:

  • Entry: The entry point is the file where the application starts executing. With this entry point option, we can designate the place where webpack will start looking for dependencies and building the bundle.
  • Output: This is where we can choose the target directory for output files. This must be an absolute path, so it is useful to use the node path module.
  • Loaders (Module Rules): The module option allows us to indicate any loaders we want to use. Loaders pre-process files as they’re imported. They allow webpack to deal with various file types, and they’re applied on a per-file basis. In the config file, it is important to note that they are loaded from right to left in an array of loaders.
  • Plugins: Plugins are extensions for webpack that alter how webpack works. Plugins are applied as the bundle is being created. They hook into key events during the compilation process to change or extend the behavior of webpack.

Here’s an example of a webpack.config file and how we might choose to define the options listed above:

const path = require("path");
const MyPlugin = require('my-plugin');
const AnotherPlugin = require('another-plugin');

module.exports = {
  entry: './src',
  output: {
    path: path.resolve(__dirname, "dist"),
    filename: "[name].ready.js"
  },
  module: {
    rules: [
      {
        test: /\.fileextension$/,
        use: ["third-loader", "second-loader", "first-loader-for-this-file-type"]
      },
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: ["babel-loader"]
      },
      {
        test: /\.scss$/,
        use: ["style-loader", "css-loader", "sass-loader"]
      }
    ]
  },
  plugins: [
    new MyPlugin(),
    new AnotherPlugin(),
  ]
};

Please note that with a few modifications, it is also possible to set up our webpack.config file with multiple configurations for different environments and/or in a language other than JavaScript.

What's happening when webpack bundles our project?

When webpack runs, it builds a dependency graph providing an interface for webpack to access our dependencies while it builds the bundle. Absolute paths and other file information are gathered in the graph. Webpack then creates modules from each of these dependencies, and those modules are parsed into an Abstract Syntax Tree. The asset that is ultimately generated for the browser is composed from boilerplate templates. Your code is taken as data from the AST and inserted into these templates to become a file specially formatted for a browser.

If we take a look at the webpack repository on GitHub, we will find files containing the definitions of some of the key classes that handle the compilation process in webpack. Webpack makes use of its Tapable library to expose hooks for events during the bundle creation process. Many of the classes in webpack extend Tapable in order to provide hooks to plugins so that they, too, can access lifecycle events.

The Compiler is one of these classes. It is where all the higher level events for webpack are fired, including the instantiation of plugins and the Compilation class.

The Compilation class builds a graph of the application's dependencies by recursively traversing the dependencies in the project. It is the class responsible for passing dependencies to loaders for conversion, parsing dependencies into an AST, and interpolating the parsed code into templates to generate the final bundles.

Building our own custom webpack plugin

Now, let's take a closer look at another important part of webpack: plugins. Plugins alter webpack’s behavior. Some examples of what plugins can be used for include file compression, specifying environment variables, and code minification.

If we take a closer look at how plugins work, it can give us a better understanding of how webpack works as a whole. A lot of the core functionality of webpack itself is built on the same infrastructure that any custom plugins make use of.

As mentioned above, many of the key classes that make up webpack extend the Tapable library to expose hooks into their own lifecycle events. Plugins take advantage of these hooks in order to access webpack at different points in the compilation process and customize webpack’s behavior.

To see how plugins make use of lifecycle hooks, we can write a simple example plugin.

In this plugin (which will be a JavaScript class), we will need to define an apply method. Webpack will use apply to install the plugin when it creates the compiler instance. It will also pass down a reference to the compiler instance as an argument to the plugin. Our plugin will tap into a hook on the compiler instance, and pass that hook a callback that will then be executed at a certain lifecycle event.

class SimplePlugin {

  apply(compiler) {
    compiler.hooks.compilation.tap("SimplePlugin", (compilation) => {
      compilation.hooks.succeedModule.tap("SimplePlugin", (module) => {

        // What we want to happen

      });
    });
  }
}

module.exports = SimplePlugin;

In the above example, we can see that we take the compiler instance as an argument and are able to access a compilation hook within the compiler's hooks. We can then call tap on that hook and pass it a callback to run our plugin.

Then, inside of that first callback, we are able to access any of the compilation lifecycle event hooks. For this example, let’s use the succeedModule hook, which is executed each time a module is successfully built during compilation. We call the tap method once more and again pass it a second callback function. Within this second callback, we are able to execute code at this particular moment in the webpack lifecycle—when a module is successfully compiled during webpack's compilation process. (Webpack will pass the completed module as an argument to our second callback.)

The compilation and succeedModule hooks that we’ve chosen to tap in the above example are just two of a long list of lifecycle events that we can access using our apply method’s compiler argument. The webpack docs provide a list of the names and descriptions of the events accessible from the compiler here, and we can use the tap method to access any of these events.

Finally, to install our plugin, we would need to include it in our webpack.config:

const SimplePlugin = require("./plugins/simple-plugin.js");

module.exports = {
...

  plugins: [
    new SimplePlugin(),
  ]
};

Now, our plugin code will be executed each time the succeedModule lifecycle event occurs during compilation.

Conclusion

So, to wrap things up: Webpack is a static module bundler. It takes modules and dependencies in our web apps and assembles them into an asset that is optimized for browsers. Webpack traverses the dependencies in our apps, converts them into modules, and parses them into an AST. This information is then inserted into templates which make up the bundle created for our browser.

We can configure webpack's entry point, output, module loaders, and plugins—among other options—in our webpack.config file. Plugins are extensions to webpack that alter how webpack works by hooking into a webpack lifecycle event and executing custom code.

If you're curious to learn more about webpack and how it works, a great place to start is the 'concepts' section of the webpack docs as well as webpack's GitHub repo.

Additional resources: alternatives to webpack

Webpack is not your only option when it comes to building your website:

  • One alternative tool is Snowpack, which ships and caches each file individually. It takes advantage of ES Modules' import and export functionality, which in recent years has come to be supported by many modern browsers.
  • Rollup is another choice that takes advantage of ES Modules.
  • Parcel is an alternative that can work well for small-scale projects. It requires less configuration than webpack—or none at all.

16