80行代码实现Koa

307 阅读3分钟

Koa 是轻量级的 HTTP 框架,用法非常简单,4 行代码就能写一个 hello world 接口:

const Koa = require('koa')
const app = new Koa()
app.use((ctx, next) => ctx.body = 'hello world')
app.listen(3000, () => console.log(`server start at localhost:3000`))

接下来就实现一个简易版的 Koa。

首先 Koa 是基于 Node.js 中的 http 模块和 events 模块的,基本结构如下:

  • listen 方法监听端口
  • use 方法添加中间件
  • 内部 middlewares 数组保存中间件函数

其核心在于 Koa 把 http 模块里面的 req 和 res 两个参数封装成了一个强大的 ctx 上下文参数,保留了原有所有 API 并扩充了自己的方法。首先要写两个对象:

  • request 封装后的请求对象,扩充 createServer 的回调参数 req
  • response 封装后的相应对象,扩充 createServer 的回调参数 res

在写这两个对象之前,先写两个辅助函数:

function defineGetter(obj, target, keys) { // 访问 A.x 被代理到返回 B.x
  keys.forEach((key) => Object.defineProperty(obj, key, {
    configurable: true,
    get() {
      return this[target][key]
    },
  }))
}
function defineSetter(obj, target, keys) { // 设置 A.x 被代理到设置 B.x
  keys.forEach((key) => Object.defineProperty(obj, key, {
    configurable: true,
    set(value) {
      this[target][key] = value
    },
  }))
}

接下来定义 request 对象,对于原生 req 的一些属性,例如 method、url、headers 直接保留,扩充了 query 和 path 属性

const url = require('url')
const request = {
  get query() { return url.parse(this.req.url, true).query }, // 请求参数对象
  get path() { return url.parse(this.req.url, true).pathname }, // 请求路径
}
const baseKeys = ['method', 'url', 'headers'] // 原生 req 对象的属性
defineGetter(request, 'req', baseKeys) // 访问 request 的属性时,代理到 req 对象

然后定义 resposne 对象,这里最核心的就是定义 body 访问器属性:

const response = {
  get body() { return this._body }, // 获取返回值
  set body(body) { this._body = body }, // 设置返回值
}

接下来就是 context 对象,把 request 和 response 对象封装在了一起:

const context = {}
defineGetter(context, 'request', baseKeys.concat(['query', 'path'])) // 访问 context 代理到 request
defineGetter(context, 'response', ['body']) // 访问 context 代理到 response
defineSetter(context, 'response', ['body']) // 设置 context 代理到 resposne

最后实现 Koa:

const http = require('http')
const events = require('events')
class Koa extends events.EventEmitter {
  request = Object.create(request) // 每次 new Koa 创建全新 request
  response = Object.create(response) // 每次 new Koa 创建全新 response
  context = Object.create(context) // 每次 new Koa 创建全新上下文
  middlewares = [] // 保存中间件
  listen(...args) { // 监听端口
    return http.createServer(this.handleRequest.bind(this)).listen(...args)
  }
  handleRequest(req, res) { // 处理请求
    const ctx = this.createContext(req, res)
    this.compose(ctx).then(() => {
      const body = ctx.body
      if (body) {
        if (typeof body === 'object') {
          res.setHeader('Content-Type', 'application/json')
          res.end(JSON.stringify(body))
        } else {
          res.end(body.toString())
        }
      } else {
        res.statusCode = 404
        res.end(`Not Found`)
      }
    })
  }
  createContext(req, res) { // 每次请求创建全新上下文
    const ctx = Object.create(this.context)
    const request = Object.create(this.request)
    const response = Object.create(this.response)
    ctx.request = request
    ctx.req = ctx.request.req = req
    ctx.response = response
    ctx.res = ctx.response.res = res
    return ctx
  }
  use(fn) {
    this.middlewares.push(fn) // 添加到中间件队列
  }
  compose(ctx) { // 实现洋葱模型
    const dispatch = (i) => {
      if (i === this.middlewares.length) return Promise.resolve()
      return Promise.resolve(this.middlewares[i](ctx, () => dispatch(i + 1)))
    }
    return dispatch(0)
  }
}

上面的代码加起来只有 80 行,涵盖了 Koa 的核心思想,可以实现 Koa 的基础功能了。实际上,Koa 的源码只有 4 个文件:

  • application.js 定义了 Koa 类
  • context.js 定义上下文 ctx 对象
  • request.js 扩充 req 对象
  • response.js 扩充 res 对象

代码非常精简,感兴趣的可以看下。