expressjs源码解读(二) —— middleware

接上一篇写到app是什么,我们在expressjs的api中我们看到了这段代码:

1
app.use(express.static(__dirname + '/public'));

api说明:设置静态文件的目录。app.use是定义到中间件的middleware中的,所以我们就去middleware文件夹一探究竟。

注:expressjs是对connectjs的一个封装,所以原理还是一样的。

1、connect.js中的middleware

打开connect.js文件,我们可以找到如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
exports.middleware = {};
/**
* Auto-load bundled middleware with getters.
*/
fs.readdirSync(__dirname + '/middleware').forEach(function(filename){
if (/\.js$/.test(filename)) {
var name = filename.substr(0, filename.lastIndexOf('.'));
exports.middleware.__defineGetter__(name, function(){
return require('./middleware/' + name);
});
}
});

将middleware对象exports,然后循环定义给middleware对象一种方法,这种方法是直接加载 middleware 文件夹中的.js文件模块。

然后connect.js利用:exports.utils.merge(exports, exports.middleware); 这句话将middleware中的方法直接exports了。

如何利用middleware呢?我们先看connect.js的api示例:

1
2
3
4
5
var server = connect.createServer(
connect.favicon()
, connect.logger()
, connect.static(__dirname + '/public')
);

或者可以这样:

1
2
3
4
var server = connect.createServer();
server.use(connect.favicon());
server.use(connect.logger());
server.use(connect.static(__dirname + '/public'));

当然还有一种是链式调用,其实无论用哪种方式,最终都是会将connect.favicon()的返回值作为参数传递给 Server.prototype.use 这个方法。然后做一下处理,将其丢入stack堆栈中,等待handle调用(前一章已经说过)。

在介绍 Server.prototype.use 之前,我先介绍下2个不怎么用到的东西:

1、javascript 1.6 数组新增方法: forEach()

代码示例:array.forEach(callback[, thisObject]);

说明:callback: 要对每个数组元素执行的回调函数,带3个参数,分别是:当前元素,当前元素的索引和当前的数组对象。

thisObject : 在执行回调函数时定义的this对象。

2、在对象定义后通过Object的defineGetterdefineSetter方法来追加定义,我们看个例子:

1
2
3
4
5
6
Date.prototype.__defineGetter__('year', function() {return this.getFullYear();}); 
Date.prototype.__defineSetter__('year', function(y) {this.setFullYear(y)});
var now = new Date;
alert(now.year);
now.year = 2006;
alert(now);

这样就一目了然了啊,是用来方便定义对象方法的,如果传参则是setter,如果不传参则是getter,当然在getter时也可以传参,但是在setter时必须传参。

接下来,我们着重看下 Server.prototype.use 这个方法是怎么做统一处理数据,并push入stack数组中的:

Server.prototype.use 方法的宗旨就是将传递过来的参数变换为{route, handle}这样形势的,route代表路由路径,而handle代表回调函数。方法用4个if做了判断:

1、如果传递进来的route不是string,则认为是function,于是将handle = route, route = ‘/‘;

2、如果传递进来的handle.handle是function,则把handle参数改写解套;

3、如果传递进来的handle是http.Server的一个实例,handle = handle.listeners(‘request’)[0]; 将request监听器的第一个回调函数复制给handle。

4、如果route的最后一位是’/‘,则把’/‘去掉,当然对于指向根目录的也清空了。

最后将拼装好的{route, handle}放入stack数组中,等待出列。

当有客户端请求触发,handle方法就执行,开始一个个从stack堆栈中取出这个 {route, handle} 对象,根据handle.length的不同分别调用。前一篇文章对于 Server.prototype.handle 方法只是一带而过,这里我们就要开始深究它了。Server.prototype.handle 这个方法在每次有客户端request时就会被调用。

1
2
path = parse(req.url).pathname;
if (undefined == path) path = '/';

当path不存在时则默认为访问根目录

1
2
3
if (0 != path.indexOf(layer.route)) return next(err);
c = path[layer.route.length];
if (c && '/' != c && '.' != c) return next(err);

首先当path和之前定义的route路由路径不匹配时,然后当访问路径比route长,并且长出的那一位不是’/‘也不是’.’时,执行next(err),转发下一个middleware并携带err参数。

3、removed = layer.route;

req.url = req.url.substr(removed.length);

剪取路由匹配掉的那部分。

举个例子:

1
2
3
4
route:/user/face
path1:user/face
path2:/user/fac
path3:/user/face/snoopy

有以上3个访问path,则path1在 indexOf 那个判断壮烈,path2 在 判断多一位的时候壮烈了,path3 顺利通过检测,剪取以后为‘/snoopy’。

4、

1
2
3
4
5
6
7
8
9
10
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();
}

这里写的真TM绕啊,如果有err参数传递,并且handle期望传递参数是4位的,则执行 layer.handle(err, req, res, next);

如果handle期望传递参数是小于4位的,并且没有err参数的,则执行 layer.handle(req, res, next);

否则执行next();

至此我们完全分析了两个功能性函数:

Server.prototype.handle 和 Server.prototype.use ,但是他们是怎么作用的呢?

下面我以favicon、static为例,跑一下它们的流程。

2、favicon.js中间件

打开favicon.js,这个.js文件

1、 module.exports = function favicon(path, options){ …}

输出 favicon 函数,并且期待传递2个参数,path路径和options设置。

2、 return function favicon(req, res, next){ …}

返回一个回调函数,接收3个参数那种

3、

1
2
3
4
if (icon) {
res.writeHead(200, icon.headers);
res.end(icon.body);
}

如果icon对象已经生成好了,则直接输出给客户端,

###4、

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
else {
fs.readFile(path, function(err, buf){
if (err) return next(err);
icon = {
headers: {
'Content-Type': 'image/x-icon'
, 'Content-Length': buf.length
, 'ETag': '"' + utils.md5(buf) + '"'
, 'Cache-Control': 'public, max-age=' + (maxAge / 1000)
},
body: buf
};
res.writeHead(200, icon.headers);
res.end(icon.body);
});
}
}

如果icon对象没有生成好,则去 readFile ,并且设置响应头,设置ETag属性和 Cache-Control ,将文件读入buf,保存icon对象,然后响应给客户端。

示例:connect.favicon(__dirname + ‘/public/favicon.ico’, {maxAge: 86400000})

3、static.js中间件

static.js是用来做静态文件输出的。

1
exports = module.exports = function static(root, options){...}

输出一个static函数,接收2个参数,静态文件路径和设置,示例:

1
connect.static(__dirname + '/public', { maxAge: oneDay })
1
2
3
4
5
6
7
8
options = options || {};
// root required
if (!root) throw new Error('static() root path required');
options.root = root;
return function static(req, res, next) {
options.path = req.url;
send(req, res, next, options);
};

对options进行设置,返回一个static函数,这个函数主要的功能是执行send方法,接收4个参数,关键是第4个options。

我们来看send函数,国际惯例,显示一些参数的赋值什么的,注意这一个:

1
head = 'HEAD' == req.method

判断是否是http请求的head请求。

1
2
if (fn) next = fn; 
if ('GET' != req.method && !head) return next();

如果传递了回调函数,改写next,如果不是get也不是head,则执行next

1
2
// when root is not given, consider .. malicious
if (!root && ~path.indexOf('..')) return utils.forbidden(res);

如果root没有给出,用恶意的‘..’想去访问文件,则干掉之。

这里 作者写的很装B,~path.indexOf(‘..’) 表示 path.indexOf(‘..’) != -1,~符号是按位的非操作,-1在2进制码中表示:1111 1111 111 111。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
fs.stat(path, function(err, stat){
// ignore ENOENT
if (err) {
if (fn) return fn(err);
return 'ENOENT' == err.code
? next()
: next(err);
// ignore directories
} else if (stat.isDirectory()) {
return fn
? fn(new Error('Cannot Transfer Directory'))
: next();
}
...}

先检查文件的状态,如果不存在,或者访问的是目录,则抛出error

1
2
3
4
5
if (utils.conditionalGET(req)) {
if (!utils.modified(req, res)) {
return utils.notModified(res);
}
}

如果在缓存之内,则直接输出

1
if (head) return res.end();

这里如果是head请求,只是来试探文件有的大小等信息的,则返回结束

1
2
var stream = fs.createReadStream(path, opts);
stream.pipe(res);

创建一个文件可读流,然后利用pipe方法(管道)将文件流响应给客户端

1
2
3
4
5
6
if (fn) {
function callback(err) { done || fn(err); done = true }
req.on('close', callback);
req.socket.on('error', callback);
stream.on('end', callback);
}

这个函数很巧妙,因为我们不能预期是传输过程中出错了,还是客户端先关闭了连接,或是传输完毕,所以这样写的好处便在于,callback内的fn(err)只会执行一次。

4、总结

我们详细了解了connect.js的middleware工作流程,如何做到在响应给用户之前将static这部分工作包揽过去,然后又将req的一些信息进行封装,比如cookie等,很巧妙,代码写的也很精炼。

不过我个人觉得,connect还是有提高性能空间的。就拿favicon来说,比如:

1
2
3
4
5
if ('/favicon.ico' == req.url) { 
fs.readFile(path, function(err, buf){
if (err) return next(err);
})
} else {next();}

当读取文件失败,将err传递给next函数,说穿了就是去执行stack堆栈下一个的handle,直到代码:

1
2
3
4
5
layer = stack[index++];
// all done
if (!layer) {
//404 error or 500 error
}

才能输出404或是500错误,可以或者有必要直接为404或500错误开辟一个方法,而不再去处理stack堆栈中的内容,这样应该还可以提高些许性能。

中间件的应用我们也分析掉拉,下一篇我要开始connect.js部分最后的一块了,router.js路由部分。