学习Koa框架之前,不得不提到Express。
Express是一个基于Node.js平台的极简、灵活的 web 应用开发框架,主要基于 Connect 中间件,并且自身封装了路由、视图处理等功能,使用人数众多。
Koa相对更为年轻,是 Express 原班人马基于 ES6/7 异步流程控制重新开发的框架, 框架自身不包含任何中间件,很多功能需要借助第三方中间件解决,解决了回调地狱和麻烦的错误处理问题。
- koa2与koa1的最大区别是koa2实现异步是通过
async/await,koa1实现异步是通过generator + co,而express实现异步是通过回调函数的方式。 - koa2与express 提供的API大致相同,express是大而全,内置了大多数的中间件,更让人省心,koa2不绑定任何的框架,干净简洁,小而精,更容易实现定制化,扩展性好。
- express是没有提供ctx来提供上下流服务,需要更多的手动处理,express本身是不支持洋葱模型的数据流入流出能力的,需要引入其他的插件。
源码结构
我们主要学习Koa2,首先看一下它的源码结构:
application.js、context.js、request.js、response.js。
-
application.js:Application(或Koa)负责管理中间件,以及处理请求
- application.js是
koa的入口文件,它向外导出了创建class实例的构造函数,它继承了events,这样就会赋予框架事件监听和事件触发的能力。 application还暴露了一些常用的api,比如toJSON、listen、use等等。 - listen的实现原理其实就是对http.createServer进行了一个封装,重点是这个函数中
传入的callback,它里面包含了中间件的合并,上下文的处理,对res的特殊处理。 - use是收集中间件,将多个中间件放入一个缓存队列中,然后通过koa-compose这个插件进行递归组合调用这一些列的中间件。
- application.js是
-
context.js:Context维护了一个请求的上下文环境
- koa的应用上下文ctx,其实就一个简单的对象暴露,里面的
重点在delegate,这个就是代理,这个就是为了开发者方便而设计的。 - ctx 主要的功能是代理 request 和 response 的功能,提供了对 request 和 response 对象的便捷访问能力。 比如我们要访问ctx.response.status但是我们通过delegate,可以直接访问ctx.status访问到它。
- koa的应用上下文ctx,其实就一个简单的对象暴露,里面的
-
request.js:Request对
req做了抽象和封装 -
response.js:Response对
res做了抽象和封装这两部分就是对原生的res、req的一些操作了,大量使用es6的get和set的一些语法,去取headers或者设置headers、还有设置body等等。
功能模块
koa框架需要实现四个大模块,分别是:
- 封装http模块的server、创建Koa类构造函数
- 构造request、response、context对象
- 中间件机制和洋葱模型的实现
- 错误捕获和错误处理
封装http模块的server、创建Koa类构造函数:
通过application.js源码得知,koa的服务器应用和端口监听,其实就是基于node的http.createServer原生代码进行了封装。我们来简单实现一下这个功能。
let http = require('http');
class Application{
constructor() {
this.callbackFunc = ()=>{};
}
//开启服务器实例并传入callback回调函数
listen(port) {
let server = http.createServer(this.callback());
server.listen(port);
}
//注册中间件和注册回调函数
use(fn) {
this.callbackFunc = fn;
}
callback() {
return (req, res) => {
this.callbackFunc(req, res);
};
}
}
module.exports = Application;
然后创建demo.js,引入application.js
let Koa = require('./application');
let app = new Koa();
app.use((req, res) => {
res.writeHead(200);
res.end('hello world');
});
app.listen(3000, () => {
console.log('listening on 3000');
});
构造request、response、context对象:
request、response两个功能模块分别对node的原生request、response进行了一个功能的封装,使用了getter和setter属性,基于node的对象req/res对象封装koa的request/response对象。
request.js封装了query、header、url、origin、path等,比如ctx.query就是返回url.parse(this.req.url, true).query。response.js封装了status、body、message等,比如ctx.status就是返回res.statusCode。context.js再将request和response挂载到了ctx上。
let http = require('http');
let context = require('./context');
let request = require('./request');
let response = require('./response');
createContext(req, res) {
let ctx = Object.create(this.context);
ctx.request = Object.create(this.request);
ctx.response = Object.create(this.response);
ctx.req = ctx.request.req = req;
ctx.res = ctx.response.res = res;
return ctx;
}
中间件机制和洋葱模型的实现:
koa的中间件机制是洋葱模型,多个中间件通过use放进一个数组队列,符合先进后出的原则。然后从外层开始执行,遇到next后进入队列中的下一个中间件,所有中间件执行完后开始回帧,执行队列中之前中间件中未执行的代码部分。
示例:
const Koa = require('koa');
const app = new Koa();
//#1
app.use(async (ctx, next) => {
console.log('中间件 1 进入');
await next();
console.log('中间件 1 退出');
});
//#2
app.use(async (ctx, next) => {
console.log('中间件 2 进入');
await next();
console.log('中间件 2 退出');
});
//#3
app.use(async (ctx, next) => {
console.log('中间件 3');
});
app.listen(3000);
访问http://localhost:3000,控制台打印:
中间件 1 进入
中间件 2 进入
中间件 3
中间件 2 退出
中间件 1 退出
当程序运行到await next()的时候就会暂停当前程序,进入下一个中间件,处理完之后才会仔回过头来继续处理。 也就是说,当一个请求进入,#1会被第一个和最后一个经过,#2则是被第二和倒数第二个经过,依次类推。
通过use传进来的中间件是一个回调函数,回调函数的参数是ctx上下文和next,next其实就是控制权的交接棒,next的作用是停止运行当前中间件,将控制权交给下一个中间件, 执行下一个中间件的next()之前的代码,当下一个中间件运行的代码遇到了next(),又会将代码执行权交给下下个中间件,当执行到最后一个中间件的时候,控制权发生反转, 开始回头去执行之前所有中间件中剩下未执行的代码,当最终所有中间件全部执行完后,会返回一个Promise对象,因为我们的compose函数返回的是一个async的函数,async函数执行完后会返回一个Promise。
function _createNext(middleware, oldNext) {
return async () => {
await middleware(ctx, oldNext);
}
}
compose() {
return async (ctx) => {
let next = async () => {
return Promise.resolve();
};
for (let i = this.middlewares.length - 1; i >= 0; i--) {
let currentMiddleware = this.middlewares[i];
next = _createNext(currentMiddleware, next);
}
await next();
};
}
错误捕获和错误处理:
错误处理和捕获,分为中间件的异常与Koa框架框架层的异常。
前者在application.js的callback方法中,根据es7的规范知道,async返回的是一个promise的对象实例,我们如果想要捕获promise的错误,只需要使用promise的catch方法,就可以把所有的中间件的异常全部捕获到。
类似这样:
return fn(ctx).then(respond).catch(onerror);
后者,通过将koa的构造函数继承events模块,使其具备事件监听on函数和事件触发emit行为的能力。
let EventEmitter = require('events');
class Application extends EventEmitter {}
这样,当我们创建koa实例的时候,就可以加上on监听函数。
let app = new Koa();
app.on('error', err => {
console.log('error happends: ', err.stack);
});