JavaScript - - By James Hibbard

How to Bundle a Simple Static Site Using Webpack

Webpack is all the rage right now. It has over 30,000 stars on GitHub and has been embraced by some of the big guns in the JavaScript world, such as React and Angular.

However, you don’t need to be working on a large-scale project to take advantage of Webpack. Webpack is primarily a bundler, and as such you can also use it to bundle just about any resource or asset you care to think of.

In this article, I’m going to show you how to install and configure Webpack, then use it to create a minified bundle for a simple static site with a handful of assets.

But Why Would You Want to Do That?

Good question. Glad you asked!

One of the primary reasons for doing this is to minimize the number of HTTP requests you make to the server. As the average web page grows, you’ll likely include jQuery, a couple of fonts, a few plugins, as well as various style sheets and some JavaScript of your own. If you’re making a network request for each of these assets, things soon add up and your page can become sluggish. If you include all of the above in one bundle however, that problem disappears.

Webpack also makes it easy to minify your code, further reducing its size, and it lets you write your assets in whatever flavor you desire. For example, in this article I will demonstrate how to have Webpack transpile ES6 to ES5. This means you can write JavaScript using the latest, most up-to-date syntax (although this might not be fully supported yet), then serve the browsers ES5 that will run almost everywhere.

And finally, it’s a fun learning exercise. Whether or not you employ any of these techniques in your own projects is up to you, but by following along you’ll get a firm understanding of what Webpack does, how it does it and whether it’s a good fit for you.

Getting up and Running

The first thing you’ll need is to have Node and npm installed on your computer. If you haven’t got Node yet, you can either download it from the Node website, or you can download and install it with the aid of a version manager. Personally, I much prefer this second method, as it allows you to switch between multiple versions of Node and it negates a bunch of permissions errors, which might otherwise see you installing Node packages with admin rights.

We’ll also need a skeleton project to work with. Here’s one I made earlier. To get it running on your machine, you should clone the project from GitHub and install the dependencies.

git clone https://github.com/sitepoint-editors/webpack-static-site-example
cd webpack-static-site-example
npm i

This will install Slick Slider and Lightbox2 — two plugins we’ll be using on the site — to a node_modules folder in the root of the project.

After that you can open index.html in your browser and navigate the site. You should see something like this:

Our static site

If you need any help getting with any of the steps above, why not head over to our forums and post a question.

Introducing Webpack to the Project

The next thing we’ll need to do is to install Webpack. We can do this with the following command:

npm i webpack --save-dev

This will install Webpack and add it as a devDependency to your package.json file:

"devDependencies": {
  "webpack": "^3.2.0"
}

Next we’ll make a dist folder which will contain our bundled JavaScript.

mkdir dist

Now we can try and run Webpack from the command line to see if it is set up correctly.

./node_modules/webpack/bin/webpack.js ./src/js/main.js ./dist/bundle.js

What I am doing here is telling Webpack to bundle the contents of src/js/main.js into dist/bundle.js. If everything is installed correctly, you should see something like this output to the command line:

Hash: 1856e2c19ecd9b2d9026
Version: webpack 3.2.0
Time: 50ms
    Asset     Size  Chunks             Chunk Names
bundle.js  2.67 kB       0  [emitted]  main
   [0] ./src/js/main.js 192 bytes {0} [built]

And Webpack will create a bundle.js file in the dist folder. If you have a look at that file in your text editor of choice, you’ll see a bunch of boilerplate and the contents of main.js at the bottom.

Automating Our Setup

Now if we had to type all of the above into the terminal every time we wanted to run Webpack, that’d be quite annoying. So let’s create an npm script we can run instead.

In package.json, alter the scripts property to look like this:

"scripts": {
  "test": "echo \"Error: no test specified\" && exit 1",
  "build": "webpack ./src/js/main.js ./dist/bundle.js"
},

Notice how we can leave out the full path to the Webpack module, as when run from a script, npm will automatically look for the module in the node_modules folder. Now when you run npm run build, the same thing should happen as before. Cool, eh?

Create a Webpack Configuration File

Notice how we’re passing the path of the file to bundle and the path of the output file as arguments to Webpack? Well, we should probably change that and specify these in a configuration file instead. This will make our life easier when we come to use loaders later on.

Create a webpack.config.js file in the project root.

touch webpack.config.js

And add the following code.

module.exports = {
  entry: './src/js/main.js',
  output: {
    path: __dirname + '/dist',
    filename: 'bundle.js'
  }
}

And change the npm script to the following:

"scripts": {
  ...
  "build": "webpack"
},

In webpack.config.js we are specifying the entry point and output location of the bundle as properties of the configuration object, which we are then exporting . Run everything again and it should all work as before.

Read Modern JavaScript
Keep up-to-date with the evolving world of JavaScript

Including the Bundle

Now we have Webpack generating a bundle for us, the next thing we need to do is to include it somewhere. But first, let’s create a different entry point, so that we can actually require main.js. This will be a file named app.js in the src/js directory.

touch src/js/app.js

Add the following to app.js:

require('./main.js');

And change the webpack config thus:

entry: './src/js/app.js',

Run npm run build again to recreate the bundle. Everything should work as before.

Now, if you have a look at index.html you’ll notice that there’s not much going on JavaScript-wise. At the bottom of the file we are including jQuery and a file called main.js, which is responsible for showing more information when you click the Read more… link.

Let’s edit index.html to include the bundle instead of main.js. Look at the bottom of the file. You should see:

    <script src="./node_modules/jquery/dist/jquery.min.js"></script>
    <script src="./src/js/main.js"></script>
  </body>
</html>

Change this to:

    <script src="./node_modules/jquery/dist/jquery.min.js"></script>
    <script src="./dist/bundle.js"></script>
  </body>
</html>

Refresh the page in the browser and satisfy yourself that the Read more… link still works.

Bundling jQuery.

Next, let’s add jQuery to the bundle. That will reduce the number of HTTP requests the page is making. To do this we have to alter the app.js file like so:

window.$ = require('jquery');
require('./main.js');

Here we are requiring jQuery, but as we installed this using npm, we don’t have to include the full path. We are also adding its usual $ alias to the global window object, so that it is accessible by the other scripts.

Alter index.html to remove the jQuery script tag:

    <script src="./dist/bundle.js"></script>
  </body>
</html>

Run npm run build and once again, refresh the page in the browser to satisfy yourself that the Read more… link still works. It does? Good!

Bundling the CSS

Looking at index.html the only other thing we are making a network request for is the CSS. As you can see, we are including main.css at the top of the page, and this file is in turn, importing another four CSS files.

Although in its standard configuration Webpack can only deal with JavaScript, we can use something called a loader to have it bundle our CSS, too. From the Webpack docs:

Loaders are transformations that are applied on the source code of a module. They allow you to pre-process files as you import or “load” them. Thus, loaders are kind of like “tasks” in other build tools, and provide a powerful way to handle front-end build steps. Loaders can transform files from a different language (like TypeScript) to JavaScript, or inline images as data URLs. Loaders even allow you to do things like import CSS files directly from your JavaScript modules!

So let’s alter app.js:

// CSS
require('../css/main.css');

// JavaScript
window.jQuery = window.$ = require('jquery');
require('./main.js');

And we need to alter webpack.config.js to tell it which loader to run when it encounters a file ending in .css:

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

As you can see, I have specified two loaders: css-loader and style-loader. Of the two, css-loader transforms CSS to a JavaScript module and style-loader injects the CSS, that is exported by the JavaScript module, into a <style> tag at runtime. Let’s install both:

npm i --save-dev css-loader style-loader

Now let’s run Webpack again using npm run build and see what happens.

Hash: 3699b503299ce447108d
Version: webpack 3.2.0
Time: 409ms
    Asset    Size  Chunks                    Chunk Names
bundle.js  289 kB       0  [emitted]  [big]  main
   [1] ./src/js/app.js 97 bytes {0} [built]
   [2] ./src/css/main.css 1.01 kB {0} [built]
   [3] ./node_modules/css-loader!./src/css/main.css 494 bytes {0} [built]
   [4] ./node_modules/css-loader!./src/css/fonts.css 353 bytes {0} [built]
   [5] ./src/fonts/open-sans/OpenSans-ExtraBold.ttf 271 bytes {0} [built] [failed] [1 error]
   [6] ./node_modules/css-loader!./src/css/layout.css 219 bytes {0} [built]
   [7] ./node_modules/css-loader!./src/css/styles.css 1 kB {0} [built]
   [8] ./node_modules/css-loader!./src/css/responsive.css 359 bytes {0} [built]
  [12] ./src/js/main.js 192 bytes {0} [built]
    + 4 hidden modules

ERROR in ./src/fonts/open-sans/OpenSans-ExtraBold.ttf
Module parse failed: /home/jim/Desktop/webpack-static-site-example/src/fonts/open-sans/OpenSans-ExtraBold.ttf Unexpected character '' (1:0)
You may need an appropriate loader to handle this file type.
(Source code omitted for this binary file)
 @ ./node_modules/css-loader!./src/css/fonts.css 6:131-183
 @ ./node_modules/css-loader!./src/css/main.css
 @ ./src/css/main.css
 @ ./src/js/app.js

Oh noes! What happened? Examining the output, it seems that there was an error in src/css/fonts.css. If you open that file and look at line 5, you’ll see that we’re including a custom font (src/fonts/open-sans/OpenSans-ExtraBold.ttf) and Webpack doesn’t know what to do with it.

But don’t worry, we’ve got that! We just need to use another loader. This time url-loader, which can convert assets such as fonts and images into Data URLs, which can then be added to the bundle:

module.exports = {
  ...
  module: {
    loaders: [
      {
        test: /\.css$/,
        loaders: [
          'style-loader',
          'css-loader'
        ]
      },
      {
        test: /\.ttf$/,
        loaders: [
          'url-loader'
        ]
      }
    ]
  }
}

And of course, we need to install it:

npm install url-loader --save-dev

Now the build should run. Test things out by removing the CSS include, recreating the bundle and refreshing the page.

Bundling 3rd Party Libraries

Now lets turn our attention to photos.html. There is a bit more going on on this page, as we are making use of two libraries, Slick Slider and Lightbox2, both of which rely on jQuery. Luckily, we can apply the techniques we have learned to include these in the bundle.

Alter app.js like this:

// CSS
require('slick-carousel/slick/slick.css')
require('slick-carousel/slick/slick-theme.css')
require('lightbox2/dist/css/lightbox.min.css')
require('../css/main.css');

// JS
window.$ = require('jquery');
window.slick = require('slick-carousel');
window.lightbox = require('lightbox2');
require('./main.js')

Also remove the CSS include from the head of the document and the script includes from the footer. This should give us:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <title>I Can Haz Cheeseburger?</title>
  </head>
  <body>
    ...

    <script src="dist/bundle.js"></script>
    <script>
      $('.slick-slider').slick({
        dots: true,
        arrows: false,
        infinite: true,
        speed: 500,
        fade: true,
        cssEase: 'linear'
      });
    </script>
  </body>
</html>

Although there would be nothing to stop us including the Slick initialization code in the bundle, I’m going to leave it on this page, as we only want to use it here.

Now let’s run Webpack and see what happens:

ERROR in ./node_modules/slick-carousel/slick/ajax-loader.gif
Module parse failed: /home/jim/Desktop/webpack-static-site-example/node_modules/slick-carousel/slick/ajax-loader.gif Unexpected character '' (1:7)
You may need an appropriate loader to handle this file type.
(Source code omitted for this binary file)
 @ ./node_modules/css-loader!./node_modules/slick-carousel/slick/slick-theme.css 6:119-147
 @ ./node_modules/slick-carousel/slick/slick-theme.css
 @ ./src/js/app.js

ERROR in ./node_modules/slick-carousel/slick/fonts/slick.eot
Module parse failed: /home/jim/Desktop/webpack-static-site-example/node_modules/slick-carousel/slick/fonts/slick.eot Unexpected character '' (1:0)
You may need an appropriate loader to handle this file type.
(Source code omitted for this binary file)
 @ ./node_modules/css-loader!./node_modules/slick-carousel/slick/slick-theme.css 6:309-337 6:362-390
 @ ./node_modules/slick-carousel/slick/slick-theme.css
 @ ./src/js/app.js

Aw noes, another error! This time there seems to be a problem with the slick-theme.css file which is referencing an image in gif format. Webpack doesn’t know what to do with the gif, so it throws its arms up and stops working. But we know what to do, right?

Add the following to webpack.config.js:

{
  test: /\.(svg|gif|png|eot|woff|ttf)$/,
  loaders: [
    'url-loader'
  ]
}

You’ll notice I’ve altered the regex to match several other file types. These are all required by Slick or by Lightbox2. Run Webpack again and make sure it completes without errors.

Refresh the page and assure yourself that everything is working.

Some Finishing Touches.

We’re almost done, but there are a couple of things we can improve upon.

Handle the Flash of Unstyled Content

Did you notice the flash of unstyled content when the page loads? That can be fixed by taking advantage of the blocking nature of <script> tags and moving the include to the top of the file.

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <title>I Can Haz Cheeseburger?</title>
    <script src="dist/bundle.js"></script>
  </head>
  <body>
    ...
  </body>
</html>

Minify the Bundle

We can also run Webpack with a p flag (short for production) and have it minify everything for us

In package.json:

"scripts": {
  ...
  "build": "webpack -p"
},

In webpack.config.js:

module.exports = {
  entry: './src/js/app.js',
  output: {
    path: __dirname + '/dist',
    filename: 'bundle.min.js'
  },
  module: {
    ...
  }
}

And in index.html and photos.html:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <title>I Can Haz Cheeseburger?</title>
    <script src="./dist/bundle.min.js"></script>
  </head>
  <body>
    ...
  </body>
</html>

This reduces the bundle size to 480kb. This might seem a bit excessive for some CSS, JS and a few other assets, but bear in mind that 222kb of that is the font.

Transpile ES6 to ES5

You could also install the babel-loader and have Webpack run JavaScript files through that, thus transpiling ES6 to ES5.

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

In webpack.config.js:

{
  test: /\.js$/,
  loader: 'babel-loader?presets[]=es2015'
},

Of course, you could swap out ES6 with almost any other compile to JavaScript language you fancy.

Conclusion

So there you have it. In this article, I have demonstrated how to use Webpack to bundle a simple static site — a process that reduces the amount of HTTP requests made, thereby making the site potentially snappier and more responsive. I also demonstrated how to have Webpack minify the resultant bundle, thus considerably reducing the file size, as well as transpile ES6 using the babel-loader.

Even if this strategy isn’t for everyone, hopefully by following along, you have gained an insight into what Webpack does and how it does it. And for those that wish to explore Webpack further, I recommend A Beginner’s Guide to Webpack 2 and Module Bundling, which goes a little more in-depth.

If you have any questions or comments, I’d be glad to hear them in the discussion below.

Sponsors