Marionette Application Example with Modules and Routing
Backbone can be frustrating to developers who just want a straightforward answer on the "right" way to build their application. It's staunchly just a library and makes no attempts to be a framework. You're expected to build your own components to drive the full application. You'll soon find that while there's no one 'right' way to this goal, there are many ways to make mistakes. So rather than setting out to build a framework from scratch, it's helpful to instead use a prebuilt package. That's where Marionette comes in, extending the Backbone library with a number of classes that are helpful in building out full applications.
While Marionette rounds out Backbone with framework elements, like its parent library it doesn't place many restrictions on the developer. It can be just as evasive as to what to use and where. You can cherry pick which components to use, say you liked the Marionette.Application
class but didn't care for Marionette.Renderer
or Marionette.ItemView
. That's okay!
Each typical documentation page will list sometimes four different ways to implement components. Each approach can be mixed and matched. It certainly confused me when I sought out to build an application with module-level routing. At this point when approaching Backbone projects I'm comfortable with a specific stack. None of the Marionette tutorials really used my approach. So in the spirit of giving, once I got my current project working I went back and set up an example Marionette application in the same vein.
You can see the source code here
The App itself is hosted at http://marionette.ditherandbicker.com
Specific examples of typical Marionette tasks:
- Defining a router for the application
- Adding modules to the application
- Regions:
- Modules:
The rest of this post will go over some of the technology and methodology behind this approach.
AMD/Require JS
In AMD you want to have one class per file and use a tool, in this case RequireJS, to handle loading. For huge projects you wind up with hundreds, if not thousands, of class files but there are advantages to the sprawl. It's easier to manage a large project where the sources are discrete and laid out in a logical directory structure. The path to the file can be used as a hard namespace when grabbing resources.
Once the project is ready for production you can minify all the sources into a single file. RequireJS provides r.js for this task.
Require JS Sugar Notation
Newer RequireJS versions support Sugar for declaring dependencies. Whereas older AMD implementations get defined like:
define(
[
"require",
"jquery",
"backbone"
],
function( require, $, Backbone )
Sugar allows you to define like:
define( function( require ) {
var $ = require( 'jquery' ),
Backbone = require( 'backbone' );
} );
You'll also notice that I don't use global namespacing for classes. Global namespacing with AMD projects leads to scenarios where a class is loaded in memory without getting required, which can then lead to application crashes if modules are loaded in certain orders.
Bower
I used bower to install all the dependencies, including Backbone and Marionette. It installs all source into the bower_components
directory by default, and leads to the slightly off-standard shim definitions.
UnderscoreJS
A lot of people who use Backbone/Marionette don't seem to realize that they also have access to UnderscoreJS. This is a pretty powerful library that opens up a lot of data manipulation functionality beyond forEach and map. Even if it fails as functional programming it still deserves more attention.
One of Underscore's features includes a minimal templating system, which I made use of in Navigation.
Code Style
Going along with AMD, you can take advantage of 'extends' for Marionette components the same way you can with Backbone. This approach is sometimes buried in the Marionette docs, so I'm not surprised that its not more popular.
For instance, rather than listening for router events on the application, I overwrote onRoute methods in the various class definitions.
Problems with this Example
There are a number of things here that probably aren't the best. Firstly the way I initialize and pass around the application instance works, but is not graceful. The idea is to allow the App.js file to be extended, which could happen even if unlikely. It feels like bad practice to use RequireJS's methods, but at the same time this does grant the hard namespacing that many favor.
There's a lot of room even in the few files here to cut down on duplicated code. In both modules, the controller's onRoute method is used to update the header (here and here). In a production application I'd write a class both would extend that had a default onRoute
method.
I betray my stated adherence to AMD in shim.js which includes both the shim definition and starts the application.