为了更好的了解Koa实现的原理,我们会自己手写一个带核心功能的Koa框架。
我们先看一下如果使用原生的node.js api 是如何实现一个http server 的。
const http = require('http')
const app = http.createServer((req, rsp) => {
rsp.writeHead(200, { 'Content-Type': 'text/plain'})
rsp.end('Hello world')
})
app.listen(3000, () => {
console.log('listening at port 3000')
})
可以看到,createServer
方法的callback 中提供两个参数req 和 rsp 分别代表 请求体和返回体,我们可以通过对rsp返回体对象进行设置header返回头的各种参数,最后返回的数据会放在end
方法当中,可以是普通的字符串,json,xml,图片或其他数据流。
实现koa框架的步骤
实现一个koa框架主要需要用到以下的文件:
request.js, 主要封装原生nodejs中
url
和method
的getter参数.response.js, 主要提供了koa中的body属性的getter 和setter。
context.js,对应koa中ctx参数类型 。原生nodejs的api中,主要通过req和rsp操纵请求参数和返回内容。Koa中把请求和返回都融合到了ctx对应的context类中。
lzh_koa.js,对应koa中Koa类,是这个自己实现的框架的主类,提供各种主要的方法如
listen
,use
以及内部的compose方法,用于把koa调用use方法之后的各个中间件融合到一起的功能。
开始实现
1.request.js
module.exports = {
get url() {
return this.req.url
},
get method() {
return this.req.method.toLowerCase()
}
}
2.response.js
module.exports = {
get body() {
return this._body
},
set body(val) {
this._body = val
}
}
3.context.js, 支持从上面自己封装的request和response中提取以及设置对应的参数
module.exports = {
get url() {
return this.request.url;
},
get body() {
return this.response.body;
},
set body(val) {
this.response.body = val;
},
get method() {
return this.request.method
}
};
4.lzh_koa.js
compose 函数
这个类当中有一个高阶函数compose,用于把middlewares 里边的每一个中间件负责装配到一起。
compose(middlewares) {
return function (ctx) {
// 执行第0个
return dispatch(0);
function dispatch(i) {
let fn = middlewares[i];
if (!fn) {
return Promise.resolve();
}
return Promise.resolve(fn(ctx, function next() {
// promise完成后,再执行下一个
return dispatch(i + 1);
})
);
}
};
}
首先middlewares
是传进来的中间件的数组, 而每一个中间件都是以闭包的形式 (ctx, next) => { // 中间件实现代码}
传进来的,所以可以把它看成是一个装满我们需要的中间件的数组。
我们会使用递归的方式完成中间件函数的装配,之所以考虑使用递归的方式因为每一个next
参数传入的值刚好是下个dispatch的返回值, 作为递归体function dispatch(i)
用于按顺序递归调用传进来的中间件。而递归结束条件则是当每一个中间件都被Promise装配过了,如下所示:
let fn = middlewares[i];
if (!fn) { return Promise.resolve() }
为了方便理解,下面是一个具体些的例子,假如有三个中间件分别是fn1
fn2
fn3
分别被装进middlewares数组里边,那被compose数组装配完后的的函数就会长成下边这个样子。
return Promise.resolve(fn1(ctx, function next() {
// promise完成后,再执行下一个
return Promise.resolve( fn2(ctx, function next () {
return Promise.resolve( fn3(ctx, function next () {} ))
}))
}))
createContext 函数
// 构建上下文
createContext(req, res) {
const ctx = Object.create(context)
ctx.request = Object.create(request)
ctx.response = Object.create(response)
ctx.req = ctx.request.req = req
ctx.res = ctx.response.res = res
return ctx
}
可以看到这个函数里边用到了我们之前声明的 context
request
response
在这里用上了,主要我们会把 req 和 res 集中到ctx里边进行处理。
use 函数
use(middlewares) {
this.middlewares.push(middlewares)
}
use 函数唯一的功能就是帮我们把中间件都加到middlewares 数组当中。
listen 函数
listen(...args) {
const server = http.createServer(async (req, res) => {
//
const ctx = this.createContext(req, res)
const func = this.compose(this.middlewares)
await func(ctx)
res.end(ctx.body)
// this.callback(req, res)
})
server.listen(...args)
}
仿照Koa的写法,最后会有一个listen 函数对服务器端口进行监听,这里会首先把请求和返回体都合并到ctx对象中,再使用之前做好的compose方法对中间件数组进行合并供后面的统一调用 ( await func(ctx)
)。
最后一步就是调用res.end(ctx.body)
把相应返回了。
测试使用
最后我们起一个新的js文件进行测试使用.
const Koa = require('./lzh_koa')
const app = new Koa()
const delay = () => Promise.resolve(resolve => setTimeout(() => resolve(), 2000));
app.use(async (ctx, next) => {
ctx.body = "1";
await next();
ctx.body += "5";
});
app.use(async (ctx, next) => {
ctx.body += "2";
await delay();
await next();
ctx.body += "6";
});
app.use(async (ctx, next) => {
ctx.body += "3";
});
app.listen(3000, () => {
console.log('listen at 3000 host')
})
输出顺序为: 12365, 与Koa框架实现一致。
说在最后
至此我们自己就实现了一个带核心功能的Koa 框架, 当然里边还有很多细节的功能例如设置ctx返回类型等没有实现,但都可以通过扩展reponse和request类达到。
Demo地址放在 https://github.com/lzhlewis2015/lzh_koa .