Spec-Zone.ru › Web APIs

Использование API Fetch

API Fetch предоставляет JavaScript-интерфейс для выполнения HTTP-запросов и обработки ответов.

Fetch — это современная замена для XMLHttpRequest: в отличие от XMLHttpRequest, использующего обратные вызовы, Fetch основан на промисах и интегрирован с современными веб-функциями, такими как сервисные рабочие потоки и Cross-Origin Resource Sharing (CORS).

С помощью API Fetch вы выполняете запрос, вызывая fetch(), который доступен как глобальная функция в контекстах как window, так и worker. Вы передаёте ему объект Request или строку с URL для извлечения, а также необязательный аргумент для настройки запроса.

Функция fetch() возвращает Promise, которая выполняется с объектом Response, представляющим ответ сервера. Затем вы можете проверить статус запроса и извлечь содержимое ответа в различных форматах, включая текст и JSON, вызвав соответствующий метод ответа.

Вот минимальная функция, которая использует fetch() для получения JSON-данных с сервера:

async function getData() {
  const url = "https://example.org/products.json";
  try {
    const response = await fetch(url);
    if (!response.ok) {
      throw new Error(`Response status: ${response.status}`);
    }

    const json = await response.json();
    console.log(json);
  } catch (error) {
    console.error(error.message);
  }
}

Мы объявляем строку с URL и затем вызываем fetch(), передавая URL без дополнительных опций.

Функция fetch() будет отклонять промис при некоторых ошибках, но не при ответе сервера со статусом ошибки, например, 404: поэтому мы также проверяем статус ответа и выбрасываем исключение, если он не OK.

В противном случае, мы извлекаем содержимое тела ответа в формате JSON, вызывая метод json() объекта Response, и выводим одно из его значений. Обратите внимание, что, как и сам fetch(), json() является асинхронным, как и все остальные методы доступа к содержимому тела ответа.

В остальной части этой страницы мы более подробно рассмотрим различные этапы этого процесса.

Выполнение запроса

Для выполнения запроса вызовите fetch(), передав:

  1. определение ресурса для извлечения. Это может быть любое из следующих:
    • строка, содержащая URL
    • объект, например, экземпляр URL, который имеет стринг-генератор, создающий строку с URL
    • экземпляр Request
  2. необязательно, объект, содержащий параметры настройки запроса.

В этом разделе мы рассмотрим некоторые из наиболее часто используемых опций. Чтобы узнать обо всех доступных опциях, см. страницу справки fetch().

Установка метода

По умолчанию fetch() выполняет запрос GET, но вы можете использовать параметр method для использования другого метода запроса:

const response = await fetch("https://example.org/post", {
  method: "POST",
  // ...
});

Если параметр mode установлен в no-cors, то method должен быть одним из GET, POST или HEAD.

Установка тела

Тело запроса — это полезная нагрузка запроса: это то, что клиент отправляет серверу. Вы не можете включить тело в запросах GET, но это полезно для запросов, которые отправляют содержимое на сервер, такие как POST или PUT запросы. Например, если вы хотите загрузить файл на сервер, вы можете выполнить запрос POST и включить файл в качестве тела запроса.

Для установки тела запроса передайте его как параметр body:

const response = await fetch("https://example.org/post", {
  body: JSON.stringify({ username: "example" }),
  // ...
});

Вы можете указать тело как любой из следующих типов:

  • строка
  • ArrayBuffer
  • TypedArray
  • DataView
  • Blob
  • File
  • URLSearchParams
  • FormData
  • ReadableStream

Обратите внимание, что, как и тела ответов, тела запросов являются потоками, и выполнение запроса считывает поток, поэтому, если запрос содержит тело, вы не можете выполнить его дважды:

const request = new Request("https://example.org/post", {
  method: "POST",
  body: JSON.stringify({ username: "example" }),
});

const response1 = await fetch(request);
console.log(response1.status);

// Will throw: "Body has already been consumed."
const response2 = await fetch(request);
console.log(response2.status);

Вместо этого вам нужно создать копию запроса перед его отправкой:

const request1 = new Request("https://example.org/post", {
  method: "POST",
  body: JSON.stringify({ username: "example" }),
});

const request2 = request1.clone();

const response1 = await fetch(request1);
console.log(response1.status);

const response2 = await fetch(request2);
console.log(response2.status);

Дополнительную информацию см. в разделе Заблокированные и нарушенные потоки.

Установка заголовков

Заголовки запроса предоставляют серверу информацию о запросе: например, заголовок Content-Type сообщает серверу о формате тела запроса.

Для установки заголовков запроса назначьте их параметру headers.

Вы можете передать здесь объект с литералом, содержащим свойства header-name: header-value:

const response = await fetch("https://example.org/post", {
  headers: {
    "Content-Type": "application/json",
  },
  // ...
});

Кроме того, вы можете создать объект Headers, добавить заголовки в этот объект с помощью Headers.append(), а затем назначить объект Headers параметру headers:

const myHeaders = new Headers();
myHeaders.append("Content-Type", "application/json");

const response = await fetch("https://example.org/post", {
  headers: myHeaders,
  // ...
});

Многие заголовки устанавливаются браузером автоматически и не могут быть установлены скриптом: они называются запрещенными именами заголовков. Если параметр mode установлен в no-cors, то набор разрешенных заголовков дополнительно ограничен.

Выполнение запросов POST

Мы можем объединить параметры method, body, и headers для выполнения запроса POST:

const myHeaders = new Headers();
myHeaders.append("Content-Type", "application/json");

const response = await fetch("https://example.org/post", {
  method: "POST",
  body: JSON.stringify({ username: "example" }),
  headers: myHeaders,
});

Выполнение запросов к другому домену

Возможность выполнения запроса к другому домену определяется значением параметра RequestInit.mode. Оно может принимать одно из трех значений: cors, same-origin, или no-cors.

  • Для запросов fetch значение mode по умолчанию — cors, что означает, что если запрос выполняется к другому домену, то используется механизм Cross-Origin Resource Sharing (CORS). Это означает, что:

    • если запрос является простым запросом, то запрос всегда будет отправлен, но сервер должен ответить с правильным заголовком Access-Control-Allow-Origin, иначе браузер не поделится ответом с вызывающим элементом.
    • если запрос не является простым, то браузер отправит предварительный запрос для проверки того, что сервер понимает CORS и разрешает запрос, а реальный запрос не будет отправлен, если сервер не ответит на предварительный запрос с соответствующими заголовками CORS.
  • Установка mode в same-origin полностью запрещает запросы к другим доменам.

  • Установка mode в no-cors отключает CORS для запросов к другим доменам. Это ограничивает заголовки, которые можно задать, и ограничивает методы GET, HEAD и POST. Ответ непрозрачный, что означает, что его заголовки и тело недоступны JavaScript. В большинстве случаев веб-сайт не должен использовать no-cors: основное его применение — для определенных сценариев использования сервисных рабочих потоков.

См. документацию по ссылке RequestInit.mode для получения дополнительной информации.

Включение учетных данных

Данные аутентификации могут быть файлами cookie, клиентом TLS, или заголовками аутентификации, содержащими имя пользователя и пароль.

Чтобы контролировать отправку данных аутентификации браузером, а также учитывать заголовки ответов Set-Cookie, установите опцию credentials, которая может принимать одно из следующих трёх значений:

  • omit: никогда не отправлять данные аутентификации в запросе или включать их в ответе.
  • same-origin (по умолчанию): отправлять и включать данные аутентификации только для запросов с тем же происхождением.
  • include: всегда включать данные аутентификации, даже для запросов с другим происхождением.

Обратите внимание, что если атрибут SameSite файла cookie установлен на Strict или Lax, то cookie не будет отправлен в запросе с другим происхождением, даже если credentials установлено на include.

Включение данных аутентификации в запросах с другим происхождением может сделать сайт уязвимым для атак CSRF, поэтому даже если credentials установлено на include, сервер также должен разрешить их включение, добавив заголовок Access-Control-Allow-Credentials в свой ответ. Кроме того, в этом случае сервер должен явно указать происхождение клиента в заголовке ответа Access-Control-Allow-Origin (то есть * недопустимо).

Это означает, что если credentials установлено на include и запрос имеет другое происхождение, то:

  • Если запрос является простым запросом, то запрос будет отправлен с данными аутентификации, но сервер должен установить заголовки ответа Access-Control-Allow-Credentials и Access-Control-Allow-Origin, иначе браузер вернёт ошибку сети вызывающему коду. Если сервер установил правильные заголовки, ответ, включая данные аутентификации, будет передан вызывающему коду.

  • Если запрос не является простым, то браузер отправит предварительный запрос без данных аутентификации, и сервер должен установить заголовки ответа Access-Control-Allow-Credentials и Access-Control-Allow-Origin, иначе браузер вернёт ошибку сети вызывающему коду. Если сервер установит правильные заголовки, браузер отправит действительный запрос, включая данные аутентификации, и передаст действительный ответ, включая данные аутентификации, вызывающему коду.

Создание объекта Request

Конструктор Request() принимает те же аргументы, что и сам fetch(). Это означает, что вместо передачи опций в fetch(), вы можете передать те же опции в конструктор Request(), а затем передать этот объект в fetch().

Например, мы можем сделать запрос POST, передав опции в fetch() с помощью такого кода:

const myHeaders = new Headers();
myHeaders.append("Content-Type", "application/json");

const response = await fetch("https://example.org/post", {
  method: "POST",
  body: JSON.stringify({ username: "example" }),
  headers: myHeaders,
});

Однако, мы можем переписать это, передав те же аргументы в конструктор Request():

const myHeaders = new Headers();
myHeaders.append("Content-Type", "application/json");

const myRequest = new Request("https://example.org/post", {
  method: "POST",
  body: JSON.stringify({ username: "example" }),
  headers: myHeaders,
});

const response = await fetch(myRequest);

Это также означает, что вы можете создать запрос из другого запроса, изменив некоторые из его свойств, используя второй аргумент:

async function post(request) {
  try {
    const response = await fetch(request);
    const result = await response.json();
    console.log("Success:", result);
  } catch (error) {
    console.error("Error:", error);
  }
}

const request1 = new Request("https://example.org/post", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ username: "example1" }),
});

const request2 = new Request(request1, {
  body: JSON.stringify({ username: "example2" }),
});

post(request1);
post(request2);

Отмена запроса

Чтобы сделать запрос отменяемым, создайте AbortController и назначьте его AbortSignal свойству signal запроса.

Для отмены запроса вызовите метод контроллера abort(). Вызов fetch() отклонит промис с исключением AbortError.

const controller = new AbortController();

const fetchButton = document.querySelector("#fetch");
fetchButton.addEventListener("click", async () => {
  try {
    console.log("Starting fetch");
    const response = await fetch("https://example.org/get", {
      signal: controller.signal,
    });
    console.log(`Response: ${response.status}`);
  } catch (e) {
    console.error(`Error: ${e}`);
  }
});

const cancelButton = document.querySelector("#cancel");
cancelButton.addEventListener("click", () => {
  controller.abort();
  console.log("Canceled fetch");
});

Если запрос прерван после того, как вызов fetch() был выполнен, но до того, как тело ответа было прочитано, попытка прочитать тело ответа отклонит промис с исключением AbortError.

async function get() {
  const controller = new AbortController();
  const request = new Request("https://example.org/get", {
    signal: controller.signal,
  });

  const response = await fetch(request);
  controller.abort();
  // The next line will throw `AbortError`
  const text = await response.text();
  console.log(text);
}

Обработка ответа

Как только браузер получит код состояния и заголовки ответа от сервера (и потенциально до получения самого тела ответа), промис, возвращаемый fetch(), выполнится с объектом Response.

Проверка статуса ответа

Промис, возвращаемый fetch(), будет отклоняться при некоторых ошибках, таких как ошибка сети или неверный протокол. Однако, если сервер отвечает ошибкой, например, 404, то fetch() выполнится с объектом Response, поэтому необходимо проверить статус перед чтением тела ответа.

Свойство Response.status содержит числовой код состояния, а свойство Response.ok возвращает true если код состояния находится в диапазоне 200.

Обычно проверяют значение ok и выбрасывают исключение, если оно false:

async function getData() {
  const url = "https://example.org/products.json";
  try {
    const response = await fetch(url);
    if (!response.ok) {
      throw new Error(`Response status: ${response.status}`);
    }
    // ...
  } catch (error) {
    console.error(error.message);
  }
}

Проверка типа ответа

Ответы имеют свойство type, которое может принимать следующие значения:

  • basic: запрос был запросом с тем же происхождением.
  • cors: запрос был запросом с другим происхождением CORS.
  • opaque: запрос был простым запросом с другим происхождением, сделанным с режимом no-cors.
  • opaqueredirect: запрос установил опцию redirect на значение manual, и сервер вернул код состояния перенаправления.

Тип определяет возможные содержимое ответа следующим образом:

  • Базовые ответы исключают заголовки ответа из списка запрещённых заголовков.

  • Ответы CORS включают только заголовки ответа из списка разрешённых заголовков CORS.

  • Ответы opaque и opaque перенаправления имеют status значение 0, пустой список заголовков и null тело.

Проверка заголовков

Так же как и в запросе, у ответа есть свойство headers, которое является объектом Headers, и оно содержит любые заголовки ответа, доступные скриптам, с учётом исключений, основанных на типе ответа.

Распространённый случай использования — это проверка типа содержимого перед чтением тела:

async function fetchJSON(request) {
  try {
    const response = await fetch(request);
    const contentType = response.headers.get("content-type");
    if (!contentType || !contentType.includes("application/json")) {
      throw new TypeError("Oops, we haven't got JSON!");
    }
    // Otherwise, we can read the body as JSON
  } catch (error) {
    console.error("Error:", error);
  }
}

Чтение тела ответа

Интерфейс Response предоставляет ряд методов для извлечения всего содержимого тела в различных форматах:

  • Response.arrayBuffer()
  • Response.blob()
  • Response.formData()
  • Response.json()
  • Response.text()

Все эти методы асинхронные, возвращая Promise, который выполнится с содержимым тела.

В этом примере мы загружаем изображение и читаем его как Blob, который мы затем можем использовать для создания URL объекта:

const image = document.querySelector("img");

const url = "flowers.jpg";

async function setImage() {
  try {
    const response = await fetch(url);
    if (!response.ok) {
      throw new Error(`Response status: ${response.status}`);
    }
    const blob = await response.blob();
    const objectURL = URL.createObjectURL(blob);
    image.src = objectURL;
  } catch (e) {
    console.error(e);
  }
}

Метод выбросит исключение, если тело ответа не в правильном формате: например, если вы вызовете json() на ответе, который не может быть проанализирован как JSON.

Потоковая обработка тела ответа

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

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

Например, рассмотрим запрос GET для получения большого текстового файла и его обработки каким-либо образом или отображения пользователю:

const url = "https://www.example.org/a-large-file.txt";

async function fetchText(url) {
  try {
    const response = await fetch(url);
    if (!response.ok) {
      throw new Error(`Response status: ${response.status}`);
    }

    const text = await response.text();
    console.log(text);
  } catch (e) {
    console.error(e);
  }
}

Если мы используем Response.text(), как выше, мы должны подождать, пока весь файл не будет получен, прежде чем мы сможем обработать его.

Если мы вместо этого обрабатываем ответ потоком, мы можем обрабатывать части тела по мере их получения из сети:

const url = "https://www.example.org/a-large-file.txt";

async function fetchTextAsStream(url) {
  try {
    const response = await fetch(url);
    if (!response.ok) {
      throw new Error(`Response status: ${response.status}`);
    }

    const stream = response.body.pipeThrough(new TextDecoderStream());
    for await (const value of stream) {
      console.log(value);
    }
  } catch (e) {
    console.error(e);
  }
}

В этом примере мы асинхронно итерируем по потоку, обрабатывая каждый фрагмент по мере его поступления.

Обратите внимание, что при прямом доступе к телу таким образом вы получаете исходные байты ответа и должны преобразовать их самостоятельно. В данном случае мы используем ReadableStream.pipeThrough() для передачи ответа через TextDecoderStream, который декодирует закодированные в UTF-8 данные тела как текст.

Обработка текстового файла построчно

В примере ниже мы получаем текстовый ресурс и обрабатываем его построчно, используя регулярное выражение для поиска символов окончания строки. Для простоты мы предполагаем, что текст закодирован в UTF-8 и не обрабатываем ошибки получения:

async function* makeTextFileLineIterator(fileURL) {
  const response = await fetch(fileURL);
  const reader = response.body.pipeThrough(new TextDecoderStream()).getReader();

  let { value: chunk, done: readerDone } = await reader.read();
  chunk = chunk || "";

  const newline = /\r?\n/gm;
  let startIndex = 0;

  while (true) {
    const result = newline.exec(chunk);
    if (!result) {
      if (readerDone) break;
      const remainder = chunk.substr(startIndex);
      ({ value: chunk, done: readerDone } = await reader.read());
      chunk = remainder + (chunk || "");
      startIndex = newline.lastIndex = 0;
      continue;
    }
    yield chunk.substring(startIndex, result.index);
    startIndex = newline.lastIndex;
  }

  if (startIndex < chunk.length) {
    // Last line didn't end in a newline char
    yield chunk.substring(startIndex);
  }
}

async function run(urlOfFile) {
  for await (const line of makeTextFileLineIterator(urlOfFile)) {
    processLine(line);
  }
}

function processLine(line) {
  console.log(line);
}

run("https://www.example.org/a-large-file.txt");

Заблокированные и изменённые потоки

Последствия того, что тела запроса и ответа являются потоками, заключаются в следующем:

  • если к потоку прикреплён читатель с помощью ReadableStream.getReader(), то поток заблокирован, и ничего другого не может прочитать поток.
  • если из потока прочитано какое-либо содержимое, то поток изменён, и ничего другого не может прочитать поток.

Это означает, что невозможно прочитать одно и то же тело ответа (или запроса) более одного раза:

async function getData() {
  const url = "https://example.org/products.json";
  try {
    const response = await fetch(url);
    if (!response.ok) {
      throw new Error(`Response status: ${response.status}`);
    }

    const json1 = await response.json();
    const json2 = await response.json(); // will throw
  } catch (error) {
    console.error(error.message);
  }
}

Если вам нужно прочитать тело более одного раза, необходимо вызвать Response.clone() перед чтением тела:

async function getData() {
  const url = "https://example.org/products.json";
  try {
    const response1 = await fetch(url);
    if (!response1.ok) {
      throw new Error(`Response status: ${response1.status}`);
    }

    const response2 = response1.clone();

    const json1 = await response1.json();
    const json2 = await response2.json();
  } catch (error) {
    console.error(error.message);
  }
}

Это распространённый шаблон при реализации кэша офлайн с помощью service worker. Service worker хочет вернуть ответ приложению, но также и кэшировать ответ. Поэтому он клонирует ответ, возвращает оригинальный и кэширует клон:

async function cacheFirst(request) {
  const cachedResponse = await caches.match(request);
  if (cachedResponse) {
    return cachedResponse;
  }
  try {
    const networkResponse = await fetch(request);
    if (networkResponse.ok) {
      const cache = await caches.open("MyCache_1");
      cache.put(request, networkResponse.clone());
    }
    return networkResponse;
  } catch (error) {
    return Response.error();
  }
}

self.addEventListener("fetch", (event) => {
  if (precachedResources.includes(url.pathname)) {
    event.respondWith(cacheFirst(event.request));
  }
});

См. также

  • API service worker
  • API потоков
  • CORS
  • HTTP
  • Примеры Fetch на GitHub

© 2005–2024 MDN contributors.
Licensed under the Creative Commons Attribution-ShareAlike License v2.5 or later.
https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API