koa 源码解析

2021-07-14 20:17:41 浏览数 (1)

本次的文章是之前 koa 专题的延续,计划书写两篇文章,本篇从零实现一个简单版的 koa 框架(里面可能涉及一点 node 知识,不会太讲,大家如果遇到不了解的可以自行百度查看,也可以看官网文档了解使用)。包括上下文 ctx 组合 req, res 的实现,中间件机制的实现。第二篇写下 bodyparserrouter 中间件的简单实现,理解其原理。

koa 使用

test.js

代码语言:txt复制
const Koa = require('koa')

const app = new Koa()
let port = 3000

app.use(async (ctx, next) => {
  ctx.body = '测不准1111'
  // 如果不调用 next, 页面会显示 测不准1111
  await next()
})

app.use(async (ctx, next) => {
  ctx.body = '测不准2222'
})
// 出现异常 端口号自动  1
app.on('error', () => {
  app.listen(  port)
})

app.listen(port, () => {
  console.log(`正在监听 ${PORT} 端口`)
})

从上面的代码我们可以看出来:

  • Koa 是一个类,有 uselisten 方法(on 监听器明显是发布订阅)
  • 由于我们启动了一个服务,所以要用到 http 模块
  • 页面显示了‘测不准2222’,所以 ctx.body 的结果会赋值到 res.end()` 中
  • use 中的两个函数都是中间件,调用 next 方法执行下一个

初始化项目结构

  • package.json

主要配置下 main 字段,包的入口

代码语言:txt复制
{
...
  "main": "./lib/application",
...
}
  • 目录结构保持一致

编写 application 文件

  • 初始化结构
代码语言:txt复制
const EventEmitter = require('events')
const http = require('http')

// 继承 可以使用发布订阅
class Koa extends EventEmitter{
  constructor() {
    super()
  }
  use() {

  }
  listen() {

  }
}

module.exports = Koa
  • listen 中创建服务
代码语言:txt复制
// es7 保证this。也可以使用bind
handleRequest = (req, res) => {

}
// 接收传的 端口号 和 回调
listen(...args) {
  const server = http.createServer(this.handleRequest)
  server.listen(...args)
}
  • 初始化 context request response 三个文件,application 中引入requestresponse 两个文件是对原生 reqres 的拓展
代码语言:txt复制
const context = {

}
module.exports = context
------------------------------
const request = {

}
module.exports = request
---------------------------------
const response = {

}
module.exports = response
  • 创建新的上下文、请求、响应
代码语言:txt复制
constructor() {
  super()
  /**
 * Koa 使用时可以创建多个 koa实例
 * let app1 = new Koa()
 * let app2 = new Koa()
 * 因为我们创建的上下文和请求响应是引用类型,如果改了一个,也会影响其他的实例。
 * 所以在创建时,就要新创建一个,互不干扰
 * 本质 this.context.__proto__ = context
 */
  this.context = Object.create(context)
  this.request = Object.create(request)
  this.response = Object.create(response)
}
use(middleware) {

}
// 
createContext(req, res) {

}

handleRequest = (req, res) => {
  // 创建服务的时候,创建上下文
  const ctx = this.createContext(req, res)
}

避免创建的上下文,请求响应冲突,也需要新创建对象,同时改写 ctx

代码语言:txt复制
// 在创建上下文中 改写 ctx
createContext(req, res) {
  const ctx = Object.create(this.context)
  const request = Object.create(this.request)
  const response = Object.create(this.response)
  // res.xx req.xx 都是原生的
  ctx.request = request
  ctx.request.req = req = ctx.req = req

  ctx.response = response
  ctx.response.res = ctx.res = res
  return ctx
}
  • 中间件操作

我们知道中间件是通过 app.use 形式注入的,可以写多个,所以用数组形式存储,执行时按顺序执行。因为 koa 中间件是洋葱模型,所以我们只是返回第一个中间件的执行,其余的在第一个的执行过程中就被执行了,我们把所有中间件拼接成 promise 返回执行;同时我们还要注意使用中间件时一定要 async await,才能确保达到想要的结果

代码语言:txt复制
constructor() {
  ...
  this.middlewares = []
}
// 中间件存储
use(middleware) {
  this.middlewares.push(middleware)
}
// 洋葱方式执行
compose() {
  let index = -1
  const dispatch = i => {
    // 传入的 i 值是不变的,如果在一个中间件中多次调用 await next(),那么内部中间件执行完执行当前中间件下一个 next 时,传入的i一定小于 index,大家可以自行打印
    if (i <= index) return Promise.reject('不要使用多个next')
    index = i
    // 执行到最后一项退出
    if (i == this.middlewares.length) return Promise.resolve()
    // 使用 Promise.resolve 包裹执行中间件
    let fn = this.middlewares[i]
    return Promise.resolve(fn(ctx, () => dispatch(i   1)))
  }

  return dispatch(0)
}

处理完中间件(执行完毕)最后返回给页面 ctx.body 即可

代码语言:txt复制
handleRequest = (req, res) => {
  const ctx = this.createContext(req, res)

  // 默认 404,当修改 ctx.body 时,改成 200
  // ctx.body 是在中间件中设置的,确保执行了
  ctx.status = 404
  this.compose(ctx).then(() => {
    // koa 可以直接读流
    if (ctx.body instanceof Stream) {
      ctx.body.pipe(res)
    } else if(typeof ctx.body == 'object' && ctx.body) {
      res.setHeader('Content-Type', 'application/json;charset=utf-8')
      res.end(JSON.parse(ctx.body))
    } else if (ctx.body) {
      res.setHeader('Content-Type', 'text/plain;charset=utf-8')
      res.end(ctx.body)
    } else {
      res.end('Not Found')
    }
  }).catch(err => {
    this.emit('error', err)
  })
}

代理 ctx、request、response

添加一个测试中间件

代码语言:txt复制
app.use(async (ctx, next) => {
  console.log(ctx.url)
  console.log(ctx.request.url)
  console.log(ctx.request.req.url) // 直接调用原生res
  await next()
})

打印结果

因为我们获取的数据都在原生的 reqres 中,我们想要获取 ctx.url,实际获取的是 req.rul ,这里我们就用到了类似 defineProperty 的代理,熟悉 vue 的小伙伴应该不陌生。

  • request.jsconst request = { get url() { // 谁调用的 this 就是谁 => ctx.request.req.url return this.req.url } } // 我们可以对每个属性进行 get 请求书写,但是比较麻烦 const url = require('url') const request = { get url() { return this.req.url }, get query() { return url.parse(this.url, true) }, get path() { return url.parse(this.url).pathname } }
  • response.js

我们把修改 body 的操作放在 response 文件中,因为 ctx.body 作为 res.end 的值得。默认的状态码是 404,在修改 body 的函数中设置状态码为 200

代码语言:txt复制
const response = {
  // 我们可以设置多个 ctx.body = 'xxx',会以最后一个为准
  // 所以一定是操作某个值,中间件执行完成后才执行 res.end()
  _body: undefined,
  get body() {
    return this._body
  },
  set body(val) {
    this.res.statusCode = 200
    this._body = val
  }
}

module.exports = response
  • context.js
代码语言:txt复制
const context = {}
// 这里使用函数操作,这三种代理方式是一样的,但是 __defineGetter__ 
// 不推荐使用了,但是源码是这种方式
function defineGetter(target, key) {
  context.__defineGetter__(key, function() {
    return this[target][key]
  })
}

function defineSetter(target, key) {
  context.__defineSetter__(key, function(val) {
    this[target][key] = val
  })
}

defineGetter('request', 'url')
defineGetter('request', 'query')
defineGetter('request', 'path')
defineGetter('request', 'body')

defineSetter('response', 'body')

module.exports = context

这时我们再看执行的 test.js 打印就是正常的了。大家如果感兴趣可以自己发布到 npm 上,当做是一个学习分享了。

koa 的源码相对较少,比较简单;相比来说 express 的内容比较多,像路由这种都封装到内部了,而 koa 只是提供了个架子,辅助操作都是用中间件的形式。下一篇我们介绍下几个中间件的实现。

有疑问可以添加小编 wx: wajh123654789

0 人点赞