This post is going to walk through converting a Marionette module system to the CommonJS module system using Browserify. This has several benefits, including the ability to run your code in node.js and the browser.
There are many ways in which you can structure your application, and while this approach won't cover every possible scenario it should apply to most cases.
Cleaning up index.html
Start by removing dependency references from index.html
. These will now be handled by Browserify.
before
<body>
<script src="/vendor/jquery.js"></script>
<script src="/vendor/backbone.js"></script>
<script src="/vendor/marionette.js"></script>
<script src="/app/app.js"></script>
<script src="/app/modules/module-one.js"></script>
<script src="/app/modules/module-two.js"></script>
<script src="/app/modules/module-three.js"></script>
<script>
window.app.start();
</script>
</body>
after
<body>
<script src="/bundle.js"></script>
</body>
As you can see in the after
snippet, you now only need a reference to the bundle.js
file - this file is generated by Browserify.
Creating a main.js file
Now that we've removed file references from index.html
we need to move them into a new file called main.js
. This file is going to be responsible for bootstrapping the application - including requiring and configuring libraries and starting the application.
The main.js
file should look something like this:
var $ = require('jquery');
var Backbone = require('backbone');
Backbone.$ = $;
Backbone.Radio = require('backbone.radio');
var app = require('./app');
require('./modules/module-one.js');
app.start();
The important thing to note about this file is the Backbone.$
and Backbone.Radio
assignments. These assignments are required in order to use these libraries with Browserify.
Refactoring modules
We need to refactor existing Marionette modules to bring them in line with the CommonJS format. Firstly, if they are not already, move each module into it’s own file.
Remove module definitions
This is simply a case of deleting the module definition and replacing it with the contents of the module.
before
app.module('my module', function(module, app) {
// module logic
});
after
var app = require('./../app');
// module logic
Remove module initialisers
The next step is to remove any module initializers that you have used. You can usually replace these with app initializers without any negative consequences.
before
this.addInitializer(function() {
// initialize something
});
after
app.addInitializer(function() {
// initialize something
});
Module properties
Marionette allows you to define properties on modules. This is no longer valid and should be refactored. Simply replace any module properties with variables like so:
before
this.moduleProperty = ‘Hello’;
after
var moduleProperty = ‘Hello’;
One complication that can arise when refactoring module properties is when a module property is defined in one file and used in another - this happens when you split your module definitions across multiple files, like this:
app.module(‘my module’, function() {
this.moduleProperty = ‘Hello’;
});
app.module(‘my module’, function() {
console.log(this.moduleProperty);
});
In these cases you’ll want to remove the direct coupling between those modules.
With true public module properties, where the property is used directy by another module, you will need to expose these through module.exports
.
Splitting modules
If you have a module definition that spans multiple files then you will need to flatten this module definition. I think there are two common solutions to this problem.
- Create new modules
- Create submodules/module dependencies
Whichever approach you choose, the solution is going to look similar in terms of implementation. The difference is in how the module/submodule is used.
If you decide to split your module into two modules you'll want to include the new module in main.js
with the existing modules.
If you decide to create a submodule/module dependency then you'll want to require that directly in the module that depends on it, like this:
Module
var app = require('./../app');
require('./module.dependency');
If you go down this path, and the submodule needs something from the parent module, then you may also want to inject dependencies directly into the submodule rather than pollute the system bus. For example, this could be as simple as this:
Module
var app = require('./../app');
var something = {};
require('./module.dependency')(something);
Dependency
var app = require('./../app');
var somethingNeeded;
module.exports = function(something) {
somethingNeeded = something;
module.exports = {};
}
If you have multiple dependencies that you need to inject you may want to create a module bus and pass that through instead, like this:
Module
var app = require('./../app');
var _ = require('underscore');
var Backbone = require('backbone');
var bus = _.extend({}, Backbone.Events);
bus = _.extend(bus, Backbone.Radio.Requests);
var something = {};
require('./module.dependency')(bus);
bus.reply('something', function() {
return something;
});
Dependency
var app = require('./../app');
module.exports = function(bus) {
var something = bus.request('something');
module.exports = {};
}
Creating bundle.js
When you have everything in place, you can compile your application into the bundle.js
file with one simple command:
$ browserify -d app/main.js > bundle.js
Recommended Approach
Take small steps and test often. Start by creating the main.js
with only library references and configuration. Then add in a reference to app.js
and compile and test again. When that works, start adding in your modules one at a time, compiling and testing after each.
Conclusion
Like most things in software engineering, there are tradeoffs. In this case the main issue that I've sufferred is with real time browser tinkering/debugging. This is due to browserify encapsulating all the usual global references, which prevents you from easily accessing global objects such as jQuery ($), the "App" object, etc.
On the positive side, you get a solid module system that enables you to run your modules in multiple environments. This allows for better code reuse and, in my opinion most importantly, easier testing of modules. This is due to the ability to run modules in isolation in an environment like node, without bootstrapping the entire application.