5

I have spent the last 2 weeks learning backbone and related tools as well as writing an application. I have hit a wall with a design issue and would like to know what kind of solutions are available and whether Backbone experts even regard this as an issue.

Problem: I am ending up having to put all my view dependencies in my router.js and am unable to figure out if their is a way around that. Below is the code from my router.js:

// router.js
define([
  'jquery',
  'underscore',
  'backbone',
  'text',
  'views/landing',
  'views/dashboard',
],  
    function($, _, Backbone, t,LandingView,DashboardView){
        var AppRouter = Backbone.Router.extend({
        routes: {
          // Define some URL routes
          '': 'showLanding',
          'projects': 'showProjects',
          // Default
          '*actions': 'defaultAction'
        },
        navigate_to: function(model){
                alert("navigate_to");
            },

        showProjects: function() {},
        showLanding: function() {},
    });

    var initialize = function() {
        var app_router = new AppRouter;
        Backbone.View.prototype.event_aggregator = _.extend({}, Backbone.Events);
        // Extend the View class to include a navigation method goTo
        Backbone.View.prototype.goTo = function (loc) {
            app_router.navigate(loc, true);
        };
        app_router.on('route:showLanding', function(){
            var landing = new LandingView();
        });
        app_router.on('route:showProjects', function(){
            var dashboard=new DashboardView();
        });
        app_router.on('defaultAction', function(actions){
            alert("No routes");
            // We have no matching route, lets just log what the URL was
            console.log('No route:', actions);
        });
        Backbone.history.start({pushState: true});
    };
    return {
        initialize: initialize
    };
});

router.js includes the LandingView and DashboardView views which in turn fetch the respective templates. The initial route loads the LandingView which has a login template. After logging in, it calls the goTo method of router.js to spawn a DashboardView(). Although this works, I feel that it's a bit ugly. But I can't figure how else to spawn a new DashboardView from LandingView without either directly referencing DashboardView() from inside of LandingView() or from the router.

If I continue doing this via router.js I will end up pulling, directly or indirectly, all my views js files from the router. Sounds a bit ugly!

I looked at Derick Baileys' event aggregator pattern but faced the question of how does the DashboardView subscribe to an event generate by the LandingView if an instance of DashboardView doesn't even exist yet? Someone has to create and initialize it for it to subscribe to an event aggregator, right? And if that someone is the router, do I need to instantiate all the views upfront in the router? That doesn't make sense.

Sid
  • 7,511
  • 2
  • 28
  • 41
  • I think I updated this the same time you did and accidentally reverted some of your changes. – zzzzBov Jan 06 '13 at 19:38
  • I think the mediator pattern would solve your issue: http://pivotallabs.com/users/mrushakoff/blog/articles/2165-simplifying-view-view-events-in-backbone-using-the-mediator-pattern – Julian Krispel-Samsel Jan 06 '13 at 20:58
  • Thanks - going over the mediator now. – Sid Jan 07 '13 at 02:47
  • @nimrod SO the Mediator is the same as the event aggregator of Marionette, correct? – Sid Jan 08 '13 at 04:05
  • 1
    @Sid it's based on that pattern, but as far as I understand Marionette does a whole lot more and has quite a few dependancies. If you want to focus just on mediation I'd recommend that blogpost. Also, if you think about building a big app, checkout other frameworks that provide all the structure so you don't need to think about architecture too much, like angular.js for example, or ember... – Julian Krispel-Samsel Jan 08 '13 at 11:39

2 Answers2

11

I've tackled this problem by only importing the views when the route is first hit:

define(['backbone'], function(Backbone) {
    var AppRouter = Backbone.Router.extend({
        routes: {
            '':      'home',
            'users': 'users'
        },

        home: function() {
            requirejs(["views/home/mainview"], function(HomeView) {
                //..initialize and render view
            });
        },

        users: function() {
            requirejs(["views/users/mainview"], function(UsersView) {
                //..initialize and render view
            });
        }
    });

    return AppRouter;
});

It doesn't solve the issue of having to eventually import all the views to the router, but the lazy requirejs calls don't force loading and evaluating all scripts and templates up front.

Fact of the matter is that someone, somewhere, must import the modules. The router is a sensible location, because typically it's the first piece of code that's hit when user navigates to a certain page (View). If you feel like one router is responsible for too much, you should consider splitting your router into multiple routers, each responsible for different "section" of your application. For a good analogy think of the Controller in a typical MVC scenario.

Example of multiple routers

userrouter.js handles all User-related views (routes under 'users/'):

define(['backbone'], function(Backbone) {
    var UserRouter = Backbone.Router.extend({
        routes: {
            'users',        'allUsers',
            'users/:id',    'userById'
        },
        allUsers: function() {
            requirejs(["views/users/listview"], function(UserListView) {
                //..initialize and render view
            });
        },
        userById: function(id) {
            requirejs(["views/users/detailview"], function(UserDetailView) {
                //..initialize and render view
            });
        }
    });
    return UserRouter;
});

postrouter.js handles all Post-related views (routes under 'posts/'):

define(['backbone'], function(Backbone) {
    var PostRouter = Backbone.Router.extend({
        routes: {
            'posts',        'allPosts',
            'posts/:id',    'postById'
        },
        allPosts: function() {
            requirejs(["views/posts/listview"], function(PostListView) {
                //..initialize and render view
            });
        },
        postById: function(id) {
            requirejs(["views/posts/detailview"], function(PostDetailView) {
                //..initialize and render view
            });
        }
    });
    return PostRouter;
});

approuter.js is the main router, which is started up on application start and initializes all other routes.

define(['backbone', 'routers/userrouter', 'routers/postrouter'], 
function(Backbone, UserRouter, PostRouter) {

    var AppRouter = Backbone.Router.extend({

        routes: {
            '',        'home',
        },
        initialize: function() {
            //create all other routers
            this._subRouters = {
                'users' : new UserRouter(),
                'posts' : new PostRouter()
            };
        },
        start: function() {
            Backbone.history.start();
        },
        home: function() {
            requirejs(["views/home/mainview"], function(HomeView) {
                //..initialize and render view
            });
        }
    });
    return UserRouter;
});

And finally, your application's main.js, which starts the app router:

new AppRouter().start();

This way you can keep each individual router lean, and avoid having to resolve dependency trees before you actually need to.

Sidenote: If you use nested requirejs calls and you're doing a build with r.js, remember to set the build option findNestedDependencies:true, so the lazily loaded modules get included in the build.

Edit: Here's a gist that explains lazy vs. immediate module loading in RequireJS.

jevakallio
  • 35,324
  • 3
  • 105
  • 112
  • +1 Thanks, the lazy loading sounds like a good idea. I will wait for a few more answers. – Sid Jan 07 '13 at 02:44
  • 1
    hey there, nice idea. but, shouldnt the last function return AppRouter instead of UserRouter? – oak Nov 13 '13 at 08:47
  • Can you tell me what is "_subroutes"? Where is it defined? is it part of backbone? How does it work? I have seen no reference to this anywhere and I'm trying to find it for days now :) – ANewGuyInTown Jun 11 '17 at 07:46
1

We use a factory for this it simply returns a view instance, it can also cache instances:

define(function() {
  // Classes are defined like this { key1: Class1, key2: Class2 }
  // not cachedObjects are defined like this { notCached : { key3: Class3 }}
  return function(Classes) {
    var objectCache = {};

    return {
      get: function(key, options) {
        var cachedObject = objectCache[key];
        if (cachedObject){
          return cachedObject;
        }

        var Class = Classes[key];
        if (Class) {
          cachedObject = new Class(options);
          objectCache[key] = cachedObject;
          return cachedObject;
        }

        Class = Classes.notCached[key];
        if (Class) {
          return new Class(options);
        }
      }
    };
  };
});

Then we have a module that creates the factory:

define([
  'common/factory',
  'views/view1',
  'views/view2',
  'views/view3',
  ], function(
    viewCache,
    View1,
    View2,
    View3
  ) {

  var views = {
    route1: View1,
    route2: View2,
    notCached: {
      route3: View3,
    }
  };

  return viewCache(views);
});

In the router you could then easily get the view by calling viewCache.get(route). The benefit is to decouple creating/caching of the views, which now can be test separately.

Also as we use Marionette we dont use the viewCache in the router but in the RegionManager which is a better fit for creating the views. Our router just trigger events with the actual state and route of the app.

Andreas Köberle
  • 106,652
  • 57
  • 273
  • 297
  • I like the factory idea but doesn't that "move" the problem of loading all views upfront from the router to the factory? As in, downloading all the view related .js files. – Sid Jan 07 '13 at 02:46
  • Yes, this will load all the code of your views, but it decouple the router from and the creating of views. But you can refactor the factory to work with requirejs, so you store just the path to your require module instead of module itself. – Andreas Köberle Jan 07 '13 at 07:27
  • Update to show how this could work with requirejs (not tested). – Andreas Köberle Jan 07 '13 at 07:34
  • @AndreasKöberle your requirejs pattern would be great, but unfortunately I don't think it works. For one the `require` API is async, so you'd have to make the factory async too. The bigger problem is that the `r.js` compiler can only resolve nested dependencies if the `require` call is used with a **literal** string path, like `require(['module/name'], cb)`. – jevakallio Jan 07 '13 at 08:47
  • Are your sure, we use `require` and it seems to wait until the file is loaded, also the main idea was not to compile the code for the view in the main app.js but load it when needed. – Andreas Köberle Jan 07 '13 at 09:10
  • 1
    @AndreasKöberle, You're right in fact that you can call `require` synchronously, but only thanks to some trickery RequireJS performs for you. Depending on whether you use the AMD or CommonJS style header, it calling `require` synchronously can either be only working due to sheer luck, or may not be doing what you imagine it would. You got me doubting myself, so I had to go test. Here's the gist of it: https://gist.github.com/4475666 – jevakallio Jan 07 '13 at 15:17
  • @AndreasKöberle, it doesn't work, because RequireJS can't parse `require(Class)`. You would need to pass it a literal: `require('views/View2')`, which would still be only a partial solution, because it would load the module as soon as the `common/factory` module is loaded for the first time. See the gist in my previous comment for examples. – jevakallio Jan 07 '13 at 15:37
  • Ok, your right, we're using it to workaround a an circular dependency, and we import the module in the define declaration. – Andreas Köberle Jan 07 '13 at 15:38
  • @Sid - Yeah, it seems to be useful. I already managed to use it at work to "educate" some folks and win a dirty bet. I edited it into my answer, maybe it'll be useful to others too. – jevakallio Jan 08 '13 at 13:43