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) + "%"); };
Обработчику события передается объект с тремя свойствами:
lengthComputable
- установитеtrue
, если прогресс можно рассчитатьtotal
- общий объем работы - илиContent-Length
- тела сообщения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
остается необходимым:
- Вы поддерживаете очень старые браузеры — это требование со временем должно снизиться.
- Вам нужно показать индикаторы выполнения загрузки.
Fetch
в конечном итоге получит поддержку, но это может произойти через несколько лет.
Обе альтернативы интересны, и их стоит знать подробнее!
Первоначально опубликовано на https://blog.openreplay.com 16 апреля 2022 г.