JavaScript Routing
One of the more important pieces of any client-side JavaScript framework is the routing of requests when users navigate to different places. Basically a JavaScript router in a client-side application will listen for changes in the hash location and update the data and UI in the page accordingly, making all of the API calls that it needs to do so. But how is this feat accomplished? How do we match a route pattern defined in a JavaScript framework to an actual route navigated to by the user? This is something we will look at in what follows.
Normally, when you talk about matching you immediately think to look at regular expressions. However, the way that routes are defined in client side applications often have patterns that are a little bit different that the actual routes that will be navigated to by users. What do we mean by this? We talked about routing in Backbone.js here. Basically a Backbone.js router is set up like the following…
var Router = Backbone.Router.extend({
routes: {
"": "home", // url:event that fires
"new": "createNew"
"edit/:id": "editItem",
"download/*anything": "downloadItem"
},
home: function() {
alert('We have loaded the home view');
},
createNew: function() {
alert('We are going to create something new');
}
editItem: function(idParam) {
alert('We are going to edit entry number ' + idParam);
}
});
var router = new Router;
In this setup, when a user navigates to a particular route, the function defined in the routes object will run. So if a user were to navigate to the route #/edit/4, the editItem function will run passing in the id 4.
Notice the parameterized definition of :id with the colon. The approach you have to take when writing a JavaScript router is to match a route like #/edit/4 to a route like edit/:id. It’s the same situation with other JavaScript libraries as well. Let’s look at an AngularJS router. This is an example router from a recipe book application written in Angular…
app.config(function ($routeProvider) {
$routeProvider
.when('/',
{
controller: 'HomeController',
templateUrl: 'app/views/home.html'
})
.when('/login',
{
controller: 'LoginController',
templateUrl: 'app/views/login.html'
})
.when('/logout',
{
controller: 'LogoutController',
templateUrl: 'app/views/login.html'
})
.when('/signup',
{
controller: 'SignUpController',
templateUrl: 'app/views/signup.html'
})
.when('/recipes',
{
controller: 'RecipesAllController',
templateUrl: 'app/views/recipes-all.html'
})
.when('/recipe/:id',
{
controller: 'RecipeDetailsController',
templateUrl: 'app/views/recipe-details.html'
})
.when('/new',
{
controller: 'RecipeCreateController',
templateUrl: 'app/views/recipe-create.html'
})
.when('/edit/:id',
{
controller: 'RecipeEditController',
templateUrl: 'app/views/recipe-edit.html'
})
.otherwise({ redirectTo: '/' });
}).run( function($rootScope, $location, userService) {
// listener to watch all route changes
$rootScope.$on("$routeChangeStart", function(event, next, current) {
});
});
Notice here the same kind of parameterized matching in the routes /recipe/:id and edit/:id. How do you sort through these defined routes when a user navigates to a route to match them up with the desired code that you want to run?
Below is a function that we’ll use to match routes. If we had, say, an object literal of key value pairs of defined routes and callback functions to run for those routes, basically what it does is iterate through all the key value pairs in the collection. It checks to see if the route is a “match” and if it is it will return an object with information about the route. In this simple example, this will essentially boil down to the parameter value that was passed into the route, but we will get a bit more sophisticated with it a bit later.
So if our object literal looked like the following…
var routes = {
"/": function() {
console.log('This is the home route');
},
"/about": function() {
console.log('This is the about route');
},
"/edit/:id": function(obj) {
console.log('This is the edit route route with the id ' + obj.id);
},
}
Our routing function for matching routes would look like the following…
function matchRoute(url, definedRoute) {
// Current route url (getting rid of '#' in hash as well):
var urlSegments = url.split('/');
var routeSegments = definedRoute.split('/');
var routeObject = {};
if(urlSegments.length !== routeSegments.length) {
// not a match
return false;
}
else {
for(var i = 0; i < urlSegments.length; i++) {
if(urlSegments[i].toLowerCase() === routeSegments[i].toLowerCase()) {
// matched path
continue;
}
else if (routeSegments[i].indexOf(':') === 0) {
// matched a param, remove query string (which is handled below) and push id onto object
var val = routeSegments[i].replace(':','');
val = val.split('?')[0];
routeObject[val] = urlSegments[i].split('?')[0];
}
else {
// not a match
return false;
}
}
}
// after all is finished, return the route object to pass to router for use by controller
return routeObject;
}
Now if we were going to put this to use, we could create another function to handle changes in the hash and call that function when we listen for changes in the hash …
function routerHandler () {
// Current route url (getting rid of '#' in hash as well):
var url = location.hash.slice(1) || '/';
for(var i in routes) {
var routeData = matchRoute(url, i);
// Do we have a route?
if (typeof routeData === "object") {
routes[i].call(this, routeData);
// we found our route, no need to continue...
break;
}
}
}
Finally, we need to make sure that we listen for changes in the hash…
// Listen on hash change...
window.addEventListener('hashchange', routerHandler);
// Listen on page load...
window.addEventListener('load', routerHandler);
Looking pretty good so far. We could also handle the cases where we have a query string (e.g. /#/edit/4?food=cheese). If we update our matchRoute function to include the following, we can handle this scenario…
function matchRoute(url, definedRoute) {
// Current route url (getting rid of '#' in hash as well):
var urlSegments = url.split('/');
var routeSegments = definedRoute.split('/');
var routeObject = {};
if(urlSegments.length !== routeSegments.length) {
// not a match
return false;
}
else {
for(var i = 0; i < urlSegments.length; i++) {
if(urlSegments[i].toLowerCase() === routeSegments[i].toLowerCase()) {
// matched path
continue;
}
else if (routeSegments[i].indexOf(':') === 0) {
// matched a param, remove query string (which is handled below) and push id onto object
var val = routeSegments[i].replace(':','');
val = val.split('?')[0];
routeObject[val] = urlSegments[i].split('?')[0];
}
else {
// not a match
return false;
}
}
}
// did we reach the end? Get querystring, if any...
var hash = window.location.hash.split("?")[1];
if(typeof hash !== "undefined") {
var queryString = hash.split('&');
var queryStringObject = {};
for(var i = 0; i < queryString.length; i++) {
var currentParameter = queryString[i].split("=");
queryStringObject[currentParameter[0]] = currentParameter[1];
}
routeObject.queryString = queryStringObject;
}
// after all is finished, return the route object to pass to router for use by controller
return routeObject;
}
Our example above shows one possible way to handle hash-based routing in JavaScript. There might be other ways to do this. Utilization of RegEx in some manner might be another route (*bad pun groan*) to go, but all in all, whatever you choose, as we have seen here, it does not have to be needlessly complicated. Take a look at the demo below…
View Demo AngularJS, Backbone.js, JavaScript





0 Responses to JavaScript Routing