最小化 JavaScript 中间件模式实现

372 阅读2分钟

原文:Basic Middleware Pattern in JavaScript。2020.10.05, by Munif Tanjim

思考过流行的 Web 框架(例如 ExpressKoa)中间件是如何工作的吗?

在 Express 中,我们这样定义中间件:

const middleware = (req, res, next) => {
    // 做事情
    next()
}

在 Koa 中,则是这样定义的:

const middleware = (ctx, next) => {
    // 做事情
    next()
}

基本就是有一些对象(对 Express 来说是 reqres,对 Koa 来说是 ctx),以及一个 next() 函数,作为中间件函数的参数。当调用 next() 时,下一个中间件函数就会被调用。如果修改了当前中间件函数中的参数,那么下一个中间件将接收到这些修改后的参数。例如:

app.use((ctx, next) => {
    ctx.name = 'Doe'
    next()
})

app.use((ctx, next) => {
    console.log(ctx.name) // `Doe`
})

app.use((ctx, next) => {
  // 这个中间件不会被调用
})

如果不调用 next() 函数,执行就会在当前的中间件停止,下一个中间件函数将不会被调用。

实现

那么,你如何实现这样的模式?用 30 行 JavaScript 代码就能搞定:

function Pipeline(...middlewares) {
    const stack = middlewares
    
    const push = (...middlewares) => {
        stack.push(...middlewares)
    }
    
    const execute = async (context) => {
        let prevIndex = -1
        
        const runner = async (index) => {
            if (index === prevIndex) {
                throw new Error('next() called multiple times')
            }
          
            prevIndex = index
        
            const middleware = stack[index]
            
            if (middleware) {
                // 执行当前中间件
                // 传入的第二个参数即 `next()`,表示执行下一个中间件
                await middleware(context, () => {
                    return runner(index + 1)
                })
            }
        }
        
        await runner(0)
    }
}

这个中间件模式的实现和 Koa 几乎一样。如果你想看看 Koa 是如何做到的,可以查看 koa-compose 包的源代码。

使用

让我们看一个使用它的例子:

// 创建一个中间件 pipeline
const pipeline = Pipeline(
    // 传入一个初始中间件
    (ctx, next) => {
        console.log(ctx)
        next()
    }
)

// 添加一些中间件
pipeline.push(
  (ctx, next) => {
    ctx.value = ctx.value + 21
    next()
  },
  (ctx, next) => {
    ctx.value = ctx.value * 2
    next()
  }
)

// 添加终止中间件
pipeline.push((ctx, next) => {
  console.log(ctx)
  // 不调用 `next()`
})

// 为了增加乐趣,再加一个 ¯\_(ツ)_/¯
pipeline.push((ctx, next) => {
  console.log('当然,这个中间件不会被调用,这块消息也不会打印')
})

// 使用初始值 `{ value: 0 }`(即 `ctx` 的值)执行 pipeline 
pipeline.execute({ value: 0 })

如果你运行上述代码,能猜出输出会是什么吗?没错,你猜对了:

{ value: 0 }
{ value: 42 }

顺便说一下,这对于异步中间件函数也绝对适用。

TypeScript

让我们为代码库添加一些 TypeScript 吧。

type Next = () => Promise<void> | void

type Middleware<T> = (context: T, next: Next) => Promise<void> | void

type Pipeline<T> = {
  push: (...middlewares: Middleware<T>[]) => void
  execute: (context: T) => Promise<void>
}

function Pipeline<T>(...middlewares: Middleware<T>[]): Pipeline<T> {
  const stack: Middleware<T>[] = middlewares

  const push: Pipeline<T>['push'] = (...middlewares) => {
    stack.push(...middlewares)
  }

  const execute: Pipeline<T>['execute'] = async (context) => {
    let prevIndex = -1

    const runner = async (index: number): Promise<void> => {
      if (index === prevIndex) {
        throw new Error('next() called multiple times')
      }

      prevIndex = index

      const middleware = stack[index]

      if (middleware) {
        await middleware(context, () => {
          return runner(index + 1)
        })
      }
    }

    await runner(0)
  }

  return { push, execute }
}

现在所有东西都声明了类型,你可以为中间件 pipeline 指定上下文对象类型,像这样:

type Context = {
  value: number
}

const pipeline = Pipeline<Context>()

搞定收工!