Разница между многомерным анализом Express и Koa

Node.js
Разница между многомерным анализом Express и Koa

Express имеет долгую историю и содержит больше учебных материалов, чем Koa.Он поставляется с такими функциями, как Router, правила маршрутизации и View, что ближе к концепции Web FrameworkWork. Koa относительно легковесен, больше похож на инкапсуляцию HTTP, с большим количеством степеней свободы, официальноkoajs/koa/wikiПредоставляется некоторое промежуточное ПО Koa, которое можно комбинировать самостоятельно.

Эта статья посвященаМетод обработки обработчика,механизм выполнения промежуточного программного обеспечения,механизм реагированияПосмотрите на разницу между Express и Koa с разных сторон.

Метод обработки обработчика

В этом ключевое отличие Express от Koa (koa1, koa2):

Express

Express использует обычные функции обратного вызова, линейную логику, для выполнения всех HTTP-запросов в одном потоке., Одной из вещей, которые нельзя допустить в Express, является обратный вызов.В частности, он очень недружественный для обработки захвата ошибок.Каждый обратный вызов имеет новый стек вызовов, поэтому вы не можете попробовать поймать захват для обратного вызова, вам нужно сделайте это в обратном вызове. Выполните перехват ошибок, а затем передайте их слой за слоем.

Koa1

В настоящее время мы используем Koa2,Koa1 — это избыточная версия, поэтому также необходимо понимать, что это «ответ сопрограммы», реализованный с помощью функции генератора генератор + со.

Давайте сначала поговорим о Генераторе и сопрограммах. Сопрограммы находятся в контексте потоков. Поток может выполнять только одну сопрограмму за раз. По сравнению с потоками он более легкий. Нет создания потока, уничтожения, переключения контекста и другого потребления. Он не управляется операционной системой и контролируется конкретными приложениями.Генератор также реализован в ES6.Он авторизован вызывающей функцией для выполнения, поэтому его также называют «полу-сопрограммой/подобной сопрограмме», полной сопрограмма Процедура такова, что все функции являются управляемыми.

Давайте поговорим о со, Генератор плюс со, убийца, полностью исключающий способ написания функций обратного вызова.coЧто тогда?Это функция автоматического управления процессом генератора на основе объекта Promise., мы можем управлять нашим асинхронным кодом так же, как мы пишем синхронный код.

Koa2 (теперь по умолчанию для Koa)

Koa2 теперь является версией Koa по умолчанию, самое большое отличие от Koa1 заключается в том, чтоИспользуйте Async/Await ES7, чтобы заменить исходный режим Generator + co, и не нужно вводить сторонние библиотеки, лежащую в основе встроенную поддержку, Async/Await теперь также известен как окончательное решение для асинхронного JS..

Koa использует луковую модель, одна из ее функций — каскад, через элемент управления await next() для вызова промежуточного программного обеспечения «вниз по течению», до тех пор, пока в «ниже по течению» не будет промежуточного программного обеспечения и стек не будет выполнен, и, наконец, не вернется к « промежуточное ПО восходящего потока. Этот метод имеет преимущество, особенно для ведения журнала (запрос->ответ занимает много времени), и поддержка обработки ошибок идеальна.

Поскольку он поддерживается промисом, Async/Await — это просто синтаксический сахар, потому что промис — это своего рода цепной вызов, когда вы не можете заранее прерывать несколько цепных вызовов, вы можете либо продолжать проходить как следующий, либо catch выдает ошибку. В соответствии со структурой Koa вы можете только контролировать, будет ли это похоже на нисходящий поток через await next() или выдать ошибку и не может завершиться заранее.

Как было сказано выше, досрочное завершение невозможно. Позже я видел фреймворк Toa, реализованный учителем Teambition Яном Цин. Он разработан на основе Koa. Одной из его особенностей является то, что его можно завершить заранее через context.end( ).Если интересно,можете съездить посмотретьtoajs/toa

Механизм реализации промежуточного программного обеспечения

Механизм промежуточного программного обеспечения Koa

Koa (>=v7.6) по умолчанию поддерживает Async/Await. В Koa объединено несколько асинхронных промежуточных программ. Одна из основных реализаций —koa-compseЭтот компонент реализуется шаг за шагом ниже.

Начните с трех функций в качестве примера, чтобы инкапсулировать функцию композиции, подобную koa-compse:

async function f1(ctx, next) {
  console.log('f1 start ->');
  await next();
  console.log('f1 end <-');
}

async function f2(ctx, next) {
  console.log('f2 start ->');
  await next();
  console.log('f2 end <-');
}

async function f3(ctx) {
  console.log('f3 service...');
}

Если это соответствует порядку выполнения Koa, это должно позволить f1 выполняться первым, следующим параметром f1 является f2, а следующим параметром f2 является f3.Вы можете видеть, что f3 является последней функцией.После обработки логики , он завершится, и смоделируем реализацию:

  • Строка {1} определяет набор промежуточного программного обеспечения.
  • Строка {2} определяет метод использования, например промежуточное программное обеспечение push в коллекции промежуточного программного обеспечения, которое можно рассматривать как похожее на app.use().
  • Строка {3} монтирует функции f1, f2, f3, которые нам нужно выполнить по очереди
  • Строка {5} выполняет next1(), то есть выполнение сначала начинается с функции f1.
  • Строка {4.3} определяет функцию выполнения next1, middlewares[0] — это функция f1, а ее функция внутренне вызывает f2, мы определяем функцию выполнения next2 в строке {4.2}.
  • Строка {4.2} определяет функцию выполнения next2, middlewares[1] — это функция f2, а f3 вызывается внутри функции, мы снова определяем функцию выполнения next3.
  • Строка {4.1} определяет функцию выполнения next1, middlewares[2] — это функция f3, потому что это последний шаг, и он заканчивается здесь.
const ctx = {}
const middlewares = []; // {1} 定义一个中间件的集合
const use = fn => middlewares.push(fn); // {2} 定义 use 方法

// {3}
use(f1);
use(f2);
use(f3);

// {4}
const next3 = () => middlewares[2](ctx); // {4.1}
const next2 = () => middlewares[1](ctx, next3); // {4.2}
const next1 = () => middlewares[0](ctx, next2); // {4.3}

// {5}
next1()

// 输出结果
// f1 start ->
// f2 start ->
// f3 service...
// f2 end <-
// f1 end <-

Приведенный выше результат — это то, что мы ожидаем, но если мы добавляем новый f4, нужно ли нам его определять? Очевидно, что это не очень разумно. Нужен более общий метод объединения наших функций. Из вышеприведенного примера видно, что он регулярен и может быть достигнут рекурсивным обходом. Реализация такова:

  • Строки {1} {2} - это граничная обработка, в первую очередь middlewares - это массив, и каждый элемент массива должен быть функцией
  • Строка {4} определяет функцию отправки, это ключ к нашей реализации.
  • Строка {5} i — текущая позиция промежуточной коллекции промежуточных программ, если она равна длине промежуточных программ, она вернется сразу после выполнения;
  • Строка {6} выводит текущую пройденную функцию, определенную как fn
  • Строка {7} выполняет функцию fn, передает функцию диспетчеризации и i+1, но обязательно биндить, потому что бинд вернет функцию, и она не будет выполнена сразу, когда она будет выполнена? То есть, когда выполняется await next() в текущей функции fn, этим next является dispatch.bind(null, (i + 1)) переданный текущей функцией fn.
  • Если какое-либо промежуточное ПО в середине строки {8} имеет ошибку, оно вернет сразу
/**
 * 中间件组合函数,可以参考 https://github.com/koajs/compose/blob/master/index.js
 * @param { Array } middlewares 
 */
function compose(ctx, middlewares) {
  // {1}
  if (!Array.isArray(middlewares)) throw new TypeError('Middlewares stack must be an array!')
  
  // {2}
  for (const fn of middlewares) {
    if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!')
  }
  
  return function() {
    const len = middlewares.length; // {3} 获取数组长度
    const dispatch = function(i) { // {4} 这里是我们实现的关键
      if (len === i) { // {5} 中间件执行完毕
        return Promise.resolve();
      } else {
        const fn = middlewares[i]; // {6}
        
        try {
          // {7} 这里一定要 bind 下,不要立即执行
          return Promise.resolve(fn(ctx, dispatch.bind(null, (i + 1))));
        } catch (err) {
          // {8} 返回错误
          return Promise.reject(err);
        }
      }
    }

    return dispatch(0);
  }
}

const fn = compose(ctx, middlewares);

fn();

Тест — это результат, который мы ожидаем.Его поток выполнения f1 -> f2 -> f3 -> f2 -> f1. Он начинает выполняться ниже по течению от f1 до тех пор, пока не будет выполнено последнее промежуточное программное обеспечение f3, и возвращается к f1 в потоке. , другое название этой модели — самая известная «луковичная модель»;

f1 start ->
f2 start ->
f3 service...
f2 end <-
f1 end <-

Выше приведена основная реализация промежуточного программного обеспечения Koa Compose.Дополнительную информацию о Koa см.Githubисходный код.

Механизм промежуточного программного обеспечения Express

Здесь автор видит версию Express 4.x.Одним из основных изменений является удаление встроенного промежуточного программного обеспечения Connect.Подробнее см.Переход на Express 4.x.

Обычно мы говорим, что Express является линейным, поэтому посмотрите на следующий код:

const Express = require('express')
const app = new Express();
const sleep = () => new Promise(resolve => setTimeout(function(){resolve(1)}, 2000))
const port = 3000

function f1(req, res, next) {
  console.log('f1 start ->');
  next();
  console.log('f1 end <-');
}

function f2(req, res, next) {
  console.log('f2 start ->');
  next();
  console.log('f2 end <-');
}

async function f3(req, res) {
  //await sleep();
  console.log('f3 service...');
  res.send('Hello World!')
}

app.use(f1);
app.use(f2);
app.use(f3);
app.get('/', f3)
app.listen(port, () => console.log(`Example app listening on port ${port}!`))

Консоль выполняет curl localhost:3000, и вывод выглядит следующим образом, что немного сбивает с толку, не так ли? Почему порядок вывода Koa соответствует тому, что мы сказали выше? Это тоже не луковичная модель?

f1 start ->
f2 start ->
f3 service...
f2 end <-
f1 end <-

Подросток, не жди, а потом посмотри на кусок кода.
Над нашей функцией f3 закомментирована строка кода, отложенная выполнением await sleep(), теперь включим этот комментарий.

async function f3(req, res) {
  await sleep(); // 改变之处
  console.log('f3 service...');
  res.send('Hello World!')
}

Консоль снова выполняет curl localhost:3000 и обнаруживает, что порядок изменился.Промежуточное ПО вышестоящего уровня не ждет завершения выполнения функции f3, а затем выполняет ее напрямую.

f1 start ->
f2 start ->
f2 end <-
f1 end <-
f3 service...

Давайте попробуем воспроизвести процесс его выполнения.Вы можете видеть, что f1 и f2 - синхронные коды, а f3 - асинхронный. После стольких слов, наконец, вышел ответ.
Реализация промежуточного программного обеспечения Express является синхронной на основе функции обратного вызова обратного вызова, она не ожидает завершения асинхронной (Promise), что также объясняет, почему я добавил асинхронные операции в приведенную выше демонстрацию, и порядок был изменен.
В механизме промежуточного программного обеспечения Koa Async/Await (все промисы, стоящие за ним) используется для синхронного управления асинхронным кодом и может ожидать асинхронных операций.

f1 (req, res) {
  console.log('f1 start ->');
  f2 (req, res) { // 第一个 next() 地方
    console.log('f2 start ->');
    async f3 (req, res) { // 第二个 next() 地方
      await sleep(); // 改变之处
      console.log('f3 service...');
      res.send('Hello World!')
    }
    console.log('f2 end <-');
  }
  console.log('f1 end <-');
}

Экспресс-анализ исходного кода промежуточного ПО

Прочитав исходный код Express, а затем взглянув на исходный код Koa, вы обнаружите, что Koa действительно лаконичен и утончен. Исходный код Express по-прежнему выглядит немного запутанным, и требуется время, чтобы разобраться в нем. опубликованы следующие две ключевые реализации.Исходный код Express 4.x, можете посмотреть, если интересно.

  1. Крепление промежуточного ПО

Во время инициализации промежуточное ПО в основном монтируется в собственный массив стека с помощью метода proto.use.

// https://github.com/expressjs/express/blob/4.x/lib/router/index.js#L428
proto.use = function use(fn) {
  var offset = 0;
  var path = '/';

  ...

  var callbacks = flatten(slice.call(arguments, offset));

  if (callbacks.length === 0) {
    throw new TypeError('Router.use() requires a middleware function')
  }

  for (var i = 0; i < callbacks.length; i++) {
    var fn = callbacks[i];

    if (typeof fn !== 'function') {
      throw new TypeError('Router.use() requires a middleware function but got a ' + gettype(fn))
    }

    // add the middleware
    debug('use %o %s', path, fn.name || '<anonymous>')

    var layer = new Layer(path, {
      sensitive: this.caseSensitive,
      strict: false,
      end: false
    }, fn);

    layer.route = undefined;

    this.stack.push(layer); // 中间件 route 的 layer 对象的 route 为 undefined,区别于路由的 router 对象
  }

  return this;
};
  1. Выполнение промежуточного программного обеспечения

Одним из основных методов выполнения ПО промежуточного слоя Express является proto.handle. Ниже опущено много кода. Подробности смотрите в исходном кодеExpress 4.x, как вызвать несколько промежуточных программ? Основная реализация метода proto.handle определяет следующую функцию, которая будет рекурсивно вызываться для извлечения промежуточного программного обеспечения, которое необходимо выполнить.

// https://github.com/expressjs/express/blob/dc538f6e810bd462c98ee7e6aae24c64d4b1da93/lib/router/index.js#L136
proto.handle = function handle(req, res, out) {
  var self = this;
  ...
  next();

  function next(err) {
    ...
    // find next matching layer
    var layer;
    var match;
    var route;

    while (match !== true && idx < stack.length) {
      layer = stack[idx++]; // 取出中间件函数
      match = matchLayer(layer, path);
      route = layer.route;

      if (typeof match !== 'boolean') {
        // hold on to layerError
        layerError = layerError || match;
      }

      if (match !== true) {
        continue;
      }

      if (!route) {
        // process non-route handlers normally
        continue;
      }

      ...
    }
    
    ...
    // this should be done for the layer
    self.process_params(layer, paramcalled, req, res, function (err) {
      if (err) {
        return next(layerError || err);
      }

      if (route) {
        return layer.handle_request(req, res, next);
      }
      
      trim_prefix(layer, layerError, layerPath, path);
    });
  }
  
  function trim_prefix(layer, layerError, layerPath, path) {
    ...
    if (layerError) {
      layer.handle_error(layerError, req, res, next);
    } else {
      // 这里进行函数调用,且递归
      layer.handle_request(req, res, next);
    }
  }
};

механизм реагирования

Механизм ответа коа

Отклик данных в Koa настраивается через ctx.body.Заметьте, здесь настройка срабатывает не сразу, а после того, как все миддвары закончатся.Исходный код написан так:

const handleResponse = () => respond(ctx);
fnMiddleware(ctx).then(handleResponse)

function respond(ctx) {
  ...
  res.end(body);
}

Преимущество этого в том, что у нас есть зарезервированное рабочее пространство перед ответом, например:

async function f1(ctx, next) {
  console.log('f1 start ->');
  await next();
  ctx.body += 'f1';
  console.log('f1 end <-');
}
async function f2(ctx, next) {
  console.log('f2 start ->');
  await next();
  ctx.body += 'f2 ';
  console.log('f2 end <-');
}
async function f3(ctx) {
  ctx.body = 'f3 '
  console.log('f3 service...');
}
fn().then(() => {
  console.log(ctx); // { body: 'f3 f2 f1' }
});

Механизм экспресс-ответа

В Express мы напрямую оперируем объектом res, а в Koa — это ctx, и ответ идет сразу после res.send(), поэтому некоторые операции в верхнем мидлваре делать немного сложно.

function f2(req, res, next) {
  console.log('f2 start ->');
  next();
  res.send('f2 Hello World!') // 第二次执行
  console.log('f2 end <-');
}

async function f3(req, res) {
  console.log('f3 service...');
  res.send('f3 Hello World!') // 第一次执行
}

app.use(f2);
app.use(f3);
app.get('/', f3)

Примечание. Как и выше, если вы выполняете множественную отправку, будет сообщено об ошибке ERR_HTTP_HEADERS_SENT.

Суммировать

В этой статье сравниваются Express и Koa по трем параметрам: метод обработки обработчика, реализация механизма выполнения промежуточного программного обеспечения и механизм ответа.Обычно говорят, что Koa представляет собой луковую модель, которая фокусируется на разработке промежуточного программного обеспечения. Но в соответствии с приведенным выше анализом вы обнаружите, что Express похож, разница в том, что механизм промежуточного программного обеспечения Express использует реализацию обратного вызова, поэтому, если есть асинхронность, это может запутать вас в порядке выполнения, поэтому, если мы хотим сделать время интерфейса -статистика потребления , Обработка ошибок Шаблон промежуточного программного обеспечения Koa более удобен в обращении. Последний пункт механизма ответа тоже очень важен: Koa не отвечает сразу, но вся обработка промежуточного ПО завершается и отвечает на самом внешнем уровне, в то время как Express отвечает сразу.

Пожалуйста, обратите внимание на публичный аккаунт WeChat "Nodejs Technology Stack", чтобы получать качественные статьи.