fetch - garevna/js-course GitHub Wiki
По соображениям безопасности браузеры ограничивают кросс-доменные запросы, инициированные из скриптов
XMLHttpRequest и Fetch API следуют политике одинакового происхождения
Это означает, что веб-приложение, использующее эти API, может запрашивать только ресурсы из того же источника, из которого было загружено приложение ( если только ответ от другого источника не содержит правильные заголовки CORS )
Cross-Origin Resource Sharing ( CORS ) - это механизм, который использует дополнительные заголовки HTTP, чтобы сообщить браузеру, что веб-приложение, работающее в одном домене, имеет разрешение на доступ к выбранным ресурсам другого домена
Fetch API является продвинутой альтернативой XMLHttpRequest
Fetch API предоставляет глобальный метод fetch() для асинхронного доступа к ресурсам в сети
Метод fetch отправляет объект запроса на сервер и возвращает промис
Самый простой объект GET-запроса может включать только URI ресурса
fetch ( "message.txt" )
.then ( response => {
...
})При возникновении ошибок HTTP ( 404, 500 и т.д. ) возвращаемый методом fetch() промис разрешится нормально
( В таком случае он вернет значение false опции статуса ok )
Когда промис завершится, он вернет объект Response
Конструктор
С его помощью можно создать объект запроса
В свойстве prototype конструктора Request находятся все наследуемые экземплярами свойства и методы:
| Свойства | Методы |
|---|---|
| bodyUsed | arrayBuffer() |
| cache | blob() |
| credentials | clone() |
| destination | formData() |
| headers | json() |
| integrity | text() |
| isHistoryNavigation | |
| keepalive | |
| method | |
| mode | |
| redirect | |
| referrer | |
| referrerPolicy | |
| signal | |
| url |
☕ 1
Создадим с помощью конструктора Request простой объект запроса:
let request = new Request ( "https://api.github.com/users" )и выведем его в консоль:
▼ Request {method: "GET", url: "https://api.github.com/users", headers: Headers, destination: "", referrer: "about:client", …}
bodyUsed: false
cache: "default"
credentials: "same-origin"
destination: ""
► headers: Headers {}
integrity: ""
isHistoryNavigation: false
keepalive: false
method: "GET"
mode: "cors"
redirect: "follow"
referrer: "about:client"
referrerPolicy: ""
► signal: AbortSignal {aborted: false, onabort: null}
url: "https://api.github.com/users"
► __proto__: RequestМы видим дефолтные значения опций запроса, которые мы не устанавливали
Давайте установим некоторые опции:
let request = new Request ( "https://api.github.com/users", {
credentials: "include",
mode: "same-origin",
headers: new Headers ({
"Content-Type" : "application/json"
})
} )
request.headers.get ( "Content-Type" )Посмотрим в консоли, что у нас получилось:
▼ Request {method: "GET", url: "https://api.github.com/users", headers: Headers, destination: "", referrer: "about:client", …}
bodyUsed: false
cache: "default"
credentials: "include"
destination: ""
► headers: Headers {}
integrity: ""
isHistoryNavigation: false
keepalive: false
method: "GET"
mode: "same-origin"
redirect: "follow"
referrer: "about:client"
referrerPolicy: ""
► signal: AbortSignal {aborted: false, onabort: null}
url: "https://api.github.com/users"
► __proto__: RequestОбратите внимание, что в консоли мы видим как бы "пустой" объект заголовков:
► headers: Headers {}Однако это объект, данные которого "спрятаны" в приватных свойствах, и для доступа к ним у этого объекта есть ряд интефейсных методов:
▼ Headers {}
▼ __proto__: Headers
► append: ƒ append()
► delete: ƒ delete()
► entries: ƒ entries()
► forEach: ƒ forEach()
► get: ƒ ()
► has: ƒ has()
► keys: ƒ keys()
► set: ƒ ()
► values: ƒ values()
► constructor: ƒ Headers()
► Symbol(Symbol.iterator): ƒ entries()
Symbol(Symbol.toStringTag): "Headers"
► __proto__: ObjectВоспользуемся методом get() для получения значения заголовка Content-Type:
request.headers.get ( "Content-Type" ) // "application/json"Для создания заголовков запроса мы воспользовались конструктором Headers, хотя точно такой же результат мы получим в результате:
let request = new Request ( "https://api.github.com/users", {
credentials: "include",
mode: "same-origin",
headers: {
"Content-Type" : "application/json"
}
} )Давайте разберемся, что означают некоторые опции объекта запроса
| Опция | Описание |
|---|---|
method |
GET - получить данные POST - создание нового ресурса PUT - обновление существующего ресурса DELETE - удаление ресурса HEAD - получение информации о ресурсе |
mode |
cors no-cors same-origin |
credentials |
omit - Никогда не использовать куки same-origin - Значение по умолчанию Учетные данные пользователя ( файлы cookie, данные http-аутентификации и т.д. ) отправляются с запросом только в том случае, если домен вызывающего скрипта и запрашиваемого ресурса совпадают include - Учетные данные пользователя ( файлы cookie, данные http-аутентификации и т.д. ) отправляются в любом случае, даже в случае кросс-доменного запроса |
integrity |
дайджест ( цифровая подпись ) ресурса ( Подробнее: SHA ) |
cache |
режим кэширования default / reload / no-cache |
Метод доступа к ресурсу ( CRUD )
Ресурс - это любые данные на стороне сервера, имеющие URI ( идентификатор ресурса )
URI ( Uniform Resource Identifier )
Ресурсом может быть файл, база данных, запись в базе данных и т.д.
var request = new Request(
'https://httpbin.org/post',
{
method: 'GET'
}
)Режим запроса
Запросы из других источников будут приводить к генерации исключения
☕ 2
Например, такой запрос из консоли пустой страницы ( about:blank )
var request = new Request(
'https://avatars2.githubusercontent.com/u/46?v=4',
{
mode: 'same-origin'
}
)
fetch ( request )
.then ( response => console.log ( response ) )приведет к генерации следующего исключения:
Fetch API cannot load https://avatars2.githubusercontent.com/u/46?v=4
Request mode is "same-origin"
but the URL's origin is not same as the request origin nullРежим запроса same-origin ( одного происхождения ), а домен, которого сделан запрос ( null ) не совпадает с доменом, на который был отправлен запрос
в результате чего промис завершится неудачей:
Promise {<rejected>: TypeError: Failed to fetchВ таком режиме при кросс-доменном запросе исключение не будет сгенерировано, но ответ будет пустым
☕ 3
var request = new Request(
'https://avatars2.githubusercontent.com/u/46?v=4',
{
mode: 'no-cors'
}
)
fetch ( request )
.then ( response => response.blob()
.then ( response => console.log ( response ) )
)На такой запрос ответ будет: Blob(0) { size: 0, type: "" }
Если тот же запрос сделать без mode: 'no-cors'
то ответ будет: Blob(35635) { size: 35635, type: "image/jpeg" }
Разрешает кросс-доменные запросы (
☕ 4
Например, запрос:
var request = new Request(
'http://bm.img.com.ua/img/prikol/images/large/0/0/307600.jpg',
{
mode: 'cors'
}
)
fetch ( request )
.then ( response => {
console.log ( response )
})⛔ приведет к генерации исключения:
Failed to load http://bm.img.com.ua/img/prikol/images/large/0/0/307600.jpg:
No 'Access-Control-Allow-Origin' header is present on the requested resource
Origin 'null' is therefore not allowed access
If an opaque response serves your needs,
set the request's mode to 'no-cors' to fetch the resource with CORS disabled⛔ и соответствующему "провалу" запроса:
Uncaught (in promise) TypeError: Failed to fetchЭто происходит потому, что в режиме cors требуется, чтобы сервер запрошенного ресурса вернул заголовок Access-Control-Allow-Origin со значением, совпадающим со значением Origin запроса ( а заголовок Origin нельзя подделать, он устанавливается браузером при отправке запроса на сервер )
Если сервер запрошенного ресурса вернет заголовок Access-Control-Allow-Origin со значением *, то запрос будет выполнен нормально
☕ 5
var request = new Request(
'https://httpbin.org/get',
{
mode: 'cors'
}
)
fetch ( request )
.then ( response => response.text()
.then ( response => console.log ( response ) )
){
"args": {},
"headers": {
"Accept": "*/*",
"Accept-Encoding": "gzip, deflate, br",
"Accept-Language": "en-US,en;q=0.9,ru;q=0.8",
"Connection": "close",
"Host": "httpbin.org",
"Origin": "null",
"Save-Data": "on",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36"
},
"origin": "185.38.217.69",
"url": "https://httpbin.org/get"
}Когда объект запроса создается с помощью конструктора Request, значение свойства mode для этого запроса устанавливается в cors
var request = new Request (
'http://bm.img.com.ua/img/prikol/images/large/0/0/307600.jpg'
)
console.log ( request.mode ) // corsВ противном случае в качестве режима обычно используется no-cors
( например, когда запрос инициируется из разметки, и атрибут crossorigin отсутствует )
для элементов <link>, <script>, <img>, <audio>, <video>, <object>, <embed> или <iframe> запрос выполняется в режиме no-cors
| Свойства | объекта Response |
|---|---|
type |
строка, информирующая о том, откуда пришел ресурс basic - запрос с того же домена cors - данные получены с другого домена с использованием CORS-заголовков opaque - непрозрачный ответ на запрос другого происхождения, который не возвращает заголовки CORS не позволяет прочитать возвращенные данные или просмотреть статус запроса ( нет возможности проверить успешность запроса ) |
url |
URL адрес ответа сервера |
status |
код статуса ответа сервера |
statusText |
текст статуса ответа сервера |
ok |
логическое выражение; принимает значение true, если получение данных произошло без ошибок ( status от 200 до 299 ) |
bodyUsed |
логическое выражение; принимает значение true, если body загружено |
Объект headers ответа сервера имеет ряд унаследованных методов
▼ Headers
▼ __proto__: Headers
► append: ƒ append()
► delete: ƒ delete()
► entries: ƒ entries()
► forEach: ƒ forEach()
► get: ƒ ()
► has: ƒ has()
► keys: ƒ keys()
► set: ƒ ()
► values: ƒ values()
► constructor: ƒ Headers()
► Symbol(Symbol.iterator): ƒ entries()
Symbol(Symbol.toStringTag): "Headers"
► __proto__: ObjectВоспользуемся методом forEach для получения значений всех возвращаемых сервером заголовков ответа
☕ 1
Отправим запрос с методом HEAD:
fetch ( 'https://api.github.com/users/5', {
method: "HEAD"
}).then ( response => response.headers.forEach (
key => console.log ( key )
) )public, max-age=60, s-maxage=60
application/json; charset=utf-8
W/"7870416c9818dd4ba65ab505535c7b79"
Fri, 28 Dec 2018 06:04:01 GMT
github.v3; format=json
60
53
1560761075Пока мы не можем посмотреть, как работают методы keys и entries, поскольку оба эти метода возвращают объект итератора, что мы будем изучать позже
Ответ ( Response ) имеет свойство type, которое может иметь значения basic, cors или opaque
Если запрос выполняется в пределах одного домена, то свойство type ответа будет basic ( запрос без ограничений )
В случае кросс-доменного запроса все зависит от ответа
Если ответ содержит CORS заголовки, то свойство type ответа будет cors
Такой ответ ограничивает доступ к заголовкам - доступны будут только заголовки Cache-Control, Content-Language, Content-Type, Expires, Last-Modified, и Pragma
Если значением опции mode запроса было cors, но удаленный ресурс не вернул CORS-заголовки, то свойство type ответа будет opaque
Это объект ReadableStream, доступ к которому обеспечивают такие методы объекта Response, как
arrayBuffer(), blob(), formData(), json() или text()
Эти методы возвращают ответ сервера в заданном формате
| Методы | возвращают промис, результатом которого будет |
|---|---|
arrayBuffer() |
ArrayBuffer ( строка из нулей и единиц ) |
blob() |
объект Blob ( данные в двоичном формате ) |
clone() |
копию объекта Response |
formData() |
данные FormData |
json() |
данные в JSON-формате |
text() |
данные в текстовом формате |
☕ 2
Воспользуемся сервисом для получения полной информации о пользователе ( в данном случае - о самом себе )
Для получения такой инфо методу fetch нужно передать в качестве аргумента строку
https://api.2ip.ua/geo.json?ip=
Метод fetch вернет промис, поэтому "повесим" обработчика успешного завершения then
Как мы знаем, метод then принимает один аргумент - функцию, которая будет вызвана в случае успешного завершения асинхронной операции:
fetch ( 'https://api.2ip.ua/geo.json?ip=' )
.then ( response => {
...
})Эта функция получит в качестве аргумента ответ сервера response ( так мы назвали эту переменную )
Нам не нужен весь объект response, который вернет нам метод fetch
Нам нужен результат ( данные ) в формате json
Используем метод json() объекта Response
Мы знаем, что этот метод также вернет промис, т.е. нам нужно еще одного обработчика then:
fetch ( 'https://api.2ip.ua/geo.json?ip=' )
.then ( response => {
response.json().then ( response =>
...
)
})Осталось добавить код, который будет выполнен при успешном завершении второго промиса
Функция, которую мы передали в качестве аргумента второму then, получит на входе объект данных, являющийся результатом парсинга json-строки
☕ 3
... подставить имя своего toilet
Сначала мы получим данные юзера гитхаба, а потом запишем эти данные в свой toilet
document.cookie = "name=garevna;token=qw4654Rzsxc-*/w5"
fetch ( 'https://api.github.com/users?since=135' )
.then ( response => response.json()
.then ( response => {
fetch (
'http://ptsv2.com/t/.../post',
{
method: 'POST',
credentials: 'include',
headers: new Headers({
'Content-Type': 'application/json'
}),
body: JSON.stringify ( response[5] )
}
)
.then ( response => console.log ( response ) )
})
)Давайте посмотрим, что такое объект Blob
☕ 4
Создадим элемент img на странице:
var picture = document.createElement ( 'img' )
document.body.appendChild ( picture )Теперь загрузим с помощью fetch изображение из сети ( например, аватар пользователя github ) и итерпретируем ответ сервера как объект Blob:
fetch ( 'https://avatars2.githubusercontent.com/u/46?v=4' )
.then ( response => {
response.blob().then ( response => {
urlObject = URL.createObjectURL( response)
picture.src = urlObject
})
})Если вывести полученный объект в консоль, то мы увидим:
► Blob(35635) { size: 35635, type: "image/jpeg" }Изображение получено нами в виде объекта Blob, и теперь оно является локальным объектом, который нам нужно отобразить на странице в нашем элементе img
Поскольку на странице могут отображаться только объекты ( ресурсы ), размещенные в сети и имеющие URL, основная задача - создать такой URL для объекта, уже находящегося в нашем распоряжении и являющимся локальным объектом текущей страницы
Для этого существует метод URL.createObjectURL
Этот формат ответа сервера представляет собой строку из нулей и единиц
Объект ArrayBuffer не фрагментирует данные, не выделяет отдельные байты или другие кластеры
Для этого у объекта ArrayBuffer есть конструкторы:
✅ Int8Array
Для представления данных в виде последовательности байт
✅ Uint8Array
Для представления данных в виде последовательности шестнадцатибитных значений ( чисел )
Результатом работы конструкторов будет итерабельный объект
☕ 5
fetch ( 'https://avatars2.githubusercontent.com/u/46?v=4' )
.then ( response => {
response.arrayBuffer().then ( response => {
console.log ( response )
console.log ( new Int8Array( response ) )
console.log ( new Uint8Array( response ) )
})
})Можно получить объект Blob из объекта arrayBuffer с помощью конструктора Blob, которому нужно передать объект arrayBuffer, "завернутый" в массив
☕ 6
Закиньте в консоль следующий код, и посмотрите результат:
console.log ( new Blob ( [
'01101000110000100000011101011010010001000100011101011'
] ) )
console.log ( new Blob ( [
'01101000110000100000011101011010010001000100011101011',
'01101000110000100000011101011010010001000100011101011'
] ) )☕ 7
Закиньте в консоль следующий код:
fetch ( 'https://avatars2.githubusercontent.com/u/46?v=4' )
.then ( response => {
response.arrayBuffer()
.then ( response => {
console.log ( new Blob ( [ response ] ) )
})
})☕ 8
Закиньте в консоль следующий код:
var picture = document.createElement ( 'img' )
document.body.appendChild ( picture )
fetch ( 'https://avatars2.githubusercontent.com/u/46?v=4' )
.then ( response => {
response.arrayBuffer()
.then ( response => {
var pixels = new Uint8Array( response )
urlObject = URL.createObjectURL( new Blob ( [ response ] ))
picture.src = urlObject
})
})☕ 9
fetch ( 'https://httpbin.org/get' )
.then ( response => response.json()
.then ( response => console.log ( response.headers ) )
)▼ {Accept: "*/*", Accept-Encoding: "gzip, deflate, br", Accept-Language: "en-US,en;q=0.9,ru;q=0.8", Connection: "close", Host: "httpbin.org", …}
Accept: "*/*"
Accept-Encoding: "gzip, deflate, br"
Accept-Language: "en-US,en;q=0.9,ru;q=0.8"
Connection: "close"
Host: "httpbin.org"
Origin: "null"
Save-Data: "on"
User-Agent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36"
► __proto__: Object☕ 10
Сделаем кросс-доменный запрос:
var request = new Request (
'https://httpbin.org/post',
{
method: 'POST',
mode: 'cors',
redirect: 'follow',
headers: new Headers({
'Content-Type': 'text/plain'
}),
body: "Hello, students!"
}
)
fetch( request )
.then ( response => {
console.log ( response )
response.json()
.then ( x => console.log ( x ) )
})Ответ сервера:
▼ Response {type: "cors", url: "https://httpbin.org/post", redirected: false, status: 200, ok: true, …}
body: (...)
bodyUsed: true
► headers: Headers {}
ok: true
redirected: false
status: 200
statusText: "OK"
type: "cors"
url: "https://httpbin.org/post"
► __proto__: Response▼ {args: {…}, data: "Hello, students!", files: {…}, form: {…}, headers: {…}, …}
► args: {}
data: "Hello, students!"
► files: {}
► form: {}
► headers: {Accept: "*/*", Accept-Encoding: "gzip, deflate, br", Accept-Language: "en-US,en;q=0.9,ru;q=0.8", Connection: "close", Content-Length: "16", …}
json: null
origin: "185.38.217.69"
url: "https://httpbin.org/post"
► __proto__: Object