Lately I’ve been building web applications with quite a bit of the app implemented on the client side with JavaScript. Backbone.js has been handy in building these applications. Here I’ll be sharing my experience building up Backbone Routers, and how I’ve come to prefer an event-based mechanism for page navigation over setting the @window.location@ directly. Why? Because then I’d be treating @window.location@ as a global variable, which I’ve learned to avoid.
h2. An Example
_Note: All of the examples here are totally contrived. I put them into the CoffeeScript compiler to make sure they’re at least legal, but I’ve spent no other time on the syntax. Instead I’m focusing on concepts._
Let’s say you’ve got Backbone driving a university department’s website. The department offers three services: records, financial aid, and admissions. The website offers each of these services as the top-level navigation for the app. Here’s what the router might look like:
class AcademicServicesRouter extends Backbone.Router
routes:
'records': 'records'
'financial_aid': 'financial_aid'
'admissions': 'admissions'
initialize: ({@view}) ->
records: => @view.show_records()
financial_aid: => @view.show_financial_aid()
admissions: => @view.show_admissions()
Right now it’s a fairly boring router. It’s been built with view that has @show_records@, @show_financial_aid@, and @show_admissions@ functions that, presumably, take care of swapping in the appropriate subviews into place. The router doesn’t know anything about that; nor does it care. I knows how to route and that’s about it. A nice separation of concerns.
And, of course, when the @window.location@ changes and includes @records@, @financial_aid@, and @admissions@ in the fragment, the router kicks in and calls the appropriate @show@ function. But here, how did the @location@ get set? Probably because the user clicked an anchor tag with the @href@ set. Or maybe some other Javascript code set it directly. And there may be other ways I’m not aware of.
@window.location@ is being treated as a global variable at this point.
h2. Who Cares?
Now it’s easy to go around talking about how global variables are horrible. It’s usually one of the first things you’re taught as a developer. That and to comment frequently. Heh. Anyway, instead of saying “global variables are bad; @window.location@ here is a global variable; don’t do this” and publishing at this point, I’ll share a poor experience I’ve had with globals and how I’ve used Backbone events to avoid setting @window.location@ directly.
Of course, early on, I was told global variables are bad. I generally avoided them as a student, but they’d come up from time to time, especially in my graduate work. And they worked okay.
Then I got a job and had to start maintaining code. This is where they start to become a pain. One of the first projects I worked on was a large system with thousands (I think about 5,000, but my memory is fuzzy) globals. They served their purpose and the system generally worked well. But extending the system’s features was difficult. I’d change the value of a global, and something else would change it’s behavior, but…sometimes I’d spend hours trying to track down that code.
Sure, grep works, but it’s not perfect. Sometimes there’s too much to sift through. Sometimes there’s another variable aliasing the global, so your search doesn’t find it. Worst of all, there’s no traceability. When state’s protected behind setter and getter methods, I can more easily track how that state is being changed with a stack trace.
And, of course, globals are a pain in the ass for testing. Want to use a global? Have fun with all the test interaction you’ll be fighting for the rest of the project.
h2. Back to Backbone Routers
Given all of that, I’ve had my personal reasons for eyeing globals suspiciously. @window.location@ is looking fishy to me.
How do I avoid using it? Instead of setting it directly to trigger my router behavior, I have my router listen to events from the view it is controlling. Then it uses its own @navigate@ function to change the location. Let’s go back to the example:
class AcademicServicesRouter extends Backbone.Router
routes:
'records': 'records'
'financial_aid': 'financial_aid'
'admissions': 'admissions'
initialize: ({@view}) =>
@view.bind 'records_selected', => @navigate('/records', true)
@view.bind 'financial_aid_selected', => @navigate('/financial_aid', true)
@view.bind 'admissions_selected', => @navigate('/admissions', true)
records: => @view.show_records()
financial_aid: => @view.show_financial_aid()
admissions: => @view.show_admissions()
class ViewThatSelectsRecordsForSomeReason extends Backbone.View
render: ->
# build @el
@$('.records-link').click => @trigger 'records_selected'
some_other_function: ->
@model.save(
success: => @trigger 'records_selected'
)
$(-> new AcademicServicesRouter(view: new ViewThatSelectsRecordsForSomeReason))
There’s that old saying about how all problems in Computer Science can be solved by another level of indirection. That’s precisely what I’ve done here. Why? Here’s what I’ve gained:
* The router is now responsible for managing the @window.location@ state. My code does not necessarily know that’s what the @routes@ and @navigate@ are manipulating. Hooray for less coupling.
* Now that I’m navigating with functions, I have the opportunity to inject stack traces when I want. If I were setting the @window.location@ state directly this wouldn’t be an option.
* I can trace the affects of an event by tracing it through the code. Want to know who triggers the @records_selected@ event? Ok. I’ll start by popping open that view and go from there. Or maybe I’m interested is seeing what happens when I trigger the @admissions_selected@ event. Cool. I can find who subscribes to it and go from there.
The obvious downside to this approach is that you need more code. And sometimes that code is truly annoying. For instance, if the @records_selected@ originates from a subview of a subview of a subview of a subview…you get the point. In this case there’d be a bunch of annoying relay code between the router and that deeply-nested view.
I’d love to hear of a way to streamline the message relaying process.
Oh yeah, I could set @window.location@ directly…errr, woops, that’s the devil’s candy. Clearly it’s an easy and tempting option. I’m not going to be critical of those who do it. But it makes me nervous. This is how I’ve avoided it. So far, so good.