Ilekroć jest to potrzebne, JavaScript potrafi wykonywać żądania sieciowe do serwera i pobierać nowe informacje.
Można na przykład użyć żądania sieciowego do:
- złożenia zamówienia,
- wyświetlenia informacji o użytkowniku,
- otrzymania najnowszych aktualizacji z serwera,
- ...itp.
...I to wszystko bez przeładowania strony!
Istnieje nadrzędny termin "AJAX" (skrót od Asynchronous JavaScript And XML) dotyczący żądań sieciowych w JavaScripcie. Nie musimy jednak używać XML-a: skrót ten pochodzi z dawnych czasów, stąd też zawiera takie właśnie słowo. Być może znasz już ten termin.
Istnieje wiele sposobów wysłania żądania sieciowego i pobrania informacji z serwera.
Metoda fetch()
jest nowoczesna i wszechstronna, dlatego od niej zaczniemy. Nie jest ona wspierana przez stare przeglądarki (można ją dodać poprzez odpowiednią łatkę - ang. polyfill), jest natomiast bardzo dobrze obsługiwana przez współczesne przeglądarki.
Podstawowa składnia jest następująca:
let promise = fetch(url, [options])
url
-- adres URL zapytania.options
-- parametry opcjonalne: metoda, nagłówki, itp.
Bez options
mamy do czynienia ze zwykłym zapytaniem GET, pobierającym zawartość adresu url
.
Przeglądarka natychmiast uruchamia zapytanie i zwraca obietnicę (ang. promise), której kod wywołujący powinien użyć do uzyskania wyniku.
Uzyskanie odpowiedzi jest zwykle procesem dwuetapowym.
Po pierwsze, obietnica promise
zwrócona przez fetch
, rozwiązuje się (ang. resolves) do obiektu wbudowanej klasy Response , gdy tylko serwer odpowie nagłówkami.
Na tym etapie możemy sprawdzić status żądania HTTP, aby dowiedzieć się, czy się ono powiodło, albo przejrzeć nagłówki. Nie mamy jednak jeszcze dostępu do ciała odpowiedzi.
Obietnica jest odrzucana (ang. rejects), jeżeli fetch
nie jest w stanie wykonać zapytania HTTP, np. ze względu na problemy sieciowe lub brak strony, do której skierowano zapytanie. Nieprawidłowe statusy HTTP, takie jak 404 lub 500, nie powodują błędu.
Informację o statusie HTTP znajdziemy wśród właściwości odpowiedzi:
status
-- kod odpowiedzi HTTP, np. 200.ok
-- typ logiczny,true
jeżeli kod odpowiedzi HTTP jest z zakresu 200-299.
Przykładowo:
let response = await fetch(url);
if (response.ok) { // jeżeli kod odpowiedzi HTTP jest z zakresu 200-299
// pobierz ciało odpowiedzi (wyjaśnione poniżej)
let json = await response.json();
} else {
alert("Błąd HTTP: " + response.status);
}
Po drugie, aby pobrać ciało odpowiedzi, należy wywołać kolejną metodę.
Obiekt klasy Response
(pol. odpowiedź) zapewnia wiele metod bazujących na obietnicach, które pozwalają na dostęp do ciała odpowiedzi i zwrócenie go w różnych formach:
response.text()
-- odczytaj odpowiedź i zwróć jako tekst,response.json()
-- odczytaj odpowiedź i zwróć jako JSON,response.formData()
-- zwróć odpowiedź jako obiekt typuFormData
(wyjaśnienie w następnym rozdziale),response.blob()
-- zwróć odpowiedź jako Blob (dane binarne z typem),response.arrayBuffer()
-- zwróć odpowiedź jako ArrayBuffer (niskopoziomowa reprezentacja danych binarnych),- ponadto
response.body
jest sam w sobie obiektem typu ReadableStream, co pozwala na odczytywanie go kawałek po kawałku. Ale o tym nieco później.
Pobierzmy dla przykładu obiekt JSON z ostatnimi commitami z GitHuba.
let url = 'https://api.github.com/repos/javascript-tutorial/en.javascript.info/commits';
let response = await fetch(url);
*!*
let commits = await response.json(); // odczytaj ciało odpowiedzi i zwróć jako JSON
*/!*
alert(commits[0].author.login);
Bądź to samo, ale bez await
, a jedynie za pomocą czystej składni obietnic:
fetch('https://api.github.com/repos/javascript-tutorial/en.javascript.info/commits')
.then(response => response.json())
.then(commits => alert(commits[0].author.login));
Aby pobrać odpowiedź jako tekst, użyj await response.text()
zamiast .json()
:
let response = await fetch('https://api.github.com/repos/javascript-tutorial/en.javascript.info/commits');
let text = await response.text(); // odczytaj ciało odpowiedzi jako tekst
alert(text.slice(0, 80) + '...');
Aby zaprezentować odczyt danych w formacie binarnym, pobierzmy obraz logo specyfikacji "fetch" (patrz rozdział Blob odnośnie operacji na obiekcie Blob
):
let response = await fetch('/article/fetch/logo-fetch.svg');
*!*
let blob = await response.blob(); // pobierz logo jako obiekt Blob
*/!*
// stwórz dla niego znacznik <img>
let img = document.createElement('img');
img.style = 'position:fixed;top:10px;left:10px;width:100px';
document.body.append(img);
// wyświetl je
img.src = URL.createObjectURL(blob);
setTimeout(() => { // ukryj po upływie trzech sekund
img.remove();
URL.revokeObjectURL(img.src);
}, 3000);
Można wybrać tyko jedną z metod odczytywania ciała odpowiedzi.
Jeśli już zdecydowaliśmy się na `response.text()`, wówczas `response.json()` nie zadziała, ponieważ zawartość ciała odpowiedzi została już wcześniej przetworzona.
```js
let text = await response.text(); // ciało odpowiedzi zostaje przetworzone
let parsed = await response.json(); // nie powiedzie się (przetworzone wcześniej)
Nagłówki odpowiedzi są dostępne w obiekcie nagłówków podobnym do obiektu Map, a konkretnie w response.headers
.
Nie jest do dokładnie Map, aczkolwiek posiada podobne metody, służące do pobrania poszczególnych nagłówków za pomocą nazwy lub poprzez iterowanie po nich:
let response = await fetch('https://api.github.com/repos/javascript-tutorial/en.javascript.info/commits');
// pobieramy jeden nagłówek
alert(response.headers.get('Content-Type')); // application/json; charset=utf-8
// iterujemy po wszystkich nagłówkach
for (let [key, value] of response.headers) {
alert(`${key} = ${value}`);
}
Aby zdefiniować nagłówek w żądaniu fetch
, użyjemy właściwości headers
, która zawiera obiekt z wychodzącymi nagłówkami:
let response = fetch(protectedUrl, {
headers: {
Authentication: 'secret'
}
});
... Istnieją również zabronione nagłówki HTTP, których nie możemy ustawić:
Accept-Charset
,Accept-Encoding
Access-Control-Request-Headers
Access-Control-Request-Method
Connection
Content-Length
Cookie
,Cookie2
Date
DNT
Expect
Host
Keep-Alive
Origin
Referer
TE
Trailer
Transfer-Encoding
Upgrade
Via
Proxy-*
Sec-*
Dzięki nim protokół HTTP działa prawidłowo i jest bezpieczny, dlatego też są pod pełną kontrolą przeglądarki.
Do wykonania żądania z metodą POST
lub jakąkolwiek inną musimy użyć opcji dostępnych w funkcji fetch
:
method
-- metoda HTTP, np.POST
,body
-- ciało żądania, może przyjąć formę:- łańcucha znaków (np. w formacie JSON),
- obiektu
FormData
, aby móc przesłać dane jakoform/multipart
, Blob
/BufferSource
, aby przesłać dane w formie binarnej,- URLSearchParams, aby przesłać dane jako
x-www-form-urlencoded
, rzadko używane.
Najczęściej używanym formatem jest JSON.
Przykładowo, ten kod przesyła obiekt user
jako JSON:
let user = {
name: 'Jan',
surname: 'Kowalski'
};
*!*
let response = await fetch('/article/fetch/post/user', {
method: 'POST',
headers: {
'Content-Type': 'application/json;charset=utf-8'
},
body: JSON.stringify(user)
});
*/!*
let result = await response.json();
alert(result.message);
Należy pamiętać, że jeżeli ciało żądania (body
) jest łańcuchem znaków, wówczas nagłówek Content-Type
domyślnie ustawiony jest na text/plain;charset=UTF-8
.
Ponieważ jednak zamierzamy wysłać obiekt JSON, użyjemy obiektu headers
do ustawienia nagłówka Content-Type
na application/json
, czyli właściwego dla danych zakodowanych w formacie JSON.
Za pomocą fetch
możemy także przesłać dane binarne, używając obiektów Blob
albo BufferSource
.
W poniższym przykładzie mamy znacznik <canvas>
, który pozwala na rysowanie poprzez poruszanie nad nim myszką. Kliknięcie na przycisk "Prześlij" wysyła obraz do serwera:
<body style="margin:0">
<canvas id="canvasElem" width="100" height="80" style="border:1px solid"></canvas>
<input type="button" value="Prześlij" onclick="submit()">
<script>
canvasElem.onmousemove = function(e) {
let ctx = canvasElem.getContext('2d');
ctx.lineTo(e.clientX, e.clientY);
ctx.stroke();
};
async function submit() {
let blob = await new Promise(resolve => canvasElem.toBlob(resolve, 'image/png'));
let response = await fetch('/article/fetch/post/image', {
method: 'POST',
body: blob
});
// serwer potwierdza zapisanie obrazu oraz podaje jego rozmiar
let result = await response.json();
alert(result.message);
}
</script>
</body>
Zauważ, że nie ustawiamy ręcznie nagłówka Content-Type
, ponieważ obiekt Blob
posiada wbudowany typ (tutaj image/png
, wymuszony przez metodę toBlob
). Dla obiektów Blob
ten typ jest przekazywany do nagłówka Content-Type
.
Funkcję submit()
można również przepisać z pominięciem składni async/await
w taki sposób:
function submit() {
canvasElem.toBlob(function(blob) {
fetch('/article/fetch/post/image', {
method: 'POST',
body: blob
})
.then(response => response.json())
.then(result => alert(JSON.stringify(result, null, 2)))
}, 'image/png');
}
Typowe żądanie sieciowe składa się z dwóch wywołań await
:
let response = await fetch(url, options); // rozwiązuje się do obiektu z nagłówkami odpowiedzi
let result = await response.json(); // odczytuje ciało jako JSON
Albo bez użycia await
:
fetch(url, options)
.then(response => response.json())
.then(result => /* zrób coś z parametrem result */)
Właściwości żądania:
response.status
-- kod odpowiedzi HTTP,response.ok
--true
dla kodów odpowiedzi z przedziału 200-299.response.headers
-- z nagłówkami HTTP, podobny do Map.
Metody służące do przetwarzania ciała odpowiedzi:
response.text()
-- zwróć odpowiedź jako tekst,response.json()
-- odczytaj odpowiedź jako obiekt JSON,response.formData()
-- zwróć odpowiedź jako obiektFormData
(kodowanie form/multipart, zobacz następny rozdział),response.blob()
-- zwróć odpowiedź jako Blob (dane binarne z typem),response.arrayBuffer()
-- zwróć odpowiedź jako ArrayBuffer (niskopoziomowa reprezentacja danych binarnych),
Poznane jak dotąd opcje metody fetch
:
method
-- metoda żądania HTTP,headers
-- obiekt z nagłówkami żądania (nie każdy dowolny nagłówek jest dozwolony),body
-- dane do wysyłki (ciało żądania) jakostring
albo obiektFormData
,BufferSource
,Blob
lubUrlSearchParams
.
W następnych rozdziałach poznamy więcej opcji i przypadków użycia metody fetch
.