Глубокое понимание механизма промежуточного программного обеспечения Koa2

Node.js исходный код koa
Глубокое понимание механизма промежуточного программного обеспечения Koa2

Мы знаем, что промежуточное ПО Koa выполняется методом каскадного кода (Cascading). Подобно тому, как скрепки, вы можете обратиться к следующему изображению:

В сегодняшней статье будет проанализировано, как промежуточное ПО Koa реализует каскадное выполнение. В koa для применения промежуточного программного обеспечения мы используемapp.use():

app
  .use(logger())
  .use(bodyParser())
  .use(helmet())

Первый взглядuse()Что это такое, его исходный код выглядит следующим образом:

  use(fn) {
    if (typeof fn !== 'function') throw new TypeError('middleware must be a function!');
    if (isGeneratorFunction(fn)) {
      deprecate('Support for generators will be removed in v3. ' +
                'See the documentation for examples of how to convert old middleware ' +
                'https://github.com/koajs/koa/blob/master/docs/migration.md');
      fn = convert(fn);
    }
    debug('use %s', fn._name || fn.name || '-');
    this.middleware.push(fn);
    return this;
  }

Цель этой функции - вызватьuse(fn)Параметры в методе (будь то обычные функции или промежуточное ПО) добавляются вthis.middlwareв этом массиве.

существуетKoa2в, даGeneratorПромежуточное ПО синтаксиса совместимо, используйтеisGeneratorFunction(fn)этот метод, чтобы определить, является лиGeneratorсинтаксис и черезconvert(fn)Этот метод преобразуется вasync/awaitграмматика. Затем добавьте все промежуточное ПО вthis.middlewareНаконец, поcallback()Этот метод выполняется. Исходный код callback() выглядит следующим образом:

  /**
   * Return a request handler callback
   * for node's native http server.
   *
   * @return {Function}
   * @api public
   */

  callback() {
    const fn = compose(this.middleware);

    if (!this.listeners('error').length) this.on('error', this.onerror);

    const handleRequest = (req, res) => {
      const ctx = this.createContext(req, res);
      return this.handleRequest(ctx, fn);
    };

    return handleRequest;
  }

В исходном коде черезcompose()Этот метод может преобразовывать и каскадировать массив промежуточного программного обеспечения, которое мы передали, и, наконец,callback()вернутьthis.handleRequest()результат выполнения. Нам все равно, какой контент будет возвращен, давайте сначала посмотримcompose()Что делает этот метод, чтобы позволить входящему промежуточному ПО быть каскадным и возвращатьPromise.

compose()Это библиотека для koa2 для реализации каскадных вызовов промежуточного программного обеспечения, называемаяkoa-compose. Исходный код очень прост, есть только одна функция, а именно:

/**
 * Compose `middleware` returning
 * a fully valid middleware comprised
 * of all those which are passed.
 *
 * @param {Array} middleware
 * @return {Function}
 * @api public
 */

function compose (middleware) {
  if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!')
  for (const fn of middleware) {
    if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!')
  }

  /**
   * @param {Object} context
   * @return {Promise}
   * @api public
   */

  return function (context, next) {
    // 记录上一次执行中间件的位置 #
    let index = -1
    return dispatch(0)
    function dispatch (i) {
      // 理论上 i 会大于 index,因为每次执行一次都会把 i递增,
      // 如果相等或者小于,则说明next()执行了多次
      if (i <= index) return Promise.reject(new Error('next() called multiple times'))
      index = i
      // 取到当前的中间件
      let fn = middleware[i]
      if (i === middleware.length) fn = next
      if (!fn) return Promise.resolve()
      try {
        return Promise.resolve(fn(context, function next () {
          return dispatch(i + 1)
        }))
      } catch (err) {
        return Promise.reject(err)
      }
    }
  }
}

можно увидетьcompose()Возвращает результат анонимной функции, которая выполняет сама себяdispatch()Эта функция и передала 0 в качестве параметра.

приди и посмотриdispatch(i)Что делает эта функция?iВ качестве параметра этой функции он используется для получения промежуточного программного обеспечения текущего индекса. надdispatch(0)0 передается, чтобы получитьmiddleware[0]промежуточное ПО.

Сначала проявите суждениеi<==index,еслиtrueЕсли да, то пояснитеnext()Метод вызывается несколько раз. Почему можно так судить? Мы ответим на этот вопрос после того, как объясним всю логику.

Далее, текущийiназначить наindex, запишите индекс выполняющегося в данный момент ПО промежуточного слоя иfnСделайте задание, чтобы получить промежуточное ПО.

index = i;
let fn = middleware[i]

После получения промежуточного программного обеспечения, как его использовать?

    try {
        return Promise.resolve(fn(context, function next () {
          return dispatch(i + 1)
        }))
      } catch (err) {
        return Promise.reject(err)
      }

Приведенный выше код выполняет промежуточное ПОfn(context, next), и прошелcontextа такжеnextфункция с двумя параметрами.contextто естьkoaобъект контекста вcontext. Что касаетсяnextфункция возвращаетdispatch(i+1)результат выполнения. Стоит упомянуть, чтоi+1Этот параметр, передача этого параметра эквивалентна выполнению следующего промежуточного программного обеспечения, тем самым формируя рекурсивный вызов. Вот почему нам нужно выполнять вручную, когда мы сами пишем промежуточное ПО.

await next()

только выполненоnextдля правильного выполнения следующего промежуточного программного обеспечения.

Таким образом, каждое промежуточное ПО может быть выполнено только один раз.next, если выполняется несколько раз в промежуточном программном обеспеченииnext, будет проблема. Возвращаясь к вопросу, упомянутому ранее, почему вы сказали пройтиi<=indexмогу судитьnextСколько раз?

потому что обычноindexдолжно быть меньше или равноi. При многократном вызове в промежуточном программном обеспеченииnext, что приведет к многократному выполнениюdispatch(i+1). С точки зрения кода каждое промежуточное ПО имеет свою собственную область закрытия, и одно и то же промежуточное ПО имеет свою собственную область закрытия.iпостоянна, иindexвыходит за рамки замыкания.

Когда первое промежуточное ПО, т.е.dispatch(0)изnext()При вызове он должен выполняться в это времяdispatch(1), когда выполняется следующее суждение,

if (i <= index) return Promise.reject(new Error('next() called multiple times'))

В настоящее времяindexравно 0, иiЗначение равно 1, что не удовлетворяетi<=indexЭто условие, продолжайте выполнять следующиеindex=iзадание, когдаindexзначение 1. Но если первое промежуточное ПО выполняется еще разnext(), то он снова выполнитсяdispatch(2). Как упоминалось выше, в одном промежуточном программном обеспеченииiЗначение не изменилось, поэтому в настоящее времяiЗначение по-прежнему равно 1, поэтому это приводит кi <= indexСлучай.

Может у кого есть вопросы? теперь, когдаasyncсам возвращаетсяPromise, почему вы все еще используетеPromise.resolve()Заверните. Это должно быть совместимо с обычными функциями, чтобы обычные функции также можно было использовать в обычном режиме.

Вернемся к механизму выполнения промежуточного программного обеспечения, чтобы посмотреть, что происходит. мы знаемasyncМеханизм исполнения таков: только когда всеawaitAsync может быть возвращен только после завершения выполненияPromise. Итак, когда мы используемasyncПри написании промежуточного ПО с синтаксисом , поток выполнения выглядит примерно следующим образом:

  1. Сначала выполните первое промежуточное программное обеспечение (потому чтоcomposeбудет выполняться по умолчаниюdispatch(0)), промежуточное ПО возвращаетPromise, а затем поKoaОтслеживайте, выполняйте соответствующую логику (успех или неудача)
  2. При выполнении логики первого мидлвара сталкиваемсяawait next(), он будет продолжать выполнятьсяdispatch(i+1), то есть выполнитьdispatch(1), который вручную инициирует выполнение второго промежуточного ПО. В это время первое промежуточное ПОawait next()Код позади будет ожидать, ожидаяawait next()вернутьPromise, продолжит выполнение первого промежуточного ПОawait next()код позади.
  3. Точно так же при выполнении второго промежуточного программного обеспечения столкнитесьawait next()Когда третье промежуточное ПО выполняется вручную,await next()Код позади все еще находится на рассмотрении, ждетawaitследующее промежуточное ПОPromise.resolve. Только после получения третьего промежуточного ПОresolveПосле этого код позади будет выполнен, а затем вернется второй промежуточныйPromise, по первому промежуточному ПОawaitCapture, затем будет выполнен последующий код первого промежуточного ПО, а затем возвратPromise
  4. По аналогии, если есть несколько промежуточных программ, они будут выполняться непрерывно в соответствии с приведенной выше логикой, первая промежуточная программа будет выполняться первой, а затем будет выполняться первая промежуточная программа.await next()Вне ожидания, продолжайте выполнять второе промежуточное ПО, продолжайтеawait next()Вне ожидания, продолжайте выполнять третье промежуточное ПО, пока не будет выполнено последнее промежуточное ПО, затем вернитесь в Promise, затем предпоследнее промежуточное ПО выполнит последующий код и вернется в Promise, затем предпоследнее промежуточное ПО и затем продолжается с Этот метод выполняется до тех пор, пока первое промежуточное ПО не будет выполнено и не вернетPromise, чтобы понять порядок выполнения картинки в начале статьи.

После приведенного выше анализа, если вы хотите написать промежуточное ПО koa2, то базовый формат должен быть следующим:

async function koaMiddleware(ctx, next){
    try{
        // do something
        await next()
        // do something
    }
    .catch(err){
        // handle err
    }    
}

В последнее время я использую koa2 + React для ведения блога.Заинтересованные студенты могут перейти по адресу GitHub для просмотра:koa-blog-api