Skip to content

Koa中间件现象

Koa 中间件(Middleware)从现象出发,理解洋葱模型

按照 Koa 中间件规范,编写两个中间件

👇 创建 Koa 实例并挂载 logTime、logUrl 中间件

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

const logTime = require('./middleware/logTime')
const logUrl = require('./middleware/logUrl')

app.use(logTime())
app.use(logUrl())

// response
app.use(async ctx => {
  ctx.body = 'Hello World'
})

app.listen(3000)

👇 调用这个中间件 会打印前/后

js
module.exports = function() {
  return async function(ctx, next) {
    console.log(
      "next前,打印时间戳:",
      new Date().getTime()
    )

    await next()

    console.log(
      "next后,打印时间戳:",
      new Date().getTime()
    )
  }
}

👇 调用这个中间件 会打印上下文的url

js
module.exports = function() {
  return async function(ctx, next) {
    console.log("next前,打印url:", ctx.url)

    await next()

    console.log("next后,打印url:", ctx.url)
  }
}

👆 以中间件代码上的next函数 为分界点:先 use 的中间件, next 前的逻辑先执行,但 next 后的逻辑反而后执行


🤔 这样相对于链式执行中间件有什么好处?

我们提出这样的需求 希望中间件修改后的状态能不关心挂载顺序,在任意中间件中读取到(不意味着中间件挂载顺序随意写,顺序决定中间件回调执行时机)

如:在第二个中间件 logUrl 里,对url加上了一个时间戳,然后在第一个中间件 logTime 里拿到这个时间戳url并打印

如果普通的链式执行所有中间件(一个回调函数),显然无法实现前面的中间件能使用之后的中间件所添加的东西 就需要在一个中间件里同时定义 1.顺序执行的回调函数 2.全部中间件执行后的回调函数 ,这样来共享一些单个作用域的变量


🤔 next函数是洋葱圈模型需要才用的吗?还是用于其他作用? 定义中间件是一个对象,对象内部提供pre函数和post函数

🤔 中间件pre的回调全部执行完,执行post回调的顺序为什么反过来

保留next分割中间件前后的功能,如中间1、2内部各自有next前/后,执行顺序为

  • 中间件1的next前
  • 中间件2的next前
  • 中间件1的next后
  • 中间件2的next后

手写Koa洋葱圈

创建中间件

js
const middleWare1 = (next) => {
  console.log('first start')
  next()
  console.log('first end')
}
const middleWare2 = (next) => {
  console.log('second start')
  next()
  console.log('second end')
}
const middleWare3 = () => {
  console.log('third no next')
}

创建koa实例,提供use挂载中间件,run执行中间件

js
export createKoa() {
  let middlewares = []

  // 挂载中间件
  const use = (middleWare) => {
    middlewares.push(middleWare)
  }

  // 执行中间件
  const run = () => { }

  return {
    use,
    run
  }
}

示例使用代码

js
import { createKoa } from 'mykoa'

const koa = createKoa()

koa.use(middleWare1)
koa.use(middleWare2)
koa.use(middleWare3)

koa.run()

编写执行中间件的run

👇 看起来直接遍历执行中间件数组就好

js
const next = () => { }

// 执行中间件
const run = () => {
  middlewares.forEach( middleware => {
    middleware(next) // 给中间件注入next
  })
}

❌ 不能遍历执行,因为要由next函数来控制下一个执行

js
// 执行中间件
const run = () => {
  let middleware1 = middlewares[0]
  middleware1(function next(){
    // next内部来执行下一个中间
    let middleware2 = middlewares[1]
    middleware2(function next(){
      ...
    })
  })
}

👆 是递归结构

js
// 执行中间件
const run = () => {
  dispatch(0)

  function dispatch (i) {
    if (i === middlewares.length) return null // 跳出递归

    const middleware = middlewares[i]

    // 中间件注入next(每次都不一样)
    const next = () => {
      dispatch(i + 1) // 递归执行自身
    }
    middleware(next)
  }
}

👆 可以发现我们只处理 next 下一个中间件(即函数执行到 next 前的逻辑内容),并没有在处理全部中间件之后,执行 next 后的步骤

其实在一个中间件执行 next 之后,执行顺序被传递到下一个中间 当这个 next 中断 也就开始执行最后一个中间件 next 下的代码块了 以此回到前一个中间件 next 下的代码块

对象结构中间件,双循环

🤔 那不使用 next函数风格方式 的对象结构的中间件遍历执行回调是否可行

👇 对象结构的中间件

js
const middleWare1 = {
  pre: () => console.log('first start'),
  post: () => console.log('first end')
}
const middleWare2 = {
  pre: () => console.log('second start'),
  post: () => console.log('second end')
}
const middleWare3 = () => console.log('third no next')

👇 执行中间件

js
const run = (middlewares) => {
  middlewares.forEach(middleware => {
    if(typeof middleware === 'function'){
      middleware()
      return
    }
    middleware.pre()
  });

  // 反转中间件数组遍历
  middlewares.reverse().forEach(middleware => {
    if(typeof middleware === 'function') return
    middleware.post()
  });
}

👆 循环2次和递归的时间复杂度应该是相同的,实现效果上也相同 如果需要中间件之间共享状态,也可以依赖被挂载的实例(Koa实例)变量实现

似乎递归形式的写法仅仅是更优雅,而不是这种使用场景的唯一解

MidWay

MidWay - A Node.js Serverless Framework for front-end/full-stack developers.

umi-request

umi-request - A request tool based on fetch

TODO: 源码笔记

Redux中间件

参考材料