浅析koa2源码与实现
源码解析
koa2源码地址:https://github.com/koajs/koa
目录
本人从公司内部系统的node modules中找到了koa的源码,目录如下:
在package.json中依赖库查找到的版本号如下:
不难看出,koa源码由application.js、context.js、request.js以及response.js组成。
application.js
application.js是koa的入口文件,它继承events创建了一个class实例,并将该实例export出去,这样就会赋予框架事件监听和事件触发的能力。application还暴露了一些常用的api,比如listen、toJSON、inspect、use等等。
listen的实现原理其实就是对http.createServer进行了一个封装,重点是这个函数中传入的callback,它里面包含了中间件的合并,上下文的处理,对res的特殊处理。
use是收集中间件,将多个中间件放入一个缓存队列中,然后通过koa-compose这个插件进行递归组合调用这一些列的中间件。
context.js
这部分就是koa的应用上下文ctx,其实就一个简单的对象暴露,里面的重点在delegate,这个就是代理,这个就是为了开发者方便而设计的,比如我们要访问ctx.repsponse.status但是我们通过delegate,可以直接访问ctx.status访问到它。
request.js、response.js
这两部分就是对原生的res、req的一些操作了,使用es6的get和set的一些语法,去读取headers、body、url等等。
简易版koa2的实现
本文实现的简易版koa2源码地址:https://github.com/TheWalkingFat/simple_koa2
通过查阅网上资料得知,实现一个简易版的Koa2框架,需要理解和实现四个大模块,分别是:
- 封装node http server、创建Koa类构造函数
- 构造request、response、context对象
- 中间件机制和剥洋葱模型的实现
- 错误捕获和错误处理
封装node http server、创建Koa类构造函数
从koa2源码里面可看出,实现koa的服务器应用和端口监听,其实就是基于node的原生代码进行了封装
let http = require('http');
let server = http.createServer((req, res) => {
res.writeHead(200);
res.end('success');
});
server.listen(3001, () => {
console.log('listenning on 3001');
});
那么我们需要将上面node.js原生代码封装成koa的模式:
const http = require('http');
const Koa = require('koa');
const app = new Koa();
app.listen(3001);
实现koa的第一步就是对以上的这个过程进行封装,为此我们需要创建application.js实现一个Application类的构造函数
let http = require('http');
class Application {
constructor() {
this.callbackFunc;
}
listen(...args) {
let server = http.createServer(this.callback());
server.listen(...args);
}
use(fn) {
this.callbackFunc = fn;
}
callback() {
return (req, res) => {
this.callbackFunc(req, res);
};
}
}
module.exports = Application;
然后创建app.js,引入application.js,运行服务器实例启动监听代码:
let Koa = require('./application');
let app = new Koa();
app.use((req, res) => {
res.writeHead(200);
res.end('success');
});
app.listen(3001, () => {
console.log('listening on 3001');
});
现在在命令行中输入node app.js
启动服务,然后再浏览器中访问localhost:3001
即可看到浏览器输出success
现在第一步我们已经完成了,对http server进行了简单的封装和创建了一个可以生成koa实例的类class,这个类里还实现了app.use用来注册中间件和注册回调函数,app.listen用来开启服务器实例并传入callback回调函数。
构造request、response、context对象
看源码发现,其中request.js、response.js、context.js三个文件分别是request、response、context三个模块的代码文件
- context: context就是我们平时写koa代码时的ctx,它相当于一个全局的koa实例上下文this,它连接了request、response两个功能模块,并且暴露给koa的实例和中间件等回调函数的参数中,起到承上启下的作用
- request、response: 分别对node的原生request、response进行了一个功能的封装,使用了getter和setter属性,基于node的对象req/res对象封装koa的request/response对象
简单实现request.js:
module.exports = {
get header() {
return this.req.headers;
},
set header(val) {
this.req.headers = val;
},
get headers() {
return this.req.headers;
},
set headers(val) {
this.req.headers = val;
}
}
基于getter和setter,在request.js里还封装了header、url、origin、path等方法,都是对原生的request上用getter和setter进行了封装,在这便不进行一一实现了,有兴趣的同学可以自行查阅源码。
response.js:
它和request原理一样,也是基于getter和setter对原生response进行了封装,那我们接下来通过对常用的ctx.body和ctx.status这个两个语句当做例子简述一下
module.exports = {
get body() {
return this._body;
},
set body(data) {
this._body = data;
},
get status() {
return this.res.statusCode;
},
set status(statusCode) {
this.res.statusCode = statusCode;
}
}
这里有一点要注意的是,对于statusCode的读写操作,我们是直接基于原生response对象的statusCode进行操作,而对于body,我们则是存放了一个私有变量,而不是直接对原对象的属性进行操作,是因为在我们编写koa代码的时候,会对body进行多次的读取和修改,所以真正返回浏览器信息的操作是在application.js里进行封装和操作。
context.js:
现在我们已经实现了request.js、response.js,获取到了request、response对象和他们的封装的方法,然后我们开始实现context.js,context的作用就是将request、response对象挂载到ctx的上面,让koa实例和代码能方便的使用到request、response对象中的方法。
let proto = {};
function delegateSet(property, name) {
proto.__defineSetter__(name, function (val) {
this[property][name] = val;
});
}
function delegateGet(property, name) {
proto.__defineGetter__(name, function () {
return this[property][name];
});
}
let requestSet = [];
let requestGet = ['query'];
let responseSet = ['body', 'status'];
let responseGet = responseSet;
requestSet.forEach(ele => {
delegateSet('request', ele);
});
requestGet.forEach(ele => {
delegateGet('request', ele);
});
responseSet.forEach(ele => {
delegateSet('response', ele);
});
responseGet.forEach(ele => {
delegateGet('response', ele);
});
module.exports = proto;
context.js文件主要是对常用的request和response方法进行挂载和代理,通过context.query直接代理了context.request.query,context.body和context.status代理了context.response.body与context.response.status。而context.request,context.response则会在application.js中挂载。
至此,我们已经得到了request、response、context三个模块对象了,接下来就是将request、response所有方法挂载到context下,让context实现它的承上启下的作用,修改application.js文件,添加如下代码:
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;
}
createContext这个方法是关键,它通过Object.create创建了ctx,并将request和response挂载到了ctx上面,将原生的req和res挂载到了ctx的子属性上,往回看一下context/request/response.js文件,就能知道当时使用的this.res或者this.response之类的是从哪里来的了,原来是在这个createContext方法中挂载到了对应的实例上,构建了运行时上下文ctx之后,我们的app.use回调函数参数就都基于ctx了。
中间件机制和剥洋葱模型的实现
从网上找了两张图来解析koa的洋葱模型:
koa的中间件机制是一个剥洋葱式的模型,多个中间件通过use放进一个数组队列然后从外层开始执行,遇到next后进入队列中的下一个中间件,所有中间件执行完后开始回帧,执行队列中之前中间件中未执行的代码部分,这就是剥洋葱模型,koa的中间件机制
koa的剥洋葱模型在koa1中使用的是generator + co.js去实现的,koa2则使用了async/await + Promise去实现
修改application.js文件,添加如下代码:
use(middleware) {
this.middlewares.push(middleware);
}
compose() {
// 将middlewares合并为一个函数,该函数接收一个ctx对象
return async ctx => {
function createNext(middleware, oldNext) {
return async () => {
await middleware(ctx, oldNext);
}
}
let len = this.middlewares.length;
let next = async () => {
return Promise.resolve();
};
for (let i = len - 1; i >= 0; i--) {
let currentMiddleware = this.middlewares[i];
next = createNext(currentMiddleware, next);
}
await next();
};
}
callback() {
return (req, res) => {
let ctx = this.createContext(req, res);
let respond = () => this.responseBody(ctx);
let onerror = (err) => this.onerror(err, ctx);
let fn = this.compose();
return fn(ctx).then(respond).catch(onerror);
};
}
koa通过use函数,把所有的中间件push到一个内部数组队列this.middlewares中,剥洋葱模型能让所有的中间件依次执行,每次执行完一个中间件,遇到next()就会将控制权传递到下一个中间件,下一个中间件的next参数,剥洋葱模型的最关键代码是compose这个函数。
compose里面的createNext函数的作用就是将上一个中间件的next当做参数传给下一个中间件,并且将上下文ctx绑定当前中间件,当中间件执行完,调用next()的时候,其实就是去执行下一个中间件。
通过一个链式反向递归模型的实现,i是从最大数开始循环的,将中间件从最后一个开始封装,每一次都是将自己的执行函数封装成next当做上一个中间件的next参数,这样当循环到第一个中间件的时候,只需要执行一次next(),就能链式的递归调用所有中间件,这个就是koa剥洋葱的核心代码机制。
错误捕获和错误处理
错误的类型分为两种:中间件执行错误以及框架层面的错误
在application.js中添加一下代码:
callback() {
return (req, res) => {
let ctx = this.createContext(req, res);
let respond = () => this.responseBody(ctx);
let onerror = (err) => this.onerror(err, ctx);
let fn = this.compose();
return fn(ctx).then(respond).catch(onerror);
};
}
onerror(err, ctx) {
if (err.code === 'ENOENT') {
ctx.status = 404;
}
else {
ctx.status = 500;
}
let msg = err.message || 'Internal error';
ctx.res.end(msg);
this.emit('error', err);
}
第一种,中间件执行错误:我们前面说过,koa2是才用async/await + promise来实现洋葱模型,所以.catch自然可以捕抓中间件执行错误。
在callback函数中我们添加了这一行代码
return fn(ctx).then(respond).catch(onerror);
第二种,框架层面的错误:application里面的类继承了EventEmitter,在触发错误的时候主动调用emit('error'),那么外层的引用只需要监听error即可实现。
onerror(err, ctx) {
if (err.code === 'ENOENT') {
ctx.status = 404;
}
else {
ctx.status = 500;
}
let msg = err.message || 'Internal error';
ctx.res.end(msg);
this.emit('error', err);
}
app.on('error', err => {
console.log('error: ', err);
});
总结
至此,我们已经实现了一个简易版本的koa2,koa2的原理主要是由四部分组成,在理解这四部分之后再去看koa2的源码,会容易得多。
本文实现的简易版koa2源码地址:https://github.com/TheWalkingFat/simple_koa2
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。