Koa源码解析
koa框架总结:主要就是四个核心概念,洋葱模型(把中间件串联起来),http请求上下文(context)、http请求对象、http响应对象。
koa的中间件机制关键在于洋葱模型和app的实例上下文(context)
中间件本质
中间件本质是一个函数,这个函数传入两个参数,参数一是实例上下文(context),参数二是一个next方法
参数一 context:
context上挂载了koa实例上的属性,会传给每个中间件方法,使得中间件共享这个属性。然后编写一个增强中间件一般也是给这个context属性进行增添属性和方法。
参数二 next:
这个方法是把当前中间件的执行权给到下一个中间件,当下一个中间件函数执行完才返回执行权。
洋葱模型执行顺序:
洋葱模型实现原理
上文gif图的原理是怎么实现的呢?关键在于compose函数
compose函数,其传入一个数组,返回一个函数fn,这个函数fn返回一个Promise实例对象;
在koa源码中,koa将所有的中间件函数存放到middleware数组里,然后传给compose函数。
而这个compose函数的奥秘在哪里?让我们来看看源码
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 参数还能传一个中间件函数执行完之后的回调 next
* @return {Promise} --返回一个promise对象
* @api public
*/
return function (context, next) {
// last called middleware #
let index = -1
return dispatch(0)
//洋葱模型的原理在于这个dispatch函数
function dispatch (i) {
if (i <= index) return Promise.reject(new Error('next() called multiple times'))
index = i
let fn = middleware[i]
//这种情况表示已经把所有的中间件函数都处理了,如果有next回调,则把next回调传出执行
if (i === middleware.length) fn = next
if (!fn) return Promise.resolve()
try {
//执行中间件函数,并把返回值抛出
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
} catch (err) {
return Promise.reject(err)
}
}
}
}
洋葱模型的原理在于这个dispatch函数,通过控制变量i来控制哪个中间件函数调用,当调用中间件函数参数中next方法的时候,其实是调用dispatch(i+1)方法,既是在当前的函数执行上下文中调用下一个中间件函数。当下一个中间件函数执行完,就继续执行当前中间件函数,如果不调用next方法,则无法执行下一个中间件函数。
相当于:
// 这样就可能更好理解了。 这段代码若川写的
// simpleKoaCompose
const [fn1, fn2, fn3] = this.middleware;
const fnMiddleware = function(context){
return Promise.resolve(
fn1(context, function next(){
return Promise.resolve(
fn2(context, function next(){
return Promise.resolve(
fn3(context, function next(){
return Promise.resolve();
})
)
})
)
})
);
};
fnMiddleware(ctx).then(handleResponse).catch(onerror);
这种写法,让人直喊:妙啊~
Koa.prototype.handleRequest源码
compose函数返回的fn将在Koa.prototype.handleRequest方法中被调用,并通过onerror方法捕获异常
handleRequest(ctx, fnMiddleware) {
// fnMiddleware 就是compose函数返回的fn方法
const res = ctx.res;
res.statusCode = 404;
const onerror = err => ctx.onerror(err);
const handleResponse = () => respond(ctx);
onFinished(res, onerror);
return fnMiddleware(ctx) //这里生成一个promise对象,当promise的状态为fulfilled时,中间件函数以及执行。
.then(handleResponse).catch(onerror);//中间件执行期间抛出的异常在这里捕获
}
onerror(err) {
if (!(err instanceof Error)) throw new TypeError(util.format('non-error thrown: %j', err));
if (404 == err.status || err.expose) return;
if (this.silent) return;
const msg = err.stack || err.toString();
console.error();
console.error(msg.replace(/^/gm, ' '));
console.error();
}
Koa.prototype.use
在koa中,通过use方法,既
app.use(async(ctx,next)=>{
//编写中间件函数
})
这个方法给app实例增加中间件。
源码
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;
}
使用这个app.use时有一层判断,是否是generator函数,如果是则用koa-convert暴露的方法convert来转换重新赋值,再存入middleware,后续再使用。
而app.use方法就是将参数中的中间件函数放到this.middleware这个数组里保存。
koa-convert源码:
function convert (mw) {
if (typeof mw !== 'function') {
throw new TypeError('middleware must be a function')
}
if (mw.constructor.name !== 'GeneratorFunction') {
// assume it's Promise-based middleware
return mw
}
const converted = function (ctx, next) {
return co.call(ctx, mw.call(ctx, createGenerator(next)))
}
converted._name = mw._name || mw.name
return converted
}
转换是通过co函数转换的。
co函数
手写简易版的co函数
简易版就写了个大概,各种错误情况没有考虑
function request(ms = 1000) {
return new Promise((resolve) => {
setTimeout(() => {
resolve('lalala');
}, ms);
});
}
function* generatorFunc(suffix = ''){
const res = yield request();
console.log(res, 'generatorFunc-res' + suffix);
const res2 = yield request();
console.log(res2, 'generatorFunc-res-2' + suffix);
const res3 = yield request();
console.log(res3, 'generatorFunc-res-3' + suffix);
const res4 = yield request();
console.log(res4, 'generatorFunc-res-4' + suffix);
}
function fulfilled(gen,resolve,returnValue) {
let ret=gen.next(returnValue)
if(ret.done){
resolve(ret.value)
return
}
let promise =ret.value;
promise.then(value => {
fulfilled(gen,resolve,value)
})
}
function autoGeneratorFunc( generatorFunc,...args) {
let gen=generatorFunc(args);
return new Promise((resolve,reject)=>{
fulfilled(gen,resolve)
});
}
autoGeneratorFunc(generatorFunc, '哈哈哈')
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) {
// 把参数传递给gen函数并执行
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) {
// 检查当前是否为 Generator 函数的最后一步,如果是就返回
if (ret.done) return resolve(ret.value);
// 确保返回值是promise对象。
var value = toPromise.call(ctx, ret.value);
// 使用 then 方法,为返回值加上回调函数,然后通过 onFulfilled 函数再次调用 next 函数。
if (value && isPromise(value)) return value.then(onFulfilled, onRejected);
// 在参数不符合要求的情况下(参数非 Thunk 函数和 Promise 对象),将 Promise 对象的状态改为 rejected,从而终止执行。
return onRejected(new TypeError('You may only yield a function, promise, generator, array, or object, '
+ 'but the following object was passed: "' + String(ret.value) + '"'));
}
});
}
参考
作者:若川
链接:juejin.cn/post/684490…
来源:稀土掘金
作者:阮一峰