Understanding Webpack

Intro

The description from the main Webpack site is:

At its core, webpack is a static module bundler for modern JavaScript applications. When webpack processes your application, it internally builds a dependency graph which maps every module your project needs and generates one or more bundles.

So in simpler terms Webpack bundles the dependencies used in a JavaScript application.

Note that it doesn’t specify JavaScript dependencies, this means you can also bundle dependencies for TypeScript, Babel, Sass, Less and even plain text. It does this through Loaders that we can either select from a list of pre-existing ones or create our own.

A key concept in the description is the module. This comes from the modular programming design technique that separates code into independent functional pieces.

The following content will get us up and running with a practical example using Webpack.

Install

Install with npm:

1
2
npm install --save-dev webpack
npm install --save-dev webpack@<version>

For Webpack v4 or later install the cli:

1
npm install --save-dev webpack-cli

Webpack Setup

Let’s create a directory to install Webpack locally. We’ll use the official documentation’s guide for this.

1
2
3
mkdir webpack-project && cd webpack-project
npm init -y
npm install webpack webpack-cli --save-dev

Now create the index.js and index.html files in the following directory structure:

1
2
3
4
5
6
 webpack-project
  |- package.json
+ |- /dist
+   |- index.html
+ |- /src
+   |- index.js

This structure has a directory for our source code which holds our development and it also has a directory for distribution which will hold minimized and optimized code.

1
2
3
4
5
6
7
8
9
10
11
12
// /src/index.js
import _ from 'lodash';

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

  element.innerHTML = _.join(['Hello', 'webpack'], ' ');

  return element;
}

document.body.appendChild(component());
1
2
3
4
5
6
7
8
9
10
11
12
<!-- /dist/index.html -->

<!doctype html>
<html>
  <head>
    <meta charset="utf-8"/>
    <title>Getting Started</title>
  </head>
  <body>
    <script src="bundle.js"></script>
  </body>
</html>

Remove the main entry from package.json and replace it with private: true.

Now let’s bundle with npx webpack. In this command npx is a Node tool, shipped with Node, that runs binaries from npm packages.

Configuration file

Our project will require additional modules and instructions so even tho Webpack doesn’t need this we will still use it to keep things tidy.

Create a file in the root of the directory touch webpack.config.js and add the following layout:

1
2
3
4
5
6
7
8
9
10
   
const path = require('path');

module.exports = {
  entry: './src/index.js',
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dist')
  }
};
  • entry refers to the module Webpack should use to start building a dependency graph.
  • output is used to tell Webpack where to put the bundles it creates. In our file above we specify the directory in one option and the file name in another.

Now we can run the build but no need to add any flags, the config file is picked up automatcially, although if you’re using a specific name then you can use --config <config-file-name>.

1
npx webpack

Now before moving forward we can add a shortcut, let’s include a script in our package.json file to build our project.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
   "name": "webpack-project",
   "version": "1.0.0",
   "description": "",
   "main": "index.js",
   "scripts": {
     "test": "echo \"Error: no test specified\" && exit 1",
+     "build": "webpack"
   },
   "keywords": [],
   "author": "",
   "license": "ISC",
   "devDependencies": {
     "webpack": "^4.0.1",
     "webpack-cli": "^2.0.9",
     "lodash": "^4.17.5"
   }
 }
 

We can now use npm run build.

Asset Management

We’ll now learn how to apply the same bundling principle to our css, images, fonts and data. This would substitute the use of other tools such as gulp or grunt.

Loading CSS

First let’s add the style-loader and css-loader to our module configuration:

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

In the webpack.config.js file add the following in the same level as entry and output:

1
2
3
4
5
6
7
8
9
10
11
module: {
 rules: [
   {
     test: /\.css$/,
     use: [
       'style-loader',
       'css-loader'
     ]
   }
 ]
}

Add a style.css to the src directory. Add to it the following style:

1
2
3
.hello {
  color: red;
}

Finally add import './style.css'; to the top of the src/index.js file and element.classList.add('hello'); over the return element; line. Run the build with npm run build. Consider minifying the css before requiring as well as using sass and less.

Loading Images

Let’s use the file-loader for our images.

1
npm install --save-dev file-loader

Now add the following to our webpack.config.js as a second element of the rules we added earlier.

1
2
3
4
5
6
{
  test: /\.(png|svg|jpg|gif)$/,
  use: [
   'file-loader'
  ]
}

Now in our src/index.js we’ll add

1
import Icon from './icon.png';

to the top of the file and

1
2
3
4
var myIcon = new Image();
myIcon.src = Icon;

element.appendChild(myIcon);

over the return element; line. In our src/style.css we wil add our image right under the color: red line.

1
background: url('./icon.png');

We can finally build and explore the result in our index.html.

Loading Fonts

We can actually use the same file-loader for our fonts. So no need to install a package all we need is to add a rule in our webpack.config.js.

1
2
3
4
5
6
{
  test: /\.(woff|woff2|eot|ttf|otf)$/,
  use: [
    'file-loader'
  ]
}

Now we can include the fonts in our src and then add them through our src/style.css in the following way:

1
2
3
4
5
6
7
8
9
10
11
12
13
+ @font-face {
+   font-family: 'MyFont';
+   src:  url('./my-font.woff2') format('woff2'),
+         url('./my-font.woff') format('woff');
+   font-weight: 600;
+   font-style: normal;
+ }

  .hello {
    color: red;
+   font-family: 'MyFont';
    background: url('./icon.png');
  }

Now rebuild the project and try it out.

Loading Data

To import data in csv or xml we can use another loader, let’s install it:

1
npm install --save-dev csv-loader xml-loader

Again we’ll have to add the rule to our webpack.config.js file:

1
2
3
4
5
6
7
8
9
10
11
12
{
  test: /\.(csv|tsv)$/,
  use: [
    'csv-loader'
  ]
},
{
  test: /\.xml$/,
  use: [
    'xml-loader'
  ]
}

Let’s try this out with the following data file in src/data.xml.

1
2
3
4
5
6
7
<?xml version="1.0" encoding="UTF-8"?>
<note>
  <to>Mary</to>
  <from>John</from>
  <heading>Reminder</heading>
  <body>Call Cindy on Tuesday</body>
</note>

When importing this in our src/index.js file we can use it as json data. This is specially useful for data visualization such as using D3.

Add import Data from './data.xml'; to the top of the file and console.log(Data); above return element;.

Build the project and try the console in the browser widnow this time to see the data output of the xml file.

Component structure

Besides being able to use a global assets directory we can also use another structure where we have all the closely coupled files inside one directory.

1
2
3
4
5
6
7
- |- /assets
+ | /components
+ |  | /my-component
+ |  |  | index.jsx
+ |  |  | index.css
+ |  |  | icon.svg
+ |  |  | img.png

Auto setup and cleanup

There are two plugins recommended by the official guide, both are meant to improve the way we manage our Webpack.

HtmlWebpackPlugin

To avoid having to keep track of the entry points in our index.html file in case of renaming or removal we have a plugin that generates this file on build every time. Let’s install it:

1
npm install --save-dev html-webpack-plugin
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const path = require('path');
+ const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  entry: {
    app: './src/index.js',
    print: './src/print.js'
  },
+   plugins: [
+     new HtmlWebpackPlugin({
+       title: 'Getting Started'
+     })
+   ],
  output: {
    filename: '[name].bundle.js',
    path: path.resolve(__dirname, 'dist')
  }
};

Now every time we run npm run build the dist/index.html will be re created.

clean-webpack-plugin

Our dist/ directory keeps all the bundles but Webpack doesn’t know which ones aren’t used anymore. To keep this directory clean we’ll install another plguin.

1
npm install clean-webpack-plugin --save-dev
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
+ const CleanWebpackPlugin = require('clean-webpack-plugin');

 module.exports = {
   entry: {
     app: './src/index.js',
     print: './src/print.js'
   },
   plugins: [
+     new CleanWebpackPlugin(['dist']),
     new HtmlWebpackPlugin({
       title: 'Output Management'
     })
   ],
   output: {
     filename: '[name].bundle.js',
     path: path.resolve(__dirname, 'dist')
   }
 };

now after every build we will only have files relevant to the current build.

Development

When we have an issue in any of our js files our debugger such as the developer tools will point to the bundle file and not to the exact file where the error occurred, to avoid this behavior we will use an option in the webpack.config.js file that will output the relevant information to improve our debugging work flow. These options should only be added in development.

Add devtool: 'inline-source-map', under the entry object.

Now let’s bring up a dev server to host all the development files.

Install it with:

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

Under devtool add the follwing snippet:

1
2
3
devServer: {
  contentBase: './dist'
},

This will bring up our dev server at localhost:8080.

We can also improve this abit by adding it as a start script within our package.json file.

1
"start": "webpack-dev-server --open",

Build for each Environment

When building we can decide to use development or production. What changes are the defaults for example development has:

  • Tooling for in browser debugging
  • Fast incremental compilation for a fast development cycle
  • Useful error messages at runtime.

While production has:

  • Small output size
  • Fast code at runtime
  • Omitting development-only code
  • Not exposing source code or file paths
  • Easy to use output assets

So to make sure you are building the for the correct environment add a couple of scripts to you package.json file:

1
2
"build-dev": "webpack --mode development",
"build-prod": "webpack --mode production"

Final Thoughts

Webpack is a very powerful tool that improves how we manage our JavaScript application. The additional concept that I would like to put out there is that Webpack also suggests a workflow and best practices when leveraging modules, allowing for component based directory structures, and much more options.

Many big tools are using Webpack as part of their framework or are compatible with it at some level so learning to use it not only allows you to build your project with this but also to better understand other tools.

Reference

[1] “Concepts.” Concepts, Webpack.js, webpack.js.org/concepts/. [2] Koppers, Tobias. “Webpack 4: Mode and Optimization – Webpack – Medium.” Medium, Augmenting Humanity, 13 Feb. 2018, medium.com/webpack/webpack-4-mode-and-optimization-5423a6bc597a.