【koa原理篇】实现一个简单的koa2库

253 阅读7分钟

Koa2介绍

为什么会有这篇文章,是因为前面尝试了koa2 + TypeScript的开发方式【实战篇】koa2+Ts项目的优雅使用和封装

koa2源码文件如下结构:

image.png

  • 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框架需要封装和实现如下四个模块

  1. 封装node http server. 创建koa类构造函数;
  2. 构造request、response、及 context 对象;
  3. compose中间件机制的实现;
  4. 错误捕获和错误处理;

一、封装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的子属性上。

三、中间件机制的实现

很清晰的表明了一个请求是如何经过中间件最后生成响应的,这种模式中开发和使用中间件都是非常方便的

image.png

image.png

那么现在我们想要实现这么一个类似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()
     }}

demo地址

github.com/kkxiaojun/m…