Backbone.js used to have controllers instead of routers in past. Imagine how complex it was to put functionality both in views (read presenters) and controllers! How would you decide where to keep which functionality? Moreover, you could never depend on a single controller if the app was a complex one. But things are much simpler now –our views take care of all the functionality of the app and our router mostly manages routing client-side pages, and connecting them to actions and events.
Routing needs to be managed very carefully (Image: Lord Bullgod)
Working with a Backbone router is fairly easy when the application is of a small size. However, it is not that straightforward while you are developing a large application. Maintaining a router for a complex app is quite difficult unless certain rules are followed. In this post, we are going to discuss some points that you should take care of while using routers.
Though the basic task of a router is to monitor the routes and trigger functions, it manages some business logics of the app too. In MVC architecture, the task of a controller is to handle the data request that is sent from the client and work upon the data that comes from the server in response. Similarly for a router, since the URL fragments reflect some part of the application’s data, the data communication, calling view methods, or updating model attributes are done using router methods.
A trend I often see in beginner-level developer code is that they frequently include a large chunk of functional code inside the router methods. On one hand, this increases the size of the router, and on the other hand, it complicates the logic. It is always advisable to keep your router methods as short as possible by pushing the functional logic in views and using events instead of callbacks. In the following section we will see how we can keep our router clean.
I have seen many developers who instantiate their application views inside router methods. While there is no such restriction in instantiating views or modifying DOM elements in router methods, it is a good practice to avoid such operations in routers. This is somewhat related to the first point that I mentioned in this section. In the following code, we have instantiated a UserView class and rendered it in the DOM inside the router method:
Backbone.Router.extend({ routes: { "users": "showUsers" }, showUsers: function () { var usersView = new UsersView(); usersView.render(); $("#user_list").html(usersView.el); } });
This looks simple and works perfect. But will it not clutter the router if there are 20 or more such methods? Why not create a controller or a high-level application object and add the method there? Then you can call this controller method from the router method as follows:
Backbone.Router.extend({ routes: { "users": "showUsers" }, showUsers: function () { UserController.showUsers(); } }); var UserController = { showUsers: function () { var usersView = new UsersView(); usersView.render(); $("#user_list").html(usersView.el); } }
Now any change in the showUsers() method functionality will not force you to touch the router. There’s really not much of a visible difference—but personally, as I have used the second pattern several times and benefited from it, I can guarantee you that the separation of concern will produce a much cleaner router along with a maintainable code base.
Also, in this context, we recommend you to check Marionette.AppRouter and Marionette.Controller. Marionette AppRouter and Controller work in the same way as our UserController and the base router. The controller actually does the work (such as assembling the data, instantiating the view, displaying them in regions) and can update the URL to reflect the application’s state (for example, displayed content). The router simply triggers the controller action based on the URL that has been entered in the address bar. Both these classes are quite useful, and you can go for either of them if needed.
Enhance flexibility by introducing regular expression in routers
If you want to trigger a router method only when a specific condition is matched, a regular expression comes to the rescue. Regular expressions are flexible with routes and Backbone supports them completely. In fact, all of the routes are first converted into RegExp objects when they are added to the routing table. However, JavaScript will not allow you to add a regular expression as a property of the routes object, unlike the other string URL fragments:
// This will give error routes: { /^user/(d+)/: 'showUserDetails' }
Here is the solution: you can add the regular expression in the routes object in the initialize() method of the router:
initialize: function () { this.route(/^user/(d+)/, 'showUserDetails'); }, showUserDetails: function (id) { console.log(id); }
This is an example where only numbers are allowed in the URL fragment after #user. If you try to open a URL ending with #user/abc, the showUserDetails() method will not be called, but with the URL fragment #user/123, the same method will be triggered. So, this is a very generic example of using a regular expression as a route. Regular expressions are very useful for more complex levels of URL fragments in order to provide a level of restriction.
Dividing the router into a number of subrouters is another useful method. A main router will anyway be there to manage all the subrouters but you can easily separate out module wise functionality in the subrouters. In the next post we are going to discuss about how you can have multiple routers for your application and the open source plugins available to provide ready made solution to this problem.