Вы когда-нибудь задумывались, как промежуточное ПО в популярных веб-фреймворках, например Экспресс или Коа, работаешь?

В Express у нас есть функции промежуточного программного обеспечения с такой сигнатурой:

const middleare = (req, res, next) => {
 // do stuffs
 next()
}

В Коа у нас есть это:

const middleware = (ctx, next) => {
  // do stuffs
  next()
}

По сути, у вас есть несколько объектов (req, res для Express или ctx для Koa) и next() функция в качестве аргументов функции промежуточного программного обеспечения. Когда вызывается next(), вызывается следующая функция промежуточного программного обеспечения. Если вы измените объекты аргументов в текущей функции промежуточного программного обеспечения, следующее промежуточное программное обеспечение получит эти измененные объекты. Например:

// Middleware usage in Koa

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

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

app.use((ctx, next) => {
  // this will not get invoked
})

И если вы не вызовете функцию 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) {
        await middleware(context, () => {
          return runner(index + 1)
        })
      }
    }

    await runner(0)
  }

  return { push, execute }
}

Эта реализация паттерна промежуточного программного обеспечения почти такая же, как и в Koa. Если вы хотите увидеть, как это делает Koa, посмотрите исходный код пакета koa-compose.

использование

Давайте посмотрим на пример его использования:

// create a middleware pipeline
const pipeline = Pipeline(
  // with an initial middleware
  (ctx, next) => {
    console.log(ctx)
    next()
  }
)

// add some more middlewares
pipeline.push(
  (ctx, next) => {
    ctx.value = ctx.value + 21
    next()
  },
  (ctx, next) => {
    ctx.value = ctx.value * 2
    next()
  }
)

// add the terminating middleware
pipeline.push((ctx, next) => {
  console.log(ctx)
  // not calling `next()`
})

// add another one for fun ¯\_(ツ)_/¯
pipeline.push((ctx, next) => {
  console.log('this will not be logged')
})

// execute the pipeline with initial value of `ctx`
pipeline.execute({ value: 0 })

Если вы запустите этот фрагмент кода, можете ли вы угадать, какой будет результат? Да, вы правильно угадали:

{ value: 0 }
{ value: 42 }

Кстати, это также будет работать с функциями промежуточного слоя async.

Машинопись

А как насчет того, чтобы отдать ему немного любви к 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 }
}

Теперь, когда все набрано, вы можете объявить тип объекта контекста для конкретного конвейера промежуточного программного обеспечения, например:

type Context = {
  value: number
}

const pipeline = Pipeline<Context>()

Ладно, пока на этом все.

Понравилась эта статья? Если да, то получите больше похожего контента, подписавшись на наш канал YouTube в Decoded!

Первоначально опубликовано на https://muniftanjim.dev 4 октября 2020 г.