Browser Bundling with Webpack

Browser Bundling with Webpack

In our progress towards browserless, modular TDD, we wound up a spot where...we can’t run in a browser! Since browsers don’t currently support modules, and certainly not CommonJS modules, we need a way to transform our code into a browser-friendly format.

In this section we look at using Webpack to bundle our modular, CommonJS code into a single file fit for the browser. Along the way, we also reduce our HTTP requests by bundling our dependencies into the same file.

Finally, we end in a super spot: we switch to using Webpack’s development server, which re-bundles in-memory on every change and automatically updates your browser, hands-free.

Overview

  • Show problem with using modules in a browser without a module loader
  • Introduce and install Webpack
  • Generate and use a bundle, including with external dependency
  • Show live bundling and reloading with webpack-dev-server

The Problem

Let’s write a small web application, based around the incrementer in Modules with CommonJS. First, an index1.html file:

Webpack index1.html
<!DOCTYPE html>
<html>
<head>
    <title>Webpack Intro</title>
</head>
<body>
<h1>Incrementer</h1>
<script src="app1.js"></script>
</body>
</html>

This loads a file app1.js:

Webpack app1.js
var incrementer = require('./lib');
console.log(incrementer(3));

...which uses CommonJS modules to import incrementer from ./lib.js:

Webpack lib.js
function incrementer (i) {
    return i+1;
}

module.exports = incrementer;

PyCharm makes this easy to run. In the editor tab for index1.html, mouse-over the symbol for one of the browsers and click it to open in the internal PyCharm webserver:

Screenshot Chrome console

As the screenshot shows, the browser console tells us we have a JavaScript error Uncaught ReferenceError: require is not defined. Not surprising: browsers don’t support CommonJS export/import and modules, so require is not available in a browser.

We need a module bundler. We’ll use Webpack.

Installation

Our goal is to take all of our Node-style, browserless, modular code and combine it into a single bundle.js file, including jQuery as a dependency. Let’s use npm to install Webpack (and its development server) as a development dependency:

$ npm install --save-dev webpack webpack-dev-server

We can now run Webpack to “bundle” our files together:

$ node_modules/.bin/webpack app1.js -o bundle.js

With this command, Webpack looks in app1.js for any require imports, then in any of those imported files for more imports, and bundles all the files into a single output file called bundle.js.

If we change our HTML to point at this bundle.js file:

Webpack index2.html
<!DOCTYPE html>
<html>
<head>
    <title>Webpack Intro</title>
</head>
<body>
<h1>Incrementer</h1>
<script src="bundle.js"></script>
</body>
</html>

...then we can see 4 in the browser console with no errors:

Screenshot Chrome console works

Note

What is the bundle.js.map file that got generated? Bundlers have a habit of making the tracebacks hard to follow back to the original line in the original file. The “source map” contains all the extra information needed for this. It is only loaded when the browser has the console window open for debugging. This source map can also be inlined into the bundle.js instead of into a separate file.

Including Library Code

Imagine our frontend application used jQuery. In this case, included via a <script> that pointed at a CDN:

Webpack index3.html
<!DOCTYPE html>
<html>
<head>
    <title>Webpack Intro</title>
</head>
<body>
<h1>Incrementer</h1>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.2.0/jquery.js"></script>
<script src="bundle.js"></script>
</body>
</html>

Instead of logging the incrementer value to the console, we might use jQuery to save it to the <h1> text node:

Webpack app2.js
$(document).ready(function () {
    var incrementer = require('./lib');
    var newVal = incrementer(3);
    $('h1').text('Incrementer: ' + newVal);
});

After re-bundling, this works fine in our browser:

Screenshot After jQuery

However, we’re not doing anything that feels like Pythonic development:

  • Record a dependency on a version of jQuery
  • Import jQuery into our application, rather than expecting it to magically appear as a global via <script>

Plus, we are making an extra HTTP request to get jQuery. Wouldn’t it be great if our frontend toolchain helped us manage this?

First we use npm to install jQuery as a dependency:

$ npm install --save jquery

This gets the code into node_modules and adds an entry in package.json. We now need to import it into our application code:

Webpack app3.js
var $ = require('jquery');

$(document).ready(function () {
    var incrementer = require('./lib');
    var newVal = incrementer(3);
    $('h1').text('Incrementer: ' + newVal);
});

We can now eliminate the <script> that loads jQuery from CDN:

Webpack index4.html
<!DOCTYPE html>
<html>
<head>
    <title>Webpack Intro</title>
</head>
<body>
<h1>Incrementer</h1>
<script src="bundle.js"></script>
</body>
</html>

Loading index4.html in your browser via PyCharm shows this still works. How did jQuery get in there? Webpack saw it imported, fetched it, and included it.

We now have our external dependencies as part of our frontend toolchain with revision control. Not too shabby. But we can take another, very cool step forward.

Note

Why is bundle.map so big? It’s now at 270 Kb. Well, jQuery isn’t tiny. But also, we haven’t done any work to make it smaller. Webpack can “minify”, which strips out any comments and does lots of tricks for shrinking. Also, we are including the full jQuery. It is now available as multiple CommonJS submodules, so you can get only what you need. Or use a jQuery alternative.

Live Bundling and Reload

It is certainly not fun having to re-bundle on every change. Wouldn’t it be nice if we had a tool that watched your files, and whenever anything changed, would re-bundle? Or even better, provide a web server which not only re-bundled, but told the browser to reload the page, hands-free?

The Webpack Dev Server does just this. In fact, it doesn’t write a bundle file to disk. It keeps things in memory.

We installed it at the top of this article, so we can jump straight into using it:

$  node_modules/.bin/webpack-dev-server app3.js

As logged to your console, this starts a web server on port 8080, so you can now load http://localhost:8080/webpack-dev-server/index4.html. You should see:

Screenshot webpack dev server

Now, every time you make a change, the bundle will be regenerated and the browser will reload the page. It’s a seemingly-small, but in practice huge, change in frontend development.

To make this more natural in PyCharm, we can first move the Webpack command line options to a config file. By default, Webpack looks in webpack.config.js in the project roo:

Webpack webpack.config.js
module.exports = {
    entry: './app1.js',
    output: {
        path: __dirname,
        filename: 'bundle.js'
    },
    devtool: 'source-map'
};

We put in the options to drive both webpack and webpack-dev-server. Next, let’s automate this task by adding an npm run script in package.json:

Webpack package.json
{
  "name": "pylyglot",
  "version": "1.0.0",
  "description": "Series of articles for Polyglot Python with PyCharm",
  "main": "index.js",
  "scripts": {
    "start": "webpack-dev-server"
  },
  "repository": {
    "type": "git",
    "url": "git+ssh://git@github.com/pauleveritt/pauleveritt.github.io.git"
  },
  "author": "Paul Everitt",
  "license": "ISC",
  "bugs": {
    "url": "https://github.com/pauleveritt/pauleveritt.github.io/issues"
  },
  "homepage": "https://github.com/pauleveritt/pauleveritt.github.io#readme",
  "devDependencies": {
    "webpack": "^1.12.12",
    "webpack-dev-server": "^1.14.1"
  },
  "dependencies": {
    "jquery": "^2.2.0"
  }
}

Because start is a pre-defined shortcut, we can run npm start from the command line. Or, we can let PyCharm browser our package.json‘s npm run scripts and execute start (and thus the webpack-dev-server) in a run tool window.

Wrapup

That wasn’t too hard. Admittedly, that’s because this introductory article took the easiest possible course. As it turns out, bundling has a ton of functionality and also thorny issues at nearly every turn.

It’s one of the bittest pills in the frontend toolchain. On one hand, you have the chance to radically improve your productivity and get a Pythonic development cycle. On the other hand, you spend a large portion of your time learning (constantly changing) tools and fighting the problems they introduce.

There are solutions to this. If you don’t want to be bleeding edge, stick to the minimum, such as the scope in this article.

Comments

comments powered by Disqus