Больше не боитесь, что интервьюер спросит вас о разнице между экспрессом и коа

Node.js

предисловие

После стольких лет использования express.js у меня наконец-то появилось время подробно изучить его, а затем, кстати, сравнить его с реализацией koa2.

Честно говоря, до того, как я прочитал исходный код express.js, я всегда считал, что express.js по-прежнему очень хорош, как с точки зрения дизайна API, так и с точки зрения использования. Но после прочтения экспресс-кода на этот раз я, возможно, изменил свое мнение.

Хотя у express.js тонкий дизайн промежуточного программного обеспечения, можно сказать, что этот тонкий дизайн слишком сложен для текущих стандартов js. Слои обратных вызовов и рекурсии внутри действительно трудно читать, не тратя определенное количество времени. А как насчет кода koa2? Его можно описать четырьмя словами: обтекаемый и прочный! Всего несколько файлов с использованием новейшего стандарта js, промежуточное ПО хорошо реализовано, а код понятен с первого взгляда.

Старые правила, прочитайте эту статью, у нас еще есть простая демонстрация для демонстрации:express-vs-koa

1. Простое отображение экспресс-использования и использования коа

Если вы используете express.js для запуска простого сервера, то основная запись должна быть такой:

const express = require('express')

const app = express()
const router = express.Router()

app.use(async (req, res, next) => {
  console.log('I am the first middleware')
  next()
  console.log('first middleware end calling')
})
app.use((req, res, next) => {
  console.log('I am the second middleware')
  next()
  console.log('second middleware end calling')
})

router.get('/api/test1', async(req, res, next) => {
  console.log('I am the router middleware => /api/test1')
  res.status(200).send('hello')
})

router.get('/api/testerror', (req, res, next) => {
  console.log('I am the router middleware => /api/testerror')
  throw new Error('I am error.')
})

app.use('/', router)

app.use(async(err, req, res, next) => {
  if (err) {
    console.log('last middleware catch error', err)
    res.status(500).send('server Error')
    return
  }
  console.log('I am the last middleware')
  next()
  console.log('last middleware end calling')
})

app.listen(3000)
console.log('server listening at port 3000')

Преобразованный в эквивалент koa2, тогда использование выглядит следующим образом:

const koa = require('koa')
const Router = require('koa-router')

const app = new koa()
const router = Router()

app.use(async(ctx, next) => {
  console.log('I am the first middleware')
  await next()
  console.log('first middleware end calling')
})

app.use(async (ctx, next) => {
  console.log('I am the second middleware')
  await next()
  console.log('second middleware end calling')
})

router.get('/api/test1', async(ctx, next) => {
  console.log('I am the router middleware => /api/test1')
  ctx.body = 'hello'
})

router.get('/api/testerror', async(ctx, next) => {
  throw new Error('I am error.')
})

app.use(router.routes())

app.listen(3000)
console.log('server listening at port 3000')

Если вас все еще интересует, как используется собственный сервер запуска nodejs, вы можете обратиться к этому файлу в демонстрации:node.js

Таким образом, разница между использованием этих двух показана в таблице следующим образом:

koa(Router = require('koa-router')) экспресс (при условии, что не используются такие методы, как app.get)
инициализация const app = new koa() const app = express()
созданный маршрут const router = Router() const router = express.Router()
промежуточное ПО уровня приложения app.use app.use
Промежуточное ПО уровня маршрута router.get router.get
Маунт-маршрутизация промежуточного программного обеспечения app.use(router.routes()) app.use('/', router)
порт прослушивания app.listen(3000) app.listen(3000)

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

Далее мы сосредоточимся на реализации промежуточного программного обеспечения из двух.

2. Принцип реализации промежуточного ПО express.js

Давайте сначала посмотрим на демонстрацию, которая показывает слабость промежуточного программного обеспечения express.js при решении определенных проблем. Демонстрационный код выглядит следующим образом:

const express = require('express')

const app = express()

const sleep = (mseconds) => new Promise((resolve) => setTimeout(() => {
  console.log('sleep timeout...')
  resolve()
}, mseconds))

app.use(async (req, res, next) => {
  console.log('I am the first middleware')
  const startTime = Date.now()
  console.log(`================ start ${req.method} ${req.url}`, { query: req.query, body: req.body });
  next()
  const cost = Date.now() - startTime
  console.log(`================ end ${req.method} ${req.url} ${res.statusCode} - ${cost} ms`)
})
app.use((req, res, next) => {
  console.log('I am the second middleware')
  next()
  console.log('second middleware end calling')
})

app.get('/api/test1', async(req, res, next) => {
  console.log('I am the router middleware => /api/test1')
  await sleep(2000)
  res.status(200).send('hello')
})

app.use(async(err, req, res, next) => {
  if (err) {
    console.log('last middleware catch error', err)
    res.status(500).send('server Error')
    return
  }
  console.log('I am the last middleware')
  await sleep(2000)
  next()
  console.log('last middleware end calling')
})

app.listen(3000)
console.log('server listening at port 3000')

При запросе в этой демонстрации/api/test1Каков результат печати?

I am the first middleware
================ start GET /api/test1
I am the second middleware
I am the router middleware => /api/test1
second middleware end calling
================ end GET /api/test1 200 - 3 ms
sleep timeout...

Если вы знаете причину этого результата печати, вы должны иметь определенное представление о промежуточной реализации express.js.

Давайте сначала посмотрим на результат печати первой демонстрации:

I am the first middleware
I am the second middleware
I am the router middleware => /api/test1
second middleware end calling
first middleware end calling

Этот отпечаток соответствует всем ожиданиям, но почему результат демонстрационного отпечатка только что не оправдывает ожиданий? Единственная разница между ними заключается в том, что во второй демонстрации добавлена ​​асинхронная обработка. При асинхронной обработке весь процесс запутывается. Поскольку поток выполнения, который мы ожидаем, будет таким:

I am the first middleware
================ start GET /api/test1
I am the second middleware
I am the router middleware => /api/test1
sleep timeout...
second middleware end calling
================ end GET /api/test1 200 - 3 ms

Так что же привело к такому результату? Мы можем получить ответ в следующем анализе.

2.1 Как быстро монтировать промежуточное ПО

Чтобы понять его реализацию, мы должны сначала знать, сколько способов Express.js может установить промежуточное программное обеспечение? Вы знаете детские туфли, которые знакомы с Express.js? Детская обувь, которую вы знаете, можно легко перечислить в своем сердце.

В настоящее время в настоящее время возможно монтировать промежуточное программное обеспечение: (метод http относится к таким методам HTTP-запроса, как GET/POST/PUT и т. д.)

  • app.use
  • app.[HTTP Method]
  • app.all
  • app.param
  • router.all
  • router.use
  • router.param
  • router.[HTTP Method]

2.2, экспресс-инициализация промежуточного ПО

Экспресс-код зависит от нескольких переменных (экземпляров): app, router, layer и route. Отношение между этими экземплярами определяет, что модель данных формируется после инициализации промежуточного программного обеспечения. На следующем рисунке нарисовано, чтобы показать:

На рисунке два экземпляра Layer, и они смонтированы в разных местах.express.jsВ качестве примера находим более яркий пример путем отладки:

Объединив их, давайте поговорим об инициализации экспресс-промежуточного программного обеспечения.Для удобства мы называем рисунок 1 выше как диаграмма модели инициализации, и рисунок 2 выше как диаграмма экземпляра инициализации

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

  • Почему существует два типа инициализированных экземпляров слоя графа модели?
  • Когда будет существовать поле маршрута в инициализированном экземпляре слоя графа модели?
  • Почему на диаграмме экземпляра инициализации смонтировано 7 промежуточных программ?
  • В примере инициализации поля маршрута круга 2 и круга 3 разные, и имена тоже разные.Почему?
  • На диаграмме экземпляра инициализации также есть экземпляр Layer в кружке 4. Отличается ли экземпляр Layer в это время от экземпляра Layer выше?

Во-первых, давайте выведем такую ​​концепцию: Экземпляр сущности слоя представляет собой сопоставленный путь и обрабатывает друг друга, каждый слой является промежуточным программным обеспечением.

В этом случае есть возможность вложить промежуточное ПО в наше промежуточное ПО, поэтому для решения этой ситуации экспресс будет шалить в Layer. Мы монтируем middleware в двух случаях:

  1. использоватьapp.use,router.useсмонтировать
    • app.useПосле серии обработок он, наконец, вызываетсяrouter.useиз
  2. использоватьapp.all,app.[Http Method],app.route,router.all,router.[Http Method],router.routeсмонтировать
    • app.all,app.[Http Method],app.route,router.all,router.[Http Method]После серии обработок он, наконец, вызываетсяrouter.routeиз

Поэтому мы ориентируемся наrouter.useа такжеrouter.routeэти два метода.

2.2.1, роутер.использование

Основной фрагмент кода в этом методе:

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);

  // 注意这个route字段设置为undefined
  layer.route = undefined;

  this.stack.push(layer);
}

Экземпляр слоя, сгенерированный в это время, соответствуетИнициализировать несколько экземпляров Layer, указанных моделью Рисунок 1, в это время сexpress.jsНапример, мы видимИнициализировать все экземпляры слоя круга графа экземпляра 1, вы обнаружите, что в дополнение к нашему специальному промежуточному программному обеспечению (всего 5) есть две системы, которые поставляются с ними. Имена слоев, которые смотрят на диаграмму экземпляра инициализации, следующие:queryа такжеexpressInit. Инициализация двух находится в [Application.js]lazyrouterметод:

app.lazyrouter = function lazyrouter() {
  if (!this._router) {
    this._router = new Router({
      caseSensitive: this.enabled('case sensitive routing'),
      strict: this.enabled('strict routing')
    });

    this._router.use(query(this.get('query parser fn'))); // 最终调用的就是router.use方法
    this._router.use(middleware.init(this)); // 最终调用的就是router.use方法
  }
};

Итак, это ответ на наш третий вопрос. 7 промежуточных программ, 2 встроенных системы, 3 промежуточных ПО уровня APP и 2 промежуточных ПО уровня маршрутизации.

2.2.2, маршрутизатор.маршрут

мы сказалиapp.all,app.[Http Method],app.route,router.all,router.[Http Method]После серии обработок он, наконец, вызываетсяrouter.route, так что у нас есть в демоexpress.js, использовано дваждыapp.get, который, наконец, вызываетrouter.route, мы рассмотрим основную реализацию этого метода:

proto.route = function route(path) {
  var route = new Route(path);

  var layer = new Layer(path, {
    sensitive: this.caseSensitive,
    strict: this.strict,
    end: true
  }, route.dispatch.bind(route));

  layer.route = route;

  this.stack.push(layer);
  return route;
};

Такая простая реализация, единственное отличие от реализации предыдущего метода в том, что здесь большеnew Routeэто. Сравнивая их, мы можем ответить на несколько из приведенных выше вопросов:

  • Почему существует два типа экземпляров Layer для инициализации графа модели?Поскольку разница в методе вызова определяет разницу между экземплярами Layer, второй экземпляр Layer монтируется под экземпляром маршрута.
  • Когда будет существовать поле маршрута в инициализированном экземпляре слоя графа модели? использоватьrouter.routeбудет существовать, когда
  • В примере инициализации поля маршрута круга 2 и круга 3 разные, и имена тоже разные.Почему? Слой круга 2, потому что мы используем функцию стрелки, имя функции отсутствует, поэтому имяanonymous, но обведите 3 из-за использованияrouter.route, поэтому его унифицированная функция обратного вызоваroute.dispath, поэтому имена его функций единообразноbound dispatchВ то же время с первого взгляда видно, назначены ли поля маршрута двух

Последний вопрос, поскольку у маршрута есть свой Слой после создания экземпляра маршрута, где его инициализация? Инициализируйте основной код:

// router/route.js/Route.prototype[method]
for (var i = 0; i < handles.length; i++) {
    var handle = handles[i];

    if (typeof handle !== 'function') {
      var type = toString.call(handle);
      var msg = 'Route.' + method + '() requires a callback function but got a ' + type
      throw new Error(msg);
    }

    debug('%s %o', method, this.path)

    var layer = Layer('/', {}, handle);
    layer.method = method;

    this.methods[method] = true;
    this.stack.push(layer);
  }

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

Теперь вернитесь и посмотрите на диаграмму модели инициализации, я думаю, все ее поймут~

2.3 Логика выполнения экспресс промежуточного ПО

ИСПОЛНЕНИЕ ЛОГИКА ИЗОБРАЖЕНИЯ ВСЕ ИМЕРНОГО СЛОЩАЮЩАЯМОДНОГО СЛОВА, ИЛИ УЛУЧНОГО УСЛОВОГО ПРИМЕНЕНИЯ, основаны на рекурсивной форме вызова, очень важную функциюnext()Поняв все это, вот блок-схема, надеюсь, вам будет полезно это понять:

давайте положимexpress.jsКод реализован в другом виде, чтобы вы могли полностью понять весь процесс.

Чтобы упростить, мы удаляем два промежуточных ПО по умолчанию, установленных в системе, и удаляем одно промежуточное ПО для маршрутизации. Конечный результат таков:

((req, res) => {
  console.log('I am the first middleware');
  ((req, res) => {
    console.log('I am the second middleware');
    (async(req, res) => {
      console.log('I am the router middleware => /api/test1');
      await sleep(2000)
      res.status(200).send('hello')
    })(req, res)
    console.log('second middleware end calling');
  })(req, res)
  console.log('first middleware end calling')
})(req, res)

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

Но нужно указать одну вещь: хотя печать становится странной, она никогда не повлияет на весь запрос, потому что ответ приходит после того, как мы ожидаем, поэтому, завершится запрос или нет, зависит от того, вызываем ли мы такие функции, как res.send

На данный момент, я надеюсь, вы можете быть знакомы с процессом выполнения всего промежуточного программного обеспечения Express.Для получения более подробной информации рекомендуется посмотреть исходный код.Этот тонкий дизайн действительно не ясен в этой статье. Эта статья просто хочет, чтобы вам было что сказать во время интервью~

Далее, давайте проанализируем удивительный Koa2.Этот не нужно объяснять в таком большом пространстве, потому что это действительно легко понять.

3. промежуточное ПО koa2

Основная логика обработки промежуточного программного обеспечения koa2 размещенаkoa-compose, что является просто функциональной вещью:

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) {
    // last called middleware #
    let index = -1
    return dispatch(0)
    function dispatch (i) {
      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, dispatch.bind(null, i + 1)));
      } catch (err) {
        return Promise.reject(err)
      }
    }
  }
}

Метод next(), вызываемый каждым промежуточным программным обеспечением, на самом деле таков:

dispatch.bind(null, i + 1)

Или использовать свойства замыканий и рекурсии для выполнения по одному, и каждое выполнение возвращает промисы, поэтому окончательный результат печати будет таким, как мы хотим. Затем, вызывается ли промежуточное ПО маршрута, не управляется koa2, и эта работа передаетсяkoa-router, так что koa2 может поддерживать обтекаемый и крепкий стиль.

Опубликуем поток выполнения промежуточного программного обеспечения koa:

middleware

наконец

С этой статьей я верю, что вы больше не будете бояться, что интервьюер спросит вас о разнице между экспрессом и коа~

Ссылаться на

  1. koa
  2. express
  3. http