Connect 是一个可扩展(中间件作为插件)的 Http 服务器框架,Connect 刚出道之时自带了许多中间件,为保证其框架的轻量级以及扩展性,最终还是将这些中间件的实现抛给了社区。可能在搜索 Connect 的相关项目时,你会发现 connect().use(connect.bodyParser())这些的写法,这对于现在的 Connect (最新版本3.6.0) 是不支持的,而只能通过 npm 下载第三方的模块 (如 body-parser) 替代原先的中间价。

基本使用

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
const connect = require('connect');
var createError = require('http-errors');
var http = require('http');

var app = connect()

app.use('/', function(req, res, next) {
res.writeHead(200,'OK',{
//'content-type': 'text/plain' //纯文本
'content-type': 'text/html;charset=utf-8'
})
res.write('<h1>你好,欢迎学习connect</h1>')
res.end()
})

// catch 404 and forward to error handler
app.use(function(req, res, next) {
next(createError(404));
});

// error handler
app.use(function(err, req, res, next) {
res.statusCode = 404;
res.end('Not Found');
});

// 两种方式监听指定端口
app.listen(3000) // 这种方式在connect里面调用了http.createServer方法

// http.createServer(app).listen(3000);

源码分析

这篇文章是基于"version": "3.6.6"这个版本来对源码进行分析的,这个版本所有的代码都在index.js文件中。下面我们来看一下connect是怎么工作的。

首先我们看一下这个文件的模块出口,可以看到导出了一个createServer函数,这个函数最终返回的是app,app本身是一个函数,在下面一段代码中我们可以的看到它是作为了request事件的处理函数。同时它既继承了proto、EventEmitter属性和方法,也有自己的route、stack属性。

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
// 模块出口
module.exports = createServer;

// 判断当前环境,初始化proto 对象
var env = process.env.NODE_ENV || 'development';
var proto = {};

function createServer() {
// app 函数对象,可以添加属性和方法
function app(req, res, next){
app.handle(req, res, next);
}
// merge: 相当于Object.assign
merge(app, proto); // 继承proto的属性和方法
merge(app, EventEmitter.prototype); // 继承EventEmitter(事件派发器)属性和方法
app.route = '/';
app.stack = []; // 存放中间件的数组,中间件会被格式化成形为{route: route , handle : fn}的匿名对象存放
return app;
}

// 监听指定端口号
proto.listen = function listen() {
var server = http.createServer(this); // this ——> app函数对象,作为request事件的处理函数
return server.listen.apply(server, arguments); // 从 arguments 拿到端口号
};

connect框架的核心是use、handle、call三个方法,我们来分析一下这三个方法分别有什么作用。

use方法:添加中间件

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
// 添加中间件
proto.use = function use(route, fn) {
var handle = fn;
var path = route;

// 如果参数只有一个,那么path默认是 '/', 传入的参数作为处理函数
if (typeof route !== 'string') {
handle = route;
path = '/';
}

// 如果fn为一个app的实例,则将其自身handle方法的包裹给fn
if (typeof handle.handle === 'function') {
var server = handle;
server.route = path;
handle = function (req, res, next) {
server.handle(req, res, next);
};
}

// 如果fn为一个http.Server实例,则fn为其request事件的第一个监听器
if (handle instanceof http.Server) {
handle = handle.listeners('request')[0];
}

// 如果route参数的以 '/' 结尾,则删除 '/'
if (path[path.length - 1] === '/') {
path = path.slice(0, -1);
}

// 把中间件添加到stack数组中
debug('use %s %s', path || '/', handle.name || 'anonymous');
this.stack.push({ route: path, handle: handle });

// 返回自身,以便继续链式调用
return this;
};

handle方法:根据当前路径找到stack中所有与之相匹配的中间件,通过call方法调用中间件处理函数

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
70
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;

// 调用next方法传递的err信息,可以在下一个中间件处理函数的err参数中获取到
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 = '';
}

// 取出第一个中间件,index+1,再次调用取出第二个中间件....
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.length > route.length && path[route.length];
if (c && 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;
}
}

// 执行handler中匹配到的中间件
call(layer.handle, route, err, req, res, next);
}

next();
};

call方法::执行handler中匹配到的中间件

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
function call(handle, route, err, req, res, next) {
// handle函数的参数个数(3个参数为一般中间件,4个参数为错误处理中间件)
// next参数接收的是上面定义的next函数,然后传入到中间件的handle函数中,handle函数同样通过next参数接 收,所以在中间件中调用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) {
// 执行错误处理中间件
handle(err, req, res, next);
return;
} else if (!hasError && arity < 4) {
// 执行一般中间件
handle(req, res, next);
return;
}
} catch (e) {
// replace the error
error = e;
}

// continue
next(error);
}

connect运行过程

通过下面的这张图,总结一下connect工作流程,app.use方法负责把中间件添加到stack数组中,中间件会被格式化成形为{route: route , handle : fn}的匿名对象存放 ;app.handle方法根据当前路径找到stack中所有与之相匹配的中间件,并通过call方法调用中间件处理函数 ;app.call方法根据handle函数的参数个数(3个参数为一般中间件,4个参数为错误处理中间件) 来执行中间件,并把接收的next函数传给中间件。

mark

完整源码

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
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
'use strict';

// 引入依赖
var debug = require('debug')('connect:dispatcher');
var EventEmitter = require('events').EventEmitter;
var finalhandler = require('finalhandler');
var http = require('http');
var merge = require('utils-merge');
var parseUrl = require('parseurl');


// 模块出口
module.exports = createServer;


// 判断当前环境,初始化proto 对象
var env = process.env.NODE_ENV || 'development';
var proto = {};


var defer = typeof setImmediate === 'function'
? setImmediate
: function(fn){ process.nextTick(fn.bind.apply(fn, arguments)) }


function createServer() {
// app 函数对象,可以添加属性和方法
function app(req, res, next){
app.handle(req, res, next);
}
// 相当于Object.assign
merge(app, proto); // 继承proto的属性和方法
merge(app, EventEmitter.prototype); // 继承EventEmitter(事件派发器)属性和方法
app.route = '/';
app.stack = []; // 存放中间件的数组,中间件会被格式化成形为{route: route , handle : fn}的匿名对象存放
return app;
}

// 监听指定端口号
proto.listen = function listen() {
var server = http.createServer(this); // this ——> app函数对象,作为request事件的处理函数
return server.listen.apply(server, arguments); // 从 arguments 拿到端口号
};

// 添加中间件
proto.use = function use(route, fn) {
var handle = fn;
var path = route;

// 如果参数只有一个,那么path默认是 '/', 传入的参数作为处理函数
if (typeof route !== 'string') {
handle = route;
path = '/';
}

// 如果fn为一个app的实例,则将其自身handle方法给fn
if (typeof handle.handle === 'function') {
var server = handle;
server.route = path;
handle = function (req, res, next) {
server.handle(req, res, next);
};
}

// 如果fn为一个http.Server实例,则fn为其request事件的第一个监听器
if (handle instanceof http.Server) {
handle = handle.listeners('request')[0];
}

// 如果route参数的以 '/' 结尾,则删除 '/'
if (path[path.length - 1] === '/') {
path = path.slice(0, -1);
}

// 把中间件添加到stack数组中
debug('use %s %s', path || '/', handle.name || 'anonymous');
this.stack.push({ route: path, handle: handle });

// 返回自身,以便继续链式调用
return this;
};

// 这个函数作用是根据当前路径找到stack中所有与之相匹配的中间件,并通过call方法调用中间件处理函数
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.length > route.length && path[route.length];
if (c && 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方法调用中间件的handle函数处理对应的路由
call(layer.handle, route, err, req, res, next);
}

next();
};

function call(handle, route, err, req, res, next) {
// handle函数的参数个数(3个参数为一般中间件,4个参数为错误处理中间件)
// next参数接收的是上面定义的next函数,然后传入到中间件的handle函数中,handle函数同样通过next参数接收,所以在中间件中调用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) {
// 执行错误处理中间件
handle(err, req, res, next);
return;
} else if (!hasError && arity < 4) {
// 执行一般中间件
handle(req, res, next);
return;
}
} catch (e) {
// replace the error
error = e;
}

// continue
next(error);
}

function logerror(err) {
if (env !== 'test') console.error(err.stack || err.toString());
}

function getProtohost(url) {
if (url.length === 0 || url[0] === '/') {
return undefined;
}

var searchIndex = url.indexOf('?');
var pathLength = searchIndex !== -1 ? searchIndex : url.length;
var fqdnIndex = url.substr(0, pathLength).indexOf('://');

return fqdnIndex !== -1 ? url.substr(0, url.indexOf('/', 3 + fqdnIndex)) : undefined;
}