Marionette.AppRouter
extends the standard Backbone.Router
, neither of them provides an interception mechanism that allow you to prevent the execution of a route callback. In need of the same features, I came up with the following code (_
is Lodash). In the original Backbone.Router.prototype.route()
, a function that will be executed on a route match is created. The following code provides a wrapper for the original Backbone.Router.prototype.route()
in order to replace the registered route callback with a function that will execute a chain of middleware functions. Each of these middleware can then perform any additional processing before or after execution of the original route callback, including preventing execution of further callback and raise exception.
Backbone.RouteMiddlewares = (function () {
var Backbone = require('backbone')
, _ = require('lodash');
/**
* A wrapper for `Backbone.Router.prototype.route()` that will use route middlewares (in addition to its
* normal callback).
*
* @param {Function} fn - The wrapped function (should be compatible with `Backbone.Router.prototype.route()`).
* @param {string|RegExp} route - A route pattern or regexp.
* @param {string} name - The name of the route.
* @param {Function} callback - The route callback.
* @returns {Backbone.Router}
*/
function RouteMiddleware(fn, route, name, callback) {
// Prepare arguments as the original Backbone.Router.prototype.route() would.
if (!_.isRegExp(route)) route = Backbone.Router.prototype._routeToRegExp.apply(this, [route]);
if (_.isFunction(name)) {
callback = name;
name = '';
}
if (!callback) callback = this[name];
// Execute the wrapper `route` method, with the callback as final middleware.
return fn.apply(this, [route, name, function () {
executeMiddlewares.apply(this, [route, name, Backbone.history.getFragment(), Array.prototype.slice.apply(arguments, [0]), callback]);
}]);
};
/**
* Add a route middelware.
*
* @param {RouteMiddleware} fn
*/
RouteMiddleware.use = function use(fn) {
middlewares.push.apply(middlewares, _.filter(arguments, _.isFunction));
};
/**
* @callback RouteMiddleware
* @param {RegExp} route - The matched route regexp.
* @param {string} name - The matched route.
* @param {string} fragment - The matched URL fragment.
* @param {Array} args - The route arguments (extracted from `fragment`).
* @param {Function} next - The function to call to execute the next middleware in the chain.
*/
/**
* @type {RouteMiddleware[]}
*/
var middlewares = [];
/**
* Execute the route middlware, ending with the provided callback.
*
* @param {RegExp} route - The matched route regexp.
* @param {string} name - The matched route.
* @param {string} fragment - The matched URL fragment.
* @param {Array} args - The route arguments (extracted from `fragment`).
* @param {Function} callback - The route handler to execute as final middleware.
*/
function executeMiddlewares(route, name, fragment, args, callback) {
var index = 0;
var h = middlewares.concat(function (route, name, fragment, args, next) {
callback.apply(this, args);
});
function next(err) {
if (err) throw err;
h[index++].apply(this, [route, name, fragment, args, next.bind(this)]);
}
next.apply(this);
}
})();
Backbone.Router.prototype.route = _.wrap(Backbone.Router.prototype.route, Backbone.RouteMiddlewares);
Using this interceptors/middlewares mechanism, redirection of the user to the login page could be achieved with the following:
Backbone.RouteMiddlewares.use(function (route, name, fragment, args, next) {
if (App.routeRequiresAuthentication(name) && !App.isUserAuthenticated()) {
Backbone.history.navigate('login', {trigger: true});
}
else {
next();
}
});
Note: Being able to execute code after the route callback via middlewares is redundant with the route
events on the router and on Backbone.history
but it comes free with the middleware pattern.