使用體驗
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的對比分析,二者各有自己的特點,在使用中可以根據需求選擇最適合的解決方案。