koa2.x

koa是继express之后的又一个主流的node服务器框架,由express的原班人马打造,相比express更加轻量,内部不包含任何的中间件,我先从最新的koa2.2看起。koa2.x使用了ex2016草案中的新特性async/await。语法更加简洁,语义更加明显。node v7.6.0+才支持async语法,低版本如果要使用还要安装babel

async/await

前端处理异步的发展经历了几个阶段,从当初的回调函数,到es2015的promise,到koa1的generator函数的使用,再到async/await的语法使用,目的都是想脱离回调地狱,使得异步回调的代码看起来像同步执行代码一样优雅。先给出async的使用小例子:

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
function resolveAfter2Seconds(x) {
return new Promise(resolve => {
setTimeout(() => {
resolve(x);
}, 2000);
});
}
async function add1(x) {
var a = resolveAfter2Seconds(20);
var b = resolveAfter2Seconds(30);
return x + await a + await b;
}
add1(10).then(v => {
console.log(v); // prints 60 after 2 seconds.
});
async function add2(x) {
var a = await resolveAfter2Seconds(20);
var b = await resolveAfter2Seconds(30);
return x + a + b;
}
add2(10).then(v => {
console.log(v); // prints 60 after 4 seconds.
});
async function add3(x){
var a = 20;
var b = 30;
return x + a + b;
}
add3(10).then(v => {
console.log(v); //立即返回
})

async函数返回的是一个promise函数。而promise状态的变化只有等到async内部流程执行完成。如果有await,而await之后又是一个promise对象,执行流程就会停在await这一步,等待异步promise返回。再执行下一步流程。从语义上确实清晰很多。

koa中间件

回到koa2.x上面,给个例子说下,如何编写中间件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var koa = require('koa');
var app = new koa();
app.use(async function (ctx, next) {
console.log('>> one');
await next();
console.log('<< one');
});
app.use(async function (ctx, next) {
console.log('>> two');
ctx.body = 'two';
await next();
console.log('<< two');
});
app.use(async function (ctx, next) {
console.log('>> three');
await next();
console.log('<< three');
});
app.listen(3000);

如果把中间件想作一个栈,请求会从顶部的第一个中间件开始处理,遇到next()调用,就会进入下一个中间件中,直到最后没有next调用,再从栈底反弹,一个一个执行之前next()之后的代码。这种实现方式可以大概猜测下,应该是将中间件存放在一个数组中进行一些操作。

koa源码

koa源码不长,也很简洁漂亮,先看package.json,”main”:”lib/application.js”表明入口就是application.js,这里简单截取了几个重要的代码片段

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
module.exports = class Application extends Emitter {
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);
}
/**/
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;
}
/**/
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;
}
}

这里直接exports的就是Application类。我们使用var app = new koa(),可见app就是Application的实例。app.listen(3000)在koa中用来创建httpServer,实际和所有的node服务器框架一样,还是基于http.createServer的封装,这里的参数this.callback()作为参数正好来处理request和response。在callback中使用了一个compose对middleware数组进行组合,middleware中存放的正是使用app.use注册的一个个中间件,在use的api中,可以看到对generator的中间件注册方法进行了deprecate的提示,当然目前依然还是支持generator的写法,不用使用到了koa-convert进行转化,所以目前最好还是与时俱进使用await语法。
接着看compose内部代码,compose来源自koa-compose包:

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
'use strict'
const Promise = require('any-promise')
/**
* Expose compositor.
*/
module.exports = compose
/**
* Compose `middleware` returning
* a fully valid middleware comprised
* of all those which are passed.
*
* @param {Array} middleware
* @return {Function}
* @api public
*/
function compose (middleware) {
if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!')
for (const fn of middleware) {
if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!')
}
/**
* @param {Object} context
* @return {Promise}
* @api public
*/
return 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)
}
}
}
}

这里compose直接返回了一个匿名函数延迟执行,这个函数在fn(ctx)的时候被执行,这里的ctx实际基于原型继承自this.context。context实际就是一个简单的对象,有一些简单的api:比如toJSON(),onerror();并且又通过delegate的方式将request和response中的方法代理到上面。先dispatch(0), Promise.resolve的用法实际是将现有对象转化为Promise对象,这个具体可以看下阮一峰的promise使用介绍:http://es6.ruanyifeng.com/#docs/promise#Promise-resolve。这里的fn刚好是async函数,返回值是promise对象,Promise.resolve会直接返回这个promise对象
以开始的app.js代码为例。fn(ctx)执行结果相当于:

1
2
3
4
5
6
7
8
var tmp = async function (ctx) {
console.log('>> one');
await function(){
return dispath(i+1)
};
console.log('<< one');
}
tmp()

代码执行到await会进一步执行下一个dispatch,如此递归回调,等到i === middleware.length时候,fn = next。fn调用一直没有传递next参数,所以next一直是undefined,直接被resolve。最终递归临界条件达到,回溯。所有中间件执行完成之后会调用handleResponse,也就是respond

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
/**
* Response helper.
*/
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);
}

在这个方法中进行发送response的操作,即res.end。