关于javascript:一步一步来手写Koa2

4次阅读

共计 5927 个字符,预计需要花费 15 分钟才能阅读完成。

之前讲过 Koa2 从零到脚手架,以及从浅入深理解 Koa2 源码

这篇文章解说如何手写一个 Koa2

Step 1:封装 HTTP 服务和创立 Koa 构造函数

之前浏览 Koa2 的源码得悉,Koa 的服务利用是基于 Node 原生的 HTTP 模块,对其进行封装造成的,咱们先用原生 Node 实现 HTTP 服务

const http = require('http')

const server = http.createServer((req, res) => {res.writeHead(200)
  res.end('hello world')
})

server.listen(3000, () => {console.log('监听 3000 端口')
})

再看看用 Koa2 实现 HTTP 服务

const Koa = require('Koa')
const app = new Koa()

app.use((ctx, next) => {ctx.body = 'hello world'})

app.listen(3000, () => {console.log('3000 申请胜利')
})

实现 Koa 的第一步,就是对 原生 HTTP 服务进行封装,咱们依照 Koa 源码的构造,新建 lib/application.js 文件,代码如下:

const http = require('http')

class Application {constructor() {this.callbackFunc}
  listen(port) {const server = http.createServer(this.callback())
    server.listen(port)
  }
  use(fn) {this.callbackFunc = fn}
  callback() {return (req, res) => this.callbackFunc(req, res)
  }
}

module.exports = Application

咱们引入手写的 Koa,并写个 demo

const Koa = require('./lib/application')

const app = new Koa()

app.use((req, res) => {res.writeHead(200)
  res.end('hello world')
})

app.listen(3000, () => {console.log('3000 申请胜利')
})

启动服务后,在浏览器中输出 http://localhost:3000,内容显示”Hello,World“

接着咱们有两个方向,一是简化 res.writeHead(200)、res.end('Hello world');二是做塞入多个中间件。要想做第一个点须要先写 context,response,request 文件。做第二点其实做到前面也须要依赖 context,所以咱们先做简化原生 response、request,以及将它集成到 context(ctx)对象上

Step 2:构建 request、response、context 对象

request、response、context 对象别离对应 request.js、response.js、context.js,request.js 解决申请体,response.js 解决响应体,context 集成了 request 和 response

// request
let url = require('url')
module.exports = {get query() {return url.parse(this.req.url, true).query
  },
}
// response
module.exporrs = {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 must be a number')
    }
    this.res.statusCode = statusCode
  },
}

这里咱们在 request 中只做了 query 解决,在 response 中只做了 body、status 的解决。无论是 request 还是 response,咱们都应用了 ES6 的 get、set,简略来说,get/set 就是能对一个 key 进行取值和赋值

当初咱们曾经实现了 request、response,获取了 request、response 对象和它们的封装办法,接下来咱们来写 context。咱们在源码剖析时已经说过,context 继承了 request 和 response 对象的参数,既有申请体中的办法,又有响应体中的办法,例如既能 ctx.query 查问申请体中 url 上的参数,又能通过 ctx.body 返回数据。

module.exports = {get query() {return this.request.query},
  get body() {return this.response.body},
  set body(data) {this.response.body = data},
  get status() {return this.response.status},
  set status(statusCode) {this.response.status = statusCode},
}

在源码中应用了 delegate,把 context 中的 context.request、context.response 上的办法代理到了 context 上,即 context.request.query === context.query; context.response.body === context.body。而 context.request,context.response 则是在 application 中挂载

总结一下:request.js 负责简化申请体的代码,response.js 负责简化响应体的代码,context.js 把申请体和响应体集成在一个对象上,并且都在 application 上生成,批改 application.js 文件,增加代码如下:

const http = require('http');
const context = require('context')
const request = require('request')
const response = require('response')
class Application {constructor() {
        this.callbackFunc
          this.context = context
        this.request = request
        this.response = response
    }
    ...
    createConext(req, res) {const 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
    }
    ...
}

因为 context、request、response 在其余办法中要用到,所以咱们在结构器中就把他们别离赋值为 this.context、this.request、this.response。咱们实现了上下文 ctx,当初咱们回到之前的问题,简写 res.writeHead(200)、res.end('Hello world')

咱们要想把 res.writeHead(200)、res.end('Hello world') 简化为 ctx.body = 'Hello world',改怎么做呢?

res.writeHead(200)、res.end('Hello world') 是原生的,ctx.body = 'Hello world' 是 Koa 的应用办法,咱们要对 ctx.body = 'Hello world' 做解析并转换为 res.writeHead(200)、res.end('Hello world')。好在 ctx 曾经通过 createContext 获取,那么再创立一个办法来封装 res.end,用 ctx.body 来示意

  responseBody(ctx) {
    let context = ctx.body
    if (typeof context === 'string') {ctx.res.end(context)
    } else if (typeof context === 'object') {ctx.res.end(JSON.stringify(context))
    }
  }

最初咱们批改 callback 办法

//   callback() {//     return (req, res) => this.callbackFunc(req, res)
//   }
callback() {return (req, res) => {
      // 把原生 req,res 封装为 ctx
      const ctx = this.createContext(req, res)
      // 执行 use 中的函数, ctx.body 赋值
      this.callbackFunc(ctx)
      // 封装 res.end,用 ctx.body 示意
      return this.responseBody(ctx)
    }
}

PS:具体代码:请看仓库中的 Step 2

Step 3:中间件机制和洋葱模型

咱们晓得,Koa2 中最重要的性能是中间件,它的表现形式是能够用多个 use,每一个 use 办法中的函数就是一个中间件,通过第二个参数 next 来示意传递给下一个两头间,例如

app.use(async (ctx, next) => {console.log(1)
  await next()
  console.log(6)
})

app.use(async (ctx, next) => {console.log(2)
  await next()
  console.log(5)
})

app.use(async (ctx, next) => {console.log(3)
  ctx.body = 'hello world'
  console.log(4)
})
// 后果 123456

所以,咱们的中间件是个数组,其次,通过 next,执行和暂停执行。一 next,就暂停本中间件的执行,去执行下一个中间件。

Koa 的洋葱模型在 Koa1 中是用 generator + co.js 实现的,Koa2 则应用了 async/await + Promise 去实现。这次咱们也是用 async/await + Promise 来实现

在源码剖析时,咱们就说了 Koa2 的中间件合成是独立成一个库,即 koa-compose,它的外围代码如下:

function compose(middleware) {return function (context, next) {
    let index = -1
    return dispatch(0)
    function dispatch(i) {if (i <= index)
        return Promise.reject(new Error('next() called multiple times'))
      index = i
      let fn = middleware[i]
      if (i === middleware.length) fn = next
      if (!fn) return Promise.resolve()
      try {return Promise.resolve(fn(context, dispatch.bind(null, i + 1)))
      } catch (err) {return Promise.reject(err)
      }
    }
  }
}

具体解读能够去源码剖析上查看,这里咱们不做探索

这里贴两种解决方案,其实都是递归它

componse() {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()}
}

还有一种就是源码,对于 compose 函数,笔者还不能很好的写出个所以然,读者们请自行了解

Step 4:谬误捕捉与监听机制

中间件中的错误代码如何捕捉,因为中间件返回的是 Promise 实例,所以咱们只须要 catch 错误处理就好,增加 onerror 办法

onerror(err, ctx) {if (err.code === 'ENOENT') {ctx.status = 404} else {ctx.status = 500}
    let msg = ctx.message || 'Internal error'
    ctx.res.end(msg)
    this.emit('error', err)
}
callback() {return (req, res) => {const ctx = this.createContext(req, res)
      const respond = () => this.responseBody(ctx)
      + const onerror = (err) => this.onerror(err, ctx)
      let fn = this.componse()
      + return fn(ctx).then(respond).catch(onerror)
    }
}

咱们当初只是对中间件局部做了谬误捕捉,然而如果其余中央写错了代码,怎么晓得以及告诉给开发者,Node 提供了一个原生模块——events,咱们的 Application 类继承它就能获取到监听性能,这样,当服务器上有谬误产生时就能全副捕捉

总结

咱们先读了 Koa2 的源码,晓得后其数据结构及应用形式后,再渐进式手写了一个,这里特别感谢第一名小蝌蚪的 KOA2 框架原理解析和实现,他的这篇文章是我写 Koa2 文章的根据。说回 Koa2,它的性能特地简略,就是对原生 req,res 做了解决,让开发者能更容易的写代码;除此之外,引入中间件概念,这就像插件,引入即可应用,不须要时能缩小代码,轻量大略就是 Koa2 的关键字吧

GitHub 地址:https://github.com/johanazhu/…

参考资料

  • KOA2 框架原理解析和实现
正文完
 0