Boot-strapping A Backbone Application - APPLICATION INFRASTRUCTURE - Building Backbone Plugins: Eliminate The Boilerplate In Backbone.js Apps (2014)

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

PART 4: APPLICATION INFRASTRUCTURE

Chapter 15: Boot-strapping A Backbone Application

Languages like C/C++, C#, Java and others typically have a main method or similar, as the entry point for an application’s code. This is the method that gets called when the application is boot-strapped for execution (however that is done - an executable from a shell, running a GUI application in windows, starting a web service, etc). In JavaScript, there isn’t a single defined entry point for code. You write code in a .js file, reference it from the HTML file, and the code gets evaluated and executed in the order that it is included in the page. Evaluation and execution happens in the same order: top-down, first-loaded is first-executed. For small applications this may be fine. But when a Backbone application starts to grow beyond a few simple views and models, something more robust is needed.

An Application Object

The typical starting point for a JavaScript app is an ‘Application’ object, which acts as a boot-strapper for the code execution. The first version of this is often an object literal that has an “init” method or “start” method - a single method to call to start the app.

1 // <app.js>

2

3 var app = {

4 someFeature: {

5 views: {},

6 models: {},

7 controllers: {}

8 },

9 anotherFeature: {},

10

11 start: function(){

12 someFeature.init();

13 anotherFeature.init();

14 Backbone.history.start();

15 }

16 };

17

18 // <someFeature.js>

19

20 app.someFeature.init = function(){

21 // do things to start up this feature, here

22 };

The object literal may set up some initial namespaces for various parts of the app by including nested object literals. The “start” method would run initialization for data, collections, and views, and set up a router, for example. This path, a simple object literal with a “start” method, is useful for small application. But for anything substantial, it becomes constraining and problematic, quickly.

Having one “start” method means that it must know about every part of the application that needs to be initialized. This leads to a monolithic method, at best. Worse, though, is that it often leads to large amount of complicated and convoluted logic to try and figure out which parts of the app need to be initialized, when.

To solve the boot-strapping and initialization problem, an application object should allow multiple initialization methods to be registered, and have them run from a single call to a start method.

An Extensible Application

An object literal is a simple way to start, but it would be more flexible if this were an object that could be extended and instantiated as needed. To do that, you’ll define a constructor function to create new object instances. But you don’t want to throw away any investment that you have in your object literal’s structure, either. Fortunately, it’s easy to handle both needs. Allow the object literal to be passed in to the Application’s constructor function. Then extend the Application instance with the object literal.

1 // An Application object constructor function.

2 function Application(options){

3 _.extend(this, options);

4 }

Now you can pass the original object literal in to the constructor of the new Application type.

1 // <app.js>

2 var app = new Application({

3 someFeature: {

4 views: {},

5 models: {},

6 controllers: {}

7 },

8 anotherFeature: {},

9

10 start: function(){

11 someFeature.init();

12 anotherFeature.init();

13 Backbone.history.start();

14 }

15 });

All of the methods and values from the object literal will be copied to the app instance, making them available as if the app object has not been changed. The result is that any existing code that relied on the structure of the object literal will still work. With a new type in place, though, you can create instances that have more behavior than an object literal. The Application type can be extended to include additional features, such as the ability to register initializer methods.

Adding Initializers

Initializers are functions that get called to initialize - or start up - other areas and feature sets of an application. They allow the Application object to be in control of when the features start, but allow each feature to be in control of how they start.

Before you get to the initializers, extend the Application prototype with Backbone.Events. This will come in handy when you get to the point where the Application is being started, allowing additional flexibility in the use of the Application object.

1 _.extend(Application.prototype, Backbone.Events, {

2 // Application instance methods go here

3 });

Next, create an addInitializer method and have it accept a callback function as a parameter. You’ll need a place to store the callback functions, so add an _initializers array to the Application instance, inside of the constructor function.

1 // An Application object constructor function.

2 function Application(options){

3 // extend this instance with all the options provided

4 _.extend(this, options);

5

6 // a place to store initializer functions

7 this._initializers = [];

8 }

9

10 // Application instance methods go here

11 _.extend(Application.prototype, Backbone.Events, {

12

13 // Add an initializer, which will be run when the start

14 // method is called on the Application instance. The order

15 // of execution is not guaranteed.

16 addInitializer: function(initializer){

17 this._initializers.push(initializer);

18 }

19 });

This code isn’t much to look at, but it provides a lot of flexibility in use. It also brings up a couple of questions, though.

Why Add The _initializers In The Constructor?

The _initializers are added in the constructor function and not extended on to the Application.prototype due to the way JavaScript passed objects around by reference.

If the initializers array were defined on the Application prototype, each instance of an Application would reference the same array. In a project where only one Application instance is used, this wouldn’t be a big deal. But this would cause serious problems in unit testing or in applications that have multiple Application instances. You don’t want the email app running the initializers for the to-do list, for example.

By adding the _initializers array in the constructor function, you will have a unique array for each Application instance.

Why Not Expose initializers Directly?

Why should you write a function called addInitializer when you could expose the array directly and just call app.initializers.push(…) directly?

The short answer is encapsulation.

Exposing the internal data structures of an object can lead to bad situations. Developers using the object and data structure may try to do things that would cause problems with the intended use.

Exposing the array would force developers to know about the API and semantics of arrays. It would also force them to jump outside of their current mode of thinking when working with the array vs the Application API. This breaks concentration and flow every time they have to switch between the semantics of “I’m working on an Application object” to “I’m working with an array”, and back.

Lastly, exposing the internal data structure will cause problems when you need to change that data structure. If someone is using the array in their code and you change it to an object literal with key: value pairs, the behavior and semantics will change dramatically. This leads to broken code and frustrated developers.

Keeping the _initializers wrapped up in an API method helps to prevent these problems. It encapsulates the internal structure, providing an API that is consistent with the semantics of the object being used. It allows you to change the data structure as needed, and helps to reduce bugs by creating an intention revealing API.

Executing The Initializers

With the initializers stored in the Application instance, you need a way to execute them. Add a start method to the Application and have it loop through the initializers, calling them.

1 _.extend(Application.prototype, Backbone.Events, {

2 // … existing methods

3

4 start: function(args){

5 var init, i;

6

7 // get the arguments passed to the start method

8 // and slice them in to an array so they can

9 // be passed to the initializers

10 args = Array.prototype.slice.call(arguments);

11

12 // get the # of initializers

13 var length = this._initializers.length;

14

15 // loop and execute

16 for (i=0, i<length, i++){

17 init = this._initializers[i];

18

19 // execute the initializer with the

20 // context set to the application,

21 // passing args along for the ride

22 init.apply(this, args);

23

24 }

25

26 }

27 });

It’s useful to provide arguments for the initializer functions that have been added. The easiest way to do this is to pass those arguments to the start function directly, forwarding them to each of the initializer functions. The start method above does exactly this by slicing the arguments object in to a proper array and then passing the args to the init function in the loop.

Each of the initializer functions will be executed in the loop, with the context (the this argument) of the initializer function set to the Application instance.

information

Context And Prototypes

For more information about manage context (this) in JavaScript, see my screencast on Context In JavaScript.

In addition to context, though, be aware that mixing the tools to manipulate context with prototypal inheritance in JavaScript can result is unexpected bugs. For an introduction to the problem and a description of one or two ways to manage this, see my screencast on When Context And Prototypes Collide.

Using Initializers And Starting The App

Now that the Application object can have initializers added, and they can be started, it’s time to finish the conversion from the old application object literal.

Adding Initializers

The original object literal had it’s own start method. When the Application instance is wrapped around that object literal, the start method of the object literal overrides the one on the Application prototype. Initializers set up with the addInitializer function will never get run if this method is left on the object literal. Fortunately, the new initializer setup is intended to be a replacement for the existing start method. The Application instance, and the other feature files, can be re-written like this:

1 // <app.js>

2 var app = new Application({

3 someFeature: {

4 views: {},

5 models: {},

6 controllers: {}

7 },

8 anotherFeature: {}

9 };

1 // <someFeature.js>

2 app.addInitializer(function(){

3 // do things to start up this feature, here

4 });

1 // <anotherFeature.js>

2 app.addInitializer(function(){

3 // init and run this feature, from here

4 });

Each of the individual feature files now has the initialization code completely contained within itself. Better yet, the “app.js” file is no longer tied directly to each feature’s init method. This means you can add as many files as you need in your application - each with its own initializer - without touching the Application’s start method again.

Starting The Application

The last thing to do is start the application. This can be done from anywhere in the code. Just call app.start() and all of the registered initializers will run. It’s common to use a jQuery DOMReady callback function to start the app, for example. And this is often done from within the DOM, at the very bottom of the HTML - that way the rest of the scripts have a chance to be loaded before the app starts.

1 <html>

2 <head>

3 <!-- … head stuff, here, including script tags as needed -->

4 <script src="/js/app.js"></script>

5 <script src="/js/someFeature.js"></script>

6 <script src="/js/anotherFeature.js"></script>

7 </head>

8 <body>

9 <!-- … the rest of the page … -->

10

11 <script>

12 // start the app when the DOM is ready

13 $(function(){

14 app.start();

15 });

16 </script>

17 </body>

18 </html>

There is one potential problem left in the initializers, though. Initializers that are added after app.start() has been called won’t be executed. This can happen if you are lazy-loading JavaScript files when a user starts new features in the app (think “GMail” where switching from mail to contacts loads new scripts and data from the server). To make this work, you would have to call the start method again, which would be another problem - all of the initializers would be run again, duplicating any work that was done previously. You might try to work around this by.pop()ing the initializers from the array in the start method. This would work to a point, but you would still have to call the .start method multiple times.

A better solution to this problem is to use jQuery’s deferred objects, which are a variant of JavaScript Promises.

Promising To Run Initializers

According to the CommonJS wiki, promises “provide a well-defined interface for interacting with an object that represents the result of an action that is performed asynchronously, and may or may not be finished at any given point in time.”

That is, a promise is an object that “promises” to run some functionality at some point in the future. The exact point in time is not known, but as long as the promise is “resolved”, then all of the registered functions will be executed.

You can add a callback function to a promise at any time, and know that the callback will be executed. Even if the callback function is added after the promise has been resolved, the callback will still be executed. This is where the “promise” name comes from - the promise to execute on or after resolution.

The initializers that you set up in the Application object are very close in purpose to the way a promise works. They are a set of callback functions that execute when the application is started. This set up seems to fit the intent of promises perfectly. Adding an initializer will add a the callback to a promise, and starting the Application will resolve the promise.

Converting Initializers To Promises

Now is the time that having encapsulated the initializers storage behind an addInitializer method pays off. Converting the Application’s initializer storage from an array to a promise object requires no change in the public API of the Application. It only requires a few changes in the implementation.

Start by replacing the _initializers array with a jQuery Deferred instance (jQuery Deferred objects are a variant of Promises, providing the same core features and intent).

1 // An Application object constructor function.

2 function Application(options){

3 // extend this instance with all the options provided

4 _.extend(this, options);

5

6 // a place to store initializer functions

7 this._initializers = $.Deferred();

8 }

Now change the addInitializer method to store the callback function inside of the new _initializers object.

1 addInitializer: function(initializer){

2 this._initializers.done(initializer);

3 }

The last change to make is to resolve the deferred object when the application starts.

1 start: function(args){

2 // get the complete args list as an array

3 args = Array.prototype.slice.call(arguments);

4

5 // resolve the initializers promise

6 this._initializers.resolveWith(this, args);

7

8 // trigger the start event

9 this.trigger("start", args);

10 }

The promise handles all of the callback execution for you - no more loops, and a lot less code! The start function has gone from not quite ten lines of code plus a dozen lines of comments, down to three lines of code and comments each. But most beneficial aspect of this change is that an initializer can now be added after the application is started, and it will still be executed.

1 var app = new Application();

2

3 app.addInitializer(function(){

4 console.log("I was added before the app was started.");

5 });

6

7 app.start();

8

9 app.addInitializer(function(){

10 console.log("I was added after the app started!");

11 });

The output of both log statements will be shown, because the promise has been resolved in the start method. With the promise already resolved, any additional callbacks that are added will be executed relatively quickly (if not immediately). This lets you lazy-load JavaScript files as needed - each with their own call to app.addInitializer - and have a guarantee that they will be initialized.

Starting Routers

The Application’s initializers provide a powerful and flexible way to organize the initialization code for an app. The ability to load scripts that add initializers at a later time is also very powerful. But this isn’t all that a boot-strapper object needs to do.

For example, it’s common for large applications to have multiple Backbone.Router instances. But where do you put the call to Backbone.history.start()? You don’t need, or want, to call this after each router instance is created. But if you put it in an Application initializer, there no guarantee that all (or any) routers will be available when the history is started.

One solution is to trigger a “start” event from the Application’s start method. If you trigger this event after all of the initializers have been run, and each of your routers are instantiated in an initializer, you’ll have everything ready to go when Backbone’s history is started.

1 _.extend(Application.prototype, Backbone.Events, {

2 // … existing methods

3

4 start: function(args){

5 // … existing code

6

7 this.trigger("start", args);

8 }

9 });

Pass the same args array along to the “start” event. Then you can run code with the same args available, after all of the initializers have been fired.

Now add an even handler to your Application and start the history, there.

1 var app = new Application({

2 // … existing code

3 });

4

5 app.on("start", function(args){

6 if (Backbone.history){

7 Backbone.history.start();

8 }

9 });

The if checks to see if there is a Backbone.history object available. If no router instances have been created, it won’t exist. If at least one router has been defined, though, it will be available. Since you can’t call the start method if there isn’t a history object, so it’s a good idea to add this check in place.

If you find yourself writing this same “start” event handler in most of your applications, you might consider adding the history start call directly to the Application’s start method.

1 start: function(args){

2 // get the complete args list as an array

3 args = Array.prototype.slice.call(arguments);

4

5 // resolve the initializers promise

6 this._initializers.resolveWith(this, args);

7

8 // trigger the start event

9 this.trigger("start", args);

10

11 // start the routers, if any are defined

12 if (Backbone.history){

13 Backbone.history.start();

14 }

15 }

Now you’ll never have to worry about starting routers again. Just start the app and you’ll know that any router you created in your initializers (or elsewhere, before the app is started) will be started, too.

Lessons Learned

Codifying an object from a concept or loosely implemented feature set can introduce a new world of freedom, flexibility and opportunity. Creating an Application object that can be extended, can migrate an existing object literal, and provide the ability to run initializer functions is an important aspects of scaling Backbone applications (or JavaScript in general). It allows you to have a single point of entry for the application, while allowing each area of the application to initialize itself correctly.

Let The Feature Own Its Initialization

Having a single start method for a simple application works well. Moving beyond small or simple, though, it quickly falls apart. By providing a method of registering initializer functions, the act of starting an application can be decoupled from the functions that run when the application starts. This allows each feature or area of functionality within an application to be in complete control of its initialization. If a feature knows that it should not be available, it does not need to register its initializer with the application.

Promises, Promises

Promises are powerful objects that can add a lot of flexibility when used appropriately. They provide a mechanism for handling return values when the time of return is completely unknown. Taking advantage of this can lead to many new opportunities and problems solved.

In the case of the Application initializers, switching from a simple array based storage to a jQuery Deferred (promise) provided several benefits. In addition to simplified code, this change allowed for initializers to be executed no matter when they were added. If an initializer is added before the application is started, the initializer is held, waiting for the app to start. If an initializer is added after the application is started, the promise executes the initializer immediately because the promise has already been resolved. This allows additional features and functional areas of an application to be loaded as needed, instead of requiring all features and files to be pre-loaded before starting the application.

Create Migration Paths

No one wants to adopt yet another all-or-nothing framework when they are looking at fixing or improving an existing application’s code base. Providing a migration path that does not require a complete rewrite of the system will increase the adoption rate of a new tool or framework significantly. If developers can simply plug in the new tool and start to use it where it adds immediate value, the will. Over time, the framework should show continued areas of added value that can be reached in incremental and non-intrusive manners.

By providing an Application object that accepts an object literal, and extends all of the methods and attributes from that object literal on to the Application itself, developers are given an opportunity to incrementally improve their own code. An existing investment in an object literal can be leveraged in the migration to the new Application object without massive changes. Once the first step is taken, the next step of migrating from a single .start method, out to many initializer methods, becomes apparent. This migration strategy allows applications to grow in to your large-scale framework, instead of requiring a re-write.

Don’t Limit Apps To Backbone’s Constructs

Code, libraries and add-ons that support general JavaScript development can often be very useful in Backbone app. And, in fact, tools and techniques from outside of Backbone should be employed. Limiting an application to only the constructs that Backbone provides will directly limit the usefulness of code and hamper a developer’s ability to create a clean, well structured system. The application initializers are a good example of how an abstraction can be built to work with any type of JavaScript application. There is no requirement in the code or in the intended purpose of these initializers, that says they can only be used with Backbone application. This same code base could be re-used across any number of JavaScript libraries and frameworks.

And remember: a Backbone application is a JavaScript application. Use all of the power, flexibility and capabilities that JavaScript provides.

Encapsulation Is More Than Just Theory

It is often easy to expose data structures as part of a public API. This allows other developers to do what they want with that data structure, and abdicates a lot of responsibility from you, the designer of the API. This is not a good thing, by any stretch of the imagination. Providing a clean and clear API for an object or type provides many benefits found in proper encapsulation. It protects internal data structures from unwanted and abusive use. It allows the data structure to change while the API remains in tact. And it allows a developer that is using the API to keep their thought process on that API. They will not have to do mental context switching between your object’s API and semantics, and the API and semantics of the data structure that your object happens to use.

JavaScript And Object References

JavaScript treats primitive types as by-value references. That is, a primitive will be copied between variables that are assigned to the same value. Non-primitive types, on the other hand, are always treated as by-reference. Assign a non-primitive type to multiple variables, and subsequent modification of any one of those variables will be reflected in all of the variables that are assigned.

When designing a JavaScript type, this is an important distinction to understand. If you assigned the _initializers to the prototype of the Application, then all instances of an Application would have the same initializers. By assigning the _initializers inside of the Application’s constructor function, though, this problem is avoided. The use of this._initializers = ... as the assignment guarantees a unique initializer set for each instance of an Application.