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