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

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

В этой статье мы рассмотрим плюсы и минусы древнего XMLHttpRequest и его современного эквивалента Fetch, чтобы решить, какой Ajax API лучше всего подходит для вашего приложения.

XMLHttpRequest

XMLHttpRequest впервые появился как нестандартный компонент ActiveX Internet Explorer 5.0 в 1999 году. Microsoft разработала его для поддержки своей браузерной версии Outlook. XML был самым популярным (или раскрученным) форматом данных в то время, но XMLHttpRequest поддерживал текст и еще не изобретенный JSON.

Джесси Джеймс Гарретт придумал термин «AJAX» в своей статье 2005 года AJAX: новый подход к веб-приложениям. Приложения на основе AJAX, такие как Gmail и Google Maps, уже существовали, но этот термин воодушевил разработчиков и привел к взрыву гладких возможностей Web 2.0.

AJAX — это мнемоника для «асинхронного JavaScript и XML», хотя, строго говоря, разработчикам не нужно было использовать асинхронные методы, JavaScript или XML. Теперь мы используем общий термин «Ajax» для любого процесса на стороне клиента, который извлекает данные с сервера и обновляет DOM, не требуя полного обновления страницы.

XMLHttpRequest поддерживается всеми основными браузерами и стал официальным веб-стандартом в 2006 году. Простой пример, который извлекает данные из конечной точки /service/ вашего домена и отображает результат JSON в консоли в виде текста:

const xhr = new XMLHttpRequest();
xhr.open("GET", "/service");
// state change event
xhr.onreadystatechange = () => {
  // is request complete?
  if (xhr.readyState !== 4) return;
  if (xhr.status === 200) {
    // request successful
    console.log(JSON.parse(xhr.responseText));
  } else {
    // request not successful
    console.log("HTTP error", xhr.status, xhr.statusText);
  }
};
// start request
xhr.send();
  • 0 (uninitialized) — запрос не инициализирован
  • 1 (загрузка) — соединение с сервером установлено
  • 2 (загружено) — запрос получен
  • 3 (интерактивный) — обработка запроса
  • 4 (полный) — запрос выполнен, ответ готов

Немногие функции делают много, пока не будет достигнуто состояние 4.

Принести

— это современный API запросов Ajax на основе Promise, который впервые появился в 2015 году и поддерживается в большинстве браузеров. Он не построен на XMLHttpRequest и предлагает лучшую согласованность с более кратким синтаксисом. Следующая цепочка обещаний функционирует идентично приведенному выше примеру XMLHttpRequest:

fetch("/service", { method: "GET" })
  .then((res) => res.json())
  .then((json) => console.log(json))
  .catch((err) => console.error("error:", err));

Или вы можете использовать async/await:

try {
  const res = await fetch("/service", { method: "GET" }),
    json = await res.json();
  console.log(json);
} catch (err) {
  console.error("error:", err);
}

Fetch чище, проще и регулярно используется в Service Workers.

Повтор сеанса с открытым исходным кодом

OpenReplay — это альтернатива FullStory и LogRocket с открытым исходным кодом. Это дает вам полную наблюдаемость, воспроизводя все, что ваши пользователи делают в вашем приложении, и показывая, как ваш стек ведет себя для каждой проблемы. OpenReplay размещается на собственном сервере для полного контроля над вашими данными.

Удачной отладки, для современных интерфейсных команд — начните бесплатно отслеживать свое веб-приложение.

Раунд 1: Fetch побед

Помимо более чистого и лаконичного синтаксиса, Fetch API предлагает несколько преимуществ по сравнению с устаревшим XMLHttpRequest.

Объекты заголовка, запроса и ответа

В простом примере fetch() выше используется строка для определения URL-адреса конечной точки. Также можно передать настраиваемый Request объект, который предоставляет ряд свойств о вызове:

const request = new Request("/service", { method: "POST" });
console.log(request.url);
console.log(request.method);
console.log(request.credentials);
// FormData representation of body
const fd = await request.formData();
// clone request
const req2 = request.clone();
const res = await fetch(request);

Объект Response предоставляет аналогичный доступ ко всем деталям ответа:

console.log(res.ok); // true/false
console.log(res.status); // HTTP status
console.log(res.url);
const json = await res.json(); // parses body as JSON
const text = await res.text(); // parses body as text
const fd = await res.formData(); // FormData representation of body

Объект Headers предоставляет простой интерфейс для установки заголовков в запросе или проверки заголовков в ответе:

// set request headers
const headers = new Headers();
headers.set("X-Requested-With", "ajax");
headers.append("Content-Type", "text/xml");
const request = new Request("/service", {
  method: "POST",
  headers,
});
const res = await fetch(request);
// examine response headers
console.log(res.headers.get("Content-Type"));

Управление кэшированием

В XMLHttpRequest сложно управлять кэшированием, и вам может понадобиться добавить случайное значение строки запроса, чтобы обойти кеш браузера. Fetch предлагает встроенную поддержку кэширования во втором параметре объекта init:

const res = await fetch("/service", {
  method: "GET",
  cache: "default",
});

cache можно установить на:

  • 'default' - кеш браузера используется, если есть свежее (не просроченное) совпадение. Если нет, браузер делает условный запрос, чтобы проверить, изменился ли ресурс, и при необходимости делает новый запрос.
  • 'no-store' - кеш браузера обойден, и сетевой ответ не обновит его
  • 'reload' - кеш браузера обойден, но ответ сети его обновит
  • 'no-cache' - аналогично 'default', за исключением того, что всегда выполняется условный запрос
  • 'force-cache' - по возможности используется кешированная версия, даже если она устарела
  • 'only-if-cached' — идентично force-cache, за исключением того, что сетевой запрос не выполняется.

Управление CORS

Совместное использование ресурсов между источниками позволяет сценарию на стороне клиента выполнять запрос Ajax к другому домену, если этот сервер разрешает исходный домен в заголовке ответа Access-Control-Allow-Origin. И fetch(), и XMLHttpRequest не будут работать, если это не установлено. Однако Fetch предоставляет свойство mode, для которого можно установить значение 'no-cors' во втором параметре объекта init:

const res = await fetch(
  'https://anotherdomain.com/service', 
  {
    method: 'GET',
    mode: 'no-cors'
  }
);

Это извлекает ответ, который не может быть прочитан, но может использоваться другими API. Например, вы можете использовать Cache API, чтобы сохранить ответ и использовать его позже, возможно, от Service Worker, чтобы вернуть изображение, сценарий или файл CSS.

Контроль учетных данных

XMLHttpRequest всегда отправляет файлы cookie браузера. Fetch API не отправляет файлы cookie, если вы явно не установили свойство credentials во втором параметре объекта init:

const res = await fetch("/service", {
  method: "GET",
  credentials: "same-origin",
});

credentials можно установить на:

  • 'omit' - исключить файлы cookie и записи HTTP-аутентификации (по умолчанию)
  • 'same-origin' — включать учетные данные с запросами на URL-адреса того же происхождения
  • 'include' - включать учетные данные во все запросы

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

Управление переадресацией

По умолчанию fetch() и XMLHttpRequest следуют перенаправлениям сервера. Однако fetch() предоставляет альтернативные варианты во втором параметре объекта init:

const res = await fetch("/service", {
  method: "GET",
  redirect: "follow",
});

redirect можно установить на:

  • 'follow' - следовать всем редиректам (по умолчанию)
  • 'error' - прервать (отклонить) при перенаправлении
  • 'manual' - вернуть ответ для ручной обработки

Потоки данных

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

const response = await fetch("/service"),
  reader = response.body
    .pipeThrough(new TextDecoderStream())
    .getReader();
while (true) {
  const { value, done } = await reader.read();
  if (done) break;
  console.log(value);
}

Поддержка на стороне сервера

Fetch полностью поддерживается в Deno и Node 18. Использование одного и того же API как на сервере, так и на клиенте помогает снизить когнитивные затраты и дает возможность использовать изоморфные библиотеки JavaScript, которые работают где угодно.

Раунд 2: XMLHttpRequest побед

Несмотря на синяк, у XMLHttpRequest есть несколько приемов, чтобы переиграть "Аякс" Fetch().

Поддержка прогресса

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

const xhr = new XMLHttpRequest();
// progress event
xhr.upload.onprogress = (p) => {
  console.log(Math.round((p.loaded / p.total) * 100) + "%");
};

Обработчику события передается объект с тремя свойствами:

  1. lengthComputable - установите true, если прогресс можно рассчитать
  2. total - общий объем работы - или Content-Length - тела сообщения
  3. loaded — объем работы или контента, выполненного на данный момент

Fetch API не предлагает никакого способа отслеживать ход загрузки.

Поддержка таймаута

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

const xhr = new XMLHttpRequest();
xhr.timeout = 5000; // 5-second maximum
xhr.ontimeout = () => console.log("timeout");

Функции-оболочки могут реализовать функцию тайм-аута в fetch():

function fetchTimeout(url, init, timeout = 5000) {
  return new Promise((resolve, reject) => {
    fetch(url, init).then(resolve).catch(reject);
    setTimeout(reject, timeout);
  });
}

В качестве альтернативы вы можете использовать Promise.race():

Promise.race([
  fetch("/service", { method: "GET" }),
  new Promise((resolve) => setTimeout(resolve, 5000)),
]).then((res) => console.log(res));

Ни один из вариантов не прост в использовании, и запрос будет продолжаться в фоновом режиме.

Отменить поддержку

Запрос в полете можно отменить, запустив метод XMLHttpRequest abort(). При необходимости можно подключить обработчик abort:

const xhr = new XMLHttpRequest();
xhr.open("GET", "/service");
xhr.send();
// ...
xhr.onabort = () => console.log("aborted");
xhr.abort();

Вы можете прервать fetch(), но это не так просто и требует AbortController object:

const controller = new AbortController();
fetch("/service", {
  method: "GET",
  signal: controller.signal,
})
  .then((res) => res.json())
  .then((json) => console.log(json))
  .catch((error) => console.error("Error:", error));
// abort request
controller.abort();

Блок catch() выполняется, когда блок fetch() прерывается.

Более очевидные сбои

Когда разработчики впервые используют fetch(), кажется логичным предположить, что ошибка HTTP, такая как 404 Not Found или 500 Internal Server Error, вызовет отклонение промиса и запуск соответствующего блока catch(). Это не так: обещание успешно разрешается при любом ответе. Отклонение возможно только при отсутствии ответа от сети или при отмене запроса.

fetch() Объект ответа предоставляет свойства status и ok, но не всегда очевидно, что их нужно проверять. XMLHttpRequest является более явным, потому что одна функция обратного вызова обрабатывает каждый результат: вы должны увидеть проверку status в каждом примере.

Поддержка браузера

Я надеюсь, вам не нужно поддерживать Internet Explorer или версии браузера до 2015 года, но в этом случае XMLHttpRequest — ваш единственный вариант. XMLHttpRequest тоже стабилен, и API вряд ли будет обновляться. Fetch новее, и в нем отсутствуют несколько ключевых функций: обновления вряд ли сломают код, но вы можете ожидать некоторого обслуживания.

Какой API следует использовать?

Большинство разработчиков обратятся к более современному Fetch API. Он имеет более чистый синтаксис и больше преимуществ по сравнению с XMLHttpRequest. Тем не менее, многие из этих преимуществ имеют нишевые варианты использования, и в большинстве приложений они вам не потребуются. Есть два случая, когда XMLHttpRequest остается необходимым:

  1. Вы поддерживаете очень старые браузеры — это требование со временем должно снизиться.
  2. Вам нужно показать индикаторы выполнения загрузки. Fetch в конечном итоге получит поддержку, но это может произойти через несколько лет.

Обе альтернативы интересны, и их стоит знать подробнее!

Первоначально опубликовано на https://blog.openreplay.com 16 апреля 2022 г.