Library of the Week: Crossroads.js

This week, let's talk about the standalone routing library Crossroads.js by Miller Medeiros. We should also mention Hasher, a library by the same author that is often used in conjunction with Crossroads to trigger routing functionality when the browser history changes.

If you're using a full-stack framework with an integrated router, like Angular or Ember, you probably won't need Crossroads. Although it's possible to integrate it with these frameworks, and Crossroads is arguably slightly more powerful (as illustrated below), the marginal benefit is not worth the extra effort.

But there is still room for a single-purpose routing library. Take React, for example, which is very much in vogue nowadays and leaves the routing layer completely up to you. On a completely different note, we achieved good results using Crossroads in a game-like application written almost completely using Pixi.js. In that case, drop-in routing functionality was exactly what we needed.

Crossroads implements one key concept: mapping string patterns to handler functions (so you can use it for dispatching arbitrary messages, not just routing URLs). Routes can be registered for a variety of patterns using placeholders or even regular expressions for more complex logic. A code sample is worth a thousand words, so let's consider a few examples from the Crossroads documentation:

// String rule with parameter:
// matches '/news/123' passing "123" to handler
var route1 = crossroads.addRoute('/news/{id}', function(id) {...})
 
// String rule with optional parameter:
// matches '/foo/123/bar' passing "123" and "bar" to handler
// matches '/foo/45' passing "45" to handler (slug is optional)
// `matched` is used to add an additional handler to the chain
var route2 = crossroads.addRoute('/foo/{id}/:slug:');
route2.matched.add(console.log, console);
 
// RegExp rule:
// matches '/lorem/ipsum' passing "ipsum" to handler
// note the capturing group around the parameter segment
var route3 = crossroads.addRoute(/^\/lorem\/([a-z]+)$/, function(id) {...});
 
// String rule with rest segments:
// (useful when an intermediate segment is repeated)
// matches '/foo/123/edit' passing "123" to handler
// matches '/foo/45/asd/123/edit' passing "45/asd/123" to handler
var route4 = crossroads.addRoute('/foo/{id*}/edit');
 
// Query String:
// matches 'foo.php?lorem=ipsum&dolor=amet'
crossroads.addRoute('foo.php{?query}', function(query){
  // query strings are parsed into objects
  console.log('lorem '+ query.lorem +' dolor sit '+ query.dolor);
});

As you can see, the syntax for defining matching rules is very flexible. Options such as shouldTypecast make Crossroads even more convenient:

crossroads.shouldTypecast = true;

This causes all numeric and boolean parameters to be converted automatically before they are passed to the handler.

Routing is triggered by calling crossroads.parse(). If you want to trigger Crossroads when the URL loaded in the browser changes (the most common use case), the aforementioned Hasher library comes in handy. The two libraries can be tied together with a few lines of code:

function parseHash(newHash, oldHash){
  crossroads.parse(newHash);
}
hasher.initialized.add(parseHash); // parse initial hash
hasher.changed.add(parseHash); // parse hash changes
hasher.init(); // start listening for history changes

Thanks to these two versatile and highly specialized libraries, you gain fine-grained control over the whole process (you can pause routing, add logging or preprocessing, etc.). This is a great illustration of the single responsibility principle in action.

Roman Krejčík

Roman Krejčík