接上一篇写到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
12exports.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
5var server = connect.createServer(
connect.favicon()
, connect.logger()
, connect.static(__dirname + '/public')
);
或者可以这样:1
2
3
4var 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的defineGetter、defineSetter方法来追加定义,我们看个例子:
1 | Date.prototype.__defineGetter__('year', function() {return this.getFullYear();}); |
这样就一目了然了啊,是用来方便定义对象方法的,如果传参则是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
2path = parse(req.url).pathname;
if (undefined == path) path = '/';
当path不存在时则默认为访问根目录1
2
3if (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
4route:/user/face
path1:user/face
path2:/user/fac
path3:/user/face/snoopy
有以上3个访问path,则path1在 indexOf 那个判断壮烈,path2 在 判断多一位的时候壮烈了,path3 顺利通过检测,剪取以后为‘/snoopy’。
4、
1 | var arity = layer.handle.length; |
这里写的真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 | if (icon) { |
如果icon对象已经生成好了,则直接输出给客户端,
###4、1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17else {
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 | options = options || {}; |
对options进行设置,返回一个static函数,这个函数主要的功能是执行send方法,接收4个参数,关键是第4个options。
我们来看send函数,国际惯例,显示一些参数的赋值什么的,注意这一个:1
head = 'HEAD' == req.method
判断是否是http请求的head请求。1
2if (fn) next = fn;
if ('GET' != req.method && !head) return next();
如果传递了回调函数,改写next,如果不是get也不是head,则执行next1
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
14fs.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();
}
...}
先检查文件的状态,如果不存在,或者访问的是目录,则抛出error1
2
3
4
5if (utils.conditionalGET(req)) {
if (!utils.modified(req, res)) {
return utils.notModified(res);
}
}
如果在缓存之内,则直接输出
1 | if (head) return res.end(); |
这里如果是head请求,只是来试探文件有的大小等信息的,则返回结束1
2var stream = fs.createReadStream(path, opts);
stream.pipe(res);
创建一个文件可读流,然后利用pipe方法(管道)将文件流响应给客户端
1 | if (fn) { |
这个函数很巧妙,因为我们不能预期是传输过程中出错了,还是客户端先关闭了连接,或是传输完毕,所以这样写的好处便在于,callback内的fn(err)只会执行一次。
4、总结
我们详细了解了connect.js的middleware工作流程,如何做到在响应给用户之前将static这部分工作包揽过去,然后又将req的一些信息进行封装,比如cookie等,很巧妙,代码写的也很精炼。
不过我个人觉得,connect还是有提高性能空间的。就拿favicon来说,比如:1
2
3
4
5if ('/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
5layer = stack[index++];
// all done
if (!layer) {
//404 error or 500 error
}
才能输出404或是500错误,可以或者有必要直接为404或500错误开辟一个方法,而不再去处理stack堆栈中的内容,这样应该还可以提高些许性能。
中间件的应用我们也分析掉拉,下一篇我要开始connect.js部分最后的一块了,router.js路由部分。