API-интерфейс Fetch

Эта практическая работа является частью учебного курса «Разработка прогрессивных веб-приложений», разработанного командой Google Developers Training. Вы получите максимальную пользу от этого курса, если будете выполнять практические работы последовательно.

Полную информацию о курсе смотрите в обзоре «Разработка прогрессивных веб-приложений» .

Введение

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

Чему вы научитесь

  • Как использовать Fetch API для запроса ресурсов
  • Как выполнять запросы GET, HEAD и POST с помощью fetch
  • Как читать и настраивать пользовательские заголовки
  • Использование и ограничения CORS

Что вам следует знать

  • Базовый JavaScript и HTML
  • Знакомство с концепцией и базовым синтаксисом ES2015 Promises

Что вам понадобится

  • Компьютер с доступом к терминалу/оболочке
  • Подключение к Интернету
  • Браузер, поддерживающий Fetch
  • Текстовый редактор
  • Узел и npm

Примечание: Хотя API Fetch в настоящее время поддерживается не всеми браузерами , существует полифилл .

Загрузите или клонируйте репозиторий pwa-training-labs с github и установите LTS-версию Node.js , если необходимо.

Откройте командную строку на компьютере. Перейдите в каталог fetch-api-lab/app/ и запустите локальный сервер разработки:

cd fetch-api-lab/app
npm install
node server.js

Вы можете завершить работу сервера в любое время с помощью сочетания Ctrl-c .

Откройте браузер и перейдите по адресу localhost:8081/ . Вы увидите страницу с кнопками для отправки запросов (они пока не будут работать).

Примечание: Отмените регистрацию всех сервис-воркеров и очистите все кэши сервис-воркеров для локального хоста, чтобы они не мешали работе лаборатории. В Chrome DevTools это можно сделать, нажав «Очистить данные сайта» в разделе «Очистить хранилище» на вкладке «Приложение» .

Откройте папку fetch-api-lab/app/ в предпочитаемом вами текстовом редакторе. В папке app/ вы будете создавать лабораторию.

Эта папка содержит:

  • echo-servers/ содержит файлы, которые используются для запуска тестовых серверов
  • examples/ содержит примеры ресурсов, которые мы используем в экспериментах с fetch
  • js/main.js — это основной JavaScript-код приложения, и именно здесь вы будете писать весь свой код.
  • index.html — это главная HTML-страница для нашего образца сайта/приложения.
  • package-lock.json и package.json — это файлы конфигурации для нашего сервера разработки и зависимостей эхо-сервера.
  • server.js — это сервер для разработки узлов

Интерфейс API Fetch относительно прост. В этом разделе объясняется, как написать простой HTTP-запрос с использованием Fetch.

Получить JSON-файл

В js/main.js кнопка приложения «Получить JSON» прикреплена к функции fetchJSON .

Обновите функцию fetchJSON для запроса файла examples/animals.json и регистрации ответа:

function fetchJSON() {
  fetch('examples/animals.json')
    .then(logResult)
    .catch(logError);
}

Сохраните скрипт и обновите страницу. Нажмите кнопку «Fetch JSON» . Консоль должна зарегистрировать ответ на запрос.

Объяснение

Метод fetch принимает в качестве параметра путь к ресурсу, который мы хотим получить, в данном случае examples/animals.json . fetch возвращает обещание, которое разрешается в объект Response . Если обещание разрешается, ответ передаётся в функцию logResult . Если обещание отклоняется, функция catch вступает в действие, и ошибка передаётся в функцию logError .

Объекты ответа представляют собой ответ на запрос. Они содержат тело ответа, а также полезные свойства и методы.

Тестирование недействительных ответов

Проверьте записанный ответ в консоли. Обратите внимание на значения свойств status , url и ok .

Замените ресурс examples/animals.json в fetchJSON на examples/non-existent.json . Обновлённая функция fetchJSON теперь должна выглядеть так:

function fetchJSON() {
  fetch('examples/non-existent.json')
    .then(logResult)
    .catch(logError);
}

Сохраните скрипт и обновите страницу. Нажмите кнопку «Получить JSON» ещё раз, чтобы попытаться получить этот несуществующий ресурс.

Обратите внимание, что выборка выполнена успешно и не привела к срабатыванию блока catch . Теперь найдите свойства status , URL и ok нового ответа.

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

Объяснение

Почему неудавшийся ответ не активировал блок catch ? Это важное замечание для fetch и promises — некорректные ответы (например, 404) всё равно разрешаются! Fetch Promise отклоняется только в том случае, если запрос не удалось выполнить, поэтому всегда необходимо проверять корректность ответа. Мы проверим корректность ответов в следующем разделе.

Для получения дополнительной информации

Проверить достоверность ответа

Нам необходимо обновить наш код, чтобы проверить корректность ответов.

В main.js добавьте функцию проверки ответов:

function validateResponse(response) {
  if (!response.ok) {
    throw Error(response.statusText);
  }
  return response;
}

Затем замените fetchJSON следующим кодом:

function fetchJSON() {
  fetch('examples/non-existent.json')
    .then(validateResponse)
    .then(logResult)
    .catch(logError);
}

Сохраните скрипт и обновите страницу. Нажмите кнопку «Fetch JSON» . Проверьте консоль. Теперь ответ на examples/non-existent.json должен активировать блок catch .

Замените examples/non-existent.json в функции fetchJSON на оригинальный examples/animals.json . Обновлённая функция должна выглядеть так:

function fetchJSON() {
  fetch('examples/animals.json')
    .then(validateResponse)
    .then(logResult)
    .catch(logError);
}

Сохраните скрипт и обновите страницу. Нажмите «Получить JSON» . Вы должны увидеть, что ответ успешно регистрируется, как и прежде.

Объяснение

Теперь, когда мы добавили проверку validateResponse , некорректные ответы (например, 404) вызывают ошибку, и функция catch берёт на себя управление. Это позволяет нам обрабатывать ошибочные ответы и предотвращает распространение непредвиденных ответов по цепочке выборки.

Прочитать ответ

Ответы на запросы представлены в виде потоков ReadableStreams ( спецификация потоков ) и должны быть прочитаны для доступа к телу ответа. Объекты ответа имеют методы для этого.

В main.js добавьте функцию readResponseAsJSON со следующим кодом:

function readResponseAsJSON(response) {
  return response.json();
}

Затем замените функцию fetchJSON следующим кодом:

function fetchJSON() {
  fetch('examples/animals.json') // 1
  .then(validateResponse) // 2
  .then(readResponseAsJSON) // 3
  .then(logResult) // 4
  .catch(logError);
}

Сохраните скрипт и обновите страницу. Нажмите «Fetch JSON» . Проверьте консоль, чтобы убедиться, что JSON из examples/animals.json регистрируется (вместо объекта Response).

Объяснение

Давайте рассмотрим, что происходит.

Шаг 1. Функция Fetch вызывается для ресурса examples/animals.json . Fetch возвращает обещание, которое разрешается в объект Response. После разрешения обещания объект ответа передаётся в validateResponse .

Шаг 2. validateResponse проверяет, является ли ответ допустимым (это код 200?). Если ответ недопустим, выдаётся ошибка, которая пропускает оставшиеся блоки then и запускает блок catch . Это особенно важно. Без этой проверки некорректные ответы передаются по цепочке и могут нарушить работу последующего кода, который может зависеть от получения допустимого ответа. Если ответ допустим, он передаётся в readResponseAsJSON .

Шаг 3. readResponseAsJSON считывает тело ответа с помощью метода Response.json() . Этот метод возвращает обещание, которое разрешается в JSON. После разрешения этого обещания данные JSON передаются в logResult . (Если обещание из response.json() отклоняется, срабатывает блок catch .)

Шаг 4. Наконец, данные JSON из исходного запроса к examples/animals.json регистрируются с помощью logResult .

Для получения дополнительной информации

Функция Fetch не ограничивается JSON. В этом примере мы извлечём изображение и добавим его на страницу.

В main.js напишите функцию showImage со следующим кодом:

function showImage(responseAsBlob) {
  const container = document.getElementById('img-container');
  const imgElem = document.createElement('img');
  container.appendChild(imgElem);
  const imgUrl = URL.createObjectURL(responseAsBlob);
  imgElem.src = imgUrl;
}

Затем добавьте функцию readResponseAsBlob , которая считывает ответы как Blob :

function readResponseAsBlob(response) {
  return response.blob();
}

Обновите функцию fetchImage следующим кодом:

function fetchImage() {
  fetch('examples/fetching.jpg')
    .then(validateResponse)
    .then(readResponseAsBlob)
    .then(showImage)
    .catch(logError);
}

Сохраните скрипт и обновите страницу. Нажмите «Принести изображение». Вы увидите на странице очаровательную собаку , приносящую палку (шутка про принос!).

Объяснение

В этом примере загружается изображение examples/fetching.jpg . Как и в предыдущем упражнении, ответ проверяется с помощью validateResponse . Затем ответ считывается как BLOB-объект (а не JSON, как в предыдущем разделе). Создаётся элемент изображения, который добавляется на страницу, а атрибут src изображения задаётся URL-адресом данных, представляющим BLOB-объект.

Примечание: Метод createObjectURL() объекта URL используется для генерации URL-адреса данных, представляющего BLOB-объект. Это важно отметить. Blob-объект нельзя напрямую указать в качестве источника изображения. Blob-объект необходимо преобразовать в URL-адрес данных.

Для получения дополнительной информации

Этот раздел является необязательным заданием.

Обновите функцию fetchText до

  1. получить /examples/words.txt
  2. проверьте ответ с помощью validateResponse
  3. прочитать ответ как текст (подсказка: см. Response.text() )
  4. и отобразить текст на странице

Эту функцию showText можно использовать как вспомогательную для отображения итогового текста:

function showText(responseAsText) {
  const message = document.getElementById('message');
  message.textContent = responseAsText;
}

Сохраните скрипт и обновите страницу. Нажмите «Получить текст» . Если вы правильно реализовали функцию fetchText , вы увидите добавленный текст на странице.

Примечание: Хотя может возникнуть соблазн получить HTML-код и добавить его с помощью атрибута innerHTML , будьте осторожны. Это может сделать ваш сайт уязвимым для атак с использованием межсайтового скриптинга !

Для получения дополнительной информации

По умолчанию fetch использует метод GET , который извлекает определённый ресурс. Однако fetch может использовать и другие HTTP-методы.

Сделайте запрос HEAD

Замените функцию headRequest следующим кодом:

function headRequest() {
  fetch('examples/words.txt', {
    method: 'HEAD'
  })
  .then(validateResponse)
  .then(readResponseAsText)
  .then(logResult)
  .catch(logError);
}

Сохраните скрипт и обновите страницу. Нажмите HEAD request . Обратите внимание, что текстовое содержимое журнала пусто.

Объяснение

Метод fetch может принимать второй необязательный параметр, init . Этот параметр позволяет настроить запрос fetch, например, метод запроса , режим кэширования, учётные данные и т. д .

В этом примере мы устанавливаем метод запроса на выборку HEAD с помощью параметра init . Запросы HEAD аналогичны запросам GET, за исключением того, что тело ответа пустое. Такой тип запроса можно использовать, когда вам нужны только метаданные файла, но не требуется передавать все данные файла.

Необязательно: найдите размер ресурса

Давайте посмотрим на заголовки ответа на запрос examples/words.txt чтобы определить размер файла.

Обновите функцию headRequest для регистрации свойства content-length headers ответа (подсказка: см. документацию по заголовкам и методу get ).

После обновления кода сохраните файл и обновите страницу. Нажмите HEAD request . В консоли должен быть выведен размер (в байтах) файла examples/words.txt .

Объяснение

В этом примере метод HEAD используется для запроса размера (в байтах) ресурса (указанного в заголовке content-length ) без фактической загрузки самого ресурса. На практике это можно использовать для определения необходимости запроса всего ресурса (или даже способа его запроса).

Дополнительно : узнайте размер файла examples/words.txt другим методом и убедитесь, что он соответствует значению из заголовка ответа (вы можете узнать, как это сделать для вашей конкретной операционной системы — бонусные баллы за использование командной строки!).

Для получения дополнительной информации

Fetch также может отправлять данные с помощью POST-запросов.

Настройте эхо-сервер

Для этого примера вам нужно запустить эхо-сервер. Из каталога fetch-api-lab/app/ выполните следующую команду (если ваша командная строка заблокирована сервером localhost:8081 , откройте новое окно или вкладку командной строки):

node echo-servers/cors-server.js

Эта команда запускает простой сервер по адресу localhost:5000/ , который возвращает отправленные ему запросы.

Вы можете завершить работу этого сервера в любое время с помощью ctrl+c .

Сделайте POST-запрос

Замените функцию postRequest следующим кодом (убедитесь, что вы определили функцию showText из раздела 4, если вы не завершили этот раздел):

function postRequest() {
  fetch('http://localhost:5000/', {
    method: 'POST',
    body: 'name=david&message=hello'
  })
    .then(validateResponse)
    .then(readResponseAsText)
    .then(showText)
    .catch(logError);
}

Сохраните скрипт и обновите страницу. Нажмите «Запрос POST» . Наблюдайте, как отправленный запрос отобразится на странице. Он должен содержать имя и сообщение (обратите внимание, что мы пока не получаем данные из формы).

Объяснение

Чтобы выполнить POST-запрос с помощью fetch, мы используем параметр init для указания метода (аналогично тому, как мы задавали метод HEAD в предыдущем разделе). Здесь же мы задаём тело запроса, в данном случае — простую строку. Тело — это данные, которые мы хотим отправить.

Примечание: в процессе производства не забывайте всегда шифровать все конфиденциальные данные пользователя.

При отправке данных методом POST на localhost:5000/ запрос возвращается в виде ответа. Затем ответ проверяется с помощью validateResponse , считывается как текст и отображается на странице.

На практике этот сервер будет представлять собой сторонний API.

Необязательно: используйте интерфейс FormData

Интерфейс FormData можно использовать для легкого извлечения данных из форм.

В функции postRequest создайте новый объект FormData из элемента формы msg-form :

const formData = new FormData(document.getElementById('msg-form'));

Затем замените значение параметра body на переменную formData .

Сохраните скрипт и обновите страницу. Заполните форму (поля « Имя» и «Сообщение ») на странице, а затем нажмите «Запрос POST» . Обратите внимание на содержимое формы, отображаемое на странице.

Объяснение

Конструктор FormData может принимать HTML- form и создавать объект FormData . Этот объект заполняется ключами и значениями формы.

Для получения дополнительной информации

Запустить не-CORS-сервер Echo

Остановите предыдущий эхо-сервер (нажав ctrl+c в командной строке) и запустите новый эхо-сервер из каталога fetch-lab-api/app/ выполнив следующую команду:

node echo-servers/no-cors-server.js

Эта команда настраивает ещё один простой эхо-сервер, на этот раз по адресу localhost:5001/ . Однако этот сервер не настроен на приём кросс-доменных запросов .

Извлечь с нового сервера

Теперь, когда новый сервер работает по адресу localhost:5001/ , мы можем отправить ему запрос на выборку.

Обновите функцию postRequest , чтобы она извлекала данные с localhost:5001/ вместо localhost:5000/ . После обновления кода сохраните файл, обновите страницу и нажмите «Запрос POST» .

В консоли должно появиться сообщение об ошибке, указывающее на то, что запрос кросс-источника заблокирован из-за отсутствия заголовка CORS Access-Control-Allow-Origin .

Обновите fetch в функции postRequest с помощью следующего кода, который использует режим no-cors (как следует из журнала ошибок) и удаляет вызовы validateResponse и readResponseAsText (см. объяснение ниже):

function postRequest() {
  const formData = new FormData(document.getElementById('msg-form'));
  fetch('http://localhost:5001/', {
    method: 'POST',
    body: formData,
    mode: 'no-cors'
  })
    .then(logResult)
    .catch(logError);
}

Сохраните скрипт и обновите страницу. Затем заполните форму сообщения и нажмите «Отправить запрос» .

Наблюдайте за объектом ответа, зарегистрированным в консоли.

Объяснение

Fetch (и XMLHttpRequest) следуют политике единого источника . Это означает, что браузеры ограничивают кросс-доменные HTTP-запросы из скриптов. Кросс-доменный запрос возникает, когда один домен (например, http://foo.com/ ) запрашивает ресурс из другого домена (например, http://bar.com/ ).

Примечание: Ограничения на запросы из разных источников часто вызывают путаницу. Многие ресурсы, такие как изображения, таблицы стилей и скрипты, загружаются из разных доменов (т.е. из разных источников). Однако это исключения из политики одного источника. Запросы из разных источников по-прежнему ограничены внутри скриптов .

Поскольку номер порта сервера нашего приложения отличается от номера порта двух echo-серверов, запросы к любому из них считаются кросс-доменными. Однако первый echo-сервер, работающий на localhost:5000/ , настроен на поддержку CORS (вы можете открыть echo-servers/cors-server.js и изучить конфигурацию). Новый echo-сервер, работающий на localhost:5001/ , не поддерживает CORS (поэтому мы и получаем ошибку).

Использование mode: no-cors позволяет получить непрозрачный ответ (opaque response ). Это позволяет пользователю получить ответ, но не позволяет получить к нему доступ с помощью JavaScript (именно поэтому мы не можем использовать validateResponse , readResponseAsText или showResponse ). Ответ по-прежнему может быть использован другими API или кэширован сервис-воркером.

Изменить заголовки запроса

Fetch также поддерживает изменение заголовков запросов. Остановите эхо-сервер localhost:5001 (без CORS) и перезапустите эхо-сервер localhost:5000 (CORS) из раздела 6:

node echo-servers/cors-server.js

Восстановите предыдущую версию функции postRequest , которая извлекает данные из localhost:5000/ :

function postRequest() {
  const formData = new FormData(document.getElementById('msg-form'));
  fetch('http://localhost:5000/', {
    method: 'POST',
    body: formData
  })
    .then(validateResponse)
    .then(readResponseAsText)
    .then(showText)
    .catch(logError);
}

Теперь используйте интерфейс Header для создания объекта Headers внутри функции postRequest с именем messageHeaders с заголовком Content-Type , равным application/json .

Затем задайте свойству headers объекта init значение переменной messageHeaders .

Обновите свойство body , чтобы оно представляло собой строковый объект JSON, например:

JSON.stringify({ lab: 'fetch', status: 'fun' })

После обновления кода сохраните файл и обновите страницу. Затем нажмите « Отправить запрос POST» .

Обратите внимание, что отраженный запрос теперь имеет Content-Type application/json (а не multipart/form-data как было ранее).

Теперь добавьте пользовательский заголовок Content-Length к объекту messageHeaders и задайте запросу произвольный размер.

После обновления кода сохраните файл, обновите страницу и нажмите «Запрос POST» . Обратите внимание, что этот заголовок не изменяется в возвращаемом запросе.

Объяснение

Интерфейс Header позволяет создавать и изменять объекты Headers . Некоторые заголовки, например, Content-Type можно изменять с помощью функции fetch. Другие, например, Content-Length , защищены и не могут быть изменены (из соображений безопасности).

Установить пользовательские заголовки запроса

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

Удалите заголовок Content-Length из объекта messageHeaders в функции postRequest . Добавьте пользовательский заголовок X-Custom с произвольным значением (например, ' X-CUSTOM': 'hello world' ).

Сохраните скрипт, обновите страницу и нажмите «Запрос POST» .

Вы должны увидеть, что отраженный запрос имеет добавленное вами свойство X-Custom .

Теперь добавьте заголовок Y-Custom к объекту Headers. Сохраните скрипт, обновите страницу и нажмите «Запрос POST» .

В консоли должна появиться ошибка, подобная этой:

Fetch API cannot load http://localhost:5000/. Request header field y-custom is not allowed by Access-Control-Allow-Headers in preflight response.

Объяснение

Как и запросы кросс-домены, пользовательские заголовки должны поддерживаться сервером, с которого запрашивается ресурс. В этом примере наш эхо-сервер настроен на прием заголовка X-Custom , но не Y-Custom (вы можете открыть echo-servers/cors-server.js и найти Access-Control-Allow-Headers , чтобы убедиться в этом). Каждый раз, когда устанавливается пользовательский заголовок, браузер выполняет предварительную проверку. Это означает, что браузер сначала отправляет серверу запрос OPTIONS, чтобы определить, какие HTTP-методы и заголовки разрешены сервером. Если сервер настроен на прием метода и заголовков исходного запроса, он отправляется, в противном случае выдается ошибка.

Для получения дополнительной информации

Код решения

Чтобы получить копию рабочего кода, перейдите в папку решения .

Теперь вы знаете, как использовать Fetch API!

Ресурсы

Чтобы увидеть все практические работы в учебном курсе PWA, ознакомьтесь с приветственными практическими работами для курса.