Analysis of the source code of the Koa onion ring model (Why can `await next()` form the onion ring model?)

write in front

In February-March of this year, I did a campus community project. It is divided into front and back ends, and was responsible for the construction of the back end at that time. The back-end uses the Koa framework, and the thought and use of the middle shocked me! (mainly to understand why await next()you can skip to the next middleware) So, I went to understand the source code of Koa and want to write a blog!

What is middleware?

Let's first understand what is 中间件?


// 注册接口
router.post('/register', userValidator, verifyUser, cryptPassword, verifySex, register)
复制代码
  • Taking the registration interface as an example, the route goes through userValidator, verifyUser, cryptPassword, verifySexmiddleware, and finally the control functionregister
    • userValidatorDetermine whether the user password input is valid
    • verifyUserQuery whether the user has registered (need to query the database)
    • cryptPasswordbcryptjsPassword encryption via plugin
    • verifySexIdentify the gender of the input
    • registerStore information in database and return information

Why use middleware?

Students should see

  • 1. Middleware makes the thinking of the code clearer. What does each step do? The semantics of middleware can greatly increase the readability of the code .
  • 2. Improve code reusability
    • Although it doesn't seem to be very obvious here, let's give another example (in the following routes, the authmiddleware is reused 十二次)
// 上传头像接口
router.post('/upload', auth, upload)
// 封号接口
router.post('/blockadeornot', auth, verifyAdmin, blockade)
// 切换管理员接口
router.post('/admin', auth, verifyAdmin, changeAdmin)
// 用户密码一键重置接口
router.post('/reset', auth, verifyAdmin, cryptPassword, reset)
// 修改密码接口
router.patch('/password', auth, cryptPassword, changePassword)
// 修改昵称接口
router.patch('/name', auth, changeName)
// 修改昵称接口
router.patch('/city', auth, changeCity)
// 修改性别接口
router.patch('/sex', auth, verifySex, changeSex)
// 修改的总接口
router.patch('/change', auth, verifySex, change)
// token更新接口
router.get('/updatetoken', updatetoken)
// 查询所有用户信息的接口
router.get('/info', auth, findall)
// 根据id查询用户信息的接口
router.get('/searchbyid', auth, findone)
// 查询active或者not_active用户,正常用户的接口
router.get('/active', auth, verifyAdmin, findAllactive)
复制代码

auth middleware source code

Let's take middleware with a lot of reuse authas an example to see how to write a middleware. All middleware is essentially a async/awaitfunction (this will be discussed in the Koa source code analysis later!)

const jwt = require('jsonwebtoken')                        // jwt插件
const { JWT_SECRET } = require('../config/config.default') // 加密使用到的私钥
const auth = async (ctx, next) => {
  const { authorization } = ctx.request.header             // 解构出authorization
  const token = authorization.replace('Bearer ', '')       // 得到token
  try {
    const user = jwt.verify(token, JWT_SECRET)             // 使用jwt解析接收到的token
    ctx.state.user = user                                  // 把解析内容挂载到ctx.state上,方便后面的中间件使用
  } catch (err) {
    switch (err.name) {                                    // 错误抛出处理
      case 'TokenExpiredError':
        console.error('token已过期', err);
        return ctx.app.emit('error', tokenExpiredError, ctx)  // 把错误抛出到app最后进行统一的处理
      case 'JsonWebTokenError':
        console.error('无效token', err);
        return ctx.app.emit('error', invalidToken, ctx)       // 把错误抛出到app最后进行统一的处理
    }
  }
  await next()                                             // 中间件的灵魂(next!!!)
}
复制代码
  • The identity of the user is verified through the token. This code can indeed be reused. When a middleware completes its mission, it canawait next()
  • The work is handed over to the next middleware

So why await next()? What is the specific process? Let us reveal the secrets together!

Analysis of Koa source code

Let's take a gif picture first

这是Koa源码中的一张解释middleware的图片!过程很清晰了! 虽然我在实际编程的时候只使用到了1-5的步骤... 本文将向你解释为什么是这样?

middleware.gif


  • 这里是app.use
  • 实际我们是use的可能是router,router里面有很多的接口
  • 可以简单理解为,最终是app.use写的所有的接口

我们的探索流程图

awaitnext.png

listen函数

从最上层开始看起,listen主要是完成了对server的监听 server怎么来的?用callback()创建的


  listen (...args) {
    debug('listen')
    const server = http.createServer(this.callback())
    return server.listen(...args)
  }
复制代码

callback函数

  • return的是this.handleRequest(ctx,fn)
    • 1.我们下面要去看createContext如何生成ctx
    • 2.再看handleRequest如何运行的
    • 3.关键点,middleware是什么?compose对它进行了怎样的封装?

  callback () {
    const fn = compose(this.middleware)
    if (!this.listenerCount('error')) this.on('error', this.onerror)
    const handleRequest = (req, res) => {
      const ctx = this.createContext(req, res)
      return this.handleRequest(ctx, fn)
    }
    return handleRequest
  }
复制代码

createContext函数

简单来说就是把reqres都挂载到context上,然后返回这个上下文

  • 这就是为什么我们的数据都是从ctx上解析出来的!!!(ctx.request.body/ctx.request.params)
  • 我们可以看到context.state是空的,这是为什么我们前面把信息挂载到这个上面

  createContext (req, res) {
    const context = Object.create(this.context)
    const request = context.request = Object.create(this.request)    // request插件 这里不细说了...
    const response = context.response = Object.create(this.response) // response插件
    context.app = request.app = response.app = this
    context.req = request.req = response.req = req
    context.res = request.res = response.res = res
    request.ctx = response.ctx = context
    request.response = response
    response.request = request
    context.originalUrl = request.originalUrl = req.url
    context.state = {}  
    return context
  }
复制代码

handleRequest函数

简单理解为将ctx传递给fnMiddleware函数执行(这里就解释了为啥我们把信息挂载到ctx.state.use后,后面的中间件可以使用ctx.state.use拿到数据)


  handleRequest (ctx, fnMiddleware) {
    const res = ctx.res
    res.statusCode = 404
    const onerror = err => ctx.onerror(err)
    const handleResponse = () => respond(ctx)
    onFinished(res, onerror)
    return fnMiddleware(ctx).then(handleResponse).catch(onerror)
  }
复制代码

middleware是什么?--- use函数

我们可以看到use函数关键的一步就是将fn放入middleware数组中。 所以app.use并不是马上执行,而是将函数先放入数组中

  this.middleware = []
  use (fn) {
    if (typeof fn !== 'function') throw new TypeError('middleware must be a function!')
    debug('use %s', fn._name || fn.name || '-')
    this.middleware.push(fn)
    return this
  }
复制代码

compose函数((await next()为什么能够形成洋葱圈模型?))

  • const compose = require('koa-compose')
  • 我们来看看这个插件的源码
    • 1.判断middleware是否是一个数组(存放中间件函数)
    • 2.判断数组中每个元素是否是一个函数
    • 3.简单来说是一个递归调用中间件
      • 1.index=-1 这里很巧妙,用了一个闭包的技巧,执行函数后,index=i,所以index>=ireject(说明多次调用了!)
      • 2.不断取fn=middle[i]fn不为空就执行
      • 3.递归调用下一层
        • 1.这里比较有趣的一点: dispatch.bind(null, i + 1))一定要用bind(null)吗?
        • 是的,这里不是单纯的递归调用,而是传入一个函数,所以必须用到bind
        • 这里也不仔细阐述bind和call,apply的区别了!
        • 2.将下一个中间件作为next参数传递下去了(这就是为什么await next()能够形成洋葱圈模型了)
function compose (middleware) {
  if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!')
  for (const fn of middleware) {
    if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!')
  }

  /**
   * @param {Object} context
   * @return {Promise}
   * @api public
   */

  return function (context, next) {
    // last called middleware #
    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)
      }
    }
  }
}
复制代码

总结

compose插件将下一个中间件作为next参数传递下去了(这就是为什么await next()能够形成洋葱圈模型了) 可能有的同学还是不理解,用代码来演示一下(重复一下gif图中的思路)

我们的实际使用

async function a (context, next) {
  console.log('1.中间件1')
  await next()
  console.log('6.中间件1next()之后')
}
async function b (context, next) {
  console.log('2.中间件2')
  await next()
  console.log('5.中间件2next()之后')
}
async function c (context, next) {
  console.log('3.中间件3')
  await next()
  console.log('4.中间件3next()之后')
}
var composeMiddles = compose([a, b, c])
composeMiddles()
复制代码

1.中间件1
2.中间件2
3.中间件3
4.中间件3next()之后
5.中间件2next()之后
6.中间件1next()之后
复制代码

compose转换后的伪代码

async function a (context, next) {
  console.log('1.中间件1')
  async function b (context, next) {
    console.log('2.中间件2')
    async function c (context, next) {
      console.log('3.中间件3')
      await next()
      console.log('4.中间件3next()之后')
    }
    console.log('5.中间件2next()之后')
  }
  console.log('6.中间件1next()之后')
}
复制代码

转换后很像洋葱了吧

next.jpg

Guess you like

Origin juejin.im/post/7086401180196634660