expressjs是目前最流行的node.js web服务器框架,很多前辈都说看expressjs的源码,特别是connect的源码有很大帮助,最近闲下来了,我也来研究一下expressjs到底有何秘密,提升一下自己的node.js水平,顺便看看expressjs有没有什么地方不合理,或是可以提升一下性能。
我使用expressjs做过一些简单的2次开发,但是都没有去修改它的源码,只是在expressjs的基础上封装了一些方法,丰富了一些api。所以对于expressjs的API比较熟悉,但是对于源码还是很陌生的,第一次看,利用expressjs搭建了一个个人站:spout.cnodejs.net
1、神秘的app
大家使用expressjs第一影响便是使用app来创建一个web服务器,接下来我就以app为线索解读源码。
1 | var app = require('express').createServer(); |
这个app是什么呢?打开lib文件夹express.js文件,我们看到:1
var exports = module.exports = connect.middleware;
原来require(‘express’)就是加载了connect.middleware,至于这个connect.middleware是什么,我们下一节讨论connect时再说。
回到app,我们看createServer这个方法:
1 | exports.createServer = function(options){ |
这里为了缩小篇幅,我把一些大括号去掉了,下同。require(‘express’).createServer()方法是expressjs为connect.middleware新增加的一个方法,返回一个httpserver实例或者httpsserver实例,我们以常用的httpserver为主来解读app。
2、http.js模块
从上我们知道,http.js模块就是exports一个httpserver的类,我们看一下这个类:1
2
3
4
5var app = HTTPServer.prototype;
function HTTPServer(middleware){
connect.HTTPServer.call(this, []);
this.init(middleware);
};
这里的app并不是我们上面说的app,而是供http.js内部使用的一个变量,虽然它也有和上面那个app一样的属性和方法。
函数里的内容是典型的js类继承方式,不理解的同学可以看我的另外一篇blog,<关于js的继承>。
通过connect.HTTPServer.call(this, []);这行代码可以让httpserver类继承connect.HTTPServer类的所有属性和方法。当然这里还少一行代码:
HTTPServer.prototype.constructor = HTTPServer; 这样实例化httpserver以后查找它的construcor属性不会出错,而是指向httpserver。
这个middleware参数是什么呢?示例代码在实例化httpserver时并没有传递任何参数,我们先不管它。
我们去connect库里找一下HTTPServer,发现:
1 | var Server = exports.Server = function HTTPServer(middleware) { |
这个方法先是将middleware文件夹下的一些中间件作为回调函数,存入this.stack数组中,即为了当有请求过来,先执行中间件的回调,然后再抛给应用者,说白了就是对请求过来的信息的封装,方便我们使用。具体middleware我们之后再讨论。
然后直接调用http.Server.call(this, this.handle);这个来注册onrequest事件,http.Server我们需要去扒开node.js源码来看了:
1 | function Server(requestListener) { |
这样我们就顺利的将HTTPServer.prototype.handle方法注册到了request事件中。
接下来我们就来看handle方法,我们究竟注册了怎样一个方法给request事件呢:
Handle server requests, punting them down
the middleware stack.
这2行是对handle方法的注释,字面意思是 处理服务器的请求,将他们传递给中间件的堆栈。根据字面理解这个方法就是执行之前放在stack数组中的一些回调函数的,做到重新封装或自动处理的目的。
不过在源码中只找到了:self.emit(‘request’, req, res); 目前还不知道Server.prototype.handle = function(req, res, out) {…} 中的out参数从何而来,可能在middleware中有应用吧。
handle方法中有一个闭包,function next()
layer = stack[index++];一个个取出来,等待屠宰,哈哈。
1、先判断是否有layer,如果没有则根据是否传递错误参数来输出500(内部错误)和404页面。当然其中有一个诡异的 out 参数,目前不清楚它的由来。
2、在根据一定的条件执行midddleware中的回调,或者next(),调用下一个stack中的回调。
3、app到底做了什么?
绕了这么一大大大圈的,我们的app到底做了什么呢?
我们的app其实只做了两件事情:
1、将api源码的http.createServer(fn);这样的形式改写,注册了一个request事件的回调函数handle,并且可以在任何时候修改stack堆栈的内容,达到修改中间件的目的。最后执行listen()方法,监听指定端口。也就是说一旦有客户端的request访问指定端口,则将触发handle方法,挨个将stack堆栈中的回调执行,对req进行封装,比如提供session服务,提供cookie服务或是直接响应静态文件给客户端,做到了web服务器的事情。
2、app是HTTPServer的实例,HTTPServer继承自http.Server,所以app具有http.Server的属性和方法,看node.js代码知道,http.Server原来继承自net模块,listen方法也是net模块中的,所以app是一个强化版本的http.Server实例。
4、总结
没想到一个app可以写这么多内容,我们顺藤摸瓜,探究了app对象的真实情况,感叹 connect 改写api的巧妙。最后我来分析一下上面橙色字部分的代码实现,简单改写一下:1
2
3
4
5
6
7
8
9
10var x = function(a){
if (!(this instanceof arguments.callee)){
return new arguments.callee(a);
};
this.u = a
}
var y = x('888');
alert(y.u);
var z = new x('888');
alert(z.u);
你会发现,alert()两次的值是相同的,这样就省略了new关键字,同时又不排斥new 关键字。扩展一下jquery的写法:1
2
3
4
5
6
7
8
9function aaa(a){
return new aaa.prototype.init(a);
}
aaa.prototype.init = function(a){
console.log(111);
this.name=a;
}
aaa.prototype.init.prototype = aaa.prototype;
var a = aaa('aaa')
同样是为了不排斥new关键字,并且达到了扩充aaa类和init类都可以达到同时扩充。
可能还有一句比较诡异的:1
Server.prototype.__proto__ = http.Server.prototype;//指向 Server.prototype 原型链的父链,
这里这么写是为了添加Server.prototype上的方法不破坏 http.Server.prototype,又可以继承 http.Server.prototype 上的方法。