How to Use Webpack for Killer Refactoring

A few weeks ago, I wrote a blog post about refactoring, stressing that it should be handled carefully because it can quickly become an addiction and a velocity nightmare. However, sometimes you just have to refactor.

When we wrote the first version of Snipcart 2 years ago, we decided to use the following stack to write the shopping cart itself and the merchant dashboard:

  • RequireJS
  • CoffeeScript
  • Backbone.JS

We also have an API that powers these two clients (cart + dashboard), but today we will focus on the front-end aspect.

Back in 2013, this stack choice made plenty of sense: tools such as Browserify were non-existent, and RequireJS seemed like the go-to solution to write large scale single page applications.

Today we have cleaner, much more powerful tools allowing us to use npm modules directly with the require directive. When we started looking into refactoring our cart and dashboard, we looked at Browserify, one of the hippest module loaders at the moment. But it was not allowing us to use our old AMD style modules, so we would have to refactor every single file we had.

Then I spoke about our refactoring issues with Yanick, a bright, young developer I work with. He told me:

"Charles, take a look at webpack.js, it seems like it allows you to combine AMD and CommonJS modules."

So I did take a look. As I was reading through the webpack documentation, I soon realized it could be the way to start our refactoring. Today, I'm going to show you how to use webpack to handle refactoring on a small Backbone.js application.

Disclaimer: If you're starting a project from scratch, I don't suggest following the steps below. There is no good reason to use two module definitions for a new project, but if you are going through the same thing we did, it can be useful!

Configure webpack.js

The first thing we need to do is to configure webpack. It can be a very simple process if your application is rather small. In our case, we had to use some loaders and bundle it with gulp to concatenate some files. I won't go too deep into that for now: there are lots of plugins you can use with webpack, so I suggest going through their documentation if you are interested.

The way webpack works is that you need to specify an entry file, which is basically your application bootstrapper.

So imagine your have the following files:

│   index.html
│   webpack.config.js
└───app
        main.js

The most basic webpack.config.js needs to look somewhat like this:

module.exports = {
  entry: ['./app/main.js'],
  output: {
    path: './assets',
    filename: 'bundle.js'
  }
}

By doing this, you basically tell webpack what is the entry point of your application and where to bundle the generated script. In our case, the entry point is main.js in the app folder. And the output file will be bundle.js in the assets folder.

For now, our application main.js file will consist of a one-liner:

alert('Hello world');

Now let's open that terminal. First, make sure you have webpack installed. It can be done using npm. In our case, we installed it globally, because we have several projects using it: npm install webpack -g. If you go to the root of your project (where webpack.config.js is), you can run this command in your terminal: webpack. That's it, your bundle.js file will be created. Now if you include this bundled file into a simple HTML page, you will see the alert box showing Hello world. At this point, webpack is pretty much configured.

Start refactoring

It's always fun to show a Hello world, but let's dive a little deeper and talk about actual refactoring. Here is what ourmain.js file now looks like:

var AddMessageView = require('./views/addmessageview');
var MessagesView = require('./views/messagesview');
var MessagesCollection = require('./collections/messagescollection');

var messages = new MessagesCollection();

new AddMessageView({collection: messages}).setElement('#add-message');
new MessagesView({collection: messages}).setElement('#todos');

Our webpack.config.js file does not need any change. As you can see in the file tree below, we keep the same file structure. So this config will remain untouched:

module.exports = {
  entry: ['./app/main.js'],
  output: {
    path: './assets',
    filename: 'bundle.js'
  }
}

We will then create the following files:

│   index.html
│   package.json
│   webpack.config.js
└───app
    │   main.js
    ├───collections
    │       messagescollection.js
    ├───models
    │       message.js
    └───views
            addmessageview.js
            messagesview.js
            messageview.js

Our quick demo here will be a small Backbone application that will allow us to add some messages to a list. A typical kind of to-do list demo if you will. Since we will use Backbone, let's start by installing it with npm.

npm install backbone

With webpack, we can use modules installed by npm directly in any AMD or CommonJS modules.

Let's start by creating our models and collections. Let's say that the message.js model is a file you wrote a couple of months ago using a AMD module that will require Backbone:

define(['backbone'], function(Backbone) {
  return Backbone.Model.extend({
    defaults: {
      message: null
    }
  });
});

Let's also say the collection messagescollection.js is brand new and you want to make it a CommonJS module:

var Backbone = require('backbone');
var Message = require('./../models/message');

var MessagesCollection = Backbone.Collection.extend({
  model: Message
});

module.exports = MessagesCollection;

You see? We require our message module which is a AMD module directly within our new collection.

Now let's write our messageview.js file which will be the view responsible for showing the message content. This view will also be a CommonJS module:

var Backbone = require('backbone');

var MessageView = Backbone.View.extend({
  tagName: 'li',
  
  render: function() {
    this.$el.text(this.model.get('message'));
    
    return this;
  }
});

module.exports = MessageView;

Now let's do our addmesssageview.js file that will make it possible to add a new message:

var Backbone = require('backbone');
var Message = require('./../models/message');

var AddMessageView = Backbone.View.extend({
  events: {
    'submit form' : 'addMessage'
  },
  
  initialize: function(opts) {
    this.collection = opts.collection;
  },
  
  getAttributes: function() {
    return {
      'message': this.$('#message').val()
    }
  },
  
  addMessage: function (ev) {
    ev.preventDefault();
    
    var message = new Message(this.getAttributes());
    
    this.collection.add(message);
    
    this.$('#message').val('');
  }
});

module.exports = AddMessageView;

This view also requires our Message model which you surely remember is a AMD module.

So now we can add a message. We have a view to show a single message, but we will need to create the view that will show each message. The one responsible for it is named messagesview.js and looks like this:

define(['backbone', './../views/messageview'], function(Backbone, MessageView) {
  return Backbone.View.extend({
    initialize: function (opts) {
      this.listenTo(opts.collection, 'add', this.messageAdded)
    },
    
    messageAdded: function (message) {
      var view = new MessageView({model: message});
      view.render().$el.appendTo(this.$el);
    }
  });
});

As you can see, this view is also a AMD module which requires MessageView that is a CommonJS we defined earlier.

Okay, we created all the components required for our application to run. Now open your terminal and go to your application root. Run the webpack command. In a matter of milliseconds, webpack will automatically bundle your application together.

λ webpack
Hash: e13993f471f987f26e31
Version: webpack 1.11.0
Time: 759ms
    Asset    Size  Chunks             Chunk Names
bundle.js  387 kB       0  [emitted]  main
   [0] multi main 28 bytes {0} [built]
   [1] ./app/main.js 363 bytes {0} [built]
   [2] ./app/views/addmessageview.js 616 bytes {0} [built]
   [6] ./app/models/message.js 132 bytes {0} [built]
   [7] ./app/views/messagesview.js 383 bytes {0} [built]
   [8] ./app/views/messageview.js 239 bytes {0} [built]
   [9] ./app/collections/messagescollection.js 202 bytes {0} [built]
    + 3 hidden modules

Now let's include our bundled file into a HTML file just to see what it looks like. The index.html file for our demo looks like this:

<!DOCTYPE html>
<html>
  <head>
    <title>
      Webpack: Ultimate refactoring tool
    </title>
  </head>
  <body>
    <h1>Webpack</h1>
    
    <div id="app-container">
      <div id="add-message">
        <form>
          <label for="message">Message</label>
          <input type="text" name="message" id="message" />
          <input type="submit" value="Add">
        </form>
      </div>
      
      <ul id="todos"></ul>
    </div>
    
    <script src="assets/bundle.js"></script>
  </body>
</html>

You might have noticed that we did not write any templates directly in our views. Except for the MessageView which we specified with the tagName. It will be a simple li element added on our ul#todos. All the other templates are directly in our index.html file and we used the setElement method in our main.js file to set our views element.

Here is a little gif of our application in action:

snipcart-webpack-demo

Isn't it amazing? With webpack you can start right now to refactor and write your new code using more recent modules. I know some people might say: "Why not use ES6 modules right away?" We could, but in Snipcart most of our code is written in CoffeeScript, and webpack supports CommonJS and AMD out of the box. But a very nice feature of webpack is that you can use what they call loaders.

Loaders are basically code that compile a file before bundling it. For example, at Snipcart we use coffee-loader to compile our CoffeeScript on the fly. So we don't need a task in our gulp or Grunt file that only compiles CoffeeScript.

If you are interested about this, let us know in the comments: we might write a part 2 on this post showing how to deal with loaders and all the possibilities they offer.

Conclusion

I'll state it simply: webpack is an amazing tool. It's really powerful, and it definitely helped us refactor our codebase gradually without having to spend weeks rewriting every modules we have. But once again, as I said earlier, if you start from scratch, please don't combine two module systems; one is more than enough!


I hope this post has been helpful for developers who need to go through the same refactoring challenges we went through. If you enjoyed it and found it valuable, please, take a second to share it on Twitter. As always, your thoughts and questions regarding refactoring and webpack are welcomed in the comments below!

Suggested posts: