使用体验
koa
const Koa = require('koa');
const app = new Koa();
app.use(ctx => {
ctx.body = 'Hello Koa';
});
app.listen(3000);
1
2
3
4
5
6
express
const app = require("express")();
app.use((req,res,next)=>{
res.status(200).send("<h1>headers ...</h1>");
});
app.listen(3001);
1
2
3
4
5
注意:本文全部采用es6语法编写,如果环境不支持请自行升级node或者使用babel进行转码。
启动方式
koa采用了new Koa()的方式,而express采用传统的函数形式,对比源码如下:
//koa
const Emitter = require('events');
module.exports = class Application extends Emitter {
...
}
//express
exports = module.exports = createApplication;
function createApplication() {
...
}
1
2
3
4
5
6
7
8
9
10
可以看到koa@2采用了es6的语法实现,继承了Emitter类,具体信息可以参考Emitter说明。这就意味着koa@2只能在es6以上的环境下运行,低版本可以考虑使用koa@1.x。而express则比较传统,使用的是function的形式导出。
2. 中间件形式二者不一样,这是由二者处理中间件的逻辑差异导致的,实际上这也是二者最根本的差别,具体的分析留作后面进行对比,这里主要对比二者的使用上的差别,如下所示:
express处理多个中间件
const app = require("express")();
app.use((req,res,next)=>{
console.log("first");
//next();
});
app.use((req,res,next)=>{
console.log("second");
//next();
});
app.use((req,res,next)=>{
console.log("third");
res.status(200).send("<h1>headers ...</h1>");
});
app.listen(3001);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
如果写成这样,终端只会打印出first,而且不会反悔,前端请求会一直等待到超时,导致这一问题的原因是:express必须主动调用next()才能让中间价继续执行,放开注释即可。这也保证了我们可以自主控制如何响应请求。
koa处理多个中间件
const Koa = require('koa');
const app = new Koa();
app.use((ctx,next) => {
ctx.body = 'Hello Koa-1';
next();
});
app.use((ctx,next) => {
ctx.body = 'Hello Koa-2';
next();
});
app.use((ctx,next) => {
ctx.body = 'Hello Koa-3';
next();
});
app.listen(3000);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
与express类似,koa中间件的入参也有两个,后一个就是next。next的功能与express一样,这里不再赘述。
上面介绍了koa的next()的功能,这里的next()需要同步调用,千万不要采用异步调用,不要写成下面的形式,这样相当于未调用next(),具体原因后面源码部分会分析:
app.use((ctx,next) => {
ctx.body = 'Hello Koa-2';
setTimeout(()=>next(),3000);
//next();
});
1
2
3
4
5
虽然上面分析了二者的使用逻辑不一样,但是由于koa在入参处给出了context,而该结构体包含了我们返回请求的所有信息,所以我们仍然可以写出下面的代码:
const Koa = require('koa');
const app = new Koa();
app.use((ctx)=>{
const res = ctx.res;
res.writeHead(200, {'Content-Type': 'text/html;charset=utf-8','Accept-Language':'zh-CN,zh;q=0.8,en;q=0.6'});
res.end('<h1>标题</h1>');
});
// response
app.use(ctx => {
ctx.body = 'Hello Koa';
});
app.listen(3000);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
这样的逻辑就和express很类似了,原理也一样。这样写以后,前端的请求得到的结果就是<h1>标题</h1>,而后续的app.use实际并没有得到执行。
express分路由处理
express的代码一般如下:
const app = require("express")();
app.use("/first",(req,res,next)=>{
console.log("first");
res.status(200).send("<h1>headers-first ...</h1>");
});
app.use("/second",(req,res,next)=>{
console.log("second");
res.status(200).send("<h1>headers-second ...</h1>");
});
app.use("/third",(req,res,next)=>{
console.log("third");
res.status(200).send("<h1>headers-third ...</h1>");
});
app.listen(3001);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
这很好理解,根据请求路径返回不同结果,koa呢?
koa分路由处理
const Koa = require('koa');
const app = new Koa();
app.use("/",ctx => {
ctx.body = 'Hello Koa';
});
app.listen(3000);
1
2
3
4
5
6
这么写会报错,因为koa本身并不支持按路由相应,如果需要这么做,可以通过引入第三方包实现。在koajs中有一个简单的router包。
具体写法如下:
//摘抄自Koa Trie Router
const Koa = require('koa')
const Router = require('koa-trie-router')
let app = new Koa()
let router = new Router()
router
.use(function(ctx, next) {
console.log('* requests')
next()
})
.get(function(ctx, next) {
console.log('GET requests')
next()
})
.put('/foo', function (ctx) {
ctx.body = 'PUT /foo requests'
})
.post('/bar', function (ctx) {
ctx.body = 'POST /bar requests'
})
app.use(router.middleware())
app.listen(3000)
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
在具体应用中也可以采用其他的路由包来做,在github上能搜到不少。
另外,由于实现的原因,下面介绍一个有意思的现象,看下面两段代码,初衷是打印请求处理耗时。
koa版本
const Koa = require('koa');
const app = new Koa();
app.use((ctx,next) => {
ctx.body = 'Hello Koa-1';
let start = new Date();
next().then(()=>{
console.log("time cost:",new Date()-start);
});
});
app.use((ctx,next) => {
ctx.body = 'Hello Koa-2';
next();
});
app.use((ctx,next) => {
ctx.body = 'Hello Koa-3';
next();
});
app.listen(3000);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
由于koa采用了promise的方式处理中间件,next()实际上返回的是一个promise对象,所以可以用上面简单的方式记录处理耗时。如果在es7下,可以采用更简单的写法:
const Koa = require('koa');
const app = new Koa();
app.use(async (ctx,next) => {
ctx.body = 'Hello Koa-1';
let start = new Date();
await next();
console.log("time cost:",new Date()-start);
});
app.use(async (ctx,next) => {
ctx.body = 'Hello Koa-2';
//这里用了一个定时器表示实际的操作耗时
await new Promise((resolve,reject)=>setTimeout(()=>{next();resolve();},3000));
});
app.use((ctx,next) => {
ctx.body = 'Hello Koa-3';
next();
});
app.listen(3000);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
这样只需要在入口放置一个中间件即可完成耗时记录。
express版本
由于express并没有使用promise而是采用了回调的方式处理中间件,所以无法采用上面这样便利的方式获取耗时。即便是对next()进行封装,也无济于事,因为必须保证后续的next()全部都被封装才能得到正确的结果。
下面给出一个参考实现:
let time = null;
.use('/', (req, res, next) => {
time = Date.now();
next()
})
.use('/eg', bidRequest)
.use('/', (req, res, next) => {
console.log(`<= time cost[${req.baseUrl}] : `, Date.now() - time, 'ms');
})
1
2
3
4
5
6
7
8
9
总结
koa和express的区别还是比较大的,koa的内容很少,就是对nodejs本身的createServer函数做了简单的封装,没有做很多的延伸;而express主要是比koa多了router。二者的的代码思路还是很不一样的,不过实际使用中并不会有太大障碍。
源码分析
koa
koa的源码主要有四个文件:application.js, context.js, request.js, response.js
context.js
context没有实际功能性代码,只是一些基础函数和变量,下面是代码片段。
inspect() {
return this.toJSON();
},
toJSON() {
return {
request: this.request.toJSON(),
response: this.response.toJSON(),
app: this.app.toJSON(),
originalUrl: this.originalUrl,
req: '<original node req>',
res: '<original node res>',
socket: '<original node socket>'
};
},
1
2
3
4
5
6
7
8
9
10
11
12
13
14
request.js
该文件中主要是一堆set和get函数,主要是用于获取请求结构体的特定字段或者修改特定字段,比如下面获取ip的函数,代码很好理解:
get ips() {
const proxy = this.app.proxy;
const val = this.get('X-Forwarded-For');
return proxy && val
? val.split(/s*,s*/)
: [];
},
1
2
3
4
5
6
7
response.js
response与request对应,主要是一些处理res的工具类,下面是代码片段,用于设置和获取res的content-length:
set length(n) {
this.set('Content-Length', n);
},
get length() {
const len = this.header['content-length'];
const body = this.body;
if (null == len) {
if (!body) return;
if ('string' == typeof body) return Buffer.byteLength(body);
if (Buffer.isBuffer(body)) return body.length;
if (isJSON(body)) return Buffer.byteLength(JSON.stringify(body));
return;
}
return ~~len;
},
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
其中用到了~~len,这个有点意思,两次取反,可以保证输出为数字,如果len为字符串则返回0。(第一次见…)
application.js
上文中用到的app就是在该文件中定义的,也是koa的核心所在,这里挑选几个成员函数进行分析(整个文件代码也就不到250行,自己看完压力也不大)。
module.exports = class Application extends Emitter {
/*
构造函数:把req,res,env等常用的变量全都塞进了context,所以我们在中间件中拿到context以后,就可以随心所欲地操作req和res了。
*/
constructor() {
super();
this.proxy = false;
this.middleware = [];
this.subdomainOffset = 2;
this.env = process.env.NODE_ENV || 'development';
this.context = Object.create(context);
this.request = Object.create(request);
this.response = Object.create(response);
}
/*
实际就是调用了nodejs本身的createServer,没有任何区别。
*/
listen() {
debug('listen');
const server = http.createServer(this.callback());
return server.listen.apply(server, arguments);
}
//下面分析
use(fn) {
if (typeof fn !== 'function') throw new TypeError('middleware must be a function!');
if (isGeneratorFunction(fn)) {
deprecate('Support for generators will be removed in v3. ' +
'See the documentation for examples of how to convert old middleware ' +
'https://github.com/koajs/koa/blob/master/docs/migration.md');
fn = convert(fn);
}
debug('use %s', fn._name || fn.name || '-');
this.middleware.push(fn);
return this;
}
//下面分析
callback() {
const fn = compose(this.middleware);
if (!this.listeners('error').length) this.on('error', this.onerror);
const handleRequest = (req, res) => {
res.statusCode = 404;
const ctx = this.createContext(req, res);
const onerror = err => ctx.onerror(err);
const handleResponse = () => respond(ctx);
onFinished(res, onerror);
return fn(ctx).then(handleResponse).catch(onerror);
};
return handleRequest;
}
//新建context
createContext(req, res) {
const context = Object.create(this.context);
const request = context.request = Object.create(this.request);
const response = context.response = Object.create(this.response);
context.app = request.app = response.app = this;
context.req = request.req = response.req = req;
context.res = request.res = response.res = res;
request.ctx = response.ctx = context;
request.response = response;
response.request = request;
context.originalUrl = request.originalUrl = req.url;
context.cookies = new Cookies(req, res, {
keys: this.keys,
secure: request.secure
});
request.ip = request.ips[0] || req.socket.remoteAddress || '';
context.accept = request.accept = accepts(req);
context.state = {};
return context;
}
//下面分析
function respond(ctx) {
// allow bypassing koa
if (false === ctx.respond) return;
const res = ctx.res;
if (!ctx.writable) return;
let body = ctx.body;
const code = ctx.status;
// ignore body
if (statuses.empty[code]) {
// strip headers
ctx.body = null;
return res.end();
}
if ('HEAD' == ctx.method) {
if (!res.headersSent && isJSON(body)) {
ctx.length = Buffer.byteLength(JSON.stringify(body));
}
return res.end();
}
// status body
if (null == body) {
body = ctx.message || String(code);
if (!res.headersSent) {
ctx.type = 'text';
ctx.length = Buffer.byteLength(body);
}
return res.end(body);
}
// responses
if (Buffer.isBuffer(body)) return res.end(body);
if ('string' == typeof body) return res.end(body);
if (body instanceof Stream) return body.pipe(res);
// body: json
body = JSON.stringify(body);
if (!res.headersSent) {
ctx.length = Buffer.byteLength(body);
}
res.end(body);
}
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
下面重点看use,callback,respond这三个函数,实际上理解koa的信息流看这三个函数的源码就差不多足够了。
use : 内容不多,其中第一个if用于安全检查,第二个if用于实现对generator函数的兼容,具体实现过程在is-generator-function这个包里面,有兴趣可以看看,还是挺有技巧的,参考借用。use最终仅仅就是把中间件push到了this.middleware数组里,并没有任何实质的逻辑操作。
respond : 该函数就是响应请求的地方,这也是为什么我们可以不用主动地响应请求。函数里做了很多判断,主要是防止二次响应以及特殊特定的响应的请求。
callback : callback用于生成createServer函数的回调,即handleRequest函数。handleRequest的返回值正是一个promise对象。注意这里调用了一个compose方法,该方法的作用就是把中间件数组转换成一个函数,以方便使用。具体的实现在koa-compose这个包里,这里摘抄其中的一段来分析。
//这就是compose(...)返回的函数
function (context, next) {
// last called middleware #
let index = -1
return dispatch(0)
function dispatch (i) {
if (i <= index) return Promise.reject(new Error('next() called multiple times'))
index = i
let fn = middleware[i]
if (i === middleware.length) fn = next
if (!fn) return Promise.resolve()
try {
return Promise.resolve(fn(context, function next () {
return dispatch(i + 1)
}))
} catch (err) {
return Promise.reject(err)
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
可以看到,实际上这里就是用闭包实现了对中间件数组的遍历。具体思路会把第i+1个中间件作为next传给第i个中间件,这也就是为什么必须主动调用next的原因,以为如果不主动调用next这一循环就会提前结束了,后续的中间件就无法得到执行。
到此为止,koa源码分析就结束了,koa源码很少,没有多余的东西,甚至连路由都需要引入其他的包。
express
express的源码比koa多了不少东西,这里仅仅对比核心部分,忽略其他部分的内容。
//express.js
function createApplication() {
var app = function(req, res, next) {
app.handle(req, res, next);
};
mixin(app, EventEmitter.prototype, false);
mixin(app, proto, false);
// expose the prototype that will get set on requests
app.request = Object.create(req, {
app: { configurable: true, enumerable: true, writable: true, value: app }
})
// expose the prototype that will get set on responses
app.response = Object.create(res, {
app: { configurable: true, enumerable: true, writable: true, value: app }
})
app.init();
return app;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
这里的express就是我们上文中引入的express对象,可以看到,实际上该函数就是把一些常用的功能和变量绑定到了app对象中去,我们在代码中使用的app.eg_funcs之类的方法都是从这里继承得到的。实际上该对象并不局限于使用app.listen()来启动一个服务,下面是listen函数的代码。
//application.js
app.listen = function listen() {
var server = http.createServer(this);
return server.listen.apply(server, arguments);
};
1
2
3
4
5
调用app.listen可以启动一个服务器,实际上我们也可以直接手动写出这两句代码来启动一个服务。在socket.io和https服务中就需要自己来完成这一过程。
下面是app.use的源码:
app.use = function use(fn) {
var offset = 0;
var path = '/';
// default path to '/'
// disambiguate app.use([fn])
if (typeof fn !== 'function') {
var arg = fn;
while (Array.isArray(arg) && arg.length !== 0) {
arg = arg[0];
}
// first arg is the path
if (typeof arg !== 'function') {
offset = 1;
path = fn;
}
}
var fns = flatten(slice.call(arguments, offset));
if (fns.length === 0) {
throw new TypeError('app.use() requires middleware functions');
}
// setup router
this.lazyrouter();
var router = this._router;
fns.forEach(function (fn) {
// non-express app
if (!fn || !fn.handle || !fn.set) {
return router.use(path, fn);
}
debug('.use app under %s', path);
fn.mountpath = path;
fn.parent = this;
// restore .app property on req and res
router.use(path, function mounted_app(req, res, next) {
var orig = req.app;
fn.handle(req, res, function (err) {
setPrototypeOf(req, orig.request)
setPrototypeOf(res, orig.response)
next(err);
});
});
// mounted an app
fn.emit('mount', this);
}, this);
return this;
}
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
这里有一些对参数判断的逻辑,比如第一个参数如果是路径还是函数,不过平时很少这么写。
从中可以看到实际上express是调用了router的use方法对中间件进行处理。router.use定义在/router/index.js中, 源码如下:
proto.use = function use(fn) {
var offset = 0;
var path = '/';
// default path to '/'
// disambiguate router.use([fn])
if (typeof fn !== 'function') {
var arg = fn;
while (Array.isArray(arg) && arg.length !== 0) {
arg = arg[0];
}
// first arg is the path
if (typeof arg !== 'function') {
offset = 1;
path = fn;
}
}
var callbacks = flatten(slice.call(arguments, offset));
if (callbacks.length === 0) {
throw new TypeError('Router.use() requires middleware functions');
}
for (var i = 0; i < callbacks.length; i++) {
var fn = callbacks[i];
if (typeof fn !== 'function') {
throw new TypeError('Router.use() requires middleware function but got a ' + gettype(fn));
}
// add the middleware
debug('use %o %s', path, fn.name || '<anonymous>')
var layer = new Layer(path, {
sensitive: this.caseSensitive,
strict: false,
end: false
}, fn);
layer.route = undefined;
this.stack.push(layer);
}
return this;
};
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
其中前大半段主要是一些准备工作(这种写法在express貌似很常见)。后面看到与koa直接把中间件push到数组的做法不同的是,express会把中间件封装成一个Layer,这样做也是为了更好地控制中间件的执行。Layer的代码在/router/layer.js中。(这里不再分析)
下面开始分析express是怎么响应请求的,从上面listen部分的代码可以看到,我们给createServer传了一个this,而这个this正是express()的返回值,定义在application.js里,源码如下:
var app = function(req, res, next) {
app.handle(req, res, next);
};
1
2
3
可以看到实际上app是调用了handle方法,而该方法是从application对象继承过来的,而查看application.js发现了下面代码:
//初始化 this._router
this._router = new Router({
caseSensitive: this.enabled('case sensitive routing'),
strict: this.enabled('strict routing')
});
this._router.use(query(this.get('query parser fn')));
this._router.use(middleware.init(this));
//使用 this._router
app.handle = function handle(req, res, callback) {
var router = this._router;
// final handler
var done = callback || finalhandler(req, res, {
env: this.get('env'),
onerror: logerror.bind(this)
});
// no routes
if (!router) {
debug('no routes defined on app');
done();
return;
}
router.handle(req, res, done);
};
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
可以看到实际上这里调用的是router.handle,下面看router的源码:
proto.handle = function handle(req, res, out) {
var self = this;
debug('dispatching %s %s', req.method, req.url);
var idx = 0;
var protohost = getProtohost(req.url) || ''
var removed = '';
var slashAdded = false;
var paramcalled = {};
// store options for OPTIONS request
// only used if OPTIONS request
var options = [];
// middleware and routes
var stack = self.stack;
// manage inter-router variables
var parentParams = req.params;
var parentUrl = req.baseUrl || '';
var done = restore(out, req, 'baseUrl', 'next', 'params');
// setup next layer
req.next = next;
// for options requests, respond with a default if nothing else responds
if (req.method === 'OPTIONS') {
done = wrap(done, function(old, err) {
if (err || options.length === 0) return old(err);
sendOptionsResponse(res, options, old);
});
}
// setup basic req values
req.baseUrl = parentUrl;
req.originalUrl = req.originalUrl || req.url;
next();
function next(err) {
var layerError = err === 'route'
? null
: err;
// remove added slash
if (slashAdded) {
req.url = req.url.substr(1);
slashAdded = false;
}
// restore altered req.url
if (removed.length !== 0) {
req.baseUrl = parentUrl;
req.url = protohost + removed + req.url.substr(protohost.length);
removed = '';
}
// signal to exit router
if (layerError === 'router') {
setImmediate(done, null)
return
}
// no more matching layers
if (idx >= stack.length) {
setImmediate(done, layerError);
return;
}
// get pathname of request
var path = getPathname(req);
if (path == null) {
return done(layerError);
}
// find next matching layer
var layer;
var match;
var route;
while (match !== true && idx < stack.length) {
layer = stack[idx++];
match = matchLayer(layer, path);
route = layer.route;
if (typeof match !== 'boolean') {
// hold on to layerError
layerError = layerError || match;
}
if (match !== true) {
continue;
}
if (!route) {
// process non-route handlers normally
continue;
}
if (layerError) {
// routes do not match with a pending error
match = false;
continue;
}
var method = req.method;
var has_method = route._handles_method(method);
// build up automatic options response
if (!has_method && method === 'OPTIONS') {
appendMethods(options, route._options());
}
// don't even bother matching route
if (!has_method && method !== 'HEAD') {
match = false;
continue;
}
}
// no match
if (match !== true) {
return done(layerError);
}
// store route for dispatch on change
if (route) {
req.route = route;
}
// Capture one-time layer values
req.params = self.mergeParams
? mergeParams(layer.params, parentParams)
: layer.params;
var layerPath = layer.path;
// this should be done for the layer
self.process_params(layer, paramcalled, req, res, function (err) {
if (err) {
return next(layerError || err);
}
if (route) {
return layer.handle_request(req, res, next);
}
trim_prefix(layer, layerError, layerPath, path);
});
}
function trim_prefix(layer, layerError, layerPath, path) {
if (layerPath.length !== 0) {
// Validate path breaks on a path separator
var c = path[layerPath.length]
if (c && c !== '/' && c !== '.') return next(layerError)
// Trim off the part of the url that matches the route
// middleware (.use stuff) needs to have the path stripped
debug('trim prefix (%s) from url %s', layerPath, req.url);
removed = layerPath;
req.url = protohost + req.url.substr(protohost.length + removed.length);
// Ensure leading slash
if (!protohost && req.url[0] !== '/') {
req.url = '/' + req.url;
slashAdded = true;
}
// Setup base URL (no trailing slash)
req.baseUrl = parentUrl + (removed[removed.length - 1] === '/'
? removed.substring(0, removed.length - 1)
: removed);
}
debug('%s %s : %s', layer.name, layerPath, req.originalUrl);
if (layerError) {
layer.handle_error(layerError, req, res, next);
} else {
layer.handle_request(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
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
这个函数很长,但是其实大部分内容都是匹配路由,类型检测等操作,实际的操作集中在next()函数中,与koa一样,这里也是采用闭包来循环遍历中间件数组。看next()中的执行部分可以看到,正常情况下,实际的操作是由layer.handle_request完成的,下面看layer.js源码:
//初始化
function Layer(path, options, fn) {
if (!(this instanceof Layer)) {
return new Layer(path, options, fn);
}
debug('new %o', path)
var opts = options || {};
this.handle = fn;
this.name = fn.name || '<anonymous>';
this.params = undefined;
this.path = undefined;
this.regexp = pathRegexp(path, this.keys = [], opts);
// set fast path flags
this.regexp.fast_star = path === '*'
this.regexp.fast_slash = path === '/' && opts.end === false
}
//处理单元
Layer.prototype.handle_request = function handle(req, res, next) {
var fn = this.handle;
if (fn.length > 3) {
// not a standard request handler
return next();
}
try {
fn(req, res, next);
} catch (err) {
next(err);
}
};
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
bingo,这里就是我们在调用use的时候初始化的Layer结构体,可以看到,实际上我们把中间件函数赋给了layer.handle,而在实际的处理函数handle_request中,正是调用了this.handle,总算找到了数据处理的根源了….
这里看到实际上router中的next()只是启动了中间件回调的过程,然后把自己传给下一个中间件,后续的中间件主动调用next()这样就可以传递下去了。
在处理中间件的逻辑上express可以理解为每次一个中间件执行完毕就去主动去通知“中心”去启动下一个中间件;而koa可以理解为链式过程,每一个中间件会启动后一个中间件。
到此为止,我们基本完成了koa和express的对比分析,二者各有自己的特点,在使用中可以根据需求选择最适合的解决方案。