Node.js之connect.js源码解析

相信用过node.js的人都多多少少听说过或用过express框架,而express正是在connect的基础上搭建的,可以是express是connect的升级版,如果我们读懂了connect的源码的话,那么接下来读懂express源代码也应该是不在话下了,其实connect只有200多行代码,读懂也不是很难的,那就让我们来分析一下它的源码吧.

先来看一个Hello world例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
var connect = require('connect');
var http = require('http');
var app = connect()
.use(connect.favicon())
.use(connect.logger('dev'))
.use(connect.static('public'))
.use(connect.directory('public'))
.use(connect.cookieParser('my secret here'))
.use(connect.session())
.use(function(req, res){
res.end('Hello from Connect!\n');
});
http.createServer(app).listen(3000);

这就是一个很清晰简洁的一个链式调用,connect提供了一种新的组织代码的方式来与请求和响应对象进行交互,称为中间件,而use函数就是用来添加中间件的,当http请求到来时,就像是水流一样流过每一个中间件,当路径相同时,就会响应该请求,否则就继续往下流,直到结束,那么它是怎么实现的,关键就在源代码里.

Connect中主要有五个函数

  1. createServer
  2. use
  3. handle
  4. call
  5. listen

ps:其实还有一个next()函数,但它是包含于handle函数里的,所有这里就不算上它,后面会分析到它.

createServer()

1
2
3
4
5
6
7
8
function createServer() {
function app(req, res, next){ app.handle(req, res, next); }
merge(app, proto);
merge(app, EventEmitter.prototype);
app.route = '/';
app.stack = [];
return app;
}

这个方法返回了一个app,而app是里面定义的一个函数,并且app函数里调用了app.handle()函数,可是handle是哪里来的呢,我们看接下来两行,原来app通过utils.merge方法,把proto和EventEmitter的方法合并到了app函数上,这样app就继承了proto和EventEmitter的方法了,而handle()方法正是从proto继承来的,接下来两句创建了两个属性app.route以及app.stack,app.route是一个路径地址,app.stack用来存放中间件.

app.use(route, fn)

route是中间件所使用的请求路径,默认为’/’,可省略,fn是中间件处理函数,有两种形式,分别为fn(req,res,next)fn(err,req,res,next)fn(req,res,next)是正常处理函数,fn(err,req,res,next)是异常处理函数.

next()

next是触发后续流程的回调函数,带一个err参数,通过传递给next传递一个err参数,告诉框架当前中间件处理出现异常。如果err为空,则会按顺序执行后面正常处理函数,忽略异常处理函数;相反,如果err非空,则会按顺序执行后续的异常处理函数,而忽略正常处理函数。
http请求到来时,当所有的中间件函数都被调用之后仍旧没有匹配到路由,则会出现错误,connect会认为这个请求没有中间件认领,如果next的err参数非空,则会给页面返回500错误,表示server出现了内部错误;如果err为空,则返回404错误,即访问的资源不存在。

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
26
27
28
29
30
31
32
33
34
35
proto.use = function use(route, fn) {
var handle = fn;
var path = route;
// default route to '/'
if (typeof route !== 'string') {
handle = route;
path = '/';
}
// wrap sub-apps
if (typeof handle.handle === 'function') {
var server = handle;
server.route = path;
handle = function (req, res, next) {
server.handle(req, res, next);
};
}
// wrap vanilla http.Servers
if (handle instanceof http.Server) {
handle = handle.listeners('request')[0];
}
// strip trailing slash
if (path[path.length - 1] === '/') {
path = path.slice(0, -1);
}
// add the middleware
debug('use %s %s', path || '/', handle.name || 'anonymous');
this.stack.push({ route: path, handle: handle });
return this;
};

通过上面的源代码可以看出,app.use()作用是向stack中添加逻辑处理函数 (中间件)的,stack数组中存储了一个个的{route: route , handle : fn}对象,从proto.use的源码可以看出,新定义的handle变量就是use要添加的中间件,path是请求的路径,fn表示处理逻辑函数,分3种情况,它可以是:

  1. 一个普通的 function(req,res[,next]){}
  2. 一个httpServer;
  3. 另一个connect的app对象(sub app特性);

3种情况本质都是处理逻辑,都可以用一个 function(req,res,next){}将它们概括起来,Connect把他们都转化为这个函数,然后把它们存起来。

如何将这三种分别转换为function(req, res, next) {}的形式呢?

  1. 不用转换;
  2. httpServer的定义是“对事件’request’后handler的对象”, 我们可以从httpServer.listeners(‘request’)中得到这个函数;
  3. 另一个connect对象,而connect()返回的app就是function(req, res, out) {}

在这里,我们也可以看出来,各个中间件之间其实并没有直接的依赖。request和response就成为它们在connect中传递信息的唯一桥梁。前面的中间件处理完毕后,把结果附到request或response之上,后面的中间件便可以从中获取到这些数据。所以,中间件的加载顺序在connect中就显得格外重要,必须将被依赖的中间件放在依赖它的模块之前。比如说,解析cookie的中间件应该放在处理session的中间件之前,因为一般session id是通过cookie来传递的。

app.handle(req, res, out)

app.handle()也来自于proto.handle,最主要的作用就是定义了next函数,app.handle()是直接处理http请求的,请求到达时,触发第一个next并触发链式调用,接下来通过next()函数顺序调用存储在stack中的中间件,layer = stack[index++]保存了当前的请求路径和中间件函数,如果不匹配则会返回next(err)重新调用next函数,此时layer的值是下一个中间件的相关信息,这样就会一直循环下去,直到堆栈已经没有值或者route的值匹配成功为止。如果匹配成功,则会调用call(layer.handle, route, err, req, res, next),这个layer.handle就是中间件函数,call的作用就是调用layer.handle(err,req, res, next)layer.handle(req, res, next).

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
proto.handle = function handle(req, res, out) {
var index = 0;
var protohost = getProtohost(req.url) || '';
var removed = '';
var slashAdded = false;
var stack = this.stack;
// final function handler
var done = out || finalhandler(req, res, {
env: env,
onerror: logerror
});
// store the original URL
req.originalUrl = req.originalUrl || req.url;
function next(err) {
if (slashAdded) {
req.url = req.url.substr(1);
slashAdded = false;
}
if (removed.length !== 0) {
req.url = protohost + removed + req.url.substr(protohost.length);
removed = '';
}
// next callback
var layer = stack[index++];
// all done
if (!layer) {
defer(done, err);
return;
}
// route data
var path = parseUrl(req).pathname || '/';
var route = layer.route;
// skip this layer if the route doesn't match
if (path.toLowerCase().substr(0, route.length) !== route.toLowerCase()) {
return next(err);
}
// skip if route match does not border "/", ".", or end
var c = path[route.length];
if (c !== undefined && '/' !== c && '.' !== c) {
return next(err);
}
// trim off the part of the url that matches the route
if (route.length !== 0 && route !== '/') {
removed = route;
req.url = protohost + req.url.substr(protohost.length + removed.length);
// ensure leading slash
if (!protohost && req.url[0] !== '/') {
req.url = '/' + req.url;
slashAdded = true;
}
}
// call the layer handle
call(layer.handle, route, err, req, res, next);
}
next();
};

call(handle, route, err, req, res, next)

因为Function.length返回函数定义的参数个数,而Connect中规定function(err, req, res, next) {}形式为错误处理函数,function(req, res, next) {}为正常的业务逻辑处理函数。那么,可以根据Function.length以判断它是否为错误处理函数,当发生错误且传递了4个参数时就会调用handle(err,req, res, next)这个函数,当没有发生错误且传递的参数小于4个时就会调用handle(req, res, next)函数.

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
function call(handle, route, err, req, res, next) {
var arity = handle.length;
var error = err;
var hasError = Boolean(err);
debug('%s %s : %s', handle.name || '<anonymous>', route, req.originalUrl);
try {
if (hasError && arity === 4) {
// error-handling middleware
handle(err, req, res, next);
return;
} else if (!hasError && arity < 4) {
// request-handling middleware
handle(req, res, next);
return;
}
} catch (e) {
// replace the error
error = e;
}
// continue
next(error);
}

listen()

listen()比较简单,就是创建一个httpServer,将Connect自己的业务逻辑作为requestHandler,监听端口.

1
2
3
4
proto.listen = function listen() {
var server = http.createServer(this);
return server.listen.apply(server, arguments);
};

参考: https://github.com/alsotang/node-lessons/tree/master/lesson18#next