Первоначально опубликовано на www.melvinkoh.me.

Прежде чем ES6 предоставит нам встроенную поддержку Promise, мы полагаемся на сторонние библиотеки, такие как Bluebird, чтобы задействовать этот полезный механизм. В этом посте мы увидим, как мы можем обещать API на основе обратных вызовов в собственном стиле ES6 и избежать общего анти-шаблона.

Что такое Promisify?

Promisify - это рефакторинг обычного API на основе обратного вызова в API на основе Promise.

Зачем обещать?

Традиционно API на основе обратного вызова используются для обработки вызова асинхронной функции. API на основе обратного вызова обычно ожидает 2 аргумента: onSuccessCallback и onErrorCallback.

// Typical callback-based API
function send (message, onSuccessCallback, onErrorCallback) {
  try {
    // Do something with the message
  } catch (error) {
    onErrorCallback(error)
  }
  onSuccessCallback()
}

Если что-то пошло не так, блок catch вызовет onErrorCallback (), который мы передали ему во время вызова функции. Если все работает без ошибок, вызывается onSuccessCallback ().

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

Пример: установка соединения с Strophe.js

Это действительно проблема, с которой я столкнулся, когда писал клиент чата XMPP на ES6 и интегрировал его с моим интерфейсом на основе Vue.js.

Strophe.js - это библиотека XMPP js, которая обрабатывает установление сеанса XMPP и связь с сервером чата XMPP. Цель Strophe.js в этом примере не важна, так как я намерен проиллюстрировать проблемы обычного API на основе обратного вызова.

Это упрощенная подпись метода connect () в Stophe.js для установления соединения с сервером XMPP:

connect: function ( jid, pass, callback) { ... }

jid и pass - эквивалент имени пользователя и пароля в XMPP. Каждый раз, когда мы вызываем этот метод, ожидается, что третий позиционный аргумент callback будет обрабатывать строфу (вы можете рассматривать строфу как сообщение), на которую отвечает сервер.

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

Это упрощенная логика при установке соединения с помощью connect ():

import Strophe from 'strophe.js'
conn = Strophe.Connection(...)
conn.connect(
  'myJID',
  'myPwd',
  (status) => {
    switch (status) {
      case Strophe.Status.CONNECTED:
        console.log('YEAH! Connection established.')
        break
      case Strophe.Status.DISCONNECTED:
        console.log('Oh No... Connection disconnected.')
        break
    }
  }
)
conn.send('something') // Result in error as connection is not ready

Если мы хотим использовать указанное выше соединение сразу после вызова connect (), вы, вероятно, получите ошибку соединения. Итак, как мы можем связать серию вызовов методов, не вызывая их из обратного вызова, переданного в connect ()?

Самый простой способ - обещать приведенный выше код.

Как обещать?

Когда ваш API на основе обратного вызова принимает только 1 обратный вызов вместо обратных вызовов onSuccess и onError.

Некоторые API ожидают только одного обратного вызова для обработки всех возможных ситуаций:

const onConnect = function (status) {
  if (status === 'ok') {
    console.log('ok')
  } else {
    console.log('not ok')
  }
}
const connect = function (onConnectCallback) {...}

Мы вызываем указанную выше функцию следующим образом:

connect(onConnect)

Чтобы обещать connect (), мы реорганизуем его до следующего:

const connectPromisified = function (jid, pwd) {
  return new Promise((resolve, reject) => {
    connect(
      jid,
      pwd,
      (status) => {
        if (status === 'ok') {
          console.log('ok')
          resolve()
        } else {
          console.log('not ok')
          reject()
        }
        }
      )
    })
  }

ТАДА! Вы можете использовать его в обещанной манере.

connect(onConnect)
  .then(() => {
      /**
        * This anonymous function will be called whenever the Promise is resolved.
        * In other word, the connection will be used only if the connection is successfully established
        */
      conn.send('something')
    })

Когда ваш API на основе обратного вызова ожидает обратные вызовы onSuccess и onError

Этот API ожидает 2 обратных вызова (onSuccess, onError):

const sendQuery = function (query, onSuccess, onError) {...}

Чтобы сделать sendQuery () обещанием, мы оборачиваем его другим методом:

const sendQueryPromisified = function (query) {
  return new Promise((resolve, reject) => {
    sendQuery(query, resolve, reject)
  })
}

Resolution () теперь является обратным вызовом onSuccess, а reject () становится обратным вызовом onError.

Реорганизовав так, мы можем использовать его как обычное обещание:

sendQueryPromisified('a query')
.then(() => {
    /* handle onSuccess here*/
  })
.catch(() => {
    /* handle onError here */
  })

Пример: установка соединения с Strophe.js (после рефакторинга)

Используя приведенный выше пример нашего Strophe Connection, мы реорганизуем connect () в это:

const connect = function (jid, pwd) {
  conn.connect(jid, pwd, (status) => {
    return new Promise((resolve, reject) => {
      switch (status) {
        case Strophe.Status.CONNECTED:
          console.log('YEAH! Connection established.')
          resolve()
          break
        case Strophe.Status.DISCONNECTED:
          console.log('Oh No... Connection disconnected.')
          reject()
          break
      })
    })  
}

Таким образом, мы можем вызвать connect () следующим образом:

connect(jid, pwd)
.then(() => {
    /* connection established */
    /* start using your connection here */
  })
.catch(() => {
    /* handle errors here */
  })

Распространенный анти-шаблон: создание обещаний на основе обратного вызова

Разработчики, которые привыкли использовать API на основе обратного вызова, как правило, допускают эту ошибку. Давайте посмотрим на сигнатуру метода then ():

then (onFulfilled, onRejected) {...}

then () предоставляет вам возможность внедрить обратный вызов onError / onRejected для обработки возврата ошибки из обещания. Некоторые могут использовать это так:

const onSuccess = function () {...}
const onError = function () {...}
connect().then(onSuccess, onError)

Вы заметили сходство then () с вашим обычным API на основе обратного вызова?

Вы не должны использовать then () таким образом, или вы просто делаете API на основе Promise на основе обратного вызова. Вы должны полагаться на catch () для обработки ошибок в обещании.

Твои хлопки обязательно подтолкнут меня к дальнейшему. Хлопните, если вам понравился этот пост.