koa实现原理

实现koa有这几个主要步骤:

  • 封装httpServer
  • 上下文context和requestresponse对象
  • 中间件函数 middleware
  • 错误处理

koa 核心源码里面包括这几个文件:

|-- koa
    |-- lib
        |-- application.js
        |-- context.js
        |-- request.js
        |-- response.js

入口文件 application

application.js是核心入口文件,包括导出Koa类函数和核心代码的实现:

const http = require("http")

class Koa {
  constructor() {
    // 上下文 context
    this.ctx = Object.create({});
    // 中间件函数
    this.middleware = null
  }
  use(fn) {
    // 将用户传入的函数绑定到中间件函数中
    this.middleware = fn
  }
  handleRequest(req, res) {
    let ctx = this.ctx;
    // 给上下文添加 request 和 response 对象
    ctx.req = req;
    ctx.res = res;
    // 执行中间件函数
    this.middleware(ctx);
    // 返回结果
    ctx.body ? res.end(ctx.body) : res.end("Not Found")
  }
  listen() {
    let server = http.createServer(this.handleRequest.bind(this))
    server.listen(...arguments)
  }
}

module.exports = Koa;

上面的代码已经完成了httpServer服务的封装,有最基础的context上下文对象,绑定了requestresponse属性,可以在根目录下创建index.js文件测试:

let Koa = require("./koa/lib/application");
let app = new Koa();

app.use((ctx) => (ctx.body = ctx.req.url))

app.listen(3000);

context、request和response

源码里面使用gettersetter属性,封装上下文contextreqres,也就是requestresponse对象。

request

创建request.js,加入下面代码:

let url = require('url');

module.exports = {
  get path() {
    return url.parse(this.req.url,true).pathname
  },
  get query() {
      return url.parse(this.req.url,true).query
  }
}

response

创建response.js,加入下面代码:

module.exports = {
  _body: "",
  get body() {
    return this._body
  },
  set body(value) {
    this.res.statusCode = 200
    this._body = value
  },
}

context

创建context.js,加入下面代码:

let ctx = {}

function defineGetter(property,key){
    // 相当于去 property 上取值
    ctx.__defineGetter__(key,function(){
        return this[property][key]
    });
}

function defineSetter(property,key){
    // 相当于给 property 赋值
    ctx.__defineSetter__(key, function (value) {
      this[property][key]=value
    })
}

defineGetter("request","path");
defineGetter("request","query");

defineGetter("response","body");
defineSetter("response", "body")

module.exports = ctx;

源码里面通过__defineSetter____defineGetter__requestresponse的属性挂载到了上下文context,接下啦修改application.js,引入contextresponserequest

const context = require("./context")
const request = require("./request")
const response = require("./response")
...
    constructor() {
        ...
        // Object.create防止用户直接修改对象,保证每次new Koa都是新的对象
        this.context = Object.create(context)
        this.request = Object.create(request)
        this.response = Object.create(response)
    }
    createContext(req, res) {
        let ctx = this.context
        /*
        * ctx.request.req\ctx.req\ctx.req
        * ctx.response.res\ctx.res\ctx.res
        */
        ctx.request = this.request
        ctx.response = this.response
        ctx.request.req = ctx.req = req
        ctx.response.res = ctx.res = res

        return ctx
    }
    handleRequest(req, res) {
        // 获取新的上下文
        let ctx = this.createContext(req, res)
        // 执行中间件函数
        this.middleware(ctx);
        ...
    }

中间件 middleware

上面的middleware函数只绑定了一个方法,我们知道koa里面是可以绑定多个中间件函数,并且中间件函数包含上下文context和是否继续执行的next函数,因为koa2中使用的是async/await的方式,所以中间件函数返回的都会是一个Promise

middleware改成数组middlewares

  constructor() {
    ...
    this.middlewares = []
    ...
  }
  use(fn){
    //先将函数保存到中间件数组中
    this.middlewares.push(fn)
  }
  ...

接下来创建compose方法处理中间件函数:

  compose(ctx, middlewares) {
    // 当前函数执行指针
    let exectIndex = -1
    let dispatch = async function (index) {
    // 防止同一个中间件函数出现两个dispatch函数抛出异常
      if (exectIndex >= index) return Promise.reject("mulit called next();")
      exectIndex = index
      // 全部执行完成返回Promise
      if (index === middlewares.length) return Promise.resolve()
      // 取出中间件函数处理
      let middleware = middlewares[index]
      // next函数继续取出下一个中间件函数执行
      let next = () => dispatch(++index);
      // 返回执行的中间件函数
      return middleware(ctx, next)
    }
    return dispatch(0)
  }

修改handleRequest函数如下:

  handleRequest(req, res) {
    let ctx = this.createContext(req, res)
    res.statusCode = 404
    let p = this.compose(ctx, this.middlewares)
    p.then(() => {
      ctx.body ? res.end(ctx.body) : res.end("Not Found")
    })
  }

错误处理

koa 里面可以通过订阅error事件捕获中间件函数运行过程中出现的异常:

app.on("error",(error,ctx)=>{
    ctx.res.end(error.toString())
})

这理可以通过继承events对象,获得发布订阅的能力:

const EventEmiter = require("events")
class Koa extends EventEmiter {
    constructor() {
        super()
        ...
    }
    ...
    handleRequest(req,res){
        ...
        p.then(() => {
        ctx.body ? res.end(ctx.body) : res.end("Not Found")
        }).catch(error=>{
            // 铺货错误后发送给error
            this.emit("error", error, ctx)
        })
    }
}

完整的application.js代码:

const http = require("http")
const Stream = require("stream")
const EventEmiter = require("events")
const context = require("./context")
const request = require("./request")
const response = require("./response")

class Koa extends EventEmiter {
  constructor() {
    super()
    this.middlewares = []

    // Object.create防止用户直接修改对象,保证每次new Koa都是新的对象
    this.context = Object.create(context)
    this.request = Object.create(request)
    this.response = Object.create(response)
  }
  use(fn) {
    this.middlewares.push(fn)
  }
  compose(ctx, middlewares) {
    let exectIndex = -1
    let dispatch = async function (index) {
      if (exectIndex >= index) return Promise.reject("mulit called next();")
      exectIndex = index
      if (index === middlewares.length) return Promise.resolve()
      let middleware = middlewares[index]
      return middleware(ctx, () => dispatch(++index))
    }
    return dispatch(0)
  }
  createContext(req, res) {
    let ctx = this.context
    /*
    * ctx.request.req\ctx.req\ctx.req
    * ctx.response.res\ctx.res\ctx.res
    */
    ctx.request = this.request
    ctx.response = this.response
    ctx.request.req = ctx.req = req
    ctx.response.res = ctx.res = res

    return ctx
  }
  handleRequest(req, res) {
    let ctx = this.createContext(req, res)
    res.statusCode = 404
    let p = this.compose(ctx, this.middlewares)
    p.then(() => {
      if (ctx.body instanceof Stream) {
        res.setHeader("Content-Type", "application/octet-stream")
        res.setHeader("Content-Disposition", `attachment;filename=download`)
        return ctx.body.pipe(res)
      }
      if (ctx.body) {
        res.end(ctx.body)
      } else {
        res.end("Not Found")
      }
    }).catch((error) => {
      this.emit("error", error, ctx)
    })
  }
  listen() {
    let server = http.createServer(this.handleRequest.bind(this))
    server.listen(...arguments)
  }
}

module.exports = Koa