My Little World

KOA框架剥洋葱原理

收集

new 一个koa实例后,调用use接口会将传入的中间件函数push到middleware属性数组中做收集

1
2
3
4
5
6
7
8
9
10
11
12
13
14
application.js

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

根据koa框架版本不同,需要将使用generator函数写的中间件转化成基于promise的函数形式

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
const convert = require('koa-convert');

function convert (mw) {
if (typeof mw !== 'function') {
throw new TypeError('middleware must be a function')
}
//再次确认是否是generator函数
if (mw.constructor.name !== 'GeneratorFunction') {
// 假设它是一个基于promise的中间件
// 就直接返回
return mw
}
//如果是generator函数就转换成promise形式
const converted = function (ctx, next) {
return co.call(ctx, mw.call(ctx, createGenerator(next)))
}
converted._name = mw._name || mw.name
return converted
}

const co = require('co')

function co(gen) {
var ctx = this;
var args = slice.call(arguments, 1)

// we wrap everything in a promise to avoid promise chaining,
// which leads to memory leak errors.
// see https://github.com/tj/co/issues/180
return new Promise(function(resolve, reject) {
if (typeof gen === 'function') gen = gen.apply(ctx, args);
if (!gen || typeof gen.next !== 'function') return resolve(gen);

onFulfilled();

/**
* @param {Mixed} res
* @return {Promise}
* @api private
*/

function onFulfilled(res) {
var ret;
try {
ret = gen.next(res);
} catch (e) {
return reject(e);
}
next(ret);
}

/**
* @param {Error} err
* @return {Promise}
* @api private
*/

function onRejected(err) {
var ret;
try {
ret = gen.throw(err);
} catch (e) {
return reject(e);
}
next(ret);
}

/**
* Get the next value in the generator,
* return a promise.
*
* @param {Object} ret
* @return {Promise}
* @api private
*/

function next(ret) {
if (ret.done) return resolve(ret.value);
var value = toPromise.call(ctx, ret.value);
if (value && isPromise(value)) return value.then(onFulfilled, onRejected);
return onRejected(new TypeError('You may only yield a function, promise, generator, array, or object, '
+ 'but the following object was passed: "' + String(ret.value) + '"'));
}
});
}

整合

调用listen函数开启服务时,整合所有中间件为一个大函数,大函数中进行剥洋葱流程
在收到请求时调用

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
application.js

listen(...args) {
debug('listen');
const server = http.createServer(this.callback());
return server.listen(...args);
}

callback() {
const fn = compose(this.middleware); //整合中间件

if (!this.listenerCount('error')) 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) {
//通过传递过来的ctx,获取到原生的可写流
const res = ctx.res;
//设置默认状态码
res.statusCode = 404;
const onerror = err => ctx.onerror(err);
const handleResponse = () => respond(ctx);
onFinished(res, onerror);
//调用中间件大函数 同时准备catch处理中间件级错误
return fnMiddleware(ctx).then(handleResponse).catch(onerror);
}

整合中间件 ,返回大函数,大函数中有剥洋葱模型

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!')
}
//调用时fnMiddleware(ctx).then(handleResponse).catch(onerror);
//next 是undefined
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 {//try-catch 用于保证错误在promise的情况能够正常捕获
return Promise.resolve(fn(context, dispatch.bind(null, 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
let index = -1
return dispatch(0) //从第一个中间件开始执行
function dispatch (i) {
//防止next 在一个中间件中被调用多次
if (i <= index) return Promise.reject(new Error('next() called multiple times'))
index = i
let fn = middleware[i] //拿到中间件
if (i === middleware.length) fn = next //如果中间件函数全部调用完,fn 赋值为next,next为undefined
if (!fn) return Promise.resolve() //fn为next=>undefined时成立,即执行到最后返回一个Promise.resolve() 可以继续写then
try {//try-catch 用于保证错误在promise的情况能够正常捕获
//执行中间件函数,传入ctx,next参数,next即dispatch.bind(null, i + 1),
//递归调用下一个中间件函数
//从这一步可以实现await next()时,先执行下一个中间件函数
//中间件有调用next,就利用await暂停当前中间件执行,开始执行下一个中间件
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
} catch (err) {
return Promise.reject(err)
}
}

利用await暂停当前中间件执行,调用next开始执行下一个中间件

如果下一个中间件没有调next,且不是最后一个中间件,
即不会再调用dispatch执行下下一个中间件,则后续中间件都不会被执行到

如果下一个中间件是最后一个中间件,且在其中调用了next,则不会走到这个流程,
在上面的流程中就return

如果下一个中间件是最后一个中间件,则执行完return Promise.resolve
上一个中间件await 拿到结果后继续执行await后面的代码
执行完await后面代码后,上上一个中间件await 拿到结果,继续执行当前中间件await后代码
依次类推,直到第一个中间件await后代码执行完毕,整个中间件流程,
即剥洋葱流程先内层后外层执行流程完毕
返回promise 接着then处理响应或者中间有错误捕获错误
fnMiddleware(ctx).then(handleResponse).catch(onerror)