Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Backbone.js "fat router" design conundrum

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.

like image 426
Sid Avatar asked Jan 06 '13 19:01

Sid


2 Answers

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.

like image 121
jevakallio Avatar answered Sep 29 '22 07:09

jevakallio


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.

like image 35
Andreas Köberle Avatar answered Sep 29 '22 06:09

Andreas Köberle