Когда Fetch API стал стандартом, я был в восторге. Мне больше не нужно будет использовать библиотеки утилит http для выполнения вызовов http в моих приложениях. XMLHttpRequest был настолько низким и неудобным (вплоть до его непоследовательной верблюжьей оболочки аббревиатур!). У вас действительно не было выбора, кроме как обернуть его чем-то более удобным или выбрать одну из множества альтернатив с открытым исходным кодом, таких как jQuery $ .ajax (), Angular $ http, superagent и мой личный фаворит аксиос . Но действительно ли мы свободны от http-инструментов?

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

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

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

Когда вы смотрите на fetch базовые примеры, они выглядят очень привлекательно и очень похожи на другие утилиты http, к которым мы привыкли. Давайте посмотрим на тривиальный пример. Вместо того, чтобы делать:

axios.get(url)
  .then(result => console.log('success:', result))
  .catch(error => console.log('error:', error));

Вы бы сделали:

fetch(url).then(response => response.json())
  .then(result => console.log('success:', result))
  .catch(error => console.log('error:', error));

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

Что является большим делом и что, я уверен, многие читатели упустили в приведенном выше примере (как и я, когда я впервые использовал выборку), так это то, что на самом деле два приведенных выше фрагмента кода делают не делать то же самое. Все упомянутые выше инструменты http (серьезно, каждый из них) рассматривают ответ с ошибкой от сервера (то есть статус 404, 500 и т. Д.) Как ошибку. Однако выборка (аналогично XMLHttpRequest) отклоняет обещание только в случае сетевой ошибки (т.е. адрес не может быть разрешен, сервер недоступен или CORS не разрешен).

Это означает, что когда сервер вернет 404, мы выведем на консоль «успех». Если мы хотим сделать что-то более интуитивное для разработчика приложения и вернуть отказ от обещания в случае, если сервер ответил с ошибкой, нам нужно будет сделать что-то вроде этого:

fetch(url)
  .then(response => {
    return response.json().then(data => {
      if (response.ok) {
        return data;
      } else {
        return Promise.reject({status: response.status, data});
      }
    });
  })
  .then(result => console.log('success:', result))
  .catch(error => console.log('error:', error));

Я уверен, что многие люди могут сказать: «В чем проблема? вы запросили данные с сервера и получили их. Так что, если сервер ответил статусом 404, это все равно ответ сервера ». И они будут правы. Это только вопрос перспективы. На мой взгляд, для разработчика приложения ответ сервера об ошибке почти всегда считается исключением и должен рассматриваться так же, как сбой сети. Чтобы исправить такое поведение, мы не можем просто изменить стандартное поведение fetch. Нам просто нужна лучшая абстракция, подходящая для разработчиков приложений.

POST-запросы к серверу

Другой очень распространенный вариант использования для разработчика приложений - это отправка запроса POST на сервер. С помощью такого инструментария http, как axios, вы можете сделать что-то вроде этого:

axios.post('/user', {
  firstName: 'Fred',
  lastName: 'Flintstone'
});

Когда я впервые начал использовать Fetch API, я был настроен оптимистично. Я подумал: парень, этот новый API так похож на то, к чему я привык. К сожалению, я потратил почти целый час на отправку запроса POST на мой сервер, потому что это не сработало:

fetch('/user', {
  method: 'POST',
  body: {
    firstName: 'Fred',
    lastName: 'Flintstone'
  }
});

Что мне нужно было усвоить на собственном горьком опыте, как и многим другим разработчикам, я уверен, что fetch, будучи низкоуровневым API, не дает вам ярлыков для решения таких распространенных вариантов использования, как этот. API Fetch очень ясен. JSON должен быть преобразован в строку, а заголовок Content-Type должен указывать, что полезная нагрузка является JSON, в противном случае сервер будет рассматривать ее как строку. На самом деле нам следовало сделать следующее:

fetch('/user', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    firstName: 'Fred',
    lastName: 'Flintstone'
  })
});

Что ж, это становится слишком много для меня, чтобы повторять в каждом вызове API, который я пишу. И это еще не все!

По умолчанию

Как вы уже заметили, fetch - это очень явный API, вы ничего не получите, если не попросите об этом. Так, например, ни один из описанных выше вызовов fetch не будет работать на моем сервере, потому что:

  1. Мой сервер использует аутентификацию на основе файлов cookie, и fetch по умолчанию не отправляет файлы cookie.
  2. Мой сервер должен знать, что клиент сможет обрабатывать ответ в формате JSON.
  3. Мой сервер находится в другом субдомене, и CORS по умолчанию отключен в fetch.
  4. Чтобы заблокировать атаки XSRF, мой сервер требует, чтобы каждый вызов API сопровождался настраиваемым заголовком X-XSRF-TOKEN, который доказывает, что вызов API выполняется со страницы моего приложения.

Итак, что я должен делать на самом деле:

fetch(url, {
  credentials: 'include',
  mode: 'cors',
  headers: {
    'Accept': 'application/json',
    'X-XSRF-TOKEN': getCookieValue('XSRF-TOKEN')
  }
});

Совершенно нормально, что fetch не выполняет все эти действия по умолчанию, но если я собираюсь использовать fetch во всем своем приложении для выполнения вызовов API, мне нужен способ изменить эти значения по умолчанию на то, что имеет смысл в моем приложении. К сожалению, fetch не дает мне никакого механизма для отмены значений по умолчанию. Вы уже догадались, кто это делает:

axios.defaults.baseURL = 'https://api.example.com';
axios.defaults.headers.common['Accept'] = 'application/json';
axios.defaults.headers.post['Content-Type'] = 'application/json';

Но это просто для галочки, потому что на самом деле все, что упомянуто выше, включая защиту XSRF, axios уже дает мне по умолчанию. Цель axios - предоставить простой в использовании инструмент для выполнения вызовов API к вашему серверу. Цель fetch намного шире, чем то, для чего я его использую, поэтому это не лучший инструмент для работы.

Заключение

Утверждение, что вам не нужен набор инструментов http, означает, что вместо одной строки:

function addUser(details) {
  return axios.post('https://api.example.com/user', details);
}

На самом деле вы собираетесь сделать следующее:

function addUser(details) {
  return fetch('https://api.example.com/user', {
    mode: 'cors',
    method: 'POST',
    credentials: 'include',
    body: JSON.stringify(details),
    headers: {
      'Content-Type': 'application/json',
      'Accept': 'application/json',
      'X-XSRF-TOKEN': getCookieValue('XSRF-TOKEN')
    }
  }).then(response => {
    return response.json().then(data => {
      if (response.ok) {
        return data;
      } else {
        return Promise.reject({status: response.status, data});
      }
    });
  });
}

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

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

Вы даже можете неплохо поработать. Но все, что вы сделали, это создали еще один набор инструментов http, и в конце концов вы использовали его в своих проектах вместо того, чтобы использовать новый горячий API Fetch. Возможно, вам лучше сэкономить время и силы и npm install --save axios (или аналогичный набор инструментов по вашему выбору).

И действительно, как вы думаете, имеет ли значение, использует ли этот http-инструментарий fetch внутренне или XMLHttpRequest?

P.S.

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