【原创】express3.4.8源码解析之中间件
前言
注意
:旧文章转成markdown格式。
中间件(middleware)的概念来自于TJ的connect库,express就是建立在connect之上。
就如同connect的意思是 连接 一样,connect通过客户端过来的http请求通过将一系列注册的中间件连接起来,而这些中间件则会按照注册的先后顺序依次来处理这个http请求,在每个中间件处理请求的过程中,得出的数据都可以传递到下一个中间件,当然我们可以有选择地决定是否继续执行后面的一些的中间件,也可以直接返回响应给客户端,这就是所谓的流式处理。
简单的例子
var express = require('express');
var app = express();
app.set('port', 3000);
app.use(function (req, res, next) {
console.log(1);
next();
});
app.use(function (req, res, next) {
console.log(2);
});
app.listen(app.get('port'), function () {
console.log('server listening...');
});
执行上面这段代码,并打开浏览器输入127.0.0.1:3000,再回头看看控制台:
很明显的我们看到控制台输出: 1 2
解释一下上述代码:
首先我们创建了app,然后两处调用了app.use(..);
,最后开启server监听。
我们调用了app.use(..);
,其实就是注册了中间件,这里显然是有了两个中间件,所以当http请求过来的时候,按常理说会依次触发这两个中间件,所以输出了1和2。
但是这里需要注意:
- 在第一个中间件中调用了
next();
方法,假设我不调用呢?结果显然是只会输出:1,也就是说这里的next的意义在于手动调用下一个中间件。 - 其实这里这样的代码会导致客户端的请求长时间处于pending状态,以致于最后超时,原因是:服务端只是执行完了代码并没有给客户端一个响应,所以我们可以在第二个中间件里面加上一段响应代码,如:
res.send('hello world');
。
应用的例子
var express = require('express');
var routes = require('./routes');
var user = require('./routes/user');
var http = require('http');
var path = require('path');
var app = express();
...
app.use(express.favicon());
app.use(express.logger('dev'));
app.use(express.json());
app.use(express.urlencoded());
app.use(express.methodOverride());
app.use(app.router);
app.use(express.static(path.join(__dirname, 'public')));
...
http.createServer(app).listen(app.get('port'), function(){
console.log('Express server listening on port ' + app.get('port'));
});
上面是用express-cli生成的app.js文件,这里其实就是一个实际的应用场景。
上面注册了很多中间件,都是connect中内置的,如:
express.favicon() --- 网站icon图标
express.logger('dev') --- 日志统计
app.router --- 路由中间件
express.static(path.join(__dirname, 'public') --- 静态文件路由
...
上述的函数返回值或者变量都是一个(中间件接口)函数,形式就是:function(req, res, next) {...}
,用来等待处理进来的http请求,并在内部调用next();
来调用下一个中间件。
其实整个流程就归纳为下面这张图(盗用朴大的):
另外还要注意的是:
我们在一个中间件中通过调用next();
来调用下一个中间件,其实从程序的角度是next函数自身的不断地迭代过程,express就是通过这样的方式进行流式处理的,这一点本人认为是这个框架最值得学习的地方。
源码学习
这里我主要讲解以下两个函数:
app.use([path], function) --- 注册中间件
app.handle(req, res, out) --- 处理中间件
app.use
上面的例子可以看出,是通过app.use(..);
注册了中间件,当时只是传递了一个函数作为参数,其实是还有另一个一个参数path(不传时,path被设为'/')。
path是用来干什么的呢?很明显是过滤中间件的。
举个例子:
app.use('/list', funciton (req, res, next) {
// 访问列表
...
});
app.use('/edit', function (req, res, next) {
// 访问编辑页
// 检查用户的合法性
if (check()) {
next();
} else {
next(new Error('不合法'))
}
});
很明显,从上面的代码和注释可以看出当一个用户通过不同的url访问不同页面时,根据path就可以进行不同处理,比如:你要编辑帖子,当然需要check以下用户身份,而看帖子就不需要。
所以总结下path的作用在于过滤中间件,不同的url请求通过匹配path,可能会由不同的中间件处理。
我们来分析下源代码:
app.use = function(route, fn){
var app;
// default route to '/'
// route不设时,默认值是'/'
if ('string' != typeof route) fn = route, route = '/';
// express app
// 如果fn为express返回的app
if (fn.handle && fn.set) app = fn;
// restore .app property on req and res
// 设置挂载的app
if (app) {
app.route = route;
// 挂载的app被当作中间件处理
fn = function(req, res, next) {
var orig = req.app;
// 调用挂载app的handle,处理自己的中间件
// 这里传递的回调,用于挂载app执行完后的还原操作
// 还原当前app的req和res
// 并且执行下一个中间件
app.handle(req, res, function(err){
req.__proto__ = orig.request;
res.__proto__ = orig.response;
next(err);
});
};
}
// 实质还是调用connect的use方法
connect.proto.use.call(this, route, fn);
// mounted an app
// 如果是挂载app
// 设置的parent为当前app
// 并触发挂载app的mount的事件,来完成一些继承
if (app) {
app.parent = this;
app.emit('mount', this);
}
return this;
};
从上面的的中文注释应该可以看出个大概:
- route默认值是'/';
- fn除了是普通函数意外,还可以是
express();
返回的app函数,这时候就是挂载app了(这里我们暂不讨论) app.use
的本质还是调用connect.proto.use
因此我们又跑到connect中use方法定义的地方:
app.use = function(route, fn){
if ('string' != typeof route) {
fn = route;
route = '/';
}
...
// strip trailing slash
// 去除尾部可能出现的'/'
if ('/' == route[route.length - 1]) {
route = route.slice(0, -1);
}
// add the middleware
// 向stack中添加该中间件
debug('use %s %s', route || '/', fn.name || 'anonymous');
this.stack.push({ route: route, handle: fn });
return this;
};
我们发现我们的path和fn最后会被作为一个对象{ route: route, handle: fn }
push到this.stack中,这样就算是注册了一个中间件,所以很容易联想到接下来的app.hanlde()
就是一个一个从stack里面取出匹配的中间件,然后执行中间件,ok,带着这样的想法我们来看看app.handle
的实现。
app.handle
首先我们必须知道当请求过来时,app.handle
何时被调用?
如果你仔细研究过我上一篇的结构图,我猜你肯定知道express()
返回的app是一个函数,再通过上面的第二个例子,你应该知道创建的server在监听到请求时会开始最先调用app,并被传递req,res这些对象作为参数。
ok,我在connect.js中找到了这样的一个函数定义的地方:
function createServer() {
function app(req, res, next){ app.handle(req, res, next); }
utils.merge(app, proto);
utils.merge(app, EventEmitter.prototype);
app.route = '/';
app.stack = [];
for (var i = 0; i < arguments.length; ++i) {
app.use(arguments[i]);
}
return app;
};
从上面代码我们看到,当请求来时,app函数便会自动调用app.handle(...)
,并传递res,req作为参数,于是中间件的执行遍开始了~~
我慢慢地找到了它的源码:
// 处理中间件的入口,也是整个app处理请求的入口
app.handle = function(req, res, out) {
var stack = this.stack
, search = 1 + req.url.indexOf('?')
, pathlength = search ? search - 1 : req.url.length
, fqdn = 1 + req.url.substr(0, pathlength).indexOf('://')
, protohost = fqdn ? req.url.substr(0, req.url.indexOf('/', 2 + fqdn)) : ''
, removed = ''
, slashAdded = false
, index = 0;
// 处理(下一个)中间件
// 会被作为中间件接口函数的形参next传入,便于调用下一个中间件
// 所以这个next函数是被迭代调用的,也就意味着上面这些变量会被共享使用
// 随时都在变化着,需要进行必要的重置和还原
function next(err) {
var layer, path, c;
// 还原操作
// 去除上一个中间件可能添加的slash
if (slashAdded) {
req.url = req.url.substr(1);
slashAdded = false;
}
// 还原操作,补回上一个中间件因为匹配成功删除的route部分
req.url = protohost + removed + req.url.substr(protohost.length);
// 保存原始的url
// 因为后面的匹配操作可能会修改req.url
req.originalUrl = req.originalUrl || req.url;
removed = '';
// next callback
// 去除中间件
layer = stack[index++];
// all done
// 如果中间件已经遍历完
// 或者已经响应了客户端,即执行了res.end(..)等操作
if (!layer || res.headerSent) {
// delegate to parent
// 用于挂载express app时,是挂载的app结束出口
if (out) return out(err);
// unhandled error
// 如果某一个中间件报错了(即next(new Error(...)));
// 那么处理错误,返回客户端应有的响应
if (err) {
// default to 500
// 错误默认返回500
if (res.statusCode < 400) res.statusCode = 500;
debug('default %s', res.statusCode);
// respect err.status
// 使用给定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 = utils.escape(msg);
// log to stderr in a non-test env
if ('test' != env) console.error(err.stack || err.toString());
// 如果请求已经发出,直接destory
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);
// 返回404,即该请求没有找到任何响应
// 其实这里有可能存在hearSent的情况
// 那么下面的res.setHeader 这个会报出异常,最后还是回到上一个判断
} else {
debug('default 404');
res.statusCode = 404;
res.setHeader('Content-Type', 'text/html');
if ('HEAD' == req.method) return res.end();
res.end('Cannot ' + utils.escape(req.method) + ' ' + utils.escape(req.originalUrl) + '\n');
}
return;
}
try {
// 解析路径
// 注意这里是pathname不是path
path = utils.parseUrl(req).pathname;
if (undefined == path) path = '/';
// skip this layer if the route doesn't match.
// 路由不匹配,直接执行到下一个中间件
// 这里的匹配用的是startWith
if (0 != path.toLowerCase().indexOf(layer.route.toLowerCase())) return next(err);
c = path[layer.route.length];
// 如果最后一个字符存在,那么最后一个字符必须是'/'或者 '.'
if (c && '/' != c && '.' != c) return next(err);
// Call the layer handler
// Trim off the part of the url that matches the route
// 将req.url去除匹配到的部分,并保证传递给callback
// 所以接下来的中间件再执行时会恢复这个req.url的(代码在上面)
removed = layer.route;
req.url = protohost + req.url.substr(protohost.length + removed.length);
// Ensure leading slash
// 如果url不是以'/'开头,那么加上
if (!fqdn && '/' != req.url[0]) {
req.url = '/' + req.url;
slashAdded = true;
}
debug('%s %s : %s', layer.handle.name || 'anonymous', layer.route, req.originalUrl);
// 通过中间件callback的参数个数
// 用来判断是处理错误的callback还是处理正常逻辑的callback
var arity = layer.handle.length;
// 如果出错
// 参数个数为4,那么传递err并执行错误处理函数
// 否则,执行下一个中间件
if (err) {
if (arity === 4) {
layer.handle(err, req, res, next);
} else {
next(err);
}
// 否则没有错误
// 参数 < 4,执行callback处理正常逻辑
// 否则,执行下一个中间件
} else if (arity < 4) {
layer.handle(req, res, next);
} else {
next();
}
// 捕获中间件的异常,并传递下去
// 其实如果中间件本身也可以自己先捕获异常
} catch (e) {
next(e);
}
}
// 执行第一个中间件
next();
};
上述的代码貌似有那么点长,不过有中文注释会好点吧_。我们把注意力全部集中到function next () {...}
这个函数,因为正是它的迭代才使得中间件得以被流式一样地一个一个有选择地被处理,而且随时可以终止这个处理流。
ok,我们逐步来分析:
首先我们从this.stack
里取出一个中间件{route:route, handle: fn}
,
接下来的判断if (!layer || res.headerSent) {..}
分别表示中间件已经遍历完毕和服务端已经响应了客户端这两种情况。
我们重点看中间件执行的过程,通过匹配将当前url的path与中间件的route进行匹配,这里的匹配条件:
- 0 === path.toLowerCase().indexOf(layer.route.toLowerCase())
- c = path[layer.route.length]; (c && '/' != c && '.' != c)
第一个条件是startWidth的形式
第二个条件是对第一个条件的补充,比如像这样:
route : /edit
url : /edit/332 匹配
url : /editXXX/332 不匹配
url : /edit.json 匹配
接下来,如果匹配不了当前中间件,调用return next(err);
执行下一个中间件;否则的话,即将开始执行中间件的handle。
在执行handle时,进行了一定的判断:
if (err) {..}
这里的err是从上一个中间件传过来
- 如果存在错误,
- 且中间件的handle的参数个数为4(表示错误处理函数),那么将执行该handle,
- 否则认为该中间件没能力处理该错误,那么直接
next(err);
交给下一个中间件处理;
- 那如果不存在错误,那么就是正常的逻辑处理
- 且中间件的handle的参数个数小于4(表示正常逻辑处理函数),那么执行该handle,
- 否则认为该中间件没有处理该逻辑的函数,同样是
next();
,交给下一个中间件处理。
当handle被调用时,req, res, next都被传递给handle,他们都有着自己的意义:
- req --- 可以往上面赋值从而达到中间件共享数据
- res --- 保证任何中间件都可以随时向客户端响应,从而阻止接下来的中间件的执行
- next --- 可以通过next手动地选择是否执行下一个匹配地中间件
最后
差不多,以上就是我对中间件的理解了。
又周五了,大家周末快乐_!