Как написать декларативную обертку обещания на JavaScript

Автор Виджит Аил✏️

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

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

В этом руководстве мы покажем, как писать декларативные вызовы асинхронных функций в JavaScript. Мы также покажем, как это помогает сделать наш код более читабельным и простым в сопровождении.

Декларативное программирование

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

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

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

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

Например, при декларативном программировании нам не нужно использовать цикл for для итерации по массиву. Вместо этого мы можем просто использовать встроенные методы массива, такие как map(), reduce() и forEach().

Вот пример императивного программирования, показывающий функцию, которая меняет строку на противоположную, используя цикл for с уменьшением:

const reverseString = (str) => {
    let reversedString = "";

    for (var i = str.length - 1; i >= 0; i--) { 
        reversedString += str[i];
    }
    return reversedString; 
}
Вход в полноэкранный режим Выход из полноэкранного режима

Но зачем писать десять строк кода, если мы можем достичь того же решения всего двумя строками кода?

Вот версия того же кода для декларативного программирования, использующая встроенные методы массивов JavaScript:

const reverseString = (str) => {
  return str.split("").reverse().join("");  
} 
Вход в полноэкранный режим Выйти из полноэкранного режима

Этот фрагмент кода использует две строки кода для обратного преобразования строки. Он очень короткий и сразу переходит к делу.

Обещания в JavaScript

Обещание — это объект JavaScript, который содержит результаты асинхронной функции. Другими словами, он представляет собой задачу, которая была выполнена или не выполнена в асинхронной функции.

const promise = new Promise (function (resolve, reject) {
    // code to execute
})
Вход в полноэкранный режим Выход из полноэкранного режима

Конструктор promise принимает один аргумент — функцию обратного вызова, также называемую исполнителем. Функция-исполнитель принимает две функции обратного вызова: resolve и reject. Если функция-исполнитель выполняется успешно, вызывается метод resolve() и состояние promise меняется с ожидающего на выполненное. Если функция-исполнитель завершается неудачно, то вызывается метод reject(), и состояние promise меняется с ожидающего на невыполненное.

Чтобы получить доступ к разрешенному значению, используйте метод .then () для цепочки с promise, как показано ниже:

promise.then(resolvedData => {
  // do something with the resolved value
})
Вход в полноэкранный режим Выйти из полноэкранного режима

Аналогично, в случае отклоненного значения используется метод .catch():

promise.then(resolvedData => {
  // do something with the resolved value
}).catch(err => {
  // handle the rejected value
})
Войти в полноэкранный режим Выход из полноэкранного режима

Async/await

Когда у нас есть несколько вложенных обратных вызовов или функций .then, часто становится трудно поддерживать код и его читабельность.

Ключевое слово async помогает нам определить функции, которые обрабатывают асинхронные операции в JavaScript. Между тем, ключевое слово await используется для указания движку JavaScript дождаться завершения работы функции перед возвратом результатов.

Синтаксис async/await — это просто синтаксический сахар вокруг обещаний. Он помогает нам добиться более чистого кода, который легче поддерживать.

const getUsers = async () => {
  const res = await fetch('https://jsonplaceholder.typicode.com/users');
  const data = await res.json();
  return data;
}
Вход в полноэкранный режим Выход из полноэкранного режима

Async/await позволяет обещаниям или асинхронным функциям выполняться синхронно. Однако всегда рекомендуется обернуть ключевое слово await блоком try...catch, чтобы избежать непредвиденных ошибок.

Вот пример, где мы обернули ключевое слово await и функцию getUsers() в блок try...catch, как показано ниже:

const onLoad = async () => {
  try {
    const users = await getUsers();
    // do something with the users
  } catch (err) {
    console.log(err)
    // handle the error
  }
}
Войти в полноэкранный режим Выйти из полноэкранного режима

Пользовательская обертка promise

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

Тем не менее, обработка ошибок от нескольких функций async может привести к чему-то вроде этого:

try {
  const a = await asyncFuncOne();
} catch (errA) {
  // handle error
}

try {
  const b = await asyncFunctionTwo();
} catch (errB) {
  // handle error
}

try {
  const c = await asyncFunctionThree();
} catch (errC) {
  // handle error
}
Вход в полноэкранный режим Выход из полноэкранного режима

Если мы добавим все функции async в один блок try, мы закончим написанием нескольких условий if в блоке catch, поскольку наш блок catch теперь более общий:

try {
  const a = await asyncFuncOne();
  const b = await asyncFunctionTwo();
  const c = await asyncFunctionThree();
} catch (err) {
  if(err.message.includes('A')) {
    // handle error for asyncFuncOne
  }
  if(err.message.includes('B')) {
    // handle error for asyncFunctionTwo
  }
  if(err.message.includes('C')) {
    // handle error for asyncFunctionThree
  }
}
Войти в полноэкранный режим Выйти из полноэкранного режима

Это делает код менее читабельным и сложным для сопровождения, даже с синтаксисом async/await.

Чтобы решить эту проблему, мы можем написать служебную функцию, которая обернет обещание и позволит избежать повторяющихся блоков try...catch.

Утилитарная функция будет принимать обещание в качестве параметра, обрабатывать ошибку внутри и возвращать массив с двумя элементами: разрешенное значение и отвергнутое значение.

Функция разрешит обещание и вернет данные в первом элементе массива. Ошибка будет возвращена во втором элементе массива. Если обещание было разрешено, второй элемент будет возвращен как null.

const promiser = async (promise) => {
  try {
    const data = await promise;
    return [data, null]
  } catch (err){
    return [null, error]
  }
}
Войти в полноэкранный режим Выход из полноэкранного режима

Мы можем продолжить рефакторинг приведенного выше кода и удалить блок try...catch, просто возвращая promise с помощью методов обработчиков .then() и .catch():

const promiser = (promise) => {
  return promise.then((data) => [data, null]).catch((error) => [null, error]);
};
Вход в полноэкранный режим Выйти из полноэкранного режима

Использование утилиты показано ниже:

const demoPromise = new Promise((resolve, reject) => {
  setTimeout(() => {
    // resolve("Yaa!!");
    reject("Naahh!!");
  }, 5000);
});

const runApp = async () => {
  const [data, error] = await promiser(demoPromise);
  if (error) {
    console.log(error);
    return;
  }
  // do something with the data
};

runApp();
Вход в полноэкранный режим Выход из полноэкранного режима

Теперь давайте рассмотрим реальный случай использования. Ниже, функция generateShortLink использует службу URL shortener для сокращения полноформатного URL.

Здесь метод axios.get() обернут функцией promiser() для возврата ответа от службы сократителя URL.

import promiser from "./promise-wrapper";
import axios from "axios";

const generateShortLink = async (longUrl) => {
  const [response, error] = await promiser(
    axios.get(`https://api.1pt.co/addURL?long=${longUrl}`)
  );

  if (error) return null;

  return `https://1pt.co/${response.data.short}`;
};
Вход в полноэкранный режим Выход из полноэкранного режима

Для сравнения, вот как выглядела бы функция без функции-обертки promiser():

const generateShortLink = async (longUrl) => {
  try {
    const response = await axios.get(
      `https://api.1pt.co/addURL?long=${longUrl}`
    );
    return `https://1pt.co/${response.data.short}`;
  } catch (err) {
    return null;
  }
};
Вход в полноэкранный режим Выход из полноэкранного режима

Теперь давайте завершим пример, создав форму, которая использует метод generateShortLink():

const form = document.getElementById("shortLinkGenerator");

const longUrlField = document.getElementById("longUrl");

const result = document.getElementById("result");

form.addEventListener("submit", async (e) => {
  e.preventDefault();
  const longUrl = longUrlField.value;
  const shortLink = await generateShortLink(longUrl);
  if (!shortLink) result.innerText = "Could not generate short link";
  else result.innerHTML = `<a href="${shortLink}">${shortLink}</a>`;
});

<!-- HTML -->
<!DOCTYPE html>
<html>
  <head>
    <title>Demo</title>
    <meta charset="UTF-8" />
  </head>
  <body>
    <div id="app">
      <form id="shortLinkGenerator">
        <input type="url" id="longUrl" />
        <button>Generate Short Link</button>
      </form>
      <div id="result"></div>
    </div>
    <script src="src/index.js"></script>
  </body>
</html>
Вход в полноэкранный режим Выход из полноэкранного режима

Вот полный код и демонстрация для справки.

Пока что функция promiser() может обернуть только одну функцию async. Однако в большинстве случаев потребуется обрабатывать несколько независимых функций async.

Для обработки многих обещаний мы можем использовать метод Promise.all() и передать массив функций async в функцию promiser:

const promiser = (promise) => {
  if (Array.isArray(promise)) promise = Promise.all(promise);
  return promise.then((data) => [data, null]).catch((error) => [null, error]);
};
Вход в полноэкранный режим Выход из полноэкранного режима

Вот пример функции promiser(), используемой с несколькими функциями async:

import axios from "axios";
import promiser from "./promiser";

const categories = ["science", "sports", "entertainment"];

const requests = categories.map((category) =>
  axios.get(`https://inshortsapi.vercel.app/news?category=${category}`)
);

const runApp = async () => {
  const [data, error] = await promiser(requests);
  if (error) {
    console.error(error?.response?.data);
    return;
  }
  console.log(data);
};

runApp();
Вход в полноэкранный режим Выход из полноэкранного режима

Заключение

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

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

Знания, изложенные в этой статье, являются хорошей отправной точкой для создания более сложных API и служебных функций, когда вы продолжите свой путь в кодинге. Удачи и счастливого кодинга!


LogRocket: Отлаживайте ошибки JavaScript проще, понимая контекст

Отладка кода — это всегда утомительное занятие. Но чем лучше вы понимаете ошибки, тем легче их исправить.

LogRocket позволяет понять эти ошибки новыми и уникальными способами. Наше решение для мониторинга фронтенда отслеживает взаимодействие пользователей с вашими JavaScript-фронтендами, чтобы дать вам возможность узнать, что именно пользователь сделал, что привело к ошибке.

LogRocket записывает журналы консоли, время загрузки страницы, трассировку стека, медленные сетевые запросы/ответы с заголовками + телами, метаданные браузера и пользовательские журналы. Понимание влияния вашего кода JavaScript никогда не будет таким простым!

Попробуйте бесплатно.

Оставьте комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *