koa2源码解读及实现一个简单的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 包含更多有用的方法等。
koa2源码解读及实现一个简单的koa2框架的更多相关文章
- koa2 源码解读 application
koa2的源码比较简单,重点解读aplication, 其中context源码比较简单,主要是一些error cookies等,重点可以关注下delegate,delegate模块中,主要通过prot ...
- koa2源码解读
最近在复习node的基础知识,于是看了看koa2的源码,写此文分享一下包括了Koa2的使用.中间件及上下文对象的大致实现原理. koa的github地址:https://github.com/koaj ...
- httprunner3源码解读(1)简单介绍源码模块内容
前言 最近想着搭建一个API测试平台,基础的注册登录功能已经完成,就差测试框架的选型,最后还是选择了httprunner,github上已经有很多开源的httprunner测试平台,但是看了下都是基于 ...
- swoft 源码解读【转】
官网: https://www.swoft.org/ 源码解读: http://naotu.baidu.com/file/814e81c9781b733e04218ac7a0494e2a?toke ...
- Vue源码--解读vue响应式原理
原文链接:https://geniuspeng.github.io/2018/01/05/vue-reactivity/ Vue的官方说明里有深入响应式原理这一节.在此官方也提到过: 当你把一个普通的 ...
- JDK容器类Map源码解读
java.util.Map接口是JDK1.2开始提供的一个基于键值对的散列表接口,其设计的初衷是为了替换JDK1.0中的java.util.Dictionary抽象类.Dictionary是JDK最初 ...
- 如何判断一个Http Message的结束——python源码解读
HTTP/1.1 默认的连接方式是长连接,不能通过简单的TCP连接关闭判断HttpMessage的结束. 以下是几种判断HttpMessage结束的方式: 1. HTTP协议约定status ...
- MFC源码解读(一)最原始一个MFC程序,手写不用向导
从这一篇开始,详细记录一下MFC的源码解读 四个文件,分别为: stdafx.h,stdafx.cpp,hello.h,hello.cpp 代码如下: //stdafx.h #include < ...
- koa源码解读
koa是有express原班人马打造的基于node.js的下一代web开发框架.koa 1.0使用generator实现异步,相比于回调简单和优雅和不少.koa团队并没有止步于koa 1.0, 随着n ...
随机推荐
- 【c#】RabbitMQ学习文档(二)Work Queues(工作队列)
今天开始RabbitMQ教程的第二讲,废话不多说,直接进入话题. (使用.NET 客户端 进行事例演示) 在第一个教程中,我们编写了一个从命名队列中发送和接收消息的程序. ...
- 如何热更新线上的Java服务器代码
一.前言 1.热更新代码的场景 (1)当线上服务器出现问题时,有些时候现有的手段不足以发现问题所在,可能需要追加打印日志或者增加一些调试代码,如果我们去改代码重新部署,会破坏问题现场,可以通过热部署的 ...
- 流式大数据计算实践(7)----Hive安装
一.前言 1.这一文学习使用Hive 二.Hive介绍与安装 Hive介绍:Hive是基于Hadoop的一个数据仓库工具,可以通过HQL语句(类似SQL)来操作HDFS上面的数据,其原理就是将用户写的 ...
- 痞子衡嵌入式:ARM Cortex-M内核那些事(3)- 功能模块
大家好,我是痞子衡,是正经搞技术的痞子.今天痞子衡给大家介绍的是ARM Cortex-M功能模块. ARM Cortex-M处理器家族发展至今(2016),已有5代产品,分别是CM0/CM0+.CM1 ...
- webpack4.0各个击破(7)—— plugin篇
webpack作为前端最火的构建工具,是前端自动化工具链最重要的部分,使用门槛较高.本系列是笔者自己的学习记录,比较基础,希望通过问题 + 解决方式的模式,以前端构建中遇到的具体需求为出发点,学习we ...
- Smobiler 4.4已正式发布!(Smobiler能让你在Visual Studio上开发APP)
Smobiler 4.4已经正式发布,还不快来看看?原文地址:https://www.smobiler.com/portal.php?mod=view&aid=53这次更新要感谢我们的用户,在 ...
- 教我徒弟Android开发入门(四)
本期知识点: 两大常用布局的简单介绍 在我们的APP使用第三方库 Android Studio常用快捷键 一.两大常用布局 1.LinearLayout线性布局 线性布局,可以垂直显示或者水平显示,设 ...
- Linux下GitLab服务器搭建
系统环境 操作系统:CentOS6.9关闭防火墙 安装步骤 1. 安装Postfix 2. 下载rpm包并安装 3. 配置gitlab,vim /etc/gitlab/gitlab.rb,指定ip+端 ...
- java多线程中的三种特性
java多线程中的三种特性 原子性(Atomicity) 原子性是指在一个操作中就是cpu不可以在中途暂停然后再调度,既不被中断操作,要不执行完成,要不就不执行. 如果一个操作时原子性的,那么多线程并 ...
- git 常用命令,上传,下载,更新线上代码
git 常用命令以及推荐git新建上传个人博客 $ git clone //本地如果无远程代码,先做这步,不然就忽略 $ git status //查看本地自己修改了多少文件 $ git add . ...