Koa洋葱圈模型源码浅析(`await next()`为什么能够形成洋葱圈模型?)

写在前面

在今年的2-3月份做了一个校园社区的项目。分为前后端,当时是负责了后端的搭建。 后端使用了Koa框架,中间间的思想和使用让我震撼!(主要是弄懂为啥await next()可以跳到下一个中间件) 所以,去了解了一下Koa的源码,想要写一篇博客!

什么是中间件?

我们先来了解一下什么是中间件


// 注册接口
router.post('/register', userValidator, verifyUser, cryptPassword, verifySex, register)
复制代码
  • 以注册接口为例,路由一次经过了userValidator,verifyUser,cryptPassword,verifySex中间件,然后最后是控制函数register
    • userValidator判断用户密码输入是否有效
    • verifyUser 查询用户是否已经注册(需要查询数据库)
    • cryptPassword通过bcryptjs插件进行密码加密
    • verifySex对输入的性别进行判别
    • register存储信息到数据库返回信息

为什么要使用中间件?

同学们应该看出来了

  • 1.中间件让代码的思路变得更加清晰,每一步干什么,通过中间件的语义化能够很好的增加代码可读性
  • 2.提高代码的复用性
    • 虽然在这里好像看的不是很明显,我们再举一个例子(在下面的这些路由里面,auth中间件复用了十二次)
// 上传头像接口
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中间件源码

我们以复用次数很多的auth中间件为例,来看看如何写一个中间件 所有的中间件本质上都是一个async/await的函数(这个在后面的Koa源码解析里面会讲到!)

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!!!)
}
复制代码
  • 通过token来验证用户的身份,这个代码确实是可以复用的,当一个中间件完成了他的使命后,就可以await next()
  • 讲工作交给下一个中间件了

那么为什么是await next()呢?具体过程是怎样的呢? 下面带大家一起揭秘!

Koa源码浅析

我们先来康一张gif图片

这是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

猜你喜欢

转载自juejin.im/post/7086401180196634660