koa 是什么
1. koa 2 做了的事情
- 基于 node 原生 req 和 res 为 request 和 response 对象赋能,并基于它们封装成一个 context 对象。
- 基于 async/await 中间件洋葱模型机制
2.koa1 和 koa2 在源码上的区别主要是于对异步中间件的支持方式的不同
- koa1 是使用 generator、yield)的模式。
- koa2 使用的是 async/await+Promise 的模式。下文主要是针对 koa2 版本源码上的讲解
Application
Application 类继承自 EventEmitter
http://nodejs.cn/api/events.html#events_class_eventemitter
1.EventEmitter
- addListener ,on 注册一个监听器
- once 为指定事件注册一个单次监听器,即 监听器最多只会触发一次,触发后立刻解除该监听器。
- removeListener 移除指定事件的某个监听器,监听器必须是该事件已经注册过的监听器。它接受两个参数,第一个是事件名称,第二个是回调函数名称。
- removeAllListeners 移除所有事件的所有监听器
- emit 按照监听器注册的顺序,同步地调用每个注册到名为 eventName 的事件的监听器,并传入提供的参数。
const EventEmitter = require('events');
const myEmitter = new EventEmitter();
// 第一个监听器。
myEmitter.on('event', function firstListener() {
console.log('第一个监听器');
});
// 第二个监听器。
myEmitter.on('event', function secondListener(arg1, arg2) {
console.log(`第二个监听器中的事件有参数 ${arg1}、${arg2}`);
});
// 第三个监听器
myEmitter.on('event', function thirdListener(...args) {
const parameters = args.join(', ');
console.log(`第三个监听器中的事件有参数 ${parameters}`);
});
console.log(myEmitter.listeners('event'));
myEmitter.emit('event', 1, 2, 3, 4, 5);
// Prints:
// [
// [Function: firstListener],
// [Function: secondListener],
// [Function: thirdListener]
// ]
// 第一个监听器
// 第二个监听器中的事件有参数 1、2
// 第三个监听器中的事件有参数 1, 2, 3, 4, 5
koa2 流程控制/中间件
koa2 中间件例子
打印顺序 1->3->5--->6->4->2
const Koa = require('koa');
const app = new Koa(); //创建一个App实例
const mid1 = async (ctx, next) => {
console.log('1');
await next();
console.log('2');
};
const mid2 = async (ctx, next) => {
console.log('3');
await next();
console.log('4');
};
const mid3 = async (ctx, next) => {
console.log('5');
await next();
console.log('6');
};
app.use(mid1);
app.use(mid2);
app.use(mid3);
//监听端口启动服务
app.listen(3000, () => {
console.log('server is running at http://localhost:3000');
});
koa-compose
1.源码
function compose(middleware) {
// 先对中间件的每一项进行类型检查,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
*/
// compose()返回一个匿名函数的结果,该匿名函数自执行了 dispatch() 这个函数,并传入了0作为参数。
return function (context, next) {
// 记录上一次执行中间件的位置 #
let index = -1;
return dispatch(0);
function dispatch(i) {
// 理论上 i 应该大于 index,因为每次执行一次都会把 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;
if (!fn) return Promise.resolve();
try {
// 传递了 context 和 next 函数两个参数。
// context 就是 koa 中的上下文对象 context。
// next 函数则是返回一个 dispatch(i+1) 的执行结果,
// i+1 ,传递这个参数就相当于执行了下一个中间件,从而形成递归调用.
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
} catch (err) {
return Promise.reject(err);
}
}
};
}
2.分析
dispatch(i+1) 的递归 如果自己写中间件,需要手动执行 await next()。只有执行了 next 函数,才能正确地执行下一个中间件。 因此每个中间件只能执行一次 next,如果在一个中间件内多次执行 next,就会出现问题。
为什么说通过 i<=index 就可以判断 next 执行多次? 因为正常情况下 index 必定会小于等于 i。如果在一个中间件中调用多次 next,会导致多次执行 dispatch(i+1)。 从代码上来看,每个中间件都有属于自己的一个闭包作用域,同一个中间件的 i 是不变的,而 index 是在闭包作用域外面的。 当第一个中间件即 dispatch(0) 的 next() 调用时,此时应该是执行 dispatch(1),在执行到下面这个判断。 c. async 本身返回的就是 Promise,为什么还要在使用 Promise.resolve() 包一层呢? 这是为了兼容普通函数,使得普通函数也能正常使用
中间件的执行机制?
先执行第一个中间件(因为 compose 会默认执行 dispatch(0)), 该中间件返回 Promise,然后被 Koa 监听,执行对应的逻辑(成功或失败)。
在执行第一个中间件的逻辑时,遇到 await next()时,会继续执行 dispatch(i+1),也就是执行 dispatch(1),会手动触发执行第二个中间件。 这时候,第一个中间件 await next() 后面的代码就会被 pending,等待 await next() 返回 Promise, 才会继续执行第一个中间件 await next() 后面的代码。
同样的在执行第二个中间件的时候,遇到 await next() 的时候,会手动执行第三个中间件,await next() 后面的代码依然被 pending, 等待 await 下一个中间件的 Promise.resolve。只有在接收到第三个中间件的 resolve 后才会执行后面的代码, 然后第二个中间件会返回 Promise,被第一个中间件的 await 捕获, 这时候才会执行第一个中间件的后续代码,然后再返回 Promise 。
以此类推,如果有多个中间件的时候,会依照上面的逻辑不断执行,先执行第一个中间件, 在 await next() 出 pending,继续执行第二个中间件,继续在 await next() 出 pending, 继续执行第三个中间,直到最后一个中间件执行完,然后返回 Promise, 然后倒数第二个中间件才执行后续的代码并返回 Promise, 然后是倒数第三个中间件, 接着一直以这种方式执行直到第一个中间件执行完, 并返回 Promise,从而实现文章开头那张图的执行顺序。
如果要写一个 koa2 的中间件,那么基本格式应该就长下面这样
jsasync function koaMiddleware(ctx, next) { try { // do something await next(); // do something } catch (err) { // handle err } }
koa2 如何做到集中处理所有中间件的错误?
- 首先 Application 类继承自 EventEmitter
- callback 中 注册 error 事件
callback() {
// 通过 compose() 这个方法,将传入的中间件数组转换并级联执行,
const fn = compose(this.middleware);
// listenerCount 返回名为 eventName 的事件的监听器数组的副本。
// 如果error 事件没有注册过 注册error事件
// 这里的是onerror是applications中
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;
}
- Application 中的 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();
}
- context 中的 onerror
onerror(err) {
// ...
this.app.emit('error', err, this);
//调用app中监听error事件
// ...
}
- handleRequest 中 如果中间执行出错,执行 onerror
handleRequest(ctx, fnMiddleware) {
const res = ctx.res;
res.statusCode = 404;
//这里使用了context的onerror;
const onerror = (err) => ctx.onerror(err);
const handleResponse = () => respond(ctx);
onFinished(res, onerror);
// 这里是中间件如果执行出错的话,都能执行到onerror的关键!!!
return fnMiddleware(ctx).then(handleResponse).catch(onerror);
}
Koa Delegator 属性代理
它的作用就是属性代理。这个代理库常用的方法有 getter,setter,method 和 access。
用法
假设准备了一个对象 target,为了方便访问其上 request 属性的内容,对 request 进行代理:
const target = {
request: {
name: 'laozhang',
say: function () {
console.log('Hello');
},
},
};
delegate(target, 'request').access('name').method('say');
console.log(target.name); // laozhang
target.name = 'zhangweijie';
console.log(target.name); // zhangweijie
console.log(target.name); // zhangweijie
console.log(target.name); // zhangweijie
target.say(); // Hello
ES6 Class 实现
/**
*
* @param { any } proto 代理的对象
* @param { string } target 被代理的对象
*/
function Delegator(proto, target) {
return new _Delegator(proto, target);
}
class _Delegator {
constructor(proto, target) {
this.proto = proto;
this.target = target;
}
/**
* Delegate method `name`.
*
* @param {String} name
* @return {Delegator} self
* @api public
*/
method(name) {
const proto = this.proto;
const target = this.target;
proto[name] = function () {
return this[target][name].apply(this[target], arguments);
};
return this;
}
/**
* Delegator accessor `name`.
*
* @param {String} name
* @return {Delegator} self
* @api public
*/
access(name) {
const proto = this.proto;
const target = this.target;
Object.defineProperty(proto, name, {
set: function (val) {
return (this[target][name] = val);
},
get: function () {
return this[target][name];
},
});
return this;
}
}
module.exports = Delegator;