Organising your app

Code organisation makes or breaks an app, and doing it right isn’t just knowing how to separate models, collections, and views. That’s the easy part. The part where most fail is knowing what should be separating concerns such as loading and runtime.

The formula we’ll use here is simple and yet very effective: we’ll write a manifest file and a boot script. The manifest will pull all your scripts in (loading), and the boot script is where the base app classes/modules will be instanced (runtime).

Isolating runtime with a boot script is especially important for keeping control over what gets used and when, so when writing a test case for a view, you’ll want to load it, setup mocks for any models/collections it uses, instance it passing the mocks to it, and then test its features, in this order. If merely loading a module means running it, it’s a lot harder to do that.

★ ★ ★

The manifest file is a list of scripts needed for your app to work. Process-wise, things will really diverge here depending on what back-end or build system you are using.

Rails uses Sprockets, which is also available on Sinatra through the sinatra-asset-pipeline project, as well as being usable on its own. For JavaScript/node.js there’s Grunt. Most Grunt workflows consist of writing a task declaring an array of files which will get concatenated into a single file. In which case, your manifest is in the task itself.

Regardless of the tool you choose, both will achieve the same, which is taking a bunch of scripts and turn them into one, allowing you to serve a single file instea of many. With adequate caching set for both HTML and this script, and appending a digest to the scripts manifest file name, your setup is pretty damn good.

There’s also an entirely different approach to this problem which is dynamically loading scripts as you need them, and the most popular tool for it is require.js. By all means, check it out. It won’t be covered here though.

★ ★ ★

With Sprockets, an expanded manifest file looks like this. This is actually an excellent way to explain how it conceptually works, even if you’re planning on using Grunt.

# App scripts manifest
# ====================

# Dependencies.
#= require vendor/jquery
#= require vendor/underscore
#= require vendor/backbone

# Data layer.
#= require models/user
#= require models/book
#= require models/cart
#= require models/comment
#= require collections/books
#= require collections/comments

# Views.
#= require views/books/list
#= require views/books/show
#= require views/user/show
# …

# The router.
#= require router

# App boot script.
#= require boot
// App scripts manifest
// ====================

// Dependencies.
//= require vendor/jquery
//= require vendor/underscore
//= require vendor/backbone

// Data layer.
//= require models/user
//= require models/book
//= require models/cart
//= require models/comment
//= require collections/books
//= require collections/comments

// Views.
//= require views/books/list
//= require views/books/show
//= require views/user/show
// …

// The router.
//= require router

// App boot script.
//= require boot

Which should be refactored to this:

# App scripts manifest
# ====================

# Dependencies.
#= require_tree ./vendor

# Data layer.
#= require_tree ./models
#= require_tree ./collections

# Views.
#= require_tree views

# The router.
#= require router

# App boot script.
#= require boot
// App scripts manifest
// ====================

// Dependencies.
//= require_tree ./vendor

// Data layer.
//= require_tree ./models
//= require_tree ./collections

// Views.
//= require_tree views

// The router.
//= require router

// App boot script.
//= require boot

When Sprockets compiles this script, it’ll inline all the files required by it (and as you probably guessed, require_tree inlines a whole directory and subdirectories of scripts).

In case it wasn’t clear before: the boot script is loaded in the manifest as well. What matters is separating runtime logic from loading. Removing the boot script from the manifest will load everything, but nothing will happen. The modules will be loaded and available, but won’t be used and they won’t modify anything. That’s the expected behaviour.

★ ★ ★

When the boot script is loaded, it will run things, as in it’ll instantiate classes, run module methods, and so on. So this script is the only one that will, when loaded, change the state of the app and the browser.

You could argue that anything that puts a variable in the window scope is, in reality, modifying the environment, and thus this is all garbage. But pragmatism is your friend: judiciously placing classes/modules in the global scope is fine, as it happens with nearly every other scripting language such as Python or Ruby (and incidentally not with node.js).

What classes/modules a boot script will refer to, it depends on its purpose and a little on the choice of architecture. For really loading the app, you’ll likely have it create collections that are shared among views, initialise the router and kick off history tracking, setup events that are to be broadcasted, and so on. For testing a specific view, for example, you’d leave all those out and only run what’s absolutely necessary to set up its test cases. It’s easier to write a boot file for each purpose.

# App boot script
# ===============
#
# Presuming that we’re not using browserify to illustrate the point.

# Main app class.
window.BookStore = new App

# The user using the app.
BookStore.User = new User

# Shared collections.
BookStore.Books = new Books
BookStore.Articles = new Articles

# We’ll use this <body> reference to put some views in it below.
$body = $ 'body'

# Views that will exist regardless of what URL you are.
header = new Header
$body.append header.render().el
menu = new MainMenu
$body.append menu.render().el

# A module for capturing global ajax errors.
GlobalErrors.initialize()

# A module for tracking and broadcasting page scrolling events.
Scrolling.initialize()

# The router. We usually don’t need to keep a reference to it.
new Router

# Kick off the router code.
Backbone.history.start pushState: yes
// App boot script
// ===============
//
// Presuming that we’re not using browserify to illustrate the point.

// Main app class.
window.BookStore = new App;

// The user using the app.
BookStore.User = new User;

// Shared collections.
BookStore.Books = new Books;
BookStore.Articles = new Articles;

// We’ll use this <body> reference to put some views in it below.
var $body = $('body');

// Views that will exist regardless of what URL you are.
var header = new Header;
$body.append(header.render().el);
var menu = new MainMenu;
$body.append(menu.render().el);

// A module for capturing global ajax errors.
GlobalErrors.initialize();

// A module for tracking and broadcasting page scrolling events.
Scrolling.initialize();

// The router. We usually don’t need to keep a reference to it.
new Router;

// Kick off the router code.
Backbone.history.start({pushState: true});

Some will argue views should always be built within the router (through a controller as we explained in the respective chapter). It depends on how you structure the application. The example above assumes those two views will always appear regardless of the URL you arrive in the app from, so it makes sense. And that’s the only rule to almost anything: it has to make sense and work well without ruining modularity and/or separation of concerns.

★ ★ ★

Aim for writing all modules as if they could run not only within the app you’re building them for, but in past and future apps too. Other concerns necessary to make happen aside, regarding how you’re able to load it, that’s one of the easier problems to solve.

Never write a module in a way it’s only loadable if you’re using browserify’s (or more accurately, node.js’) module system, nor as if it requires exporting to a global scope exclusively. Instead, address both simultaneously:

# Book list view
# ==============

class BookList extends Backbone.View
  # …
  # …
  # …

(module?.exports = BookList) or @BookList = BookList
// Book list view
// ==============

(function(global) {
  var BookList = Backbone.View.extend({
    # …
    # …
    # …
  });

  if (typeof module !== undefined) {
    module.exports = BookList;
  } else {
   global.BookList = BookList;
  }
})(this);

That last line will check for the existence of the module keyword, and if it exists, it’ll assign the BookList object to it. Or if module doesn’t exist, assign BookList to the global scope. This ensures regardless of whether you’re using a module system or just loading the script as is, the end result is the product is always where you expect it to be.

So the above will work with or without browserify. If you are using it, you’ll need to require('book-list') first to get the class out in order to be able to instance it. Otherwise, just loading the script will make BookList available in the global scope.

★ ★ ★

Almost never a JavaScript app is written without it being the front-end for a server application. And most server applications use templates to render HTML, templates that have access to its data. A good build will make use of that to avoid unnecessary roundtrips to the server. This may sound like a departure from the model you’re going for, which is the app fetching data from the server via ajax when it needs it, but the gains are tremendous, so forgoing that over dogma is stupid.

If on the landing screen of our app we display an initial set of books, we can preload the book objects in the markup itself within a <script> tag, which our app can then pick up in its boot script.

…
%script
  window.InitialBooks = <%= initial_set_of_books_as_json %>;
%script(src="/javascripts/manifest.js")
…
<script>
  window.InitialBooks = <%= initial_set_of_books_as_json %>
</script>
<script src="/javascripts/manifest.js"></script>

And in the boot file:

…
if InitialBooks? and not _.isEmpty(InitialBooks)
  BookStore.Books.reset InitialBooks
…
if ((typeof InitialBooks !== undefined) && (!_.isEmpty(InitialBooks))) {
  BookStore.Books.reset(InitialBooks);
}

By the time the corresponding route/controller gets a view to act on BookStore.Books, the view will find the collection has records and it’ll just render them. Given how fast that’ll happen, the experience will feel as it those records were rendered with the HTML itself (instantly).

Do not fall for arguments over whether this creates tangling between the server and the client, or baseless “this is a bad idea” point of views. It simply isn’t. If you remove the part of the template that exports the records, when rendering the screen the route/controller will still perform check if the collection is empty, and since in that case it would be, a fetch() would occur and everything would work fine minus the benefit of avoiding the roundtrip.

C J