Skip to content

Latest commit

 

History

History
297 lines (234 loc) · 8.09 KB

2018-11-24-koa2.md

File metadata and controls

297 lines (234 loc) · 8.09 KB

通过开发精简版的 koa2 了解其原理实现

Koa2 号称是下一代的 web 框架,由 Express 幕后原版人马打造,据说更小、更快、更加精简,主要采用了 es7 的语法糖asyncawait,并且 koa2 摒弃了 express 中的回调函数机制,这么多特性不得不去学习下它的原理,因此,自己通过实现精简版的 Qoa(koa2)来了解其原理实现,只是为了学习使用,欢迎来交流(github)。

介绍

简单实现 koa 基本功能,仅供学习参考,了解 koa 的核心思想以及实现过程。

环境要求

依赖 node v7.6.0 或 ES2015 及更高版本和 async 方法支持。

实现功能

主要依据koa官网介绍,实现简易核心功能,基本实现以下几个功能

  • use
  • listen
  • callback
  • keys
  • context 上下文
  • response
  • request
  • middleware

use

实现useAPI 其实就是将定义的方法发push进入定义的中间件数组变量middleware中,核心代码如下:

use(cb) {
  this.middleware.push(cb)
}

listen

主要是调用了 node 原生 API 中的http.createServer()方法,在这会运行两个方法,基本如下:

1、创建ctx 2、定义fn变量,保存use中的push进去的方法, 3、运行所有的fn中的方法,同时通过定义的next来触发fn中下一个中间件方法

const server = http.createServer(async (req, res) => {
  const ctx = this.createCtx(req, res)
  const fn = compose(this.middleware)
  await fn(ctx)
  ctx.res.end(ctx.body)
})
return server.listen(...args)

callback

之前定义的listen中,使用http.createServer()起的端口服务,如果需要定义多个端口,或者定义https的话,你就可以使用callback这个 API,并且将之前定义的中间件方法再次运行一次,然后挂在到新定义的服务上,代码如下:

const fn = compose(this.middleware)
const handleRequest = (req, res) => {
  const ctx = this.createCtx(req, res)
  return this[_handleRequest](ctx, fn)
}
return handleRequest

定义this[_handleRequest]主要是为了使用私有方法,这样外部就能被访问。

keys

这个主要是为了设置签名的 Cookie 密钥使用的,跟 koa2 实现基本一致,片段代码如下:

// context.js
get cookies() {
  if (!this[COOKIES]) {
    this[COOKIES] = new Cookies(this.req, this.res, {
      keys: this.app.keys
    });
  }
  return this[COOKIES];
},

set cookies(_cookies) {
  this[COOKIES] = _cookies;
}

context.js 中能获取到 this.app 上定义的属性,主要是因为在application.js中创建上下文的时候将this赋值给当前contextrequestresponseapp,片段代码如下:

// application.js
context.app = request.app = response.app = this

context

这里并没有实现完整的context代码,主要是使用了get set来定义方法,这样能监听对上下文的改变,并没有按照 koa 实现完整逻辑,只是为了实现基本流程,完整代码如下:

// context.js
const Cookies = require('cookies')
const COOKIES = Symbol('context#cookies')

module.exports = {
  get url() {
    return this.request.url
  },
  get body() {
    return this.response.body
  },
  set body(val) {
    this.response.body = val
  },
  get cookies() {
    if (!this[COOKIES]) {
      this[COOKIES] = new Cookies(this.req, this.res, {
        keys: this.app.keys
      })
    }
    return this[COOKIES]
  },
  set cookies(_cookies) {
    this[COOKIES] = _cookies
  }
}

response 和 request

为了实现基本流程,所以代码并不复杂,基本如下:

// response.js
module.exports = {
  get body() {
    return this._body
  },

  set body(val) {
    this._body = val
  }
}
// request.js
module.exports = {
  get url() {
    return this.req.url
  }
}

middleware

koa2 的中间件其实就是洋葱结构,从上往下一层一层进来,再从下往上一层一层回去。实现简易版的中间件机制,完整代码如下:

// compose.js
function compose(middleware) {
  return context => {
    // 第一次出发use中的中间件的函数内容
    return dispatch(0)
    function dispatch(i) {
      let fn = middleware[i]
      if (!fn) return Promise.resolve()
      return Promise.resolve(
        fn(context, next => {
          // 这里的next就是外层use中间件传入的next,用来确定执行下一个中间件的标记
          return dispatch(i + 1)
        })
      )
    }
  }
}

module.exports = compose

这个得结合示例代码以及application.js中的方法来读懂compose方法,以下是application.js中的uselistencreateCtx三个方法:

module.exports = class Application {
  // 省略
  use(cb) {
    this.middleware.push(cb)
  }

  listen(...args) {
    const server = http.createServer(async (req, res) => {
      const ctx = this.createCtx(req, res)
      const fn = compose(this.middleware)
      await fn(ctx) // 将上下文ctx传入compose.js中的compose方法中return出来的函数
      ctx.res.end(ctx.body)
    })
    return server.listen(...args)
  }

  createCtx(req, res) {
    const ctx = Object.create(this.context)
    const request = (ctx.request = Object.create(this.request))
    const response = (ctx.response = Object.create(this.response))
    ctx.app = request.app = response.app = this
    ctx.req = request.req = response.req = req
    ctx.res = request.res = response.res = res
    return ctx
  }
  // 省略
}

根据 demo 测试代码,你会发现最后浏览器预览输出的结果是123654123好理解,根据上面的源码,发现 use 只是个同步代码,只有在触发next时会触发compose中的dispatch方法,继续执行下一个异步中间件逻辑操作,因此会接着拼出654,至于为什么不是123456可以查看关于node 环境下的事件循环机制

再谈上下文

这里主要看看入口文件,对于上下文到底做了什么,片段代码如下:

// application.js
// 省略
module.exports = class Application {
  // 省略
  createCtx(req, res) {
    const ctx = Object.create(this.context)
    const request = (ctx.request = Object.create(this.request))
    const response = (ctx.response = Object.create(this.response))
    ctx.app = request.app = response.app = this
    ctx.req = request.req = response.req = req
    ctx.res = request.res = response.res = res
    return ctx
  }
  // 省略
}

这里需要解释下,ctx.reqctx.resctx.apprequest.resresponse.req

  • request - request 继承于 Request.js 静态类,包含操作 request 的一些常用方法
  • response - response 继承于 Response.js 静态类,包含操作 response 的一些常用方法
  • req - nodejs 原生的 request 对象
  • res - nodejs 原生的 response 对象
  • app - koa 的原型对象

demo 测试

运行test文件夹下的 js 文件即可,基本实现以上几个简易的功能,测试代码如下:

const Qoa = require('../core/application')
const http = require('http')
const https = require('https')

const app = new Qoa()

app.use(async (ctx, next) => {
  ctx.body = '1'
  ctx.cookies.set('name', 'tobi', { signed: true })
  await next()
  ctx.body += '4'
})

app.use(async (ctx, next) => {
  ctx.body += '2'
  await next()
  ctx.body += '5'
})

app.use(async (ctx, next) => {
  ctx.body += '3'
  await next()
  ctx.body += '6'
})

app.keys = ['im a newer secret', 'i like turtle']

app.listen(3000, () => {
  console.log('server start port 3000')
})

http.createServer(app.callback()).listen(1234, () => {
  console.log('开启1234端口')
})
https.createServer(app.callback()).listen(443, () => {
  console.log('开启443端口')
})

总结 Koa 相关 api 介绍

最后

如果有什么不对的地方,欢迎提issues