JavaScript의 기본 미들웨어 패턴

대중적인 웹 프레임워크의 미들웨어가 어떻게 되는지 궁금합니다. Express 또는 Koa, 작동?

Express에는 다음과 같은 서명이 있는 미들웨어 기능이 있습니다.

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


Koa에는 다음이 있습니다.

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 }


그건 그렇고, 이것은 비동기 미들웨어 기능에서도 절대적으로 작동합니다.

타입스크립트



이제 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>()


좋아, 지금은 그게 다야.


2020년 10월 4일 muniftanjim.dev에 원래 게시되었습니다.

좋은 웹페이지 즐겨찾기