Building Backbone Plugins: Eliminate The Boilerplate In Backbone.js Apps (2014)

PART 5: APPENDICES

Appendix B: 3 Stages of App Initialization

There are three different stages in the initialization of a Backbone application. Each of these stages has it’s own responsibilities that should be kept separate if an application is to scale properly. Smaller applications may be able to ignore this. Moving beyond anything trivial without accounting for these stages will lead toward more complexity than is necessary in an application.

The three stages of an application’s initialization include:

1.    Download And Parse

2.    Application Initialization

3.    State Restoration (optional)

This could break this down in to a significantly larger number of stages if a much more fine-grained and detailed analysis is done. But as a general place to start, identifying 3 stages works well.

Stage 1: Download And Parse

Consider this the stage where the browser is simply grabbing all of the specified resources from the server and running through the first pass of parsing the HTML, laying out what the static HTML has provided, styling it with the basic CSS definitions, displaying any images and text from the static HTML, and parsing the JavaScript files so it can get ready to run them. Every app does this because without it, your browser doesn’t have anything to do or show.

Stage 2: Application Initialization

Every application has an initialization stage. Some are more elaborate than others. At times you can get away with the default top-down, parse what is found first method of ad-hoc code. Anything more than a few functions, though, and you’ll need application object of some sort - whether it’s an object literal with an “init” method on it, or a complete Application type with initializers as described in chapter 12. Whatever form this application object takes, your app will need a series of initializers - functions that start up a given area of an application.

What goes in the initializers?

Initializers are the bits of code, functionality, data and display that are absolutely required for your app to do it’s job, no matter what part of the application the user is trying to load up and use. They are the bits that must be initialized before the user can do anything meaningful with your application.

If your app has a menu structure generated by Backbone code, and it must always be present in the application, this should be in an initializer. If you’re building a multi-room chat application and you need to list the rooms that the user has favorited in a small block on the screen that is always visible to the user, this should be an initializer. If you’re building an image gallery and you need to load a thumb-nail list of images to show, no matter which image the user is trying to view, this is an initializer.

Other parts of your application code that the user doesn’t directly see may also be contenders for initializers, too. For example, if you have a router that needs to be up and running, the router probably needs to be instantiated inside of an initializer. But – and this is an important but – the routes on that router should not be executed during initialization. Instantiate the router and wait until after initialization has completed to call the “Backbone.history.start()” method, kicking off your route handlers.

Stage 3: State Restoration (optional)

This stage is optional because not every application has a context that needs to be restored on start up. Sometimes an application only needs initializers. The classic “todo” application is a great example of this - there is no contextual start up for this app. When the application is loaded, it initializes itself with the list of to do items and then it waits for the user to interact with the list.

When a Backbone application uses a router to respond to url pushState or hash fragment changes, though, it does have a contextual starting point. Each route that a user is allowed to bookmark or copy as a direct link will provide the context that our application needs to use, to get the user back to where they want to be. Even if a Backbone app has a router and contextual start, though, it may not be used. If there user hits the root of the application, there may not be any additional code to run for the empty (“”) route. When the user hits a route, though, that route must server up the context and application state that the user expects to see.

The problem in most routed Backbone applications, though, is that they bundle the application initialization with the contextual start. That is, developers tend to use the router and its callback methods as the sole place to get the application initialized and get the user back to the context of the route that they requested. For trivial applications, this may be fine. The initialization code may be so small that it doesn’t really matter if it’s crammed in to the router. But for any real application with any amount of complexity, this is a bad idea. It couples two very distinct parts of the application startup very tightly, and it can lead to bloated and unmaintainable routers with limited entry points in to the application.

So… what goes in the contextual start, other than just saying route callbacks?

Your route callbacks should be as simple as possible. They shouldn’t initialize your system as a whole. Rather, they should be used to determine the state of the application that the user wishes to see. Therefore, the code that goes in to a route callback should be the smallest amount of code that you can write to get your application from it’s initial state (the state that it was in when the initializers completed) to the desired state.

Contextual Examples

In an image gallery application, a user may hit a bookmark that points to a specific image. For example, “#images/4”. The route callback that executes should load the requested image and display it on the screen:

 1 Backbone.Router.extend({

 2   routes: {

 3     "image/:id": "imageById"

 4   },

 5     

 6   imageById: function(id){

 7     var image = imageCollection.get(id);

 8     App.showImage(image);

 9   }

10 });

In a multi-room chat application, a user may hit a bookmark that should take them directly in to a specific chat room. For example, “#backbone”. The route callback that executes should take this room name and call the code that is necessary to enter the chat room.

Chances are this is a fairly involved set of code. You’ll need to load the list of users in the chat room. You’ll need to clear the current chat windows and possibly pre-load recent messages to be displayed. You may also need to change a browser’s websocket event listeners to pick up events for this specific room instead, so that messages for this room can be displayed as they come in.

This is a lot of code to run, and it’s fare more code than should be allowed in a route callback method. It should, then, be encapsulated in an object that is responsible for registering the user as having entered that chat room. This object is then called from the router:

1 Backbone.Router.extend({

2   routes: {

3     ":roomname": "chatroom"

4   },

5     

6   chatroom: function(roomname){

7     ChatApp.enterRoom(roomname);

8   }

9 });

Furthermore, there’s a high likelihood that the user will be able to enter chat rooms using some interaction on the web page. They may click on a chat room name in their favorite’s list, or they may type in a command like “/join #backbone” in to the chat application’s command area. For any of the the multiple ways to enter a chatroom, the code that is executed should be the same. Having the route callback be as simple and stupid as possible will promote code re-use and allow these three options to be easily implemented. If you only need to call ChatApp.enterRoom("someRoom")to enter any room that the user specifies, providing options for how the user enters a room becomes trivial.

Lessons Learned

Building a layer of abstraction can often help to identify patterns in applications, as shown with the application initializers. The idea of having two or three distinct phases of an application’s start up can help to identify the boundaries between objects and methods, as well. And there are still more valuable lessons that can be pulled from this code, such as not limiting an application’s codebase to pure Backbone or Backbone-augmenting objects.

Making The Implicit Explicit

One of the largest challenges that developers face is understanding and recognizing the implicit or implied parts of an application or process. And a JavaScript application’s initialization is no different. Developers often write code that ignores the implicit phases of an application’s lifecycle. The code will simply start itself up when it has been loaded, or it will litter a handful of jQuery DOMReady callback methods throughout the application. This can cause maintainability and performance problems in an application.

By making the application start up process more explicit, though, many of these problems can be avoided and new opportunities for organizing code and optimizing the application can be exposed. Having an explicit application definition phase, separated from an explicit initialization phase allows code to know when it will be executed and know what will be available. The third and optional contextual rehydration - using a route or other preserved state to spin up a specific part of the application - allows for even more flexibility, giving the application a solid foundation to run from before moving in to the specific context that the user requested.

Divide And Conquer

In the previous chapters, the principles of Separation of Concerns and Single Responsibility have been applied in the object and methods within the plugins and add-ons. These principles can and should be applied at higher level abstractions, though. The use of modules in JavaScript in a necessary part of creating large-scale, well functioning, maintainable systems. As Justin Meyer says, “The secret to building large apps is never build large apps. Break your applications into small pieces. Then, assemble those testable, bite-sized pieces into your big application.”

Application initializers are not a complete solution for this separation and modularization, of course. Using a module system of some sort - whether it’s a simple Immediately Invoking Function Expression, or a framework like RequireJS - helps to keep code separated and organized correctly. The use of application initializers, then, allows a module to be initialized at the appropriate time.