I\'m struggling a bit with coming up with a clean, solid way to organize my Backbone application. I\'m using Requirejs, Handlebars, and the Requirejs Text plugin to dynamic
Here are a few tips after working on very large backbone apps. It's not exhaustive or final..
Divide into two directories
server/
www/
Also when you run your build task it would build the app into a distributable version into a build/
or dist/
directory. Probably using Gulp or Grunt.
Extend Backbone
Your entire app will consist of:
You should extend the Backbone classes even if they are empty at first. The most useful two extensions are:
views
object/function with more views, which get cleaned up when you remove the parent view). Models and collections called model
or collection
get automatically passed down to sub views.Use pod architecture
As in organise your app around self-contained modules e.g.:
www/app/modules/home/router.js
<-- sub router, calls methods in modules.js
www/app/modules/home/module.js
<-- prepares endpoints - changing layout, initializing views & models etc
www/app/modules/home/views/...
all the views (can have subfolders too)
www/app/modules/home/templates/
www/app/modules/home/models/
www/app/modules/home/collections
Start seeing your app in terms of views and sub views
A page doesn't consist of just one view. It would have perhaps a special "layout" view and inside that would be many views - one which splits the page in half, one which has pagination with more views inside for each page number, a view for a form with lots of sub views inside for each form element and message etc etc
You can start thinking of views as shadowing the DOM tree and divide logically - anything which you think is re-useable on your page make it a package (it's own views and models/collections if it needs them).
Models are for any data and any logic performed on data, if a view was showing anything from the server/api/database it would typically be passed to the view which would pass all or some of the model attributes to the template.
If that item displaying information was in a list, then a collection would manage each model for each item.
Do communication with models
If you find yourself wanting to communicate something from a view to another view, use a shared model. A view should be as decoupled as possible (it shouldn't need to be aware of it's parent).
Have an app state
Create a model called AppState to broadly communicate across the app using triggers and listens.
Have a packages folder (optional)
Whenever you come across stuff in your app which you think could be re-useable, even in other future apps, create a package. These would typically be hosted on their own git repos and you could pull them into projects using package.json or the command line.
Have a folder where you extend inter-app stuff
Have an extensions folder for modules which are consumed by multiple apps - e.g. your backbone extensions could go here. Or, if you created a package for forms but want to do something specifically for this app, then extend it here.
e.g.
www/app/extensions/view.js
www/app/extensions/model.js
www/app/extensions/collection.js
www/app/extensions/buttons/link.js
// Extending the link view from a "buttons" package.
assets
The reason why I would have an app/
folder in the public www/
folder is so that I could also have an assets folder in there for fonts and images etc:
www/assets/css
www/assets/images
Note: Maybe you want to try and keep assets in the module folders (inline with pod architecture). I haven't done this before but it's worth considering.
index.html
Typically if you are using CommonJS or AMD your index.html would just be boilerplate with no actual DOM elements and you would have one call in there to an entry js file. Since CommonJS has to compile this would just be something like <script src="/app.js"></script>
but for AMD it would be more like:
<!--IF NOT BUILD-->
<script data-main="/app/config" src="/packages/require.js"></script>
<!--ELSE
<script src="/app.js"></script>
-->
So when running in dev (non-build) RequireJS will load up app/config.js
but in build the whole app will be in app.js
. There are various Grunt/Gulp build tasks which will do something like the above for you (obviously that conditional syntax is just made up).
Layouts
I would create a extensions/layout.js
which extends extensions/view.js
and it would be a simple extension that could have sub views like normal (e.g. header and footer), but also a special subview which I could attach any view to (for the body subview) e.g. a method like setContentView(view)
.
I would maybe create a module called layouts and in there have a directory modules/layout/default
which has a view that has a header and footer subviews. Then reaching the index route would flow something like this:
app/router.js => app/modules/home/router.js => app/modules/home/module.js@index => setContentView(view from app/modules/home/views/index.js)"
Routing
I would have a app router located at e.g. www/app/router.js
which could have some special routes but would largely just subroute with an object that pointed at sub routers:
subRouters: {
'store-locator': StoreLocatorRouter,
myaccount: MyAccountRouter,
sitemap: SitemapRouter
}
I would make this possible by extending the normal Backbone router with something like (note in your extension you need to call initSubRouters
in initialize
) -
define([
'underscore',
'backbone'
],
function(_, Backbone) {
'use strict';
/**
* Extended Backbone Boilerplate Router
* @class extensions/router
* @extends backbone/view
*/
var Router = Backbone.Router.extend(
/** @lends extensions/router.prototype */
{
/**
* Holds reference to sub-routers
* @type {Object}
*/
subRouters: {},
/**
* Adds sub-routing
* based on https://gist.github.com/1235317
* @param {String} prefix The string to be prefixed to the route values
*/
constructor: function(options) {
if (!options) {
options = {};
}
var routes = {}, prefix = options.prefix;
if (prefix) {
// Ensure prefixes have exactly one trailing slash
prefix.replace(/\/*$/, '/');
} else {
// Prefix is optional, set to empty string if not passed
prefix = '';
}
if (prefix) {
// Every route needs to be prefixed
_.each(this.routes, function(callback, path) {
if (path) {
routes[prefix + '/' + path] = callback;
} else {
// If the path is "" just set to prefix, this is to comply
// with how Backbone expects base paths to look gallery vs gallery/
routes[prefix + '(/)'] = callback;
}
});
// Must override with prefixed routes
this.routes = routes;
}
// .navigate needs subrouter prefix
this.prefix = prefix;
// Required to have Backbone set up routes
Backbone.Router.prototype.constructor.apply(this, arguments);
},
/**
* Sets up 'beforeRoute' event.
*/
initialize: function() {
// This is a round about way of adding a beforeRoute event and must
// happen before any other routes are added.
Backbone.history.route({
test: this.beforeRoute
}, function() {});
},
/**
* Called before routes.
* @return {Boolean} false This ensures the 'route' is disabled.
*/
beforeRoute: function() {
Backbone.history.trigger('beforeRoute');
return false;
},
/**
* Adds prefix to navigation routes
* @param {String} route Non-prefixed route
* @param {Object} options Passed through to Backbone.router.navigate
*/
navigate: function(route, options) {
if (route.substr(0, 1) !== '/' && route.indexOf(this.prefix.substr(0,
this.prefix.length - 1)) !== 0) {
route = this.prefix + route;
}
Backbone.Router.prototype.navigate.call(this, route, options);
},
/**
* Initializes sub-routers defined in `this.subRouters`
*/
initSubRouters: function() {
_.each(this.subRouters, function(Router, name) {
this[name] = new Router({
prefix: name
});
}, this);
}
});
return Router;
});