express中间件机制补充说明
发布于 9 年前 作者 i5ting 6253 次浏览 来自 分享

中间件2种用法

  • 筒子
  • 变胖的筒子

筒子

app.use('/1', function(req, res, next){
})
app.use('/2', function(req, res, next){
})
app.use('/3', function(req, res, next){
})

变胖的筒子

function m1(req, res, next){
}
function m2(req, res, next){
}
app.use('/', m1, m2, function(req, res, next){
})

路由匹配规则

  • 先根据【筒子】栈,匹配到请求
  • 然后看该请求,是不是【变胖的筒子】,如果是,还需要继续解析该路由上的横向的栈

express的next说明

  • 3.x 使用connect作为中间件
  • 4.x 在express/lib/router/里实现router.handle(req, res, next);
  • 5.x 使用独立的router模块
4.0.0 / 2014年04月09日
==================
 * remove:
 - node 0.8 support
 - connect and connect's patches except for charset handling
 - express(1) - moved to [express-generator](https://github.com/expressjs/generator)
 - `express.createServer()` - it has been deprecated for a long time. Use `express()`
 - `app.configure` - use logic in your own app code
 - `app.router` - is removed
 - `req.auth` - use `basic-auth` instead
 - `req.accepted*` - use `req.accepts*()` instead
 - `res.location` - relative URL resolution is removed
 - `res.charset` - include the charset in the content type when using `res.set()`
 - all bundled middleware except `static`
 * change:
 - `app.route` -> `app.mountpath` when mounting an express app in another express app
 - `json spaces` no longer enabled by default in development
 - `req.accepts*` -> `req.accepts*s` - i.e. `req.acceptsEncoding` -> `req.acceptsEncodings`
 - `req.params` is now an object instead of an array
 - `res.locals` is no longer a function. It is a plain js object. Treat it as such.
 - `res.headerSent` -> `res.headersSent` to match node.js ServerResponse object
 * refactor:
 - `req.accepts*` with [accepts](https://github.com/expressjs/accepts)
 - `req.is` with [type-is](https://github.com/expressjs/type-is)
 - [path-to-regexp](https://github.com/component/path-to-regexp)
 * 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

大家知道变更即可,过多细节也没啥用

express里的use

主要2个

  • app.use
  • 还有router.use

给出application里的use实现

app.use = function use(fn) {
 var offset = 0;
 var path = '/';
 // default path to '/'
 // disambiguate app.use([fn])
 if (typeof fn !== 'function') {
 var arg = fn;
 while (Array.isArray(arg) && arg.length !== 0) {
 arg = arg[0];
 }
 // first arg is the path
 if (typeof arg !== 'function') {
 offset = 1;
 path = fn;
 }
 }
 var fns = flatten(slice.call(arguments, offset));
 if (fns.length === 0) {
 throw new TypeError('app.use() requires middleware functions');
 }
 // setup router
 this.lazyrouter();
 var router = this._router;
 fns.forEach(function (fn) {
 // non-express app
 if (!fn || !fn.handle || !fn.set) {
 return router.use(path, fn);
 }
 debug('.use app under %s', path);
 fn.mountpath = path;
 fn.parent = this;
 // restore .app property on req and res
 router.use(path, function mounted_app(req, res, next) {
 var orig = req.app;
 fn.handle(req, res, function (err) {
 req.__proto__ = orig.request;
 res.__proto__ = orig.response;
 next(err);
 });
 });
 // mounted an app
 fn.emit('mount', this);
 }, this);
 return this;
};

简单点说就是把中间件变成了路由来处理。

所有的fns中间件都被router.use真正处理了

router.use(path, function mounted_app(req, res, next) {
 var orig = req.app;
 fn.handle(req, res, function (err) {
 req.__proto__ = orig.request;
 res.__proto__ = orig.response;
 next(err);
 });
});

给出router里use实现

/**
 * Use the given middleware function, with optional path, defaulting to "/".
 *
 * 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.
 *
 * @public
 */
proto.use = function use(fn) {
 var offset = 0;
 var path = '/';
 // default path to '/'
 // disambiguate router.use([fn])
 if (typeof fn !== 'function') {
 var arg = fn;
 while (Array.isArray(arg) && arg.length !== 0) {
 arg = arg[0];
 }
 // first arg is the path
 if (typeof arg !== 'function') {
 offset = 1;
 path = fn;
 }
 }
 var callbacks = flatten(slice.call(arguments, offset));
 if (callbacks.length === 0) {
 throw new TypeError('Router.use() requires middleware functions');
 }
 for (var i = 0; i < callbacks.length; i++) {
 var fn = callbacks[i];
 if (typeof fn !== 'function') {
 throw new TypeError('Router.use() requires middleware function but got a ' + gettype(fn));
 }
 // add the middleware
 debug('use %s %s', path, fn.name || '<anonymous>');
 var layer = new Layer(path, {
 sensitive: this.caseSensitive,
 strict: false,
 end: false
 }, fn);
 layer.route = undefined;
 this.stack.push(layer);
 }
 return this;
};
  1. 把参数搞成callbacks
 var callbacks = flatten(slice.call(arguments, offset));
  1. 遍历callback,转成中间件
 var layer = new Layer(path, {
 sensitive: this.caseSensitive,
 strict: false,
 end: false
 }, fn);
 layer.route = undefined;
  1. 入栈
 this.stack.push(layer);

总结一下:核心还是在Layer里

Layer

layer代码非常少,也就100多,方法不过5个

构造函数

function Layer(path, options, fn) {
 if (!(this instanceof Layer)) {
 return new Layer(path, options, fn);
 }
 debug('new %s', path);
 var opts = options || {};
 this.handle = fn;
 this.name = fn.name || '<anonymous>';
 this.params = undefined;
 this.path = undefined;
 this.regexp = pathRegexp(path, this.keys = [], opts);
 if (path === '/' && opts.end === false) {
 this.regexp.fast_slash = true;
 }
}

主要看看this上有那些变量,这是一个请求会产生处理的。比如name我们在遍历中间件的时候可能会出现anonymous,原因就在这里。

通过pathRegexp进行匹配url,然后把fn处理了,很明显这是单个的或者说是final的处理。

var pathRegexp = require('path-to-regexp');
  • handle_error
  • handle_request
Layer.prototype.handle_request = function handle(req, res, next) {
 var fn = this.handle;
 if (fn.length > 3) {
 // not a standard request handler
 return next();
 }
 try {
 fn(req, res, next);
 } catch (err) {
 next(err);
 }
};

不知道大家是否好奇

给出express 4.x里的express/lib/router/index.js里的实现

proto.handle = function handle(req, res, out) {
 var self = this;
 debug('dispatching %s %s', req.method, req.url);
 var search = 1 + req.url.indexOf('?');
 var pathlength = search ? search - 1 : req.url.length;
 var fqdn = req.url[0] !== '/' && 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;
 var paramcalled = {};
 // store options for OPTIONS request
 // only used if OPTIONS request
 var options = [];
 // middleware and routes
 var stack = self.stack;
 // manage inter-router variables
 var parentParams = req.params;
 var parentUrl = req.baseUrl || '';
 var done = restore(out, req, 'baseUrl', 'next', 'params');
 // setup next layer
 req.next = next;
 // for options requests, respond with a default if nothing else responds
 if (req.method === 'OPTIONS') {
 done = wrap(done, function(old, err) {
 if (err || options.length === 0) return old(err);
 sendOptionsResponse(res, options, old);
 });
 }
 // setup basic req values
 req.baseUrl = parentUrl;
 req.originalUrl = req.originalUrl || req.url;
 next();
 function next(err) {
 var layerError = err === 'route'
 ? null
 : err;
 // remove added slash
 if (slashAdded) {
 req.url = req.url.substr(1);
 slashAdded = false;
 }
 // restore altered req.url
 if (removed.length !== 0) {
 req.baseUrl = parentUrl;
 req.url = protohost + removed + req.url.substr(protohost.length);
 removed = '';
 }
 // no more matching layers
 if (idx >= stack.length) {
 setImmediate(done, layerError);
 return;
 }
 // get pathname of request
 var path = getPathname(req);
 if (path == null) {
 return done(layerError);
 }
 // find next matching layer
 var layer;
 var match;
 var route;
 while (match !== true && idx < stack.length) {
 layer = stack[idx++];
 match = matchLayer(layer, path);
 route = layer.route;
 if (typeof match !== 'boolean') {
 // hold on to layerError
 layerError = layerError || match;
 }
 if (match !== true) {
 continue;
 }
 if (!route) {
 // process non-route handlers normally
 continue;
 }
 if (layerError) {
 // routes do not match with a pending error
 match = false;
 continue;
 }
 var method = req.method;
 var has_method = route._handles_method(method);
 // build up automatic options response
 if (!has_method && method === 'OPTIONS') {
 appendMethods(options, route._options());
 }
 // don't even bother matching route
 if (!has_method && method !== 'HEAD') {
 match = false;
 continue;
 }
 }
 // no match
 if (match !== true) {
 return done(layerError);
 }
 // store route for dispatch on change
 if (route) {
 req.route = route;
 }
 // Capture one-time layer values
 req.params = self.mergeParams
 ? mergeParams(layer.params, parentParams)
 : layer.params;
 var layerPath = layer.path;
 // this should be done for the layer
 self.process_params(layer, paramcalled, req, res, function (err) {
 if (err) {
 return next(layerError || err);
 }
 if (route) {
 return layer.handle_request(req, res, next);
 }
 trim_prefix(layer, layerError, layerPath, path);
 });
 }
 function trim_prefix(layer, layerError, layerPath, path) {
 var c = path[layerPath.length];
 if (c && '/' !== c && '.' !== c) return next(layerError);
 // Trim off the part of the url that matches the route
 // middleware (.use stuff) needs to have the path stripped
 if (layerPath.length !== 0) {
 debug('trim prefix (%s) from url %s', layerPath, req.url);
 removed = layerPath;
 req.url = protohost + req.url.substr(protohost.length + removed.length);
 // Ensure leading slash
 if (!fqdn && req.url[0] !== '/') {
 req.url = '/' + req.url;
 slashAdded = true;
 }
 // Setup base URL (no trailing slash)
 req.baseUrl = parentUrl + (removed[removed.length - 1] === '/'
 ? removed.substring(0, removed.length - 1)
 : removed);
 }
 debug('%s %s : %s', layer.name, layerPath, req.originalUrl);
 if (layerError) {
 layer.handle_error(layerError, req, res, next);
 } else {
 layer.handle_request(req, res, next);
 }
 }
};

核心在next();方法

 while (match !== true && idx < stack.length) {
 layer = stack[idx++];
 match = matchLayer(layer, path);
 route = layer.route;
 if (typeof match !== 'boolean') {
 // hold on to layerError
 layerError = layerError || match;
 }
 if (match !== true) {
 continue;
 }
 if (!route) {
 // process non-route handlers normally
 continue;
 }
 if (layerError) {
 // routes do not match with a pending error
 match = false;
 continue;
 }
 var method = req.method;
 var has_method = route._handles_method(method);
 // build up automatic options response
 if (!has_method && method === 'OPTIONS') {
 appendMethods(options, route._options());
 }
 // don't even bother matching route
 if (!has_method && method !== 'HEAD') {
 match = false;
 continue;
 }
 }

看到这个

layer = stack[idx++];

你会想到什么?遍历栈要做什么呢?

一个路由,其实就是stack,可以1个也可以多个中间件,想想上面说的"先根据【筒子】栈,匹配到请求"

然后,根据遍历结果,match和route来处理。只有匹配了路由,才会走到具体的处理

 // no match
 if (match !== true) {
 return done(layerError);
 }
 // store route for dispatch on change
 if (route) {
 req.route = route;
 }
 // Capture one-time layer values
 req.params = self.mergeParams
 ? mergeParams(layer.params, parentParams)
 : layer.params;
 var layerPath = layer.path;
 // this should be done for the layer
 self.process_params(layer, paramcalled, req, res, function (err) {
 if (err) {
 return next(layerError || err);
 }
 if (route) {
 return layer.handle_request(req, res, next);
 }
 trim_prefix(layer, layerError, layerPath, path);
 });

整体来说,设计的职责清晰,还算是比较精巧的。代码的可读性能做到这样,还是真的很难得的,推荐学习。

3 回复

最近事儿较多,先写点,有问题大家大家评论即可

👍 前一段也去阅读 Express 的源码了,看到 next 这里理解起来还是需要些时间的,狼叔要是稍微详细点解释一下更好了 😄,方便以后其他人阅读

回到顶部

AltStyle によって変換されたページ (->オリジナル) /