Эта практическая работа является частью учебного курса «Разработка прогрессивных веб-приложений», разработанного командой 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
до
- получить
/examples/words.txt
- проверьте ответ с помощью
validateResponse
- прочитать ответ как текст (подсказка: см. Response.text() )
- и отобразить текст на странице
Эту функцию 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, ознакомьтесь с приветственными практическими работами для курса.