Aviator: Centralized Client-Side Routing
URLs are core to how the web works. They should be first class citizens in your web applications.
At Swipely, we have a large client-side application. YUI has helped us keep our codebase nice and modular. But we didn’t love the way our route declarations were dispersed amongst a number of controller-like router objects, each dispatching for a subset of the paths available to the user.
We considered refactoring our application's routing as a YUI-specific pattern, but ultimately decided on a new strategy that would free us from future dependencies. The result was Aviator, a front-end router that plugs in easily to any JavaScript application.
Centralize your routes
We believe routes should be defined in one place, and declared separately from the logic that handles the consequences of visiting a particular route. Some benefits of this approach:
- application URL hierarchy is easy to grok at a glance
- controllers/route targets are not bloated with route setup and logic
- you can be confident about multiple actions occurring in a specific order for any route
Abstract what's browser-specific
The different ways browsers handle URLs and history should not be top of mind when you are writing application code.
So with Aviator, you write your hrefs in your links normally, and Aviator will figure out whether or not it needs to use hash-based routing. Same goes for navigating to a route in code as the result of some action.
Separate Concerns
Aviator doesn't care how you render your views, how you load your data, or what libraries and frameworks you do it with. Its single responsibility is to call the right method on the right object.
We call these objects route targets, and it's entirely up to you what they look like. At Swipely, they serve as controllers; they instantiate views and models and load the data.
It’s fine if one route target receives all the requests, and that might make sense for a small application. But Aviator really shines by making it easy to delegate the route handling to different targets, keeping each one small and focused.
Set up Aviator
Set up is declarative. Call Aviator.setRoutes
with a nested object. The object lays out all routes and corresponding actions to take for those routes.
Let's say you have an application where users upload documents and keep track of their document revisions.
Aviator.setRoutes({
'/docs': {
target: documentRouteTarget,
'/*': 'renderLayout',
'/': 'list',
'/:documentID': {
target: documentRouteTarget,
'/': 'show',
'/revisions': {
target: revisionsRouteTarget,
'/': 'list',
'/:revisionID': 'show'
}
}
}
});
Given this setup, Aviator knows that when a user goes to /docs/2013_potluck_guest_list/revisions/20130722140801?readonly=true
, the steps to take are:
- call
documentRouteTarget#renderLayout
- call
revisionsRouteTarget#show
This really lends itself to specializing the responsibilities of your route targets. You could imagine one route target that tracks metrics, and gets called on every request, or another that handles a sidebar, but only on some requests. It's easy to be explicit about these things with the nested object setup.
Within the renderLayout
and show
functions, you get a request object as a parameter. So you have access to the documentID, the revisionID, and the readonly flag through request.params
. You can also always get the URI, the query string, and the route that mapped to this function.
Navigate the World
Aviator exposes a small API. Besides setRoutes
for configuration, it handles functionality that really should be taken care of by a router rather than by application code.
To name a few, the Aviator
object abstracts navigating to a route, updating the URL silently, serializing query params and accessing existing params.
Try it out
Aviator is open source! We’d love to hear how it plugs in to your application. Check it out on github.