实现一个简易 koa

346 阅读4分钟

用了一阵子 koa,今天心血来潮,想要自己动手实现一个简易版的 koa

这是我的目录结构

在想要写这篇文章之前是还没有开始写的,如果非要说有什么的话,大概只有以下的代码

// app.js
const Koa = require('koa');

const koa = new Koa();

koa.use(async (ctx, next) => {
  console.log(1);
  await next();
  console.log(2);
});

koa.use(async (ctx, next) => {
  console.log(3);
  await next();
  console.log(4);
});

koa.use(async (ctx, next) => {
  console.log(5);
  ctx.body = 'hello word';
});

koa.listen(3033);

当我们在浏览器输入 localhost:3033,会看到页面的 hello world,以及控制台的

1
3
5
4
2

熟悉的小伙伴都知道这是 koa 的洋葱模型,我们现在就来实现这个过程!

首先我们使用自定义的文件 myKoa/application 来替换掉引入的 koa

我们会使用 http 模块来创建服务,以及继承 events,所以会得到以下代码雏形

// app.js
const Koa = require('./myKoa/application');

// application.js
const http = require('http');
const Event = require('events');

class MyKoa extends Event {
  constructor () {
    super();
    this.middlewares = [];
  }

  use (fn) {
    this.middlewares.push(fn);
  }

  // 对 middleware 进行处理 
  compose () {
    return async ctx => {
    }
  }
  
  

  callback () {
    return (req, res) => {
      const ctx = 'ctx';
      const handleRequest = () => 'handleRequest';
      const onerror = () => 'onerror';
      const fn = this.compose();
      return fn(ctx).then(handleRequest).catch(onerror);
    };
  }

  listen (...args) {
    const server = http.createServer(this.callback());
    server.listen(...args);
  }

}

module.exports = MyKoa;

以上的代码中,看一下我们一共做了几件事:

  1. 生成 uselisten 方法,use 方法将 middleware 放入我们定义的中间件队列中,listen 方法调用了 http 模块
  2. 生成 callback 方法,返回了 http 模块中 createServer 的回调函数
  3. callback 方法中,我们定义了上下文对象 ctx 对象,对页面请求的处理函数 handleRequest, 以及错误回调函数 onerror

接下来我们首先实现洋葱模型

那么怎样实现洋葱模型的打印顺序呢?我们在最初的代码上考虑一下,洋葱模型的打印顺序是不是就和以下的代码顺序是一样的呢?

const Koa = require('./myKoa/application');

const koa = new Koa();

koa.use(async (ctx, next) => {
  console.log(1);
  await async (ctx, next) => {
    console.log(3);
    await async (ctx, next) => {
      console.log(5);
      ctx.body = 'hello word';
    }
    console.log(4);
  }
  console.log(2);
});

koa.listen(3033);

当然,以上的代码是有错的。但是这给我们提供了一种思路,那就是将每一个中间件中的 next 方法,替换成下一个要执行的中间件。

// application.js compose
compose () {
  return async ctx => {
    // 对最后一个中间件的 next 方法进行处理
    let next = async () => {
      return Promise.resolve();
    }

    // 对每一个中间件进行调用
    const processNext = (middleware, next) => {
      return async () => {
        await middleware(ctx, next);
      };
    }

    // 从后向前遍历中间件,保证每一个中间件的 next 都是下一个中间件
    for (let i = this.middlewares.length - 1; i >= 0; i--) {
      const fn = this.middlewares[i];
      next = processNext(fn, next);
    }

    // 执行最初的中间件
    await next();
  }
}

好了,写到这里我们就能惊喜的发现控制台真的按照洋葱模型的顺序打印出了1 3 5 4 2,源码中的 compose 的实现在 koa-compose 中,使用递归实现的,感兴趣的小伙伴也可以去看一下~

但是同时我们也发现浏览器的页面和下图一样一直在转圈圈

那是因为我们还没有对浏览器的请求作出相应。接下来我们就来实现这一模块,另外,偷一下懒,将源码中的 context.jsrequest.jsresponse.js 直接复制到了我们 mykoa 的文件下,每个文件的主要作用如下:

  • request.js:对 http 请求中的一些字段进行了 set, get 方法的绑定
  • response.js:对 http 响应中的一些字段进行了 set, get 方法的绑定
  • context.js:将 request.jsresponse.js 中的方法挂载到了 proto 对象上并导出,这里使用了 delegates 的库,感兴趣的同学也可以去看一下~

对浏览起的请求作出响应,也就是处理一下 callback 中的上下文对象 ctxhandleRequestonerror

所以我们最终的代码如下所示:

// application
const http = require('http');
const Event = require('events');
const context = require('./context');
const request = require('./request');
const response = require('./response');

class MyKoa extends Event {
  constructor () {
    super();
    this.middlewares = [];
    this.context = context;
    this.request = request;
    this.response = response;
  }

  use (fn) {
    this.middlewares.push(fn);
  }

  // 对 middleware 进行处理 
  compose () {
    return async ctx => {
      let next = async () => {
        return Promise.resolve();
      }

      const processNext = (middleware, next) => {
        return async () => {
          await middleware(ctx, next);
        };
      }

      for (let i = this.middlewares.length - 1; i >= 0; i--) {
        const fn = this.middlewares[i];
        next = processNext(fn, next);
      }

      await next();
    }
  }

  createContext(req, res) {
    const ctx = Object.create(this.context);
    ctx.request = Object.create(this.request);
    ctx.response = Object.create(this.response);
    ctx.req = ctx.response.req = req;
    ctx.res = ctx.response.res = res;
    return ctx;
  }

  handleRequest (ctx) {
    const context = ctx.body;
    if (typeof context === "string") {
      ctx.res.end(context);
    } else if (typeof context === "object") {
      ctx.res.end(JSON.stringify(context));
    }
  }

  onerror (ctx, error) {
    if (error.code === "ENOENT") {
      ctx.status = 404;
    } else {
      ctx.status = 500;
    }
  }

  callback () {
    return (req, res) => {
      const ctx = this.createContext(req, res);
      const handleRequest = () => this.handleRequest(ctx);
      const onerror = () => this.onerror(ctx, error);
      const fn = this.compose();
      return fn(ctx).then(handleRequest).catch(onerror);
    };
  }

  listen (...args) {
    const server = http.createServer(this.callback());
    server.listen(...args);
  }

}

module.exports = MyKoa;

以上的代码就是所有我们实现的简易 koa 的版本了,也能支持大部分使用场景

PS:源码中 createContext 的实现使用了一些技巧,感兴趣的小伙伴可以自己去仔细查看一下~