koa2探秘

阅读背景:

  • 已经读过上一篇koa1的文章
  • async await
  • class

还是跟前面koa1文章类似,先看看koa2的使用方法;

koa2使用方法

1
2
3
4
5
6
7
8
9
10
const Koa = require('koa');
const app = new Koa();
app.use(async (ctx, next) => {
const start = Date.now();
await next();
const ms = Date.now() - start;
console.log(`${ctx.method} ${ctx.url} - ${ms}ms`);
});`
app.listen(8888);

koa2原理

从上面代码可以看出,koa2中间件函数使用了异步函数、关键字async await;

使用async与await带来了什么呢?

async await class

熟悉async与await的童鞋一定会知道这是JS最新处理异步的关键字;

async会把正常函数转化为async函数,执行后会返回promise;async 函数中可能会有 await 表达式,这将会使 async 函数暂停执行,等待 Promise 正常解决后继续执行 async 函数并返回解决结果。

async/await的目的是简化同步使用 promises 的做法,并对一组Promises执行一些操作。正如Promises类似于结构化回调,async/await类似于组合生成器和 promises。

先看看koa现在主体代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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(...args) {
debug('listen');
const server = http.createServer(this.callback());
return server.listen(...args);
}
...
}

从上面看出现在koa主体代码使用了最新es6中的class、解构与展开. 这样带来一个限制,只能使用new关键字来生成实例,不然报错.

app.callback

下面是callback源码以及req来临的处理代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
callback() {
const fn = compose(this.middleware);
if (!this.listeners('error').length) this.on('error', this.onerror);
const handleRequest = (req, res) => {
const ctx = this.createContext(req, res);
return this.handleRequest(ctx, fn);
};
return handleRequest;
}
handleRequest(ctx, fnMiddleware) {
const res = ctx.res;
res.statusCode = 404;
const onerror = err => ctx.onerror(err);
const handleResponse = () => respond(ctx);
onFinished(res, onerror);
return fnMiddleware(ctx).then(handleResponse).catch(onerror);
}

上面代码与koa1最大不同的是中间件函数的处理,之前是co.wrap(compose(this.middleware)), 现在是compose(this.middleware); 这个到最后执行的关键函数是什么,怎么调用参数来执行;

compose

先看看现在的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
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!')
}
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)
}
}
}
}

中间件处理之后生成函数其实就是

1
2
3
4
5
function(context, next) {
return Promise resolve(fn(context, function next() {
return dispatch(i+1)
}))
}

koa2执行流程

  1. 请求到来
  2. 执行fnMiddleware(ctx), 也就是上面的compose处理中间件得到的函数
  3. 执行dispatch(0)
  4. 其实就是执行中间件数组第一个中间件,直到await next()停止;
  5. 而next实际就是传递给第一个函数的第二个参数function next() {return dispatch(i+1)}
  6. 执行next函数,也就是执行dispatch(1), 从而进入到第二个中间件;
  7. 依次往复,直到执行到最后一个中间件next
  8. 当执行dispatch(n+1)时,n+1取到fn为空,则return Promise.resolve(), 接着执行最后一个中间件await后面的内容, 并返回一个resolve的Promise
  9. 然后返回到上一个中间件,执行await后面的内容,依次返回, 直到第一个中间件返回resolve了的promise
  10. 最后执行return fnMiddleware(ctx).then(handleResponse).catch(onerror);语句then后面的handleResponse
  11. 至此整个请求执行完所有中间件,并返回结果;

dispatch注意点

dispatch函数中有个需要注意的地方:

第一句if (i <= index) return Promise.reject(new Error('next() called multiple times'))

这个异常什么时候会执行呢, 当一个中间件中有多次await next()的时候,由于执行某一个中间件的时候根据闭包原理会存储一个i值,而多个await意味着多次执行next函数

1
function next() {return dispatch(i+1)}

第一次执行上面的函数返回后index就会等于i+1,当第二次执行到dispatch(i+1)里面,i值没变,就会得到i+1 === index, 报错。这也是koa1与koa2的不同点,也是解决在上一篇koa1文章结尾的问题.

总结

至此,就完成了koa2原理的核心部分,其余部分都是小变化。

也可以看出await与async简化了koa的实现,并且代码比koa1中的多次封装中间件、co函数的协助、委托生成器与迭代器好理解多了.