Skip to main content
Home

Huy Dang Lê-Ngô

Senior Front-End Engineer @Slack

Thursday, August 18th, 2016

Bundling assets with Webpack

As a developer, sharing code with others is one of the most crucial aspect of our work. That is why using modules in our code is useful, as it creates small and manageable packages that can be share throughout our applications and across teams and individuals.

In the JavaScript world, there are multiple module formats that were conceived to solve different problems. However, different formats lead to a lot of friction when we are trying to glue our modules together. Module bundlers such as Browserify and jspm were conceived to simplify that process. We will be using Webpack for our application because it has certain features that makes it more desirable compared to other module bundlers.


Starting with the code from the last post, we will need to install Webpack as a dependency to our application. Webpack will automatically use the file webpack.config.js as its configuration file. Thus, we will need to also create it.

$ npm i -D webpack
$ touch webpack.config.js

The Webpack configuration file is fairly overwhelming, so let's break down every part of it. First, to be used by Webpack itself, it needs to export a JavaScript object literal under the CommonJS format. The CommonJS module format implemented in Node.js is fairly simple. We import and export modules by using the following constructs.

// Importing modules
const nodeJsModule = require('name-of-package');
const relativeModule = require('./relative/path/to/file.js');

// Exporting modules
module.exports = moduleToExport;

Therefore, the content of the webpack.config.js file should contain the following code.

module.exports = {
    // Rest of the Webpack configuration object
};

Webpack works by creating a dependency tree from the entry files, and it bundles them up together in a bundle file. It is also smart enough to split the bundle in multiple files when certain parts should be loaded asynchronously.

Thus, the entry attribute of the configuration object should describe what files to bundle together for our application. Since we don't have a JavaScript entry point for the client code, we should create a src/application.js file.

$ touch src/application.js

Let's also modify our src/index.html file accordingly.

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8" />
        <title>Agate</title>
    </head>
    <body>
-        <h1>Hello, World!</h1>
+        <div id="root"></div>
    </body>
</html>

Finally, we want to show that the file is loaded properly. We can tell it to create an <h1> node that contains a string to show that it is working properly.

const rootNode = document.getElementById('root');
const headingNode = document.createElement('h1');
const greetings = document.createTextNode('Hello, World!');

headingNode.appendChild(greetings);
rootNode.appendChild(headingNode);

Now that we have an entry file, we can fill the entry attribute of the Webpack configuration file!

module.exports = {
+    entry: './src/application.js'
};

Even though we have an entry attribute, Webpack will not be able to output the bundle if the output object is not present in the configuration file. For our case, we will only need 2 attributes, path and filename, in the output object.

module.exports = {
-    entry: './src/application.js'
+    entry: './src/application.js',
+    output: {
+        path: 'dist',
+        filename: '[name].[hash].js'
+    }
};

The more astute of you will realize that we are not currently referencing the application.js file in the index.html file, so we will not be displaying anything yet. You would be right to think so! Let's fix that.

Webpack has a fairly large ecosystem of plugins. One of the more interesting one is html-webpack-plugin. That plugin let's us inject the generated bundle files inside of an HTML file. We would then need to install it as a dependency of our project.

$ npm i -D html-webpack-plugin

Next, we will need to modify the webpack.config.js file to indicate in which HTML file Webpack should inject the bundle and its location. We do so by adding an entry in the plugins attribute.

+const HtmlWebpackPlugin = require('html-webpack-plugin');
+
module.exports = {
    entry: './src/application.js',
    output: {
        path: 'dist',
        filename: '[name].[hash].js'
-    }
+    },
+    plugins: [
+        new HtmlWebpackPlugin({
+            template: 'src/index.html',
+            inject: 'body'
+        })
+    ]
};

Finally, to use Webpack as part of our build process, let's modify our NPM build script in the package.json file to use the Webpack CLI.

"scripts": {
    "clean": "rimraf dist",
    "prebuild": "npm run clean",
-    "build": "babel src -d dist && cp src/index.html dist",
+    "build": "babel src -d dist && webpack",
    "prestart": "npm run build",
    "start": "electron dist"
},

Webpack has a few more features that pushes it from useful to really interesting. One of them is that Webpack overloads the require function from the CommonJS module format to be able to load other file formats than JavaScript files. Indeed, we can load other type of files such as CSS and images. Then, as Webpack loads those files, it can preprocess them and transform them. For now, let's preprocess all of our JavaScript files to be transpiled by Babel using babel-loader.

$ npm i -D babel-loader

We must also configure Webpack to preprocess the JavaScript by adding the babel-loader in the module.loaders attribute.

const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
    entry: './src/application.js',
    output: {
        path: 'dist',
        filename: '[name].[hash].js'
    },
    plugins: [
        new HtmlWebpackPlugin({
            template: 'src/index.html',
            inject: 'body'
        })
-    ]
+    ],
+    module: {
+        loaders: [
+            {
+                test: /\.js$/,
+                loader: 'babel',
+                exclude: /node_modules/
+            }
+        ]
+    }
}

The test attribute corresponds to a regular expression that will match all the files it must preprocess. The loader attribute specifies which loader to be used. Note that we can omit the -loader part of babel-loader. Finally, the exclude attribute is used to ignore certain files, specially the ones coming from the node_modules folder.


Similarly to what we just did, we can create another Webpack configuration file to process the main Electron files. We'll start by creating the webpack.config.electron.js file.

$ touch webpack.config.electron.js

Using the same process as we went through before, we should end up with a webpack.config.electron.js file that looks similar to the following one.

module.exports = {
    entry: {
        index: './src/index.js'
    },
    output: {
        path: 'dist',
        filename: '[name].js'
    },
    module: {
        loaders: [
            {
                test: /\.js$/,
                loader: 'babel',
                exclude: /node_modules/
            }
        ]
    }
};

However, there are some last parts that are missing to make this configuration file working with Electron. Those changes are described by the following diff.

module.exports = {
    entry: {
        index: './src/index.js'
    },
    output: {
        path: 'dist',
        filename: '[name].js'
    },
    module: {
        loaders: [
            {
                test: /\.js$/,
                loader: 'babel',
                exclude: /node_modules/
            }
        ]
-    }
+    },
+    node: {
+        __dirname: false // Tell webpack to not mock or polyfill the __dirname variable
+    },
+    target: 'electron' // We are using this file with Electron
};

Finally, we can update our NPM build script to use Webpack to build both parts of our application.

"scripts": {
    "clean": "rimraf dist",
    "prebuild": "npm run clean",
-    "build": "babel src -d dist && webpack",
+    "build": "webpack --config webpack.config.electron.js && webpack",
    "prestart": "npm run build",
    "start": "electron dist"
},

Since we stopped using babel-cli to transpile our application, we can also remove it using the following command.

npm uninstall -D babel-cli

Just like the install command, the uninstall command has a shortcut, un. Thus, the command above can be rewritten as the following one.

npm un -D babel-cli

Adding Webpack in our application might not look too beneficial at this point. However, the benefits will become more obvious as we move along in the development of the application. In the next post, I will be reorganizing the code base to make it future-proof, adding ESLint to use a standard coding style and adding unit tests to validate the application.

The code created in this post is available here.