Koa v2 недавно упал, поскольку Node сделал async-await общедоступным без флага. Кажется, что Express по-прежнему побеждает в конкурсе на популярность, но я с радостью использую Koa с момента анонса версии 2 и всегда боюсь возвращаться в Express для старых проектов.

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

Многие новички Koa работали с Express раньше, поэтому я буду проводить много сравнений между ними.

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

Основы

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

выражать

const express = require('express')
const app = express()
// Middleware 1
app.use((req, res, next) => {
  res.status(200)
  console.log('Setting status')
  // Call the next middleware
  next()
})
// Middleware 2
app.use((req, res) => {
  console.log('Setting body')
  res.send(`Hello from Express`)
})
app.listen(3001, () => console.log('Express app listening on 3001'))

Коа

const Koa = require('koa')
const app = new Koa()
// Middleware 1
app.use(async (ctx, next) => {
  ctx.status = 200
  console.log('Setting status')
  // Call the next middleware, wait for it to complete
  await next()
})
// Middleware 2
app.use((ctx) => {
  console.log('Setting body')
  ctx.body = 'Hello from Koa'
})
app.listen(3002, () => console.log('Koa app listening on 3002'))

Давайте поразим их обоих curl:

$ curl http://localhost:3001
Hello from Express
$ curl http://localhost:3002
Hello from Koa

Оба примера делают одно и то же, и оба выводят на терминал один и тот же вывод:

Setting status
Setting body

Это показывает, что в обоих случаях промежуточное ПО выполняется сверху вниз.

Большая разница здесь в том, что цепочка промежуточного программного обеспечения Express основана на обратных вызовах, в то время как Koa - на основе Promise.

Посмотрим, что произойдет, если мы опустим вызов next() в обоих примерах.

выражать

$ curl http://localhost:3001

… Он никогда не завершается. Это связано с тем, что в Express вам нужно либо позвонить next(), либо отправить ответ, иначе запрос не будет выполнен.

Коа

$ curl http://localhost:3002
OK

Ах, значит, приложение Koa завершит запрос, но без тела. Тем не менее, он установил код состояния. Так что 2-е промежуточное ПО не было вызвано.

Но есть еще одна важная вещь для Коа. Если вы позвоните next(), вы должны дождаться этого!

Лучше всего это проиллюстрировано на следующем примере:

// Simple Promise delay
function delay (ms) {
  return new Promise((resolve) => {
    setTimeout(resolve, ms)
  })
}
app.use(async (ctx, next) => {
  ctx.status = 200
  console.log('Setting status')
  next() // forgot await!
})
app.use(async (ctx) => {
  await delay(1000) // simulate actual async behavior
  console.log('Setting body')
  ctx.body = 'Hello from Koa'
})

Давай посмотрим что происходит.

$ curl http://localhost:3002
OK

Хм, мы позвонили next(), но тела не прислали? Это потому, что Koa завершает запрос после разрешения цепочки промисов промежуточного программного обеспечения. Это означает, что ответ был отправлен клиенту до того, как мы успели установить ctx.body!

Еще одна проблема, если вы используете простой Promise.then() вместо async-await, заключается в том, что промежуточное ПО должно возвращать обещание. Когда возвращенное обещание разрешится, Koa возобновит работу с предыдущим промежуточным программным обеспечением.

app.use((ctx, next) => {
  ctx.status = 200
  console.log('Setting status')
  // need to return here, not using async-await
  return next()
})

Лучший пример использования простых обещаний:

// We don't call `next()` because
// we don't want anything else to happen.
app.use((ctx) => {
  return delay(1000).then(() => {
    console.log('Setting body')
    ctx.body = 'Hello from Koa'
  })
})

Промежуточное ПО Koa - особенность, меняющая правила игры

В предыдущем разделе я писал:

Тогда Koa возобновит работу с предыдущим промежуточным ПО.

И это могло немного сбить вас с толку. Позвольте мне объяснить.

В Express промежуточное ПО может делать полезные вещи только до вызова next(), но не после. Как только вы позвоните next(), этот запрос никогда больше не коснется промежуточного программного обеспечения. Это может быть своего рода облом. Люди (включая самих авторов Express) нашли хитрые обходные пути, например, наблюдая за потоком ответов, когда пишутся заголовки, но для среднего потребителя это просто неудобно.

Например, для реализации промежуточного программного обеспечения, которое записывает количество времени, необходимое для выполнения запроса и отправки его в заголовке X-ResponseTime, потребуется кодовая точка «до следующего вызова» и кодовая точка «после вызова следующего». В Express это реализовано с помощью техники просмотра потокового видео.

Попробуем реализовать это на Koa.

async function responseTime (ctx, next) {
  console.log('Started tracking response time')
  const started = Date.now()
  await next()
  // once all middleware below completes, this continues
  const ellapsed = (Date.now() - started) + 'ms'
  console.log('Response time is:', ellapsed)
  ctx.set('X-ResponseTime', ellapsed)
}
app.use(responseTime)
app.use(async (ctx, next) => {
  ctx.status = 200
  console.log('Setting status')
  await next()
})
app.use(async (ctx) => {
  await delay(1000)
  console.log('Setting body')
  ctx.body = 'Hello from Koa'
})

8 строк. Это все, что нужно. Никакого фанкового обнюхивания потока, только великолепный код async-await. Давай ударим! Флаг -i указывает curl также показать нам заголовки ответа.

$ curl -i http://localhost:3002
HTTP/1.1 200 OK
Content-Type: text/plain; charset=utf-8
Content-Length: 14
X-ResponseTime: 1001ms
Date: Thu, 30 Mar 2017 12:52:48 GMT
Connection: keep-alive
Hello from Koa

Отлично! Мы получили время ответа в HTTP-заголовке. Давайте проверим наш терминал на наличие журналов, чтобы увидеть, в каком порядке были записаны журналы консоли.

Started tracking response time
Setting status
Setting body
Response time is: 1001ms

Вот и все. Koa дает нам полный контроль над потоком промежуточного программного обеспечения. Внедрить такие вещи, как аутентификация и обработка ошибок, будет очень просто!

Обработка ошибок

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

Для наглядности давайте посмотрим, как мы это сделаем в Express.

выражать

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

app.use((req, res) => {
  if (req.query.greet !== 'world') {
    throw new Error('can only greet "world"')
  }
  res.status(200)
  res.send(`Hello ${req.query.greet} from Express`)
})
// Error handler
app.use((err, req, res, next) => {
  if (!err) {
    next()
    return
  }
  console.log('Error handler:', err.message)
  res.status(400)
  res.send('Uh-oh: ' + err.message)
})

Это лучший пример сценария. Если вы имеете дело с асинхронными ошибками из обратных вызовов или обещаний, они становятся чрезвычайно подробными. Например:

app.use((req, res, next) => {
  loadCurrentWeather(req.query.city, (err, weather) => {
    if (err) {
      return next(err)
    }
    
    loadForecast(req.query.city, (err, forecast) => {
      if (err) {
        return next(err)
      }
      
      res.status(200).send({
        weather: weather,
        forecast: forecast
      })
    })
  })
  
  next()
})

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

Коа

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

Промежуточное ПО для обработки ошибок находится наверху, потому что оно «оборачивает» каждое последующее промежуточное ПО. Это означает, что любая ошибка, возникшая в промежуточном программном обеспечении, добавленном после обработки ошибок, будет обнаружена (да, почувствуйте силу!)

app.use(async (ctx, next) => {
  try {
    await next()
  } catch (err) {
    ctx.status = 400
    ctx.body = `Uh-oh: ${err.message}`
    console.log('Error handler:', err.message)
  }
})
app.use(async (ctx) => {
  if (ctx.query.greet !== 'world') {
    throw new Error('can only greet "world"')
  }
  
  console.log('Sending response')
  ctx.status = 200
  ctx.body = `Hello ${ctx.query.greet} from Koa`
})

да. А try-catch. Для обработки ошибок. Как кстати! Не async-await способ:

app.use((ctx, next) => {
  return next().catch(err => {
    ctx.status = 400
    ctx.body = `Uh-oh: ${err.message}`
    console.log('Error handler:', err.message)
  })
})

Попробуем вызвать ошибку.

$ curl http://localhost:3002?greet=jeff
Uh-oh: can only greet "world"

И вывод консоли, как ожидалось:

Error handler: can only greet "world"

Маршрутизация

В отличие от Express, у Koa нет практически ничего из коробки. Ни бодипарсера, ни роутера тоже.

В Koa есть несколько вариантов маршрутизации, например koa-route и koa-router. Я предпочитаю последнее.

выражать

Маршрутизация в Express встроена.

app.get('/todos', (req, res) => {
  res.status(200).send([{
    id: 1,
    text: 'Switch to Koa'
  }, {
    id: 2,
    text: '???'
  }, {
    id: 3,
    text: 'Profit'
  }])
})

Коа

В этом примере я выбрал koa-router, потому что это то, что я использую.

const Router = require('koa-router')
const router = new Router()
router.get('/todos', (ctx) => {
  ctx.status = 200
  ctx.body = [{
    id: 1,
    text: 'Switch to Koa',
    completed: true
  }, {
    id: 2,
    text: '???',
    completed: true
  }, {
    id: 3,
    text: 'Profit',
    completed: true
  }]
})
app.use(router.routes())
// makes sure a 405 Method Not Allowed is sent
app.use(router.allowedMethods())

Заключение

Коа классный. Полный контроль над цепочкой промежуточного программного обеспечения и тот факт, что все это основано на обещаниях, значительно упрощает работу со всем. Нет больше if (err) return next(err) повсюду, только обещания.

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

Вот список промежуточного программного обеспечения, которое я часто использую (в произвольном порядке):

На заметку: не все промежуточное ПО поддерживает Koa 2, однако их можно преобразовать во время выполнения с помощью koa-convert, так что не беспокойтесь.

Надеюсь, эта статья оказалась для вас полезной. Если да, нажмите кнопку «Рекомендовать». Это было бы прекрасно! :)

Вы можете найти меня в Twitter: @Jeffijoe

Переводы