Axios — это широко известная JavaScript-библиотека. Она представляет собой HTTP-клиент, основанный на промисах и предназначенный для браузеров и для Node.js. Если вы работали в последние несколько лет JavaScript-программистом, то вы, совершенно определённо, этой библиотекой пользовались. В октябре 2019 года пакет Axios был загружен из npm 25 миллионов раз. Кажется, что будущее Axios безоблачно. Но что если я скажу вам, что Axios — это мёртвый проект. Именно этому было посвящено одно обсуждение на Reddit. А именно, речь идёт о следующем:

  • В GitHub-репозитории Axios наблюдается весьма низкий уровень активности разработчиков.
  • Проблемы и PR игнорируются.
  • Команда разработчиков хранит молчание.

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

Когда библиотека Axios стала популярной, в браузерах не было API, реализующего HTTP-клиент, основанный на промисах. Стандартный интерфейс XML HTTP Request (XHR) был неудобным, работать с ним было тяжело. Разработчики с радостью приняли Axios из-за того, что эта библиотека облегчала им жизнь.



В 2015 вышел API Fetch. Почему же мы, в 2019 году, до сих пор используем Axios? Давайте сравним эти две технологии.

Объём шаблонного кода


?Fetch


fetch('https://jsonplaceholder.typicode.com/todos/1')
  .then(response => response.json())
  .then(json => console.log(json))
// {
//   "userId": 1,
//   "id": 1,
//   "title": "delectus aut autem",
//   "completed": false
// }

?Axios


axios.get("https://jsonplaceholder.typicode.com/todos/1")
  .then(response => console.log("response", response.data))
// {
//   "userId": 1,
//   "id": 1,
//   "title": "delectus aut autem",
//   "completed": false
// }

При использовании Fetch приходится иметь дело с двумя промисами. А вот при работе с Axios у нас есть прямой доступ к JSON-результату в свойстве data объекта ответа.

Метод json() миксина Body принимает поток Response и полностью читает его. Он возвращает промис, который разрешается JSON-результатом разбора текста тела запроса.

Ещё больше шаблонного кода в Fetch приходится использовать при работе с POST-запросами.

?Fetch


fetch("https://jsonplaceholder.typicode.com/posts", {
  method: "POST",
  body: JSON.stringify({
    title: "Title of post",
    body: "Post Body"
  })
})
  .then(res => {
    if (!response.ok) throw Error(response.statusText);
    return response.json();
  })
  .then(data => console.log(data))
  .catch(error => console.log(error));

?Axios


axios
  .post("https://jsonplaceholder.typicode.com/posts", {
    title: "Title of post",
    body: "Body of post"
  })
  .then(response => console.log(response.data))
  .catch(error => console.log(error));

Использование Axios позволяет избежать написания больших объёмов шаблонного кода и сделать код чище и понятнее.

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


?Fetch


fetch("https://jsonplaceholder.typicode.com/todos/100000")
  .then(response => {
    if (!response.ok) throw Error(response.statusText);
    return response.json();
  })
  .then(data => console.log("data", data))
  .catch(error => {
    console.log("error", error);
  });
// error Error: Not Found

?Axios


axios
  .get("https://jsonplaceholder.typicode.com/todos/100000")
  .then(response => {
    console.log("response", response);
  })
  .catch(error => {
    console.log("error", error);
  });
// error Error: Not Found

Библиотека Axios выдаёт сведения о сетевых ошибках, а API Fetch — нет. Работая с Fetch всегда нужно проверять свойство response.ok. Для того чтобы упростить решение данной задачи, проверку этой ошибки можно оформить в виде отдельной функции:

const checkForError = response => {
  if (!response.ok) throw Error(response.statusText);
  return response.json();
};
fetch("https://jsonplaceholder.typicode.com/todos/100000")
  .then(checkForError)
  .then(data => console.log("data", data))
  .catch(error => {
    console.log("error", error);
  });

Отсутствующие возможности


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

const config = {
  onUploadProgress: event => console.log(event.loaded)
};
axios.put("/api", data, config);

Альтернативные библиотеки


Вот пара альтернатив Axios и API Fetch:

  • Ky — миниатюрный и продуманный HTTP-клиент, основанный на window.fetch.
  • Superagent — маленькая прогрессивная клиентская HTTP-библиотека, основанная на XMLHttpRequest.

Итоги


Чем стоит пользоваться в 2019 году? Это зависит от многого. Например, если вам нужно отслеживать прогресс выгрузки материалов на сервер, то вам лучше всего подойдёт Axios или Superagent. Если вы можете справиться с ограничениями Fetch — тогда вам лучше воспользоваться именно этим API. А для того, чтобы немного улучшить Fetch-код, попробуйте библиотеку-обёртку наподобие Ky.

Уважаемые читатели! Какими HTTP-клиентами вы пользуетесь в своих проектах?


Комментарии (40)


  1. Sabubu
    29.11.2019 13:26
    -2

    Нужна была библиотека для выполнения запросов в браузере. Погуглил, посмотрел гитхабы, пришлось потратить час на написание своей микробиблиотеки из 1 функции на основе XHR. Использует XHR, поддерживает промисы.


    Некоторые библиотеки гигантские (есть даже монстры с плагинами, плагины для выполнения HTTP запроса!), некоторые увлеклись модными технологиями и поддерживают мало браузеров. Моя функция оказалась работающей даже на 10-летних браузерах и компактней других, так как не включает кучу никому не нужного функционала. Это наверно беда многих open source проектов, что каждый добавляет в проект никому кроме него не нужную функцию, и получается монстр.


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


    Промисы спроектированы глупо, людьми, у которых фобия исключений. Ловить надо только то, что явно запрошено пользователем, и не перехватывать остальные типы исключений. Но у людей необъяснимая фобия, и они ловят все подряд. Видимо от страха, что их кривая поделка упадет.


    1. Suvitruf
      29.11.2019 14:25
      +2

      Нафига перехватывать обращение к несуществующей переменной и заворачивать в промис? Это недостаток в дизайне промисов. Нужно не перехватывать такие ошибки, а завершать программу
      Убивать весь сервис из-за какой-то мелкой ошибки? (:


      1. Sabubu
        30.11.2019 02:30
        -1

        Во-первых, тут речь о браузере, и убивается не "сервис", а лишь вызов функции обработчика событий. Во-вторых, а почему в синхронном коде ошибка приводит к выбросу исключения и завершению программы, а в асинхронном — нет? Это нелогично. В третьих, если у вас веб-сервис, то вы можете ловить эти непойманные исключения на более высоком уровне и показывать страницу 503 вместо того, чтобы завершать процесс сервера. Точно так же, как и в синхронном коде. А с промисами все получается через одно место — исключение ловится там, где оно никому не нужно.


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


        Единственное, что нормализует ситуацию — это async/await. А без них получается ад, все работает нелогично. Вообще, асинхронный код это боль, и если кому-то кажется, что это не так, то скорее всего только потому, что он не видит всех граблей.


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


        Я логику промисов не могу объяснить ничем, кроме фобии исключений. Видел еще объяснение, что логику для JS-промисов просто скопировали из кода на Си++, реализовывашего промисы внутри браузера, а его разработчики, понятно, боятся что их браузер будет падать.


        1. kahi4
          30.11.2019 16:03
          +1

          Во-вторых, а почему в синхронном коде ошибка приводит к выбросу исключения и завершению программы, а в асинхронном — нет?

          У вас какое-то свое представление о промисах.


          Для начала если никто не сделал .catch, то промис завершится с Uncaught (in promise) в глобальном стеке и никуда ошибка не пропадает и нигде не теряется.


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


          У промисов, конечно же, есть недостатки, один из главных — их нельзя отменять, а так же они не таскают с собой "контекст" (это создает проблемы как с адекватным, но не репрезетативным стек трейсом, так и, например, в nest.js нельзя в обработчике получить request из контекста, не передавая его напрямую. Хотя это вроде починили с https://nodejs.org/api/async_hooks.html). Часть из этих проблем решают стримы, но это уже прям большой шаг вперед.


          Но точно у промисов нет проблем с исключениями, если грамотно проектировать систему.


          P.S.


          Во-вторых, а почему в синхронном коде ошибка приводит к выбросу исключения и завершению программы, а в асинхронном — нет?

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


        1. faiwer
          30.11.2019 22:23

          Я логику промисов не могу объяснить ничем, кроме фобии исключений

          А как вы видите это?


          try {
            runSomethingAsync();
          } catch(err) {
            // I caught it
          }

          так? Если да, то у меня для вас плохие новости. Тут проблема уже не в синтаксисе и чувстве прекрасного. Тут дело в том как устроены программы в реальности во время выполнения.


          Единственное, что нормализует ситуацию — это async/await

          async/await это promises


          1. zoryamba
            01.12.2019 00:08
            -1

            try {
              await runSomethingAsync();
            } catch(err) {
              // I caught it
            }
            

            Очевидно же, так. Нет?


            1. faiwer
              01.12.2019 00:20
              -2

              Очевидно же, так. Нет?

              Теперь стало совсем не очевидно. Если runSomethingAsync возвращает promise, то оно уже работает как вы хотите. Вам не обязательно писать catch(callback). В случае ошибки оно само перейдёт в catch-блок try-catch-а. И не важно руками этот promise сделан или посредством async-метода.


              Собственно в чём проблема? :)


              1. zoryamba
                01.12.2019 01:22

                В случае ошибки оно само перейдёт в catch-блок try-catch-а.


                эммм… вообще-то нет… и как я понял, именно об этом Вы говорили в комменте, на который я ответил… я запутался…

                jsfiddle.net/0nakzhqr/3


                1. faiwer
                  01.12.2019 01:38
                  +1

                  Я тоже запутался. Давайте разбираться.


                  const run = () => Promise.reject('something has gone wrong');
                  
                  const asyncFunc = async () => {
                      try {
                          await run();
                      }
                      catch(err) {
                          console.log('I got it', err);
                      }
                  };
                  
                  asyncFunc(); 
                  // "I got it something has gone wrong"
                  // Promise {<resolved>: undefined}

                  Как видите catch поймал reject-ый Promise. Стало быть до тех пор пока await ловит reject-ые Promise ваш catch в async-методе срабатывает на ура. Именно из-за того, что он цепляется к promise.catch (автоматически).


                  Сломается это к примеру тут:


                  const runSomeOldApi = callback => setTimeout(
                          () => callback(null, 'someresult'), 
                          1000
                      );
                  
                  const run = () => new Promise((resolve, reject) => {
                      runSomeOldApi((error, result) => {
                          doSomethingWrongHereThatWillThrowAnError();
                  
                          if (error) reject(error);
                          else resolve(result);
                      })
                  });
                  
                  // остальная часть кода прежняя

                  Теперь мы получим Promise {<pending>} и Uncaught ReferenceError. Причина: мы выполнили это в отрыве от контекста try-catch. В случае работы с async-await либо напрямую с promise-ми (.then + chains of promises) — оно проставляет try-catch за нас. И в итоге случайная ошибка не приводит к повисшим await-ам и необработанным ошибкам.


                  Посему я не понимаю вашего негодования по поводу синтаксиса промисов, и как вы его противопоставляете async-await (хотя это одно и то же + сахар). Тут нет никакой боязни try-catch-ей. Она то тут как раз не причём :)


    1. Igelko
      29.11.2019 14:32
      +2

      Пожалуй соглашусь с тем, что как правило проще написать под себя маленькую обёртку в Promise над уже существующим http клиентом, чем брать библиотеку.
      Но вот про то, что пятисотка от сервера и исключения из-за обращения к неопределенной переменной — это принципиально разное, не соглашусь. И то, и другое — ошибки выполнения, и те и другие нуждаются в обработке.
      Сама идея промисов подразумевает, что у нас есть две цепочки выполнения кода, нормальная и обработки ошибок.
      Это целый подход к потоку выполнения, и он заразен из-за асинхронной природы, т.к. расползается по всему коду — почти все места должны быть готовы к тому, что тебе из функции сразу придёт промис и нужно будет выстраивать цепочку выполнения для обработки результатов.


      Здорово прочищает мозги статья Railway oriented programming, где примеры даны на F#, но во многом перекликаются с нынешней реализацией промисов.


    1. epishman
      29.11.2019 14:45

      Нафига перехватывать обращение к несуществующей переменной и заворачивать в промис? Это недостаток в дизайне промисов. Нужно не перехватывать такие ошибки, а завершать программу.

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


      1. mayorovp
        29.11.2019 16:12
        -1

        Теоретически, промисы-фьючи могут работать и в многопоточке

        Таки не могут.


        1. epishman
          30.11.2019 21:58
          +1

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


    1. kahi4
      29.11.2019 19:11
      +8

      Да, все писали обертку в 10 строчек. А потом понадобилось добавить обработку форм, потом картинок, данных, потом пред-обработчики, пост-обработчики, установка хедеров и так далее. В итоге получили свой axios, только хуже оттестированный.


      Ловить надо только то, что явно запрошено пользователем, и не перехватывать остальные типы исключений

      В js нет разницы между пользовательскими ошибками и программными. С чего механизм отлова ошибок в одном месте должен в таком случае отличаться от других? А ещё, интересно, в каком контексте по-вашему должны сыпаться ошибки вроде обращения к необъявленной переменной если я хочу обернуть ее и показывать пользователю хотя бы что-то вместо белого экрана? А ещё как разделить? Нет доступа к сети к чему относится? А закончилась память? А ublock заблокировал запрос?


      PS для fetch никакой статус ответа не является ошибкой и это, в общем случае, правильно. Но не удобно.


      1. faiwer
        30.11.2019 22:37

        Ну если общение с backend-ом сводится к тому чтобы гонять туда сюда JSON-ики и formData в одном и том же стиле и формате, то необходимость подключать axios так и не приходит. К тому же в любом большом приложении должна быть прослойка для работы с API, чтобы лишняя фигня вроде "установки хедеров" и "пост-обработчики" и "пред-обработчики" не пролазили в код. А написаны они поверх axios или поверх fetch, для большинства проектов, момент не принципиальный.


        Я думаю axios всё таки больше актуален для шибко мутных проектов, в частности когда backend это сплошное слоёное legacy.


  1. akass
    29.11.2019 14:30
    +5

    Ну Axios всё таки имеет куда больше функционала. Многим нужны инстансы клиентов и интерсепторы.


  1. AriesUa
    29.11.2019 14:48
    +3

    Много лет использовал axios как основную библиотеку для запросов из браузера. Но в последнем проекте решил, что тянуть огромную либу в проект только для запросов не разумно.

    В итоге написал обертку class API над fetch. Все красиво завернул в async/await, плюс обработка ответа и перехват ошибок. В итоге вышло чуть более 100 строк кода на TS. Что совсем ничего, по сравнении в кодом axios.

    Так что думаю, имеет смысл переходить на нативный функционал. Да, пришлось немного допилить, что бы вот совсем хорошо было. Но доработка минимальна.

    PS Что касается загрузки файлов и показа прогресса. В данном кейсе пришлось написать обортку над XMLHttpRequest. Увы…


  1. GCU
    29.11.2019 15:01

    Спасибо за статью, хоть узнал что такое Axios.
    A то всё по-старинке через XMLHttpRequest с колбеками :)
    Недавно попробовал fetch — вроде работает отлично.
    О том, что есть ещё какие-то другие способы даже не знал.


  1. EaGames
    29.11.2019 15:37

    «Но что если я скажу вам, что Axios — это мёртвый проект.»

    скрин
    image


  1. valis
    29.11.2019 15:44

    Спасибо, не знал о fetch.
    Axios хотя и выглядит секси, но честно как-то жаба давит каждый раз подключая лищьнюю либу.
    Хотя стоп. Не все так однозначно — теперь чтобы замокать fetch в тестах я не смогу юзать jest.mock, выход ребята нашли но опять же не сильно красивый


    1. faiwer
      30.11.2019 22:51
      +1

      теперь чтобы замокать fetch в тестах

      А вы мокайте не fetch а сами api-методы. В идеале fetch должен использоваться только в файлах, которые содержат инструментарий для взаимодействия с сервером. Пред и постобработка запросов, склеивание URL, возможно кеширование и многое другое. Это должно быть отдельной прослойкой в приложении (во всяком случае если оно большое). А в сам код импортируется уже готовый к использованию promise-based метод. Вот его и mock-ать :)


  1. mikhailian
    29.11.2019 16:12

    Не статья, а запоздалый FUD. Вот последние коммиты в axios. Всё живенько.


  1. Senyaak
    29.11.2019 16:21
    +1

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

    commented on Apr 15, 2018
    merged 2 commits into axios:master from unknown repository on May 7
    очень уж преувеличено 2 года -> 1 :)


  1. Fragster
    29.11.2019 18:30

    Axios хорош из-за того, что при ssr какого-нибудь Nuxt.js не надо делить код на два варианта — для сервера и для клиента.


    1. Azirel
      01.12.2019 00:09
      +1

      есть же isomorphic-fetch, например


  1. Rasalom
    29.11.2019 19:31

    В 2019 году вместо Promise стоит использовать async/await, если уж на то пошло
    fetch не позволяет отслеживать прогресс запроса(аплоад\даунлоад файла), не знаю как с этим у axios, но думаю что всё хорошо. Хотя всё равно каждый раз пишу свою обёртку над нативными функциями, пока что так и не столкнулся с необходимостью использовать axios.

    сравнения тоже довольно странные:

    fetch("https://jsonplaceholder.typicode.com/posts", {
      method: "POST",
      body: JSON.stringify({
        title: "Title of post",
        body: "Post Body"
      })
    })
      .then(res => {
        if (!response.ok) throw Error(response.statusText);
        return response.json();
      })
    


    ничего не стоит обернуть это в функцию, не дублировать код и получить то же самое что и с axios.


  1. mm3
    29.11.2019 22:18

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


  1. rpiontik
    30.11.2019 00:15

    Удалено.


  1. evil_random
    30.11.2019 03:34

    Какая-то воистину хейтерская статья. Axios лучший на данный момент.
    Axios убьёт только Axios2. Который под капотом будет использовать fetch. Не удивлюсь если команда Axios нечто подобное уже пилит.


    1. xobotyi
      30.11.2019 20:38

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


      1. monochromer
        30.11.2019 22:02
        +1

        и не может быть отменен

        Для этих целей существует Abort Controller, который, к слову, предназначен для отмены любых асинхронных операций. Есть полифилл.


  1. kahi4
    30.11.2019 16:12

    Лучшее, что я видел, это rxjs ajax. Ну или еще лучше — ангуларовская (2+) обертка $http. Но стримы — то еще зло, поэтому в для небольших проектов, конечно, не подходит.


    1. NerVik
      30.11.2019 19:50
      +1

      могу ошибаться, но Axios это отделившийся от Ангуляра $http


      1. kahi4
        30.11.2019 21:26

        От первого? Просто начиная со второго (который я и имел ввиду), $http завязан на rxjs, а аксиос на промисах работает.


        1. NerVik
          01.12.2019 09:46

          если сравнивать доки, то скорее от первого. прям очень сильно похожи.


  1. xobotyi
    30.11.2019 20:33

    deleted


  1. spaceatmoon
    01.12.2019 00:09

    Axios подкупает своим «Axios.get» и простым дизайном функции, а не вот этими трешовыми конфигами. Не помню точно, но вот с fetch какие-то мелкие косяки всё время убивали желание использовать его, т.к. по сути ничего толком от xmlhttprequest не изменилось. Так зачем тогда его использовать?


    1. faiwer
      01.12.2019 00:22

      Так зачем тогда его использовать?

      У fetch главное преимущество в том, что он весит 0 байт (без полифила). Если это для вашего проекта не аргумент, то да, вам fetch может быть не нужен. Просто очередное API, которое не подошло.


      1. spaceatmoon
        01.12.2019 15:50

        Весит 13КБ, что вообще очень мало. Можно и пренебречь. Больше расстраивает когда тащится фреймворк в 3МБ(Framework7), что дико напрягает, вот его желательно ручками бы сократить.


  1. Zmeevsky
    01.12.2019 00:09

    Есть удобные легковесные альтернативы, на которые стоит обратить внимание — для сервера и для браузера.