前面介绍了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
8function 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
3for (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
13methods.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
2this._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
23app.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 | var req = exports = module.exports = { |
这里定义的一些req的方法是直接在http.IncomingMessage原型链上定义的。
这个文件大部分是方便获取客户端request过来的请求头信息的。
4、response.js
1 | res.send = function(body){...} |
对请求的响应,我们来看一下这个方法1
2
3
4if (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
25switch (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
5if (undefined !== body && !this.header('Content-Length')) {
this.header('Content-Length', Buffer.isBuffer(body)
? body.length
: Buffer.byteLength(body));
}
定义Content-Length
1 | // respond |
返回给客户端,并且关闭连接。这如果想多次调用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
19res.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 | if (req.accepts('html')) { |
这里如果是访问html的,则返回一段html代码表示跳转,否则返回一段文字,最后设置Location头的url,表示跳转到此url地址,然后客户端重新请求此url地址,获取正确的内容。
这里直接修改ServerResponse.prototype.statusCode属性,node.js的api里并不支持这么做,API推荐使用:response.setHeader(name, value) 来设置修改http请求状态码,看来作者在写expressjs时很详细的了解了node.js http模块的源码,改天我也来写份心得。