Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

5分钟实现一个Koa (Write Koa in 5 minutes) #12

Open
GeoffZhu opened this issue Jun 3, 2018 · 1 comment
Open

5分钟实现一个Koa (Write Koa in 5 minutes) #12

GeoffZhu opened this issue Jun 3, 2018 · 1 comment

Comments

@GeoffZhu
Copy link
Owner

GeoffZhu commented Jun 3, 2018

首发地址

周五组内同学讨论搞一些好玩的东西,有人提到了类似『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!

@GeoffZhu GeoffZhu changed the title 5分钟实现一个koa (Write koa in 5 minutes) 5分钟实现一个Koa (Write Koa in 5 minutes) Jun 3, 2018
@jsm1003
Copy link

jsm1003 commented Jun 8, 2018

58行代码😄

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants