fetch - garevna/js-course GitHub Wiki

🎓 AJAX


🎓 CORS

По соображениям безопасности браузеры ограничивают кросс-доменные запросы, инициированные из скриптов
XMLHttpRequest и Fetch API следуют политике одинакового происхождения
Это означает, что веб-приложение, использующее эти API, может запрашивать только ресурсы из того же источника, из которого было загружено приложение ( если только ответ от другого источника не содержит правильные заголовки CORS )

Cross-Origin Resource Sharing ( CORS ) - это механизм, который использует дополнительные заголовки HTTP, чтобы сообщить браузеру, что веб-приложение, работающее в одном домене, имеет разрешение на доступ к выбранным ресурсам другого домена


🎓 Fetch API

Fetch API является продвинутой альтернативой XMLHttpRequest

Fetch API предоставляет глобальный метод fetch() для асинхронного доступа к ресурсам в сети

Метод fetch отправляет объект запроса на сервер и возвращает промис

Самый простой объект GET-запроса может включать только URI ресурса

fetch ( "message.txt" )
    .then ( response => {
        ...
    })

При возникновении ошибок HTTP ( 404, 500 и т.д. ) возвращаемый методом fetch() промис разрешится нормально
( В таком случае он вернет значение false опции статуса ok )
⚠️ Он завершится с ошибкой только при сбое сети

Когда промис завершится, он вернет объект Response


🎓 Request

Конструктор

С его помощью можно создать объект запроса

В свойстве 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

ℹ️ Опция method

Метод доступа к ресурсу ( CRUD )

Ресурс - это любые данные на стороне сервера, имеющие URI ( идентификатор ресурса )
URI ( Uniform Resource Identifier )
Ресурсом может быть файл, база данных, запись в базе данных и т.д.

var request = new Request( 
    'https://httpbin.org/post', 
    {
        method: 'GET'
    }
)

ℹ️ Опция mode

Режим запроса


same-origin

Запросы из других источников будут приводить к генерации исключения

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

no-cors

В таком режиме при кросс-доменном запросе исключение не будет сгенерировано, но ответ будет пустым

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" }


cors

Разрешает кросс-доменные запросы ( ⚠️ если домен, куда направляется запрос, поддерживает CORS )

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

Свойства объекта 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


📎 body

Это объект ReadableStream, доступ к которому обеспечивают такие методы объекта Response, как
arrayBuffer(), blob(), formData(), json() или text()

Эти методы возвращают ответ сервера в заданном формате

Методы возвращают промис, результатом которого будет
arrayBuffer() ArrayBuffer ( строка из нулей и единиц )
blob() объект Blob ( данные в двоичном формате )
clone() копию объекта Response
formData() данные FormData
json() данные в JSON-формате
text() данные в текстовом формате

ℹ️ json()

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()

Давайте посмотрим, что такое объект 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 не фрагментирует данные, не выделяет отдельные байты или другие кластеры

Для этого у объекта 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 ) )
        })
    })

ℹ️ arrayBuffer --> blob

Можно получить объект 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
▼ 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
распарсенный как json:
▼ {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

💼 Упражнения

⚠️ **GitHub.com Fallback** ⚠️