本文参加了由公众号@若川视野 发起的每周源码共读活动, 点击了解详情一起参与。
这是
学习源码整体架构系列第七篇,链接: juejin.cn/post/684490… 。
Koa-compose 中间件
在koa中,请求和响应都放在中间件的第一个参数context对象中
引用Koa中文文档中的一段:
如果您是前端开发人员,您可以将
next(); 之前的任意代码视为“捕获”阶段,这个简易的gif说明了async函数如何使我们能够恰当地利用堆栈流来实现请求和响应流:
- 创建一个跟踪响应时间的日期
- next()
- 创建另一个跟踪响应时间的日期
- next()
- next()
- 处理请求,响应体为'Hello World';
- 计算持续时间
- 输出日志行
- 设置X-Response-Time头字段
- 交给 Koa 处理响应
new Koa()结果app是什么?
app 实例、context、request、request 官方API文档
Koa主流程简化梳理
class EventEmitter {
// nodejs内置模块
constructor(options?: EventEmitterOptions) {};
}
class Application extends Emitter {
constructor(options) {
super()
options = options || {}
this.middleware = []
// context from require('./context')
/**
* inspect()
* response
* request
*/
this.context = Object.create(context)
}
use(fn){
this.middleware.push(fn);
return this;
}
listen(){
// callback() {}
// handleRequest() {}
const fnMiddleware = compose(this.middleware);
const ctx = this.context;
const handleResponse = () => respond(ctx);
const onerror = function(){
console.log('onerror');
};
fnMiddleware(ctx).then(handleResponse).catch(onerror);
}
}
function respond(ctx){
console.log('handleResponse');
console.log('response.end', ctx.body);
}
主流程的关键代码在listen函数中的compose这个函数,接下来分析compose函数
koa-compose 源码(洋葱模型实现)
通过app.use()添加若干函数,但要把它们串联起来执行。compose函数:传入一个数组,返回一个函数。对传入的参数做类型校验数组的每一项是不是函数。
// 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-compose返回的是一个Promise,Promise中取出第一个函数(app.use添加的中间件),传入context和第一个next函数来执行。
第一个next函数里返回的是一个Promise,Promise中取出第二个函数(app.use添加的中间件),传入context和第二个next函数来执行。
第二个next函数里返回的是一个Promise,Promise中取出第三个函数(app.use添加的中间件),传入context和第三个next函数来执行。
第三个...
以此类推。最后一个中间件有调用next函数,则返回Promise.resolve。如果没有,则不执行next函数。这样就把所有的中间件串联起来。这就是常说的洋葱模型。
错误处理
文档中写了三种捕获错误的方式:
- ctx.onerror 中间件中的错误捕获
- app.on('error', (err) => {})最外层实例事件监听形式
- app.onerror => (err) => {} 重写onerror自定义形式
ctx.on error
module.exports = {
onerror(){
// delegate
// app 是在new Koa() 实例
this.app.emit('error', err, this);
}
}
app.use(async (ctx, next) => {
try {
await next();
} catch (err) {
err.status = err.statusCode || err.status || 500;
throw err;
}
});
try catch 错误或被fnMiddleware(ctx).then(handleResponse).catch(onerror);这里的onerror是ctx.onerror
而ctx.onerror 函数中有调用了this.app.emit('error', err, this);,所以在最外围的app.on('error',err => {})可以捕获中间件链中的错误。因为koa继承自events模块,所以有emit和on等方法。
koa2 和 koa1的简单对比
koa1中主要是generator函数,koa会自动转换generator函数
// koa 将转换
app.use(function *(next) {
const start = Date();
yield next;
const ms = Date.now() - start;
console.log(`${this.method} ${this.url} - ${ms}ms`);
})
koa-convert 源码
class Koa extends Emitter {
use(fn) {
if (typeof fn !== 'function') throw new TypeError('middleware must be a function!');
// 判断是否为generator函数,如果是则使用koa-convert暴露的方法convert来转换重新赋值,再存入middleware,后续再使用。
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-convert 核心源码
function convert() {
return function (ctx, next) {
return co.call(ctx, mw.call(ctx, createGenerator(next)))
}
function * createGenerator(next) {
return yield next()
}
}
最后还是通过co转换的。
co源码
Koa与express对比
总结
koa-compose是将app.use添加到middleware数组中的中间件(函数),通过Promise串联起来,next()返回的是一个Promise。
koa-convert判断app.use传入的函数是否为generator函数,如果是则用koa-convert来转换,最后还是调用co函数。
co原理:其实就是通过不断调用generator函数的next函数,来达到自动执行generator函数的效果(类似于async、await函数的自动执行)。
koa框架总结:主要就是四个核心概念,洋葱模型(把中间件串联起来),http请求上下文,http请求对象,http响应对象。
此文章为01月Day1源码共读,每一次脑海里闪过努力的念头,都是未来的你在向你求救。