周五组内同学讨论搞一些好玩的东西,有人提到了类似『5分钟实现koa』,『100行实现react』的创意,仔细想了以后,5分钟实现koa并非不能实现,遂有了这篇博客。
准备
先打开koa官网,随意找出了一个代表koa核心功能的的demo就可以,如下
const Koa = require('koa');
const app = new Koa();
// x-response-time
app.use(async (ctx, next) => {
const start = Date.now();
await next();
const ms = Date.now() - start;
ctx.set('X-Response-Time', `${ms}ms`);
});
// logger
app.use(async (ctx, next) => {
const start = Date.now();
await next();
const ms = Date.now() - start;
console.log(`${ctx.method} ${ctx.url} - ${ms}`);
});
// response
app.use(async ctx => {
ctx.body = 'Hello World';
});
app.listen(3000);
最终要实现的效果是实现的一个5min-koa模块,直接将代码中第一行替换为const Koa = require('./5min-koa');
,程序可以正常执行就可以了。
Koa的核心
通过koa官网得知,app.listen方法实际上是如下代码的简写
const http = require('http');
const Koa = require('koa');
const app = new Koa();
http.createServer(app.callback()).listen(3000);
所以我们可以先把app.listen实现出来
class Koa {
constructor() {}
callback() {
return (req, res) => {
// TODO
}
}
listen(port) {
http.createServer(this.callback()).listen(port);
}
}
koa的核心分为四部分,分别是
- context 上下文
- middleware 中间件
- request 请求
- responce 响应
Context
我们先来实现一个最简化版的context,如下
class Context {
constructor(app, req, res) {
this.app = app
this.req = req
this.res = res
// 为了尽可能缩短实现时间,我们直接使用原生的res和req,没有实现ctx上的ctx.request ctx.response
// ctx.request ctx.response只是在原生res和req上包装处理了一层
}
// 实现一些demo中使用到的ctx上代理的方法
get set() { return this.res.setHeader }
get method() { return this.req.method }
get url() { return this.req.url }
}
这样就完成了一个最基本的Context,别看小,已经够用了。
每一次有新的请求,都会创建一个新的ctx对象。
Middleware
koa的中间件是一个异步函数,接受两个参数,分别是ctx和next,其中ctx是当前的请求上下文,next是下一个中间件(也是异步函数),这样想来,我们需要一个维护中间件的数组,每次调用app.use就是往数组中push一个一步函数。所以use方法实现如下
use(middleware) {
this.middlewares.push(middleware)
}
每次有新的请求,我们都需要把这次请求的上下文灌进数组中的每一个中间件里。单单灌进ctx还不够,还要使每个中间件都能通过next函数调用到下一个中间件。当我们调用next函数时,一般是不需要传参数的,而被调用的中间件中一定会接收到ctx和next两个参数。
调用方不需要传参,被调用方却能接到参数,这让我立刻想到bind方法,只要将每一个中间件所需要的ctx和next都提前绑定好,问题就解决了。下面的代码就是通过bind方法,将用户传入的middleware列表转换成next函数列表
let bindedMiddleware = []
for (let i = middlewares.length - 1; i >= 0; i--) {
if (middlewares.length == i + 1) {
// 最后一个中间件,next方法设置为Promise.resolve
bindedMiddleware.unshift(middlewares[i].bind(ctx.app, ctx, Promise.resolve))
} else {
bindedMiddleware.unshift(middlewares[i].bind(ctx.app, ctx, bindedMiddleware[0]))
}
}
最后我们就得到了一个next函数数组,也就是bindedMiddleware这个变量了。
Request
http.createServer中的回调函数,每次接收到请求的时候会被调用,所以我们在上面callback方法的TODO位置,编写处理请求的代码, 并将上面的middleware列表转next函数列表的代码放入其中。
function handleRequest(ctx, middlewares) {
if (middlewares && middlewares.length > 0) {
let bindedMiddleware = []
for (let i = middlewares.length - 1; i >= 0; i--) {
if (middlewares.length == i + 1) {
bindedMiddleware.unshift(middlewares[i].bind(ctx.app, ctx, Promise.resolve))
} else {
bindedMiddleware.unshift(middlewares[i].bind(ctx.app, ctx, bindedMiddleware[0]))
}
}
return bindedMiddleware[0]()
} else {
return Promise.resolve()
}
}
Responce
我们简单出来下相应就好了,直接将ctx.body发送给客户端。
function handleResponse (ctx) {
return function() {
ctx.res.writeHead(200, { 'Content-Type': 'text/plain' });
ctx.res.end(ctx.body);
}
}
完成Koa类的实现
koa的app实例上面带有on,emit等方法,这是node events模块实现好的东西。直接让Koa类继承自events模块就好了。
我们再将上面实现出来的handleRequest和handleResponse方法放入koa类的callback方法中,得到最终我们实现的Koa,一共58行代码,如下
const http = require('http');
const Emitter = require('events');
class Context {
constructor(app, req, res) {
this.app = app;
this.req = req;
this.res = res;
}
get set() { return this.res.setHeader }
get method() { return this.req.method }
get url() { return this.req.url }
}
class Koa extends Emitter{
constructor(options) {
super();
this.options = options
this.middlewares = [];
}
use(middleware) {
this.middlewares.push(middleware);
}
callback() {
return (req, res) => {
let ctx = new Context(this, req, res);
handleRequest(ctx, this.middlewares).then(handleResponse(ctx));
}
}
listen(port) {
http.createServer(this.callback()).listen(port);
}
}
function handleRequest(ctx, middlewares) {
if (middlewares && middlewares.length > 0) {
let bindedMiddleware = [];
for (let i = middlewares.length - 1; i >= 0; i--) {
if (middlewares.length == i + 1) {
bindedMiddleware.unshift(middlewares[i].bind(ctx.app, ctx, Promise.resolve));
} else {
bindedMiddleware.unshift(middlewares[i].bind(ctx.app, ctx, bindedMiddleware[0]));
}
}
return bindedMiddleware[0]();
} else {
return Promise.resolve();
}
}
function handleResponse (ctx) {
return function() {
ctx.res.writeHead(200, { 'Content-Type': 'text/plain' });
ctx.res.end(ctx.body);
}
}
module.exports = Koa;
试试跑一下篇首的Demo,没什么问题。
结语
简版实现,码糙理不糙,展示出了koa核心的东西,但少了错误处理,也完全没有考虑性能啥的,需要完善的地方还很多很多。
笔者在写了这个5分钟koa以后去看了koa源码,发现实现思路基本就是这样,相信经过我的这个5分钟koa的洗礼,你去看koa源码一样小菜一碟。
Done!