阅读目录

koa2源码文件如下结构:
|-- lib
| |--- application.js
| |--- context.js
| |--- request.js
| |--- response.js
|__ package.json

application.js 是Koa2的入口文件,它封装了 context, request, response, 及 中间件处理的流程, 及 它向外导出了class的实列,并且它继承了Event, 因此该框架支持事件监听和触发的能力,比如代码: module.exports = class Application extends Emitter {}.

context.js 是处理应用的上下文ctx。它封装了 request.js 和 response.js 的方法。
request.js 它封装了处理http的请求。
response.js 它封装了处理http响应。

因此实现koa2框架需要封装和实现如下四个模块:

1. 封装node http server. 创建koa类构造函数。
2. 构造request、response、及 context 对象。
3. 中间件机制的实现。
4. 错误捕获和错误处理。

一:封装node http server. 创建koa类构造函数

首先,如果我们使用node的原生模块实现一个简单的服务器,并且打印 hello world,代码一般是如下所示:

const http = require('http');

const server = http.createServer((req, res) => {
res.writeHead(200);
res.end('hello world....');
}); server.listen(3000, () => {
console.log('listening on 3000');
});

因此实现koa的第一步是,我们需要对该原生模块进行封装一下,我们首先要创建application.js实现一个Application对象。

基本代码封装成如下(假如我们把代码放到 application.js里面):

const Emitter = require('events');
const http = require('http'); class Application extends Emitter {
/* 构造函数 */
constructor() {
super();
this.callbackFunc = null;
}
// 开启 http server 并且传入参数 callback
listen(...args) {
const server = http.createServer(this.callback());
return server.listen(...args);
}
use(fn) {
this.callbackFunc = fn;
}
callback() {
return (req, res) => {
this.callbackFunc(req, res);
}
}
} module.exports = Application;

然后我们在该目录下新建一个 test.js 文件,使用如下代码进行初始化如下:

const testKoa = require('./application');
const app = new testKoa(); app.use((req, res) => {
res.writeHead(200);
res.end('hello world....');
}); app.listen(3000, () => {
console.log('listening on 3000');
});

如上基本代码我们可以看到,在application.js 我们简单的封装了一个 http server,使用app.use注册回调函数,app.listen监听server,并传入回调函数。

但是如上代码有个缺点,app.use 传入的回调函数参数还是req,res, 也就是node原生的request和response对象,使用该对象还是不够方便,它不符合框架的设计的易用性,我们需要封装成如下的样子:

const Koa = require('koa');
const app = new Koa(); app.use(async (ctx, next) => {
console.log(11111);
await next();
console.log(22222);
});
app.listen(3000, () => {
console.log('listening on 3000');
});

基于以上的原因,我们需要构造 request, response, 及 context对象了。

二:构造request、response、及 context 对象。

2.1 request.js

该模块的作用是对原生的http模块的request对象进行封装,对request对象的某些属性或方法通过重写 getter/setter函数进行代理。

因此我们需要在我们项目中根目录下新建一个request.js, 该文件只有获取和设置url的方法,最后导出该文件,代码如下:

const request = {
get url() {
return this.req.url;
},
set url(val) {
this.req.url = val;
}
}; module.exports = request;

如需要理解get/set对对象的监听可以看我这篇文章

如上代码很简单,导出一个对象,该文件中包含了获取和设置url的方法,代码中this.req是node原生中的request对象,this.req.url则是node原生request中获取url的方法。

2. response.js

response.js 也是对http模块的response对象进行封装,通过对response对象的某些属性或方法通过getter/setter函数进行代理。

同理我们需要在我们项目的根目录下新建一个response.js。基本代码像如下所示:

const response = {
get body() {
return this._body;
},
set body(data) {
this._body = data;
},
get status() {
return this.res.statusCode;
},
set status(statusCode) {
if (typeof statusCode !== 'number') {
throw new Error('statusCode 必须为一个数字');
}
this.res.statusCode = statusCode;
}
}; module.exports = response;

代码也是如上一些简单的代码,该文件中有四个方法,分别是 body读取和设置方法。读取一个名为 this._body 的属性。
status方法分别是设置或读取 this.res.statusCode。同理:this.res是node原生中的response对象。

3. context.js

如上是简单的 request.js 和 response.js ,那么context的核心是将 request, response对象上的属性方法代理到context对象上。也就是说 将会把 this.res.statusCode 就会变成 this.ctx.statusCode 类似于这样的代码。request.js和response.js 中所有的方法和属性都能在ctx对象上找到。

因此我们需要在项目中的根目录下新建 context.js, 基本代码如下:

const context = {
get url() {
return this.request.url;
},
set url(val) {
this.request.url = val;
},
get body() {
return this.response.body;
},
set body(data) {
this.response.body = data;
},
get status() {
return this.response.statusCode;
},
set status(statusCode) {
if (typeof statusCode !== 'number') {
throw new Error('statusCode 必须为一个数字');
}
this.response.statusCode = statusCode;
}
}; module.exports = context;

如上代码可以看到context.js 是做一些常用方法或属性的代理,比如通过 context.url 直接代理了 context.request.url.
context.body 代理了 context.response.body, context.status 代理了 context.response.status. 但是 context.request、context.response会在application.js中挂载的。

注意:想要了解 getter/setter 的代理原理可以看这篇文章.

如上是简单的代理,但是当有很多代理的时候,我们一个个编写有点繁琐,因此我们可以通过 __defineSetter__ 和 __defineGetter__来实现,该两个方法目前不建议使用,我们也可以通过Object.defineProperty这个来监听对象。
但是目前在koa2中还是使用 delegates模块中的 __defineSetter__ 和 __defineGetter来实现的。delegates模块它的作用是将内部对象上的变量或函数委托到外部对象上。具体想要了解 delegates模块 请看我这篇文章。

因此我们的context.js 代码可以改成如下(当然我们需要引入delegates模块中的代码引入进来);

const delegates = require('./delegates');

const context = {
// ..... 其他很多代码
};
// 代理request对象
delegates(context, 'request').access('url'); // 代理response对象
delegates(context, 'response').access('body').access('status'); /*
const context = {
get url() {
return this.request.url;
},
set url(val) {
this.request.url = val;
},
get body() {
return this.response.body;
},
set body(data) {
this.response.body = data;
},
get status() {
return this.response.statusCode;
},
set status(statusCode) {
if (typeof statusCode !== 'number') {
throw new Error('statusCode 必须为一个数字');
}
this.response.statusCode = statusCode;
}
};
*/
module.exports = context;

如上代码引入了 delegates.js 模块,然后使用该模块下的access的方法,该方法既拥有setter方法,也拥有getter方法,因此代理了request对象中的url方法,同时代理了context对象中的response属性中的 body 和 status方法。

最后我们需要来修改application.js代码,引入request,response,context对象。如下代码:

const Emitter = require('events');
const http = require('http'); // 引入 context request, response 模块
const context = require('./context');
const request = require('./request');
const response = require('./response'); class Application extends Emitter {
/* 构造函数 */
constructor() {
super();
this.callbackFunc = null;
this.context = Object.create(context);
this.request = Object.create(request);
this.response = Object.create(response);
}
// 开启 http server 并且传入参数 callback
listen(...args) {
const server = http.createServer(this.callback());
return server.listen(...args);
}
use(fn) {
this.callbackFunc = fn;
}
callback() {
return (req, res) => {
// this.callbackFunc(req, res);
// 创建ctx
const ctx = this.createContext(req, res);
// 响应内容
const response = () => this.responseBody(ctx);
this.callbackFunc(ctx).then(response);
}
}
/*
构造ctx
@param {Object} req实列
@param {Object} res 实列
@return {Object} ctx实列
*/
createContext(req, res) {
// 每个实列都要创建一个ctx对象
const ctx = Object.create(this.context);
// 把request和response对象挂载到ctx上去
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;
}
/*
响应消息
@param {Object} ctx 实列
*/
responseBody(ctx) {
const content = ctx.body;
if (typeof content === 'string') {
ctx.res.end(content);
} else if (typeof content === 'object') {
ctx.res.end(JSON.stringify(content));
}
}
} module.exports = Application;

如上代码可以看到在callback()函数内部,我们把之前的这句代码 this.callbackFunc(req, res); 注释掉了,改成如下代码:

// 创建ctx
const ctx = this.createContext(req, res);
// 响应内容
const response = () => this.responseBody(ctx);
this.callbackFunc(ctx).then(response);

1. 首先是使用 createContext() 方法来创建ctx。然后把request对和response对象都直接挂载到了 ctx.request 和 ctx.response上,并且还将node原生的req/res对象挂载到了 ctx.request.req/ctx.req 和 ctx.response.res/ctx.res上了。

我们再来看下 request.js 的代码:

const request = {
get url() {
return this.req.url;
},
set url(val) {
this.req.url = val;
}
}; module.exports = request;

我们之前request.js 代码是如上写的,比如 get url() 方法,返回的是 this.req.url, this.req是从什么地方来的?之前我们并不理解,现在我们知道了。

1. 首先我们把request挂载到ctx实列上了,如代码:ctx.request = Object.create(this.request);然后node中的原生的req也挂载到ctx.req中了,如代码:ctx.req = ctx.request.req = req; 因此request.js 中的this指向了createContext方法中挂载到了对应的实例上。因此 this.req.url 实际上就是 ctx.req.url了。同理 this.res 也是一样的道理的。

2. 其次,我们使用 const response = () => this.responseBody(ctx); 该方法把ctx实列作用参数传入 responseBody方法内作为
响应内容。代码如下:

responseBody(ctx) {
const content = ctx.body;
if (typeof content === 'string') {
ctx.res.end(content);
} else if (typeof content === 'object') {
ctx.res.end(JSON.stringify(content));
}
}

如上我们创建了 responseBody方法,该方法的作用是通过ctx.body读取信息,判断该 ctx.body是否是字符串或对象,如果是对象的话,也会把它转为字符串,最后调用 ctx.res.end() 方法返回信息并关闭连接。

3. 最后我们调用该代码:this.callbackFunc(ctx).then(response); this.callbackFunc()函数就是我们使用koa中传入的方法,比如如下koa代码:

app.use(async ctx => {
console.log(ctx.status); // 打印状态码为200
ctx.body = 'hello world';
});

该回调函数是一个async函数,然后返回给我们的参数是ctx对象,async函数返回的是一个promise对象,因此在源码中我们继续调用then方法,把返回的内容挂载到ctx上。因此我们可以拿着ctx对象做我们自己想要做的事情了。

三:中间件机制的实现。

koa中的中间件是洋葱型模型。具体的洋葱模型的机制可以看这篇文章。

koa2中使用了async/await作为执行方式,具体理解 async/await的含义可以看我这篇文章介绍。

koa2中的中间件demo如下:

const Koa = require('koa');
const app = new Koa(); app.use(async (ctx, next) => {
console.log(11111);
await next();
console.log(22222);
}); app.use(async (ctx, next) => {
console.log(33333);
await next();
console.log(44444);
}); app.use(async (ctx, next) => {
console.log(55555);
await next();
console.log(66666);
});
app.listen(3001);
console.log('app started at port 3000...'); // 执行结果为 11111 33333 55555 66666 44444 22222

如上执行结果为 11111 33333 55555 66666 44444 22222,koa2中的中间件模型为洋葱型模型,当对象请求过来的时候,会依次经过各个中间件进行处理,当碰到 await next()时候就会跳到下一个中间件,当中间件没有 await next执行的时候,就会逆序执行前面的中间件剩余的代码,因此,先打印出 11111,然后碰到await next()函数,所以跳到下一个中间件去,就接着打印33333, 然后又碰到 await next(),因此又跳到下一个中间件,因此会打印55555, 打印完成后,继续碰到 await next() 函数,但是后面就没有中间件了,因此执行打印66666,然后逆序打印后面的数据了,先打印44444,执行完成后,就往上打印22222.

逆序如果我们不好理解的话,我们继续来看下如下demo就能明白了。

function test1() {
console.log(1)
test2();
console.log(5)
return Promise.resolve();
}
function test2() {
console.log(2)
test3();
console.log(4)
} function test3() {
console.log(3)
return;
}
test1();

如上代码打印的顺序分别为 1, 2, 3, 4, 5; 上面的代码就是和koa2中的中间件逆序顺序是一样的哦。可以自己理解一下。

那么现在我们想要实现这么一个类似koa2中间件的这么一个机制,我们该如何做呢?

我们都知道koa2中是使用了async/await来做的,假如我们现在有如下三个简单的async函数:

// 假如下面是三个测试函数,想要实现 koa中的中间件机制
async function fun1(next) {
console.log(1111);
await next();
console.log('aaaaaa');
} async function fun2(next) {
console.log(22222);
await next();
console.log('bbbbb');
} async function fun3() {
console.log(3333);
}

如上三个简单的函数,我现在想构造出一个函数,让这三个函数依次执行,先执行fun1函数,打印1111,然后碰到 await next() 后,执行下一个函数 fun2, 打印22222, 再碰到 await next() 就执行fun3函数,打印3333,然后继续打印 bbbbb, 再打印 aaaaa。

因此我们需要从第一个函数入手,因为首先打印的是 11111, 因此我们需要构造一个调用 fun1函数了。fun1函数的next参数需要能调用 fun2函数了,fun2函数中的next参数需要能调用到fun3函数了。因此代码改成如下:

// 假如下面是三个测试函数,想要实现 koa中的中间件机制
async function fun1(next) {
console.log(1111);
await next();
console.log('aaaaaa');
} async function fun2(next) {
console.log(22222);
await next();
console.log('bbbbb');
} async function fun3() {
console.log(3333);
} let next1 = async function () {
await fun2(next2);
}
let next2 = async function() {
await fun3();
}
fun1(next1);

然后我们执行一下,就可以看到函数会依次执行,结果为:1111,22222,3333,bbbbb, aaaaaa;

如上就可以让函数依次执行了,但是假如页面有n个中间件函数,我们需要依次执行怎么办呢?因此我们需要抽象成一个公用的函数出来,据koa2中application.js 源码中,首先会把所有的中间件函数放入一个数组里面去,比如源码中这样的:
this.middleware.push(fn); 因此我们这边首先也可以把上面的三个函数放入数组里面去,然后使用for循环依次循环调用即可:

如下代码:

async function fun1(next) {
console.log(1111);
await next();
console.log('aaaaaa');
} async function fun2(next) {
console.log(22222);
await next();
console.log('bbbbb');
} async function fun3() {
console.log(3333);
} function compose(middleware, oldNext) {
return async function() {
await middleware(oldNext);
}
} const middlewares = [fun1, fun2, fun3]; // 最后一个中间件返回一个promise对象
let next = async function() {
return Promise.resolve();
}; for (let i = middlewares.length - 1; i >= 0; i--) {
next = compose(middlewares[i], next);
}
next();

最后依次会打印 1111 22222 3333 bbbbb aaaaaa了。

如上代码是怎么执行的呢?首先我们会使用一个数组 middlewares 保存所有的函数,就像和koa2中一样使用 app.use 后,会传入async函数进去,然后会依次通过 this.middlewares 把对应的函数保存到数组里面去。然后我们从数组末尾依次循环该数组最后把返回的值保存到 next 变量里面去。如上代码:

因此for循环第一次打印 middlewares[i], 返回的是 fun3函数,next传进来的是 async function { return Promise.resolve()} 这样的函数,最后返回该next,那么此时该next保存的值就是:

next = async function() {
await func3(async function(){
return Promise.resolve();
});
}

for 循环第二次的时候,返回的是 fun2函数,next传进来的是 上一次返回的函数,最后返回next, 那么此时next保存的值就是

next = async function() {
await func2(async function() {
await func3(async function(){
return Promise.resolve();
});
});
}

for循环第三次的时候,返回的是 fun1 函数,next传进来的又是上一次返回的async函数,最后也返回next,那么此时next的值就变为:

next = async function(){
await fun1(async function() {
await fun2(async function() {
await fun3(async function(){
return Promise.resolve();
});
});
});
};

因此我们下面调用 next() 函数的时候,会依次执行 fun1 函数,执行完成后,就会调用 fun2 函数,再执行完成后,接着调用fun3函数,依次类推..... 最后一个函数返回 Promise.resolve() 中Promise成功状态。

如果上面的async 函数依次调用不好理解的话,我们可以继续看如下demo;代码如下:

async function fun1(next) {
console.log(1111);
await next();
console.log('aaaaaa');
} async function fun2(next) {
console.log(22222);
await next();
console.log('bbbbb');
} async function fun3() {
console.log(3333);
} const next = async function(){
await fun1(async function() {
await fun2(async function() {
await fun3(async function(){
return Promise.resolve();
});
});
});
}; next();

最后结果也会依次打印 1111, 22222, 3333, bbbbb, aaaaaa;

因此上面就是我们的koa2中间件机制了。我们现在把我们总结的机制运用到我们application.js中了。因此application.js代码变成如下:

const Emitter = require('events');
const http = require('http'); // 引入 context request, response 模块
const context = require('./context');
const request = require('./request');
const response = require('./response'); class Application extends Emitter {
/* 构造函数 */
constructor() {
super();
// this.callbackFunc = null;
this.context = Object.create(context);
this.request = Object.create(request);
this.response = Object.create(response);
// 保存所有的中间件函数
this.middlewares = [];
}
// 开启 http server 并且传入参数 callback
listen(...args) {
const server = http.createServer(this.callback());
return server.listen(...args);
}
use(fn) {
// this.callbackFunc = fn;
// 把所有的中间件函数存放到数组里面去
this.middlewares.push(fn);
return this;
}
callback() {
return (req, res) => {
// this.callbackFunc(req, res);
// 创建ctx
const ctx = this.createContext(req, res);
// 响应内容
const response = () => this.responseBody(ctx);
//调用 compose 函数,把所有的函数合并
const fn = this.compose();
return fn(ctx).then(response);
}
}
/*
构造ctx
@param {Object} req实列
@param {Object} res 实列
@return {Object} ctx实列
*/
createContext(req, res) {
// 每个实列都要创建一个ctx对象
const ctx = Object.create(this.context);
// 把request和response对象挂载到ctx上去
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;
}
/*
响应消息
@param {Object} ctx 实列
*/
responseBody(ctx) {
const content = ctx.body;
if (typeof content === 'string') {
ctx.res.end(content);
} else if (typeof content === 'object') {
ctx.res.end(JSON.stringify(content));
}
}
/*
把传进来的所有的中间件函数合并为一个中间件
@return {function}
*/
compose() {
// 该函数接收一个参数 ctx
return async ctx => {
function nextCompose(middleware, oldNext) {
return async function() {
await middleware(ctx, oldNext);
}
}
// 获取中间件的长度
let len = this.middlewares.length;
// 最后一个中间件返回一个promise对象
let next = async function() {
return Promise.resolve();
};
for (let i = len; i >= 0; i--) {
next = nextCompose(this.middlewares[i], next);
}
await next();
};
}
} module.exports = Application;

1. 如上代码在构造函数内部 constructor 定义了一个变量 this.middlewares = []; 目的是保存app.use(fn)所有的中间件函数,

2. 然后我们在use函数内部,不是把fn赋值,而是把fn放到一个数组里面去,如下代码:

use(fn) {
// this.callbackFunc = fn;
// 把所有的中间件函数存放到数组里面去
this.middlewares.push(fn);
return this;
}

3. 最后把所有的中间件函数合并为一个中间件函数;如下compose函数的代码如下:

compose() {
// 该函数接收一个参数 ctx
return async ctx => {
function nextCompose(middleware, oldNext) {
return async function() {
await middleware(ctx, oldNext);
}
}
// 获取中间件的长度
let len = this.middlewares.length;
// 最后一个中间件返回一个promise对象
let next = async function() {
return Promise.resolve();
};
for (let i = len; i >= 0; i--) {
next = nextCompose(this.middlewares[i], next);
}
await next();
};
}

该compose函数代码和我们之前的demo代码是一样的。这里就不多做解析哦。

4. 在callback函数内部改成如下代码:

callback() {
return (req, res) => {
/*
// 创建ctx
const ctx = this.createContext(req, res);
// 响应内容
const response = () => this.responseBody(ctx);
this.callbackFunc(ctx).then(response);
*/
// 创建ctx
const ctx = this.createContext(req, res);
// 响应内容
const response = () => this.responseBody(ctx);
//调用 compose 函数,把所有的函数合并
const fn = this.compose();
return fn(ctx).then(response);
}
}

如上代码和之前版本的代码,最主要的区别是 最后两句代码,之前的是直接把fn函数传入到 this.callbackFunc函数内。现在是使用 this.compose()函数调用,把所有的async的中间件函数合并成一个中间件函数后,把返回的合并后的中间件函数fn再去调用,这样就会依次调用和初始化各个中间件函数,具体的原理机制我们上面的demo已经讲过了,这里就不再多描述了。

最后我们需要一个测试文件,来测试该代码:如下在test.js 代码如下:

const testKoa = require('./application');
const app = new testKoa(); const obj = {}; app.use(async (ctx, next) => {
obj.name = 'kongzhi';
console.log(1111);
await next();
console.log('aaaaa');
}); app.use(async (ctx, next) => {
obj.age = 30;
console.log(2222);
await next();
console.log('bbbbb')
}); app.use(async (ctx, next) => {
console.log(3333);
console.log(obj);
});
app.listen(3001, () => {
console.log('listening on 3001');
});

我们运行下即可看到,在命令行中会依次打印如下所示:

如上是先打印1111,2222,3333,{'name': 'kongzhi', 'age': 30}, bbbbb, aaaaa.

因此如上就是koa2中的中间件机制了。

四:错误捕获和错误处理。

一个非常不错的框架,当异常的时候,都希望能捕获到该异常,并且希望把该异常返回给客户端,让开发者知道异常的一些信息。

比如koa2中的异常情况下,会报错如下信息:demo如下:

const Koa = require('koa');
const app = new Koa(); app.use((ctx) => {
str += 'hello world'; // 没有声明该变量, 所以直接拼接字符串会报错
ctx.body = str;
}); app.on('error', (err, ctx) => { // 捕获异常记录错误日志
console.log(err);
}); app.listen(3000, () => {
console.log('listening on 3000');
});

如上代码,由于str是一个未定义的变量,因此和字符串拼接的时候会报错,但是koa2中我们可以使用 app.on('error', (err, ctx) => {}) 这样的error方法来进行监听的。因此在命令行中会报如下错误提示:

因此我们现在也是一样,我们需要有对于某个中间件发生错误的时候,我们需要监听error这个事件进行监听。
因此我们需要定义一个onerror函数,当发生错误的时候,我们可以使用Promise中的catch方法来捕获该错误了。

因此我们可以让我们的Application继承于Event这个对象,在koa2源码中的application.js 中有 onerror函数,我们把它复制到我们的Application.js 中,代码如下:

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();
}

然后在我们我们的callback()函数中最后一句代码使用catch去捕获这个异常即可:代码如下:

callback() {
return (req, res) => {
/*
// 创建ctx
const ctx = this.createContext(req, res);
// 响应内容
const response = () => this.responseBody(ctx);
this.callbackFunc(ctx).then(response);
*/
// 创建ctx
const ctx = this.createContext(req, res);
// 响应内容
const response = () => this.responseBody(ctx); // 响应时 调用error函数
const onerror = (err) => this.onerror(err, ctx); //调用 compose 函数,把所有的函数合并
const fn = this.compose();
return fn(ctx).then(response).catch(onerror);
}
}

因此Application.js 所有代码如下:

const Emitter = require('events');
const http = require('http'); // 引入 context request, response 模块
const context = require('./context');
const request = require('./request');
const response = require('./response'); class Application extends Emitter {
/* 构造函数 */
constructor() {
super();
// this.callbackFunc = null;
this.context = Object.create(context);
this.request = Object.create(request);
this.response = Object.create(response);
// 保存所有的中间件函数
this.middlewares = [];
}
// 开启 http server 并且传入参数 callback
listen(...args) {
const server = http.createServer(this.callback());
return server.listen(...args);
}
use(fn) {
// this.callbackFunc = fn;
// 把所有的中间件函数存放到数组里面去
this.middlewares.push(fn);
return this;
}
callback() {
return (req, res) => {
/*
// 创建ctx
const ctx = this.createContext(req, res);
// 响应内容
const response = () => this.responseBody(ctx);
this.callbackFunc(ctx).then(response);
*/
// 创建ctx
const ctx = this.createContext(req, res);
// 响应内容
const response = () => this.responseBody(ctx); // 响应时 调用error函数
const onerror = (err) => this.onerror(err, ctx); //调用 compose 函数,把所有的函数合并
const fn = this.compose();
return fn(ctx).then(response).catch(onerror);
}
}
/**
* Default error handler.
*
* @param {Error} err
* @api private
*/ 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();
}
/*
构造ctx
@param {Object} req实列
@param {Object} res 实列
@return {Object} ctx实列
*/
createContext(req, res) {
// 每个实列都要创建一个ctx对象
const ctx = Object.create(this.context);
// 把request和response对象挂载到ctx上去
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;
}
/*
响应消息
@param {Object} ctx 实列
*/
responseBody(ctx) {
const content = ctx.body;
if (typeof content === 'string') {
ctx.res.end(content);
} else if (typeof content === 'object') {
ctx.res.end(JSON.stringify(content));
}
}
/*
把传进来的所有的中间件函数合并为一个中间件
@return {function}
*/
compose() {
// 该函数接收一个参数 ctx
return async ctx => {
function nextCompose(middleware, oldNext) {
return async function() {
await middleware(ctx, oldNext);
}
}
// 获取中间件的长度
let len = this.middlewares.length;
// 最后一个中间件返回一个promise对象
let next = async function() {
return Promise.resolve();
};
for (let i = len; i >= 0; i--) {
next = nextCompose(this.middlewares[i], next);
}
await next();
};
}
} module.exports = Application;

然后我们使用test.js 编写测试代码如下:

const testKoa = require('./application');
const app = new testKoa(); app.use((ctx) => {
str += 'hello world'; // 没有声明该变量, 所以直接拼接字符串会报错
ctx.body = str;
}); app.on('error', (err, ctx) => { // 捕获异常记录错误日志
console.log(err);
}); app.listen(3000, () => {
console.log('listening on 3000');
});

当我们在浏览器访问的 http://localhost:3000/ 的时候,我们可以在命令行中看到如下报错信息了:

总结:如上就是实现一个简单的koa2框架的基本原理,本来想把koa2源码也分析下,但是篇幅有限,所以下篇文章继续把koa2所有的源码简单的解读下,其实看懂这篇文章后,已经可以理解95%左右的koa2源码了,只是说koa2源码中,比如request.js 会包含更多的方法,及 response.js 包含更多有用的方法等。

github源码

koa2源码解读及实现一个简单的koa2框架的更多相关文章

  1. koa2 源码解读 application

    koa2的源码比较简单,重点解读aplication, 其中context源码比较简单,主要是一些error cookies等,重点可以关注下delegate,delegate模块中,主要通过prot ...

  2. koa2源码解读

    最近在复习node的基础知识,于是看了看koa2的源码,写此文分享一下包括了Koa2的使用.中间件及上下文对象的大致实现原理. koa的github地址:https://github.com/koaj ...

  3. httprunner3源码解读(1)简单介绍源码模块内容

    前言 最近想着搭建一个API测试平台,基础的注册登录功能已经完成,就差测试框架的选型,最后还是选择了httprunner,github上已经有很多开源的httprunner测试平台,但是看了下都是基于 ...

  4. swoft 源码解读【转】

      官网: https://www.swoft.org/ 源码解读: http://naotu.baidu.com/file/814e81c9781b733e04218ac7a0494e2a?toke ...

  5. Vue源码--解读vue响应式原理

    原文链接:https://geniuspeng.github.io/2018/01/05/vue-reactivity/ Vue的官方说明里有深入响应式原理这一节.在此官方也提到过: 当你把一个普通的 ...

  6. JDK容器类Map源码解读

    java.util.Map接口是JDK1.2开始提供的一个基于键值对的散列表接口,其设计的初衷是为了替换JDK1.0中的java.util.Dictionary抽象类.Dictionary是JDK最初 ...

  7. 如何判断一个Http Message的结束——python源码解读

    HTTP/1.1 默认的连接方式是长连接,不能通过简单的TCP连接关闭判断HttpMessage的结束. 以下是几种判断HttpMessage结束的方式: 1.      HTTP协议约定status ...

  8. MFC源码解读(一)最原始一个MFC程序,手写不用向导

    从这一篇开始,详细记录一下MFC的源码解读 四个文件,分别为: stdafx.h,stdafx.cpp,hello.h,hello.cpp 代码如下: //stdafx.h #include < ...

  9. koa源码解读

    koa是有express原班人马打造的基于node.js的下一代web开发框架.koa 1.0使用generator实现异步,相比于回调简单和优雅和不少.koa团队并没有止步于koa 1.0, 随着n ...

随机推荐

  1. 爬虫入门(三)——动态网页爬取:爬取pexel上的图片

    Pexel上有大量精美的图片,没事总想看看有什么好看的自己保存到电脑里可能会很有用 但是一个一个保存当然太麻烦了 所以不如我们写个爬虫吧(๑•̀ㅂ•́)و✧ 一开始学习爬虫的时候希望爬取pexel上的 ...

  2. 5分钟入门git模式开发

    本文由云+社区发表 作者:唐维黎 导语 基于gui工具TortoiseGit让你快速进入git开发模式. 目前项目已逐步从svn移步到git开发模式,其中也针对git统一协议了适合git的开发规范, ...

  3. 【响应式编程的思维艺术】 (2)响应式Vs面向对象

    目录 一. 划重点 二. 面向对象编程实例 2.1 动画的基本编程范式 2.2 参考代码 2.3 小结 三. 响应式编程实现 四. 差异对比 4.1 编程理念差异 4.2 编程体验差异 4.3 数学思 ...

  4. Docker在Linux/Windows上运行NetCore文章系列

    Windows系列 因为Window很简单,VS提供界面化配置,所以只写了一篇文章 Docker在Windows上运行NetCore系列(一)使用命令控制台运行.NetCore控制台应用 Linux( ...

  5. .Net语言 APP开发平台——Smobiler学习日志:获取或存储图像路径设置

    ResourcePath属性 一.属性介绍 获取或设置图像存储路径,默认设置为“image”,表示的ResourcePath是在程序运行路径下的Image文件夹(bin\Debug\Image): 该 ...

  6. JS输入框去除负号(限定输入正数)

    onkeyup="(this.v=function(){this.value=this.value.replace(/\-/g,\'\');}).call(this)" 示例: & ...

  7. 5-Redis 的持久化之 RDB

    2016-12-22 13:58:48 该系列文章链接NoSQL 数据库简介Redis的安装及及一些杂项基础知识Redis 的常用五大数据类型(key,string,hash,list,set,zse ...

  8. 灵活使用 console 让 js 调试更简单

    摘要: 玩转console. 原文:灵活使用 console 让 js 调试更简单 作者:前端小智 Fundebug经授权转载,版权归原作者所有. Web 开发最常用的就是 console.log , ...

  9. bootstrap思考一

    bootstrap是一种热门的Web前端流行框架,如果要兼容PC端.手机端和响应式布局,那他一定是我的首选.bootstrap内容很多,功能强大,其中最好入门也是很重要的就是他的栅格系统.他有四个典型 ...

  10. Windows中nvm使用

    介绍:在两个项目且使用的node版本不一样时,维护多个版本的node, 安装:下载安装目录:https://github.com/coreybutler/nvm-windows/releasesnvm ...