remove app.router and refactor middleware processing

This is an overhaul of middleware processing, Router and Route. Connect is no
longer used to process the middleware stack. This functionality has been
split into two parts: middleware stack and default error response.

The entry point for request processing is the `app.handle` method. It
sets up the default error response handle (to run in the event of no
other error handler) and then triggers the app router (instance of
Router) to handle the request.

The app router `handle` function contains the middleware dispatch layer
previously in the connect codebase. This layer handle the logic for
dispatching `.use` calls (stripping paths if needed). The app contains a
base router `app._router`. New routes can be created and `.use`d on this
router to organize routes into files.

Routers now have the following methods `.use`, `.all`, `.param` which
are all public.

Additionally, Routers have a `.route(path)` method which returns a new
instance of Route for the requested path. Route(s) are isolated
middleware stacks and contain methods for the HTTP verbs as well as an
`.all` method to act similar to middleware. These methods are chainable
to easily describe requirements for a route.

  var route = Router.route('/foo'); // or 'app.route('/foo')'

  route
  .all(auth)
  .get(function(...) {})
  .all(more_checks)
  .post(function(...) {})

Any Route and Router methods which accept handlers also accept error
(arity 4) handlers which will also behave as expected.

Finally, the `app.router` getter has been removed. Middleware and
handlers are run IN THE ORDER they are seen in the file. This means that
code which injected the `app.router` and then added error handlers (or
other middleware) will need to be updated to move those handlers after
any requests added on the app object. The examples have been updated
accordingly. This is the largest breaking change to codebases in this
commit.
Esse commit está contido em:
Roman Shtylman
2014-01-25 17:57:25 -05:00
commit f41d09a3cf
15 arquivos alterados com 841 adições e 526 exclusões
+6
Ver Arquivo
@@ -6,14 +6,20 @@
- `req.accepted*` - use `req.accepts*()` instead
- `app.configure` - use logic in your own app code
- `express.createServer()` - it has been deprecated for a long time. Use `express()`
- `app.router` - is removed
* change:
- `req.accepts*` -> `req.accepts*s` - i.e. `req.acceptsEncoding` -> `req.acceptsEncodings`
- `req.params` is now an object instead of an array
- `json spaces` no longer enabled by default in development
- `res.locals` is no longer a function. It is a plain js object. Treat it as such.
- `app.route` -> `app.mountpath` when mounting an express app in another express app
* refactor:
- `req.accepts*` with [accepts](https://github.com/expressjs/accepts)
- `req.is` with [type-is](https://github.com/expressjs/type-is)
* add:
- `app.router()` - returns the app Router instance
- `app.route()` - Proxy to the app's `Router#route()` method to create a new route
- Router & Route - public API
3.4.7 / 2013-12-10
==================
+27 -33
Ver Arquivo
@@ -25,14 +25,32 @@ app.use(express.favicon());
silent || app.use(express.logger('dev'));
// "app.router" positions our routes
// above the middleware defined below,
// this means that Express will attempt
// to match & call routes _before_ continuing
// on, at which point we assume it's a 404 because
// no route has handled the request.
// Routes
app.use(app.router);
app.get('/', function(req, res){
res.render('index.jade');
});
app.get('/404', function(req, res, next){
// trigger a 404 since no other middleware
// will match /404 after this one, and we're not
// responding here
next();
});
app.get('/403', function(req, res, next){
// trigger a 403 error
var err = new Error('not allowed!');
err.status = 403;
next(err);
});
app.get('/500', function(req, res, next){
// trigger a generic (500) error
next(new Error('keyboard cat!'));
});
// Error handlers
// Since this is the last non-error-handling
// middleware use()d, we assume 404, as nothing else
@@ -44,7 +62,7 @@ app.use(app.router);
app.use(function(req, res, next){
res.status(404);
// respond with html page
if (req.accepts('html')) {
res.render('404', { url: req.url });
@@ -81,32 +99,8 @@ app.use(function(err, req, res, next){
res.render('500', { error: err });
});
// Routes
app.get('/', function(req, res){
res.render('index.jade');
});
app.get('/404', function(req, res, next){
// trigger a 404 since no other middleware
// will match /404 after this one, and we're not
// responding here
next();
});
app.get('/403', function(req, res, next){
// trigger a 403 error
var err = new Error('not allowed!');
err.status = 403;
next(err);
});
app.get('/500', function(req, res, next){
// trigger a generic (500) error
next(new Error('keyboard cat!'));
});
if (!module.parent) {
app.listen(3000);
silent || console.log('Express started on port 3000');
}
}
+6 -8
Ver Arquivo
@@ -8,13 +8,6 @@ var express = require('../../')
, test = app.get('env') == 'test';
if (!test) app.use(express.logger('dev'));
app.use(app.router);
// the error handler is strategically
// placed *below* the app.router; if it
// were above it would not receive errors
// from app.get() etc
app.use(error);
// error handling middleware have an arity of 4
// instead of the typical (req, res, next),
@@ -42,7 +35,12 @@ app.get('/next', function(req, res, next){
});
});
// the error handler is placed after routes
// if it were above it would not receive errors
// from app.get() etc
app.use(error);
if (!module.parent) {
app.listen(3000);
console.log('Express started on port 3000');
}
}
+20 -25
Ver Arquivo
@@ -40,29 +40,6 @@ app.use('/api', function(req, res, next){
next();
});
// position our routes above the error handling middleware,
// and below our API middleware, since we want the API validation
// to take place BEFORE our routes
app.use(app.router);
// middleware with an arity of 4 are considered
// error handling middleware. When you next(err)
// it will be passed through the defined middleware
// in order, but ONLY those with an arity of 4, ignoring
// regular middleware.
app.use(function(err, req, res, next){
// whatever you want here, feel free to populate
// properties on `err` to treat it differently in here.
res.send(err.status || 500, { error: err.message });
});
// our custom JSON 404 middleware. Since it's placed last
// it will be the last middleware called, if all others
// invoke next() and do not respond.
app.use(function(req, res){
res.send(404, { error: "Lame, can't find that" });
});
// map of valid api keys, typically mapped to
// account info with some sort of database like redis.
// api keys do _not_ serve as authentication, merely to
@@ -104,12 +81,30 @@ app.get('/api/repos', function(req, res, next){
app.get('/api/user/:name/repos', function(req, res, next){
var name = req.params.name
, user = userRepos[name];
if (user) res.send(user);
else next();
});
// middleware with an arity of 4 are considered
// error handling middleware. When you next(err)
// it will be passed through the defined middleware
// in order, but ONLY those with an arity of 4, ignoring
// regular middleware.
app.use(function(err, req, res, next){
// whatever you want here, feel free to populate
// properties on `err` to treat it differently in here.
res.send(err.status || 500, { error: err.message });
});
// our custom JSON 404 middleware. Since it's placed last
// it will be the last middleware called, if all others
// invoke next() and do not respond.
app.use(function(req, res){
res.send(404, { error: "Lame, can't find that" });
});
if (!module.parent) {
app.listen(3000);
console.log('Express server listening on port 3000');
}
}
+138 -74
Ver Arquivo
@@ -4,6 +4,7 @@
var connect = require('connect')
, mixin = require('utils-merge')
, escapeHtml = require('escape-html')
, Router = require('./router')
, methods = require('methods')
, middleware = require('./middleware')
@@ -44,13 +45,11 @@ app.defaultConfiguration = function(){
// default settings
this.enable('x-powered-by');
this.enable('etag');
this.set('env', process.env.NODE_ENV || 'development');
var env = process.env.NODE_ENV || 'development';
this.set('env', env);
this.set('subdomain offset', 2);
debug('booting in %s mode', this.get('env'));
// implicit middleware
this.use(connect.query());
this.use(middleware.init(this));
debug('booting in %s mode', env);
// inherit protos
this.on('mount', function(parent){
@@ -60,19 +59,12 @@ app.defaultConfiguration = function(){
this.settings.__proto__ = parent.settings;
});
// router
this._router = new Router(this);
this.routes = this._router.map;
this.__defineGetter__('router', function(){
this._usedRouter = true;
this._router.caseSensitive = this.enabled('case sensitive routing');
this._router.strict = this.enabled('strict routing');
return this._router.middleware;
});
// setup locals
this.locals = Object.create(null);
// top-most app is mounted at /
this.mountpath = '/';
// default locals
this.locals.settings = this.settings;
@@ -81,14 +73,94 @@ app.defaultConfiguration = function(){
this.set('views', process.cwd() + '/views');
this.set('jsonp callback name', 'callback');
if (this.get('env') === 'production') {
if (env === 'production') {
this.enable('view cache');
}
Object.defineProperty(this, 'router', {
get: function() {
throw new Error('\'app.router\' is deprecated!\nPlease see the 3.x to 4.x migration guide for details on how to update your app.');
}
});
};
/**
* lazily adds the base router if it has not yet been added.
*
* We cannot add the base router in the defaultConfiguration because
* it reads app settings which might be set after that has run.
*
* @api private
*/
app.lazyrouter = function() {
if (!this._router) {
this._router = new Router({
caseSensitive: this.enabled('case sensitive routing'),
strict: this.enabled('strict routing')
});
this._router.use(connect.query());
this._router.use(middleware.init(this));
}
};
/**
* Proxy `connect#use()` to apply settings to
* mounted applications.
* Dispatch a req, res pair into the application. Starts pipeline processing.
*
* If no _done_ callback is provided, then default error handlers will respond
* in the event of an error bubbling through the stack.
*
* @api private
*/
app.handle = function(req, res, done) {
var env = this.get('env');
this._router.handle(req, res, function(err) {
if (done) {
return done(err);
}
// unhandled error
if (err) {
// default to 500
if (res.statusCode < 400) res.statusCode = 500;
debug('default %s', res.statusCode);
// respect err.status
if (err.status) res.statusCode = err.status;
// production gets a basic error message
var msg = 'production' == env
? http.STATUS_CODES[res.statusCode]
: err.stack || err.toString();
msg = escapeHtml(msg);
// log to stderr in a non-test env
if ('test' != env) console.error(err.stack || err.toString());
if (res.headerSent) return req.socket.destroy();
res.setHeader('Content-Type', 'text/html');
res.setHeader('Content-Length', Buffer.byteLength(msg));
if ('HEAD' == req.method) return res.end();
res.end(msg);
return;
}
// 404
debug('default 404');
res.statusCode = 404;
res.setHeader('Content-Type', 'text/html');
if ('HEAD' == req.method) return res.end();
res.end('Cannot ' + escapeHtml(req.method) + ' ' + escapeHtml(req.originalUrl) + '\n');
});
};
/**
* Proxy `Router#use()` to add middleware to the app router.
* See Router#use() documentation for details.
*
* If the _fn_ parameter is an express app, then it will be
* mounted at the _route_ specified.
*
* @param {String|Function|Server} route
* @param {Function|Server} fn
@@ -97,20 +169,21 @@ app.defaultConfiguration = function(){
*/
app.use = function(route, fn){
var app;
var mount_app;
// default route to '/'
if ('string' != typeof route) fn = route, route = '/';
// express app
if (fn.handle && fn.set) app = fn;
if (fn.handle && fn.set) mount_app = fn;
// restore .app property on req and res
if (app) {
app.route = route;
if (mount_app) {
debug('.use app under %s', route);
mount_app.mountpath = route;
fn = function(req, res, next) {
var orig = req.app;
app.handle(req, res, function(err){
mount_app.handle(req, res, function(err) {
req.__proto__ = orig.request;
res.__proto__ = orig.response;
next(err);
@@ -118,17 +191,31 @@ app.use = function(route, fn){
};
}
connect.proto.use.call(this, route, fn);
this.lazyrouter();
this._router.use(route, fn);
// mounted an app
if (app) {
app.parent = this;
app.emit('mount', this);
if (mount_app) {
mount_app.parent = this;
mount_app.emit('mount', this);
}
return this;
};
/**
* Proxy to the app `Router#route()`
* Returns a new `Route` instance for the _path_.
*
* Routes are isolated middleware stacks for specific paths.
* See the Route api docs for details.
*
* @api public
*/
app.route = function(path){
};
/**
* Register the given template engine callback `fn`
* as `ext`.
@@ -171,30 +258,10 @@ app.engine = function(ext, fn){
};
/**
* Map the given param placeholder `name`(s) to the given callback(s).
* Proxy to `Router#param()` with one added api feature. The _name_ parameter
* can be an array of names.
*
* Parameter mapping is used to provide pre-conditions to routes
* which use normalized placeholders. For example a _:user_id_ parameter
* could automatically load a user's information from the database without
* any additional code,
*
* The callback uses the same signature as middleware, the only difference
* being that the value of the placeholder is passed, in this case the _id_
* of the user. Once the `next()` function is invoked, just like middleware
* it will continue on to execute the route, or subsequent parameter functions.
*
* app.param('user_id', function(req, res, next, id){
* User.find(id, function(err, user){
* if (err) {
* next(err);
* } else if (user) {
* req.user = user;
* next();
* } else {
* next(new Error('failed to load user'));
* }
* });
* });
* See the Router#param() docs for more details.
*
* @param {String|Array} name
* @param {Function} fn
@@ -203,27 +270,17 @@ app.engine = function(ext, fn){
*/
app.param = function(name, fn){
var self = this
, fns = [].slice.call(arguments, 1);
var self = this;
self.lazyrouter();
// array
if (Array.isArray(name)) {
name.forEach(function(name){
fns.forEach(function(fn){
self.param(name, fn);
});
});
// param logic
} else if ('function' == typeof name) {
this._router.param(name);
// single
} else {
if (':' == name[0]) name = name.substr(1);
fns.forEach(function(fn){
self._router.param(name, fn);
name.forEach(function(key) {
self.param(key, fn);
});
return this;
}
self._router.param(name, fn);
return this;
};
@@ -267,7 +324,7 @@ app.set = function(setting, val){
app.path = function(){
return this.parent
? this.parent.path() + this.route
? this.parent.path() + this.mountpath
: '';
};
@@ -341,16 +398,17 @@ methods.forEach(function(method){
app[method] = function(path){
if ('get' == method && 1 == arguments.length) return this.set(path);
this.lazyrouter();
// deprecated
if (Array.isArray(path)) {
console.trace('passing an array to app.VERB() is deprecated and will be removed in 4.0');
}
// if no router attached yet, attach the router
if (!this._usedRouter) this.use(this.router);
// setup route
this._router[method].apply(this._router, arguments);
var route = this._router.route(path);
for (var i=1 ; i<arguments.length ; ++i) {
route[method](arguments[i]);
}
return this;
};
});
@@ -366,10 +424,16 @@ methods.forEach(function(method){
*/
app.all = function(path){
this.lazyrouter();
var route = this._router.route(path);
var args = arguments;
methods.forEach(function(method){
app[method].apply(this, args);
}, this);
for (var i=1 ; i<args.length ; ++i) {
route[method](args[i]);
}
});
return this;
};
+8 -1
Ver Arquivo
@@ -2,6 +2,8 @@
* Module dependencies.
*/
var EventEmitter = require('events').EventEmitter;
var connect = require('connect')
, merge = require('merge-descriptors')
, mixin = require('utils-merge')
@@ -32,8 +34,13 @@ exports.mime = connect.mime;
*/
function createApplication() {
var app = connect();
var app = function(req, res, next) {
app.handle(req, res, next);
};
mixin(app, proto);
mixin(app, EventEmitter.prototype);
app.request = { __proto__: req, app: app };
app.response = { __proto__: res, app: app };
app.init();
+268 -218
Ver Arquivo
@@ -24,22 +24,47 @@ exports = module.exports = Router;
function Router(options) {
options = options || {};
var self = this;
this.map = {};
this.params = {};
this._params = [];
this.caseSensitive = options.caseSensitive;
this.strict = options.strict;
this.middleware = function router(req, res, next){
self._dispatch(req, res, next);
};
self.params = {};
self._params = [];
self.caseSensitive = options.caseSensitive;
self.strict = options.strict;
self.stack = [];
self.middleware = self.handle.bind(self);
}
/**
* Register a param callback `fn` for the given `name`.
* Map the given param placeholder `name`(s) to the given callback.
*
* @param {String|Function} name
* Parameter mapping is used to provide pre-conditions to routes
* which use normalized placeholders. For example a _:user_id_ parameter
* could automatically load a user's information from the database without
* any additional code,
*
* The callback uses the same signature as middleware, the only difference
* being that the value of the placeholder is passed, in this case the _id_
* of the user. Once the `next()` function is invoked, just like middleware
* it will continue on to execute the route, or subsequent parameter functions.
*
* Just like in middleware, you must either respond to the request or call next
* to avoid stalling the request.
*
* app.param('user_id', function(req, res, next, id){
* User.find(id, function(err, user){
* if (err) {
* return next(err);
* } else if (!user) {
* return next(new Error('failed to load user'));
* }
* req.user = user;
* next();
* });
* });
*
* @param {String} name
* @param {Function} fn
* @return {Router} for chaining
* @return {app} for chaining
* @api public
*/
@@ -55,6 +80,10 @@ Router.prototype.param = function(name, fn){
, len = params.length
, ret;
if (name[0] === ':') {
name = name.substr(1);
}
for (var i = 0; i < len; ++i) {
if (ret = params[i](name, fn)) {
fn = ret;
@@ -72,250 +101,271 @@ Router.prototype.param = function(name, fn){
};
/**
* Route dispatcher aka the route "middleware".
* Dispatch a req, res into the router.
*
* @param {IncomingMessage} req
* @param {ServerResponse} res
* @param {Function} next
* @api private
*/
Router.prototype._dispatch = function(req, res, next){
var params = this.params
, self = this;
debug('dispatching %s %s (%s)', req.method, req.url, req.originalUrl);
// route dispatch
(function pass(i, err){
var paramCallbacks
, paramIndex = 0
, paramVal
, route
, keys
, key;
// match next route
function nextRoute(err) {
pass(req._route_index + 1, err);
}
// match route
req.route = route = self.matchRequest(req, i);
// implied OPTIONS
if (!route && 'OPTIONS' == req.method) return self._options(req, res, next);
// no route
if (!route) return next(err);
debug('matched %s %s', route.method, route.path);
// we have a route
// start at param 0
req.params = route.params;
keys = route.keys;
i = 0;
// param callbacks
function param(err) {
paramIndex = 0;
key = keys[i++];
paramVal = key && req.params[key.name];
paramCallbacks = key && params[key.name];
try {
if ('route' == err) {
nextRoute();
} else if (err) {
i = 0;
callbacks(err);
} else if (paramCallbacks && undefined !== paramVal) {
paramCallback();
} else if (key) {
param();
} else {
i = 0;
callbacks();
}
} catch (err) {
param(err);
}
};
param(err);
// single param callbacks
function paramCallback(err) {
var fn = paramCallbacks[paramIndex++];
if (err || !fn) return param(err);
fn(req, res, paramCallback, paramVal, key.name);
}
// invoke route callbacks
function callbacks(err) {
var fn = route.callbacks[i++];
try {
if ('route' == err) {
nextRoute();
} else if (err && fn) {
if (fn.length < 4) return callbacks(err);
fn(err, req, res, callbacks);
} else if (fn) {
if (fn.length < 4) return fn(req, res, callbacks);
callbacks();
} else {
nextRoute(err);
}
} catch (err) {
callbacks(err);
}
}
})(0);
};
/**
* Respond to __OPTIONS__ method.
*
* @param {IncomingMessage} req
* @param {ServerResponse} res
* @api private
*/
Router.prototype._options = function(req, res, next){
var path = parse(req).pathname
, body = this._optionsFor(path).join(',');
if (!body) return next();
res.set('Allow', body).send(body);
};
/**
* Return an array of HTTP verbs or "options" for `path`.
*
* @param {String} path
* @return {Array}
* @api private
*/
Router.prototype._optionsFor = function(path){
Router.prototype.handle = function(req, res, done) {
var self = this;
return methods.filter(function(method){
var routes = self.map[method];
if (!routes || 'options' == method) return;
for (var i = 0, len = routes.length; i < len; ++i) {
if (routes[i].match(path)) return true;
}
}).map(function(method){
return method.toUpperCase();
});
};
/**
* Attempt to match a route for `req`
* with optional starting index of `i`
* defaulting to 0.
*
* @param {IncomingMessage} req
* @param {Number} i
* @return {Route}
* @api private
*/
debug('dispatching %s %s', req.method, req.url);
Router.prototype.matchRequest = function(req, i, head){
var method = req.method.toLowerCase()
, url = parse(req)
, path = url.pathname
, routes = this.map
, i = i || 0
, route;
var method = req.method.toLowerCase();
// HEAD support
if (!head && 'head' == method) {
route = this.matchRequest(req, i, true);
if (route) return route;
method = 'get';
var search = 1 + req.url.indexOf('?');
var pathlength = search ? search - 1 : req.url.length;
var fqdn = 1 + req.url.substr(0, pathlength).indexOf('://');
var protohost = fqdn ? req.url.substr(0, req.url.indexOf('/', 2 + fqdn)) : '';
var idx = 0;
var removed = '';
var slashAdded = false;
// store options for OPTIONS request
// only used if OPTIONS request
var options = [];
// middleware and routes
var stack = this.stack;
// for options requests, respond with a default if nothing else responds
if (method === 'options') {
var old = done;
done = function(err) {
if (err || options.length === 0) return old(err);
var body = options.join(',');
return res.set('Allow', body).send(body);
};
}
// routes for this method
if (routes = routes[method]) {
(function next(err) {
if (err === 'route') {
err = undefined;
}
// matching routes
for (var len = routes.length; i < len; ++i) {
route = routes[i];
if (route.match(path)) {
req._route_index = i;
return route;
var layer = stack[idx++];
if (!layer) {
return done(err);
}
if (slashAdded) {
req.url = req.url.substr(1);
slashAdded = false;
}
req.url = protohost + removed + req.url.substr(protohost.length);
req.originalUrl = req.originalUrl || req.url;
removed = '';
try {
var path = parse(req).pathname;
if (undefined == path) path = '/';
// route object and not middleware
var route = layer.route;
// handle route
if (route) {
// we don't run any routs with error first
if (err || !route.match(path)) {
return next(err);
}
req.params = route.params;
// we can now dispatch to the route
if (method === 'options' && !route.methods['options']) {
options.push.apply(options, route._options());
}
return self.process_params(route, req, res, function(err) {
if (err) {
return next(err);
}
route.dispatch(req, res, next);
});
}
// skip this layer if the path doesn't match.
if (0 != path.toLowerCase().indexOf(layer.path.toLowerCase())) return next(err);
var c = path[layer.path.length];
if (c && '/' != c && '.' != c) return next(err);
// Trim off the part of the url that matches the route
// middleware (.use stuff) needs to have the path stripped
debug('trim prefix (%s) from url %s', removed, req.url);
removed = layer.path;
req.url = protohost + req.url.substr(protohost.length + removed.length);
// Ensure leading slash
if (!fqdn && '/' != req.url[0]) {
req.url = '/' + req.url;
slashAdded = true;
}
debug('%s %s : %s', layer.handle.name || 'anonymous', layer.path, req.originalUrl);
var arity = layer.handle.length;
if (err) {
if (arity === 4) {
layer.handle(err, req, res, next);
} else {
next(err);
}
} else if (arity < 4) {
layer.handle(req, res, next);
} else {
next(err);
}
} catch (err) {
next(err);
}
})();
};
/**
* Process any parameters for the route.
*
* @api private
*/
Router.prototype.process_params = function(route, req, res, done) {
var self = this;
var params = this.params;
// captured parameters from the route, keys and values
var keys = route.keys || [];
var i = 0;
var paramIndex = 0;
var key;
var paramVal;
var paramCallbacks;
// process params in order
// param callbacks can be async
function param(err) {
if (err) {
return done(err);
}
if (i >= keys.length ) {
return done();
}
paramIndex = 0;
key = keys[i++];
paramVal = key && req.params[key.name];
paramCallbacks = key && params[key.name];
try {
if (paramCallbacks && undefined !== paramVal) {
return paramCallback();
} else if (key) {
return param();
}
} catch (err) {
done(err);
}
done();
};
// single param callbacks
function paramCallback(err) {
var fn = paramCallbacks[paramIndex++];
if (err || !fn) return param(err);
fn(req, res, paramCallback, paramVal, key.name);
}
param();
};
/**
* Attempt to match a route for `method`
* and `url` with optional starting
* index of `i` defaulting to 0.
* Use the given middleware function, with optional path, defaulting to "/".
*
* @param {String} method
* @param {String} url
* @param {Number} i
* @return {Route}
* @api private
* Use (like `.all`) will run for any http METHOD, but it will not add
* handlers for those methods so OPTIONS requests will not consider `.use`
* functions even if they could respond.
*
* The other difference is that _route_ path is stripped and not visible
* to the handler function. The main effect of this feature is that mounted
* handlers can operate without any code changes regardless of the "prefix"
* pathname.
*
* @param {String|Function} route
* @param {Function} fn
* @return {app} for chaining
* @api public
*/
Router.prototype.match = function(method, url, i, head){
var req = { method: method, url: url };
return this.matchRequest(req, i, head);
Router.prototype.use = function(route, fn){
// default route to '/'
if ('string' != typeof route) {
fn = route;
route = '/';
}
// strip trailing slash
if ('/' == route[route.length - 1]) {
route = route.slice(0, -1);
}
// add the middleware
debug('use %s %s', route || '/', fn.name || 'anonymous');
this.stack.push({ path: route, handle: fn });
return this;
};
/**
* Route `method`, `path`, and one or more callbacks.
* Create a new Route for the given path.
*
* Each route contains a separate middleware stack and VERB handlers.
*
* See the Route api documentation for details on adding handlers
* and middleware to routes.
*
* @param {String} method
* @param {String} path
* @param {Function} callback...
* @return {Router} for chaining
* @api private
* @return {Route}
* @api public
*/
Router.prototype.route = function(method, path, callbacks){
var method = method.toLowerCase()
, callbacks = utils.flatten([].slice.call(arguments, 2));
// ensure path was given
if (!path) throw new Error('Router#' + method + '() requires a path');
// ensure all callbacks are functions
callbacks.forEach(function(fn){
if ('function' == typeof fn) return;
var type = {}.toString.call(fn);
var msg = '.' + method + '() requires callback functions but got a ' + type;
throw new Error(msg);
});
// create the route
debug('defined %s %s', method, path);
var route = new Route(method, path, callbacks, {
Router.prototype.route = function(path){
var route = new Route(path, {
sensitive: this.caseSensitive,
strict: this.strict
});
// add it
(this.map[method] = this.map[method] || []).push(route);
return this;
this.stack.push({ path: path, route: route });
return route;
};
Router.prototype.all = function(path) {
var self = this;
var args = [].slice.call(arguments);
/**
* Special-cased "all" method, applying the given route `path`,
* middleware, and callback to _every_ HTTP method.
*
* @param {String} path
* @param {Function} ...
* @return {app} for chaining
* @api public
*/
Router.prototype.all = function(path, fn) {
var route = this.route(path);
methods.forEach(function(method){
self.route.apply(self, [method].concat(args));
route[method](fn);
});
return this;
};
// create Router#VERB functions
methods.forEach(function(method){
Router.prototype[method] = function(path){
var args = [method].concat([].slice.call(arguments));
this.route.apply(this, args);
return this;
Router.prototype[method] = function(path, fn){
var self = this;
self.route(path)[method](fn);
return self;
};
});
+146 -6
Ver Arquivo
@@ -3,7 +3,9 @@
* Module dependencies.
*/
var utils = require('../utils');
var utils = require('../utils')
, debug = require('debug')('express:router:route')
, methods = require('methods')
/**
* Expose `Route`.
@@ -20,22 +22,25 @@ module.exports = Route;
* - `sensitive` enable case-sensitive routes
* - `strict` enable strict matching for trailing slashes
*
* @param {String} method
* @param {String} path
* @param {Array} callbacks
* @param {Object} options.
* @api private
*/
function Route(method, path, callbacks, options) {
function Route(path, options) {
debug('new %s', path);
options = options || {};
this.path = path;
this.method = method;
this.callbacks = callbacks;
this.params = {};
this.regexp = utils.pathRegexp(path
, this.keys = []
, options.sensitive
, options.strict);
this.stack = undefined;
// route handlers for various http methods
this.methods = {};
}
/**
@@ -77,3 +82,138 @@ Route.prototype.match = function(path){
return true;
};
/**
* @return {Array} supported HTTP methods
* @api private
*/
Route.prototype._options = function(){
return Object.keys(this.methods).map(function(method) {
return method.toUpperCase();
});
};
/**
* dispatch req, res into this route
*
* @api private
*/
Route.prototype.dispatch = function(req, res, done){
var self = this;
var method = req.method.toLowerCase();
if (method === 'head' && !this.methods['head']) {
method = 'get';
}
req.route = self;
// single middleware route case
if (typeof this.stack === 'function') {
this.stack(req, res, done);
return;
}
var stack = self.stack;
if (!stack) {
return done();
}
var idx = 0;
(function next_layer(err) {
if (err && err === 'route') {
return done();
}
var layer = stack[idx++];
if (!layer) {
return done(err);
}
if (layer.method && layer.method !== method) {
return next_layer(err);
}
var arity = layer.handle.length;
if (err) {
if (arity < 4) {
return next_layer(err);
}
return layer.handle(err, req, res, next_layer);
}
if (arity > 3) {
return next_layer();
}
layer.handle(req, res, next_layer);
})();
};
/**
* Add a handler for all HTTP verbs to this route.
*
* Behaves just like middleware and can respond or call `next`
* to continue processing.
*
* You can use multiple `.all` call to add multiple handlers.
*
* function check_something(req, res, next){
* next();
* };
*
* function validate_user(req, res, next){
* next();
* };
*
* route
* .all(validate_user)
* .all(check_something)
* .get(function(req, res, next){
* res.send('hello world');
* });
*
* @param {function} handler
* @return {Route} for chaining
* @api public
*/
Route.prototype.all = function(fn){
if (typeof fn !== 'function') {
var type = {}.toString.call(fn);
var msg = 'Route.use() requires callback functions but got a ' + type;
throw new Error(msg);
}
if (!this.stack) {
this.stack = fn;
}
else if (typeof this.stack === 'function') {
this.stack = [{ handle: this.stack }, { handle: fn }];
}
else {
this.stack.push({ handle: fn });
}
return this;
};
methods.forEach(function(method){
Route.prototype[method] = function(fn){
debug('%s %s', method, this.path);
if (!this.methods[method]) {
this.methods[method] = true;
}
if (!this.stack) {
this.stack = [];
}
this.stack.push({ method: method, handle: fn })
return this;
};
});
+153
Ver Arquivo
@@ -0,0 +1,153 @@
var express = require('../')
, Route = express.Route
, methods = require('methods')
, assert = require('assert');
describe('Route', function(){
describe('.match', function(){
it('should match', function(){
var route = new Route('/foo/bar');
assert(route.match('/foo/bar'));
assert(!route.match('/foo/baz'));
})
})
describe('.all', function(){
it('should add handler', function(done){
var route = new Route('/foo');
route.all(function(req, res, next) {
assert.equal(req.a, 1);
assert.equal(res.b, 2);
next();
});
route.dispatch({ a:1, method: 'GET' }, { b:2 }, done);
})
it('should handle VERBS', function(done) {
var route = new Route('/foo');
var count = 0;
route.all(function(req, res, next) {
count++;
});
methods.forEach(function testMethod(method) {
route.dispatch({ method: method }, {});
});
assert.equal(count, methods.length);
done();
})
it('should stack', function(done) {
var route = new Route('/foo');
var count = 0;
route.all(function(req, res, next) {
count++;
next();
});
route.all(function(req, res, next) {
count++;
next();
});
route.dispatch({ method: 'GET' }, {}, function(err) {
assert.ifError(err);
count++;
});
assert.equal(count, 3);
done();
})
})
describe('.VERB', function(){
it('should support .get', function(done){
var route = new Route('');
var count = 0;
route.get(function(req, res, next) {
count++;
})
route.dispatch({ method: 'GET' }, {});
assert(count);
done();
})
it('should limit to just .VERB', function(done){
var route = new Route('');
route.get(function(req, res, next) {
assert(false);
done();
})
route.post(function(req, res, next) {
assert(true);
})
route.dispatch({ method: 'post' }, {});
done();
})
it('should allow fallthrough', function(done){
var route = new Route('');
var order = '';
route.get(function(req, res, next) {
order += 'a';
next();
})
route.all(function(req, res, next) {
order += 'b';
next();
});
route.get(function(req, res, next) {
order += 'c';
})
route.dispatch({ method: 'get' }, {});
assert.equal(order, 'abc');
done();
})
})
describe('errors', function(){
it('should handle errors via arity 4 functions', function(done){
var route = new Route('');
var order = '';
route.all(function(req, res, next){
next(new Error('foobar'));
});
route.all(function(req, res, next){
order += '0';
next();
});
route.all(function(err, req, res, next){
order += 'a';
next(err);
});
route.all(function(err, req, res, next){
assert.equal(err.message, 'foobar');
assert.equal(order, 'a');
done();
});
route.dispatch({ method: 'get' }, {});
})
})
})
+51 -75
Ver Arquivo
@@ -1,121 +1,97 @@
var express = require('../')
, Router = express.Router
, request = require('./support/http')
, methods = require('methods')
, assert = require('assert');
describe('Router', function(){
var router, app;
beforeEach(function(){
router = new Router;
app = express();
})
describe('.match(method, url, i)', function(){
it('should match based on index', function(){
router.route('get', '/foo', function(){});
router.route('get', '/foob?', function(){});
router.route('get', '/bar', function(){});
var method = 'GET';
var url = '/foo?bar=baz';
var route = router.match(method, url, 0);
route.constructor.name.should.equal('Route');
route.method.should.equal('get');
route.path.should.equal('/foo');
var route = router.match(method, url, 1);
route.path.should.equal('/foob?');
var route = router.match(method, url, 2);
assert(!route);
url = '/bar';
var route = router.match(method, url);
route.path.should.equal('/bar');
})
})
describe('.matchRequest(req, i)', function(){
it('should match based on index', function(){
router.route('get', '/foo', function(){});
router.route('get', '/foob?', function(){});
router.route('get', '/bar', function(){});
var req = { method: 'GET', url: '/foo?bar=baz' };
var route = router.matchRequest(req, 0);
route.constructor.name.should.equal('Route');
route.method.should.equal('get');
route.path.should.equal('/foo');
var route = router.matchRequest(req, 1);
req._route_index.should.equal(1);
route.path.should.equal('/foob?');
var route = router.matchRequest(req, 2);
assert(!route);
req.url = '/bar';
var route = router.matchRequest(req);
route.path.should.equal('/bar');
})
})
describe('.middleware', function(){
it('should dispatch', function(done){
router.route('get', '/foo', function(req, res){
var router = new Router();
router.route('/foo').get(function(req, res){
res.send('foo');
});
app.use(router.middleware);
request(app)
.get('/foo')
.expect('foo', done);
var res = {
send: function(val) {
val.should.equal('foo');
done();
}
}
router.handle({ url: '/foo', method: 'GET' }, res);
})
})
describe('.multiple callbacks', function(){
it('should throw if a callback is null', function(){
assert.throws(function () {
router.route('get', '/foo', null, function(){});
var router = new Router();
router.route('/foo').all(null);
})
})
it('should throw if a callback is undefined', function(){
assert.throws(function () {
router.route('get', '/foo', undefined, function(){});
var router = new Router();
router.route('/foo').all(undefined);
})
})
it('should throw if a callback is not a function', function(){
assert.throws(function () {
router.route('get', '/foo', 'not a function', function(){});
var router = new Router();
router.route('/foo').all('not a function');
})
})
it('should not throw if all callbacks are functions', function(){
router.route('get', '/foo', function(){}, function(){});
var router = new Router();
router.route('/foo').all(function(){}).all(function(){});
})
})
describe('.all', function() {
it('should support using .all to capture all http verbs', function() {
describe('error', function(){
it('should skip non error middleware', function(done){
var router = new Router();
router.all('/foo', function(){});
router.get('/foo', function(req, res, next){
next(new Error('foo'));
});
router.get('/bar', function(req, res, next){
next(new Error('bar'));
});
router.use(function(req, res, next){
assert(false);
});
router.use(function(err, req, res, next){
assert.equal(err.message, 'foo');
done();
});
router.handle({ url: '/foo', method: 'GET' }, {}, done);
});
})
describe('.all', function() {
it('should support using .all to capture all http verbs', function(done){
var router = new Router();
var count = 0;
router.all('/foo', function(){ count++; });
var url = '/foo?bar=baz';
methods.forEach(function testMethod(method) {
var route = router.match(method, url);
route.constructor.name.should.equal('Route');
route.method.should.equal(method);
route.path.should.equal('/foo');
router.handle({ url: url, method: method }, {}, function() {});
});
assert.equal(count, methods.length);
done();
})
})
})
+17 -5
Ver Arquivo
@@ -25,7 +25,7 @@ describe('app.parent', function(){
})
})
describe('app.route', function(){
describe('app.mountpath', function(){
it('should return the mounted path', function(){
var app = express()
, blog = express()
@@ -34,9 +34,21 @@ describe('app.route', function(){
app.use('/blog', blog);
blog.use('/admin', blogAdmin);
app.route.should.equal('/');
blog.route.should.equal('/blog');
blogAdmin.route.should.equal('/admin');
app.mountpath.should.equal('/');
blog.mountpath.should.equal('/blog');
blogAdmin.mountpath.should.equal('/admin');
})
})
describe('app.router', function(){
it('should throw with notice', function(done){
var app = express()
try {
app.router;
} catch(err) {
done();
}
})
})
@@ -71,4 +83,4 @@ describe('in production', function(){
app.enabled('view cache').should.be.true;
process.env.NODE_ENV = 'test';
})
})
})
+1 -1
Ver Arquivo
@@ -58,4 +58,4 @@ describe('app.options()', function(){
.expect('GET')
.expect('Allow', 'GET', done);
})
})
})
-30
Ver Arquivo
@@ -87,36 +87,6 @@ describe('app.router', function(){
next();
});
app.use(app.router);
app.use(function(req, res, next){
calls.push('after');
res.end();
});
app.get('/', function(req, res, next){
calls.push('GET /')
next();
});
request(app)
.get('/')
.end(function(res){
calls.should.eql(['before', 'GET /', 'after'])
done();
})
})
it('should be auto .use()d on the first app.VERB() call', function(done){
var app = express();
var calls = [];
app.use(function(req, res, next){
calls.push('before');
next();
});
app.get('/', function(req, res, next){
calls.push('GET /')
next();
-48
Ver Arquivo
@@ -1,48 +0,0 @@
var express = require('../')
, assert = require('assert')
, request = require('./support/http');
describe('app.routes', function(){
it('should be initialized', function(){
var app = express();
app.routes.should.eql({});
})
it('should be populated with routes', function(){
var app = express();
app.get('/', function(req, res){});
app.get('/user/:id', function(req, res){});
var get = app.routes.get;
get.should.have.length(2);
get[0].path.should.equal('/');
get[0].method.should.equal('get');
get[0].regexp.toString().should.equal('/^\\/\\/?$/i');
get[1].path.should.equal('/user/:id');
get[1].method.should.equal('get');
})
it('should be mutable', function(done){
var app = express();
app.get('/', function(req, res){});
app.get('/user/:id', function(req, res){});
var get = app.routes.get;
get.should.have.length(2);
get[0].path.should.equal('/');
get[0].method.should.equal('get');
get[0].regexp.toString().should.equal('/^\\/\\/?$/i');
get.splice(1);
request(app)
.get('/user/12')
.expect(404, done);
})
})
-2
Ver Arquivo
@@ -8,13 +8,11 @@ describe('req', function(){
var app = express();
app.get('/user/:id/:op?', function(req, res, next){
req.route.method.should.equal('get');
req.route.path.should.equal('/user/:id/:op?');
next();
});
app.get('/user/:id/edit', function(req, res){
req.route.method.should.equal('get');
req.route.path.should.equal('/user/:id/edit');
res.end();
});