Koa2介绍
为什么会有这篇文章,是因为前面尝试了
koa2 + TypeScript
的开发方式【实战篇】koa2+Ts项目的优雅使用和封装
koa2源码文件如下结构:
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框架需要封装和实现如下四个模块:
- 封装
node http serve
r. 创建koa类构造函数
; - 构造
request、response
、及context
对象; compose中间件机制
的实现;错误捕获和错误处理
;
一、封装node http server. 创建koa类构造函数
使用node的原生模块实现一个简单的服务器,并且打印 hello koa,代码:
const http = require('http');
const server = http.createServer((req, res) => {
res.writeHead(200);
res.end('hello koa....');
});
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 koa....');
});
app.listen(3000, () => {
console.log('listening on 3000');
});
如上代码有个缺点,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 对象。
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;
简单讲,就是需要将http的res和req转化为koa的res和req方便使用
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中挂载的。
因此我们的context.js 代码可以改成如下:
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;
当然,也可以不自己定义delegateGet
, delegateSet
,直接引入delegates
这个库
最后我们需要来修改application.js
代码,引入request,response,context
对象。如下代码:
const Emitter = require('events')
const http = require('http')
const context = require('./context')
const request = require('./request')
const response = require('./response')
class Application extends Emitter {
constructor() {
super()
this.context = Object.create(context)
this.request = Object.create(request)
this.response = Object.create(response)
this.middleWares = []
}
listen(port, callback) {
const server = http.createServer(this.callback())
server.listen(port)
callback()
}
use(fn) {
this.middleWares.push(fn)
// 保持use的链式调用
return this
}
callback() {
return (req, res) => {
let ctx = this.createContext(req, res)
// 创建响应内容
let response = () => this.responseBody(ctx)
// 调用 compose 函数,把所有的函数合并; 中间件的时候再解析
const fn = this.compose()
return fn(ctx).then(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
}
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
我们添加了createContext这个方法,这个方法是关键,它通过Object.create创建了ctx,并将request和response挂载到了ctx上面,将原生的req和res挂载到了ctx的子属性上。
三、中间件机制的实现
很清晰的表明了一个请求是如何经过中间件最后生成响应的,这种模式中开发和使用中间件都是非常方便的
那么现在我们想要实现这么一个类似koa2中间件的这么一个机制,我们该如何做呢?
koa的剥洋葱模型在koa1中使用的是generator + co.js
去实现的,koa2则使用了async/await + Promise
去实现的,接下来我们基于async/await + Promise
去实现koa2中的中间件机制。首先,假设当koa的中间件机制已经做好了,那么它是能成功运行下面代码的:
我们都知道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。
接下来,我们来修改application.js
的代码:
const Emitter = require('events')
const http = require('http')
const context = require('./context')
const request = require('./request')
const response = require('./response')
class Application extends Emitter {
constructor() {
super()
this.context = Object.create(context)
this.request = Object.create(request)
this.response = Object.create(response)
this.middleWares = []
}
listen(port, callback) {
const server = http.createServer(this.callback())
server.listen(port)
callback()
}
use(fn) {
this.middleWares.push(fn)
// 保持use的链式调用
return this
}
callback() {
return (req, res) => {
let ctx = this.createContext(req, res)
// 创建响应内容
let response = () => this.responseBody(ctx)
// 调用 compose 函数,把所有的函数合并
const fn = this.compose()
return fn(ctx).then(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
}
compose() {
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()
}
}
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
最核心的就是compose
函数,每一次都是将自己的执行函数封装成next当做上一个中间件的next参数,这样当循环到第一个中间件的时候,只需要执行一次next(),就能链式的递归调用所有中间件
四、错误处理
这里,我们是直接继承events
来实现:
let EventEmitter = require('events');
class Application extends EventEmitter {
callback() {
return (req, res) => {
let ctx = this.createContext(req, res)
// 创建响应内容
let response = () => this.responseBody(ctx)
// 创建异常捕获
let onerror = (err) => this.onerror(err, ctx)
// 调用 compose 函数,把所有的函数合并
const fn = this.compose()
return fn(ctx).then(response).catch(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()
}}