expressjs源码解读(四) —— 3.0版本expressjs

前面介绍了connect.js的源代码和工作原理,除了route.js实现比较复杂外,其他的例如middleware实现都非常巧妙,值得学习。

我们来看下3.0版本的expressjs有什么新东西,初看3.0 版本的expressjs比起2.4版本代码变化不少,值得我们学习的route模块也全部重新改写了,我们来简单看下:

1、express.js

文件输出 createApplication 方法:

1
2
3
4
5
6
7
8
function createApplication() {
var app = connect();
utils.merge(app, proto);
app.request = { __proto__: req };
app.response = { __proto__: res };
app.init();
return app;
}

app继承自 connect,并且app合并proto对象,然后将app.requset对象的原型链指向req,同理respose。最后初始化,返回app对象。下面是一些输出对象,值得关注的是这一段:

1
2
3
for (var key in connect.middleware) {
Object.defineProperty(exports, key , Object.getOwnPropertyDescriptor(connect.middleware, key));
}

我们先看 Object.defineProperty(obj, prop, descriptor) 这个方法,这是javaScript 1.8.5的功能之一,是用来给obj参数定义属性或是方法的,参数解释如下:(以下例子拷贝自司徒正美,原文地址)

1、obj:目标对象

2、prop:需要定义的属性或方法的名字。

3、descriptor:目标属性所拥有的特性,可以为如下几种:

value:属性的值

writable:如果为false,属性的值就不能被重写。

get: 一旦目标属性被访问就会调回此方法,并将此方法的运算结果返回用户。

set:一旦目标属性被赋值,就会调回此方法。

configurable:如果为false,则任何尝试删除目标属性或修改属性以下特性(writable, configurable, enumerable)的行为将被无效化。

enumerable:是否能在for…in循环中遍历出来或在Object.keys中列举出来

举个例子:

1、Object.defineProperty(a,”bloger”,{get:function(){return “司徒正美”}}); //a.bloger是司徒正美

2、Object.defineProperty(b, “p”, {value:”这是不可改变的默认值” ,writable: false }); //这样定义的p是不可以被修改的

其他的例子不列举了,可以自己尝试下。

而 Object.getOwnPropertyDescriptor(object, propertyname) 方法是配合上述方法使用的,获取obj参数的对应key的值,并且以上述 descriptor的方式返回一个data property 或 Accessor Properties 对象, 此方法接收2个参数:

1、object:Required. The object that contains the property. This can be a native JavaScript object or a Document Object Model (DOM) object.

2、propertyname :Required. A string that contains the name of the property.

返回的结果可能是以下2种:

1、Data descriptor attribute:{value :value, writable:writable, enumerable:enumerable, configurable:configurable}

2、Accessor descriptor attribute:{get:function(){}, set:function(value){}, enumerable:enumerable, configurable:configurable}

言归正传,那段for循环就是给exports对象定义和connect.middleware一样的属性和方法的,并且复制同样的数据属性和访问属性,上面那些新特性可以让我们不必再定义闭包,轻松实现对象的私有变量和可读可写可枚举。

2、application.js

貌似expressjs的核心就在这个文件上了,我们简单看一下:

methods = Router.methods.concat(‘del’, ‘all’)

为methods数组添加两个新内容,methods数组存放就是get,post,option,head等http请求方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
methods.forEach(function(method){
self.lookup[method] = function(path){
return self._router.lookup(method, path);
};

self.match[method] = function(path){
return self._router.match(method, path);
};

self.remove[method] = function(path){
return self._router.lookup(method, path).remove();
};
});

注册路由方法函数,具体 self._router 是什么,我们下一节讨论路由时再说。

1
2
this._router = new Router(this);
this.routes = this._router.routes;

实例化Router类,

1
app.use = function(route, fn){ ... }

借助connect.js的use方法,做中间件。

1
methods.forEach(function(method){ ... }

对之前methods数组进行重组,让其支持appmethod 等于调用 app.router(method,path,callback)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
app.param = function(name, fn){
var self = this
, fns = [].slice.call(arguments, 1);

// array
if (Array.isArray(name)) {
name.forEach(function(name){
fns.forEach(function(fn){
self.param(name, fn);
});
});
// param logic
} else if ('function' == typeof name) {
this._router.param(name);
// single
} else {
if (':' == name[0]) name = name.substr(1);
fns.forEach(function(fn){
self._router.param(name, fn);
});
}
return this;
};

这个方法是用来匹配或者获取参数的,借助的还是 _router.param 方法

其他代码不多说了,主要是实现api的一些方法。

3、request.js

1
2
3
var req = exports = module.exports = {
__proto__: http.IncomingMessage.prototype
};

这里定义的一些req的方法是直接在http.IncomingMessage原型链上定义的。

这个文件大部分是方便获取客户端request过来的请求头信息的。

4、response.js

1
res.send = function(body){...}

对请求的响应,我们来看一下这个方法

1
2
3
4
if (2 == arguments.length) {
this.statusCode = body;
body = arguments[1];
}

如果参数2个,则认为第一个参数是状态码,第二个是响应的主体内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
switch (typeof body) {
// response status
case 'number':
this.header('Content-Type') || this.contentType('.txt');
this.statusCode = body;
body = http.STATUS_CODES[body];
break;
// string defaulting to html
case 'string':
if (!this.header('Content-Type')) {
this.charset = this.charset || 'utf-8';
this.contentType('.html');
}
break;
case 'boolean':
case 'object':
if (null == body) {
body = '';
} else if (Buffer.isBuffer(body)) {
this.header('Content-Type') || this.contentType('.bin');
} else {
return this.json(body);
}
break;
}

如果是数字,认为是输出状态码,则body为: http.STATUS_CODES[num],这里不得不再次批评写node.js api的人,STATUS_CODES也是 http模块对外exports的一个对象,里面记录了状态码和英文内容对应值。
如果是字符串,则认为是html,修改请求头信息mimetype值;
如果是buffer,则输出buffer,否则认为是json数据输出。

1
2
3
4
5
if (undefined !== body && !this.header('Content-Length')) {
this.header('Content-Length', Buffer.isBuffer(body)
? body.length
: Buffer.byteLength(body));
}

定义Content-Length

1
2
3
// respond
this.end(head ? null : body);
return this;

返回给客户端,并且关闭连接。这如果想多次调用res.send()恐怕不行了,这个方法很强大,可以做一些自动的处理,建议安装好expressjs第一件事情就是把这里的this.end改为this.write,然后我们可以手动调用res.end关闭连接,可能有些地方需要使用类似bigpipe的多次响应不关闭。

我们来看下跳转的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
res.redirect = function(url){...}

if (!~url.indexOf('://')) {
var path = app.path();
// relative to path
if (0 == url.indexOf('./') || 0 == url.indexOf('..')) {
url = req.path + '/' + url;
// relative to mount-point
} else if ('/' != url[0]) {
url = path + '/' + url;
}

// Absolute
var host = req.header('Host')
, proto = req.header('X-Forwarded-Proto')
, tls = 'https' == proto || req.secure;

url = 'http' + (tls ? 's' : '') + '://' + host + url;
}

这段代码是用来拼装 将要跳转的url的

这又是作者装B的写法,如果没有找到://则将path设置为网站的域名目录

如果是相对目录是 ./ 或者 ../ 开头的,则直接将当前路径与它拼上。

如果不是/开头的,则拼上path+‘/’+url

如果是绝对路径,就是以/开头的,则加上请求host头等信息拼上url

1
2
3
4
5
6
7
8
9
10
11
12
if (req.accepts('html')) {
body = '<p>' + statusCodes[status] + '. Redirecting to <a href="' + url + '">' + url + '</a></p>';
this.header('Content-Type', 'text/html');
} else {
body = statusCodes[status] + '. Redirecting to ' + url;
this.header('Content-Type', 'text/plain');
}

// Respond
this.statusCode = status;
this.header('Location', url);
this.end(head ? null : body);

这里如果是访问html的,则返回一段html代码表示跳转,否则返回一段文字,最后设置Location头的url,表示跳转到此url地址,然后客户端重新请求此url地址,获取正确的内容。

这里直接修改ServerResponse.prototype.statusCode属性,node.js的api里并不支持这么做,API推荐使用:response.setHeader(name, value) 来设置修改http请求状态码,看来作者在写expressjs时很详细的了解了node.js http模块的源码,改天我也来写份心得。