Análisis del código fuente del modelo de aros de cebolla de Koa (¿Por qué `await next()` puede formar el modelo de aros de cebolla?)

escribir delante

En febrero-marzo de este año, hice un proyecto comunitario del campus. Se divide en extremos delantero y trasero, y fue responsable de la construcción del extremo trasero en ese momento. ¡El back-end usa el marco Koa, y la idea y el uso del medio me sorprendieron! (principalmente para entender por await next()qué puede pasar al siguiente middleware) Entonces, fui a entender el código fuente de Koa y ¡quiero escribir un blog!

¿Qué es el software intermedio?

Primero entendamos ¿Qué es 中间件?


// 注册接口
router.post('/register', userValidator, verifyUser, cryptPassword, verifySex, register)
复制代码
  • Tomando la interfaz de registro como ejemplo, la ruta pasa por userValidator, verifyUser, cryptPassword, verifySexmiddleware y finalmente la función de controlregister
    • userValidatorDeterminar si la contraseña de usuario ingresada es válida
    • verifyUserConsultar si el usuario se ha registrado (es necesario consultar la base de datos)
    • cryptPasswordbcryptjsCifrado de contraseña a través de complemento
    • verifySexIdentificar el género de la entrada
    • registerAlmacenar información en la base de datos y devolver información

¿Por qué usar software intermedio?

Los estudiantes deben ver

  • 1. El middleware hace que el pensamiento del código sea más claro. ¿Qué hace cada paso? La semántica del middleware puede aumentar en gran medida la legibilidad del código .
  • 2. Mejorar la reutilización del código
    • Aunque aquí no parece muy obvio, pongamos otro ejemplo (en las siguientes rutas authse reutiliza el middleware 十二次)
// 上传头像接口
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)
复制代码

código fuente del middleware de autenticación

Tomemos el middleware con mucha reutilización authcomo ejemplo para ver cómo escribir un middleware. Todo el middleware es esencialmente una async/awaitfunción (¡esto se discutirá en el análisis del código fuente de Koa más adelante!)

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!!!)
}
复制代码
  • La identidad del usuario se verifica a través del token. Este código sí puede ser reutilizado. Cuando un middleware completa su misión, puedeawait next()
  • El trabajo se entrega al siguiente middleware.

Entonces, ¿por qué esperar al siguiente ()? ¿Cuál es el proceso específico? ¡Revelemos los secretos juntos!

Análisis del código fuente de Koa

Tomemos una imagen gif primero

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

Supongo que te gusta

Origin juejin.im/post/7086401180196634660
Recomendado
Clasificación