Fetch API - następca XMLHttpRequest

Artykuł dodany: 16 listopada 2015.

Stopień trudności (1 - dla początkujących, 5 - dla ekspertów): 1

Podstawą każdej nowoczesnej strony są żądania XHR. Towarzyszą nam od lat, każda większa biblioteka typu JQuery, Dojo Toolkit, Angular posiada własny wrapper umożliwiający dostęp w bardziej przyjemny sposób do niskopoziomowych metod. Największym problemem jak zawsze był brak wsparcia obiektu XMLHttpRequest w przeglądarce Internet Explorer. Programiści giganta z Redmond dopiero od wersji 10 poprawili silnik IE na tyle, że możliwe jest w miarę bezbolesne korzystanie z tej technologii. Mimo wszystko to dalej nie jest do końca miłe i przyjemne. Podstawowy kod może wyglądać mniej więcej tak:

var oReq = new XMLHttpRequest();
oReq.addEventListener("load", reqListener);
oReq.open("GET", "http://wwwgo.pl/api/user/1");
oReq.responseType = 'json';
oReq.send();

Natomiast jeżeli chcemy aby całość działała również w IE kodu jest znacznie więcej:

// Źródło Wikipedia: https://en.wikipedia.org/wiki/XMLHttpRequest
if (typeof XMLHttpRequest === "undefined") {
  XMLHttpRequest = function () {
    try { return new ActiveXObject("Msxml2.XMLHTTP.6.0"); }
    catch (e) {}
    try { return new ActiveXObject("Msxml2.XMLHTTP.3.0"); }"
    catch (e) {}
    try { return new ActiveXObject("Microsoft.XMLHTTP"); }
    catch (e) {}
    throw new Error("This browser does not support XMLHttpRequest.");
  };
}

Dla kontrastu, używając JQuery mamy do dyspozycji metodę .load() której użycie wygląda następująco:

$( "#result" ).load( "/api/user/1" );

W porównaniu do wcześniej podanego kodu dla obiektu XHR, load() jest banalnie proste. Mając to na uwadze, powstał nowy standard nazwany Fetch API.

Fetch API

Więcej o samym standardzie w jego obecnej formie można przeczytać na stronie społeczności WHATWG. Główną różnicą pomiędzy XMLHttpRequest a Fetch API, oczywiście pomijając prostotę, jest fakt że Fetch API korzysta z obietnic (Promise). Kod przystaje dzięki temu bardziej do współczesnych realiów programowania. Oczywiście nie ma róży bez kolców. Na chwilę obecną technologię wspierają natywnie Chrome od wersji 42, Firefox 39+ oraz Opera zaczynając od wersji 29. W chwili pisania artykułu, na stronie dev.windows.com możemy przeczytać że Fetch API ma status “Under consideration”, zatem jeszcze długa droga do pełnego działania w IE. Nie jest to jednak aż tak wielkim problemem gdyż na Github istnieje odpowiedni polyfill wprowadzający funkcjonalność Fetch API w starszych przeglądarkach. Tyle pisałem o prostocie, jak zatem wygląda podstawowy request?

fetch('/api/user/1').then(function(response) {

}).catch(function(error) {

});

Domyślnie wysyłane jest żądanie GET. Jako pierwszy argument funkcji podajemy adres, w wyniku zwracany jest Promise obiektu Response który oczywiście możemy łączyć. Dla osób wykorzystujących politykę Content Security Policy ważna może być informacja, że należy w regułach CSP dodać wyjątek dla dyrektywy “connect-src”. Na potrzeby przykładu metodę fetch() mocno skróciłem. Zgodnie z dokumentacją pełna definicja wygląda następująco:

fetch(input, init).then(function(response) { ... });

Input definiuje zasób który chcemy pobrać. Może być to zwykły string, lub obiekt Request. Init jest opcjonalnym obiektem zawierającym dodatkowe ustawienia.

Obiekt Headers

Bardzo ważnym elementem podczas pracy z siecią web są nagłówki. Więcej o ich funkcjonowaniu można przeczytać we wcześniejszym artykule. W skrócie, kontrolują sposób transmisji danych, zawierają ważne informacje o przesyłanej treści. Obiekt “Headers” zawiera następujące metody:

append(nazwa, wartość) – jeżeli nagłówek istnieje – dodaje wartość, w innym wypadku dodaje nowy nagłówek
delete(nazwa) – kasuje nagłówek
get(nazwa) – zwraca wartość nagłówka
getAll(nazwa) – zwraca tablicę nagłówków dla konkretnej nazwy
has(nazwa) – sprawdza czy nagłówek istnieje w obiekcie, zwraca boolean
set(nazwa, wartość) – ustawia nową wartość dla istniejącego nagłówka lub dodaje nową jeśli jeszcze nie istnieje

Być może opis brzmi skomplikowanie ale w rzeczywistości obiekt jest bardzo prosty i logiczny. Kilka przykładów powinno mocno rozjaśnić sprawę.

// Tworzy nową instancję obiektu
var headers = new Headers();

// Dodaje nowe nagłówki
headers.append('Content-Type', 'text/html');
headers.append('X-Custom-Header', 'nasz prywatny nagłówek');

// Sprawdzanie, pobieranie i ustawianie
headers.has('Content-Type'); // true
headers.get('Content-Type'); // "text/html"
headers.set('Content-Type', 'application/json');

// Wartości przekazane do konstruktora
var headers = new Headers({
	'Content-Type' : 'text/html',
	'X-Custom-Header' : 'nasz prywatny nagłówek'
});

Nasza zmodyfikowana metoda fetch może teraz przybrać postać:

fetch('/api/user/1', {
	method: 'POST', 
	redirect: 'follow',
	headers: new Headers({
		'Content-Type': 'text/html'
	})
}).then(function() {  });

Obiekt Request

Cała komunikacja pomiędzy serwerem i klientem opiera się na żądaniu (Request) – wysyłany przez stronę inicjującą połączenie, oraz odpowiedzi (Response) serwera na nasze żądanie. Obie strony wysyłają nagłówki przesyłając w nich ważne dane oraz informując o statusie. W Fetch API zarówno Request jak i Response implementują “mixin” Body. Czym jest mixin? To zbiór metod i własności który inne klasy albo interface’y są w stanie zaimplementować. W innych językach moglibyśmy za taki uznać np. klasę abstrakcyjną. Jak wygląda obiekt Request w teorii?

Własności:

url – adres URL
method – metoda żądania, przykładowo GET, POST
headers – nagłówki wysyłane podczas żądania. Może zawierać obiekty ByteString albo Headers
body – zawartość dodatkowa, ciało żądania, będąca typu Blob, BufferSource, FormData, URLSearchParams, USVString. Metody GET i HEAD nie mogą mieć body
mode – dodatkowy tryb, np. cors, no-cors, same-origin
credentials – omit, same-origin, include
cache – default, no-store, reload, no-cache, force-cache, only-if-cached

Metody:

clone() – tworzy kopię obiektu Request (przydatna w trakcie pracy z technologią Service Worker)

Implementacja Body (będzie wspólna dla Request i Response), zwraca Promise dla odpowiednich obiektów:

arrayBuffer() – ArrayBuffer
blob() – Blob
formData() – FormData, inaczej dane z formularza traktowanego jako “multipart/form-data”
json()JSON
text()USVString (tekst)

Po utworzeniu instancji obiektu własności stają się wyłącznie do odczytu.

Jak wspomniałem wcześniej, do metody fetch możemy przekazać adres lub cały obiekt Request. Ten drugi sposób umożliwia znacznie dokładniejszą kontrolę nad wartościami przesyłanymi do serwera.

var request = new Request({
	url: '/api/user/1',
	method: 'POST', 
	redirect: 'follow',
	headers: new Headers({
		'Content-Type': 'text/html'
	})
});

fetch(request).then(function() {  });

Do fetch() możemy również przekazać dodatkowe opcje. Może nim być obiekt zachowujący się jak Request:

fetch('/api/user/1', {
	method: 'GET', 
	headers: {
		'Content-Type': 'text/html'
	}
}).then(function() {  });

Obiekt Response

W odpowiedzi na nasze żądanie zwracany jest obiekt Response. Nic jednak nie stoi na przeszkodzie aby zainicjować go samodzielnie (ponownie dotyczy sytuacji z Service Workers). Tworzenie instancji wygląda wtedy tak:

var myResponse = new Response(body, init);

Gdzie body to jeden z obiektów Blob, BufferSource, FormData, URLSearchParams, USVString definiujących ciało odpowiedzi. Init natomiast jest obiektem naszych własnych ustawień:

status – status odpowiedzi np. 200, 404
statusText – tekstowa reprezentacja statusu, czyli OK, NOT FOUND itd.
headers – nagłówki odpowiedzi w postaci obiektu Headers

Metody:

clone() – klonuje obiekt Response
error() – zwraca nowy obiekt Response powiązany z błędem sieci
redirect(url, status) – tworzy przekierowanie pod inny adres

Oraz pozostałe metody z mixin Body. Podobnie jak w obiekcie Request, jest też kilka własności tylko do odczytu:

Własności:

url – adres URL którego dotyczyło żądanie
ok – Boolean jeżeli odpowiedzią był kod z zakresu 200 – 299
status – kod statusu np. 200, 404
statusText – tekstowa reprezentacja statusu, np. “OK”
type – basic, error, opaque, cors

Teorii trzeba przyznać jest sporo, jednak zawsze najlepiej uczyć się nowych technologii na przykładach. Najlepiej takich, które można wykorzystać na co dzień.

Przykłady użycia

Jedną z najciekawszych właściwości Promise jest możliwość ich łączenia. Daje nam to korzyść w postaci odseparowania poszczególnych funkcji. Przykładowo, wiadomo że strona nie wysyła wyłącznie jednego żądania. Powtarzanie zatem ciągle i ciągle kodu, odpowiedzialnego za obsługę błędów czy różnych typów odpowiedzi (text, json, html) nie ma najmniejszego sensu. Możemy to w łatwy sposób osiągnąć dzieląc nasz kod na funkcje, z których każda zajmuje się tylko jedną czynnością. Funkcja taka powinna zwracać kolejne Promise.

function status(response) {  
    if (response.status >= 200 && response.status < 300) {  
        return Promise.resolve(response)  
    } else {  
        return Promise.reject(new Error(response.statusText))  
    }  
}

function parseJson(response) {  
    return response.json()  
}

function parseText(response) {  
    return response.text()  
}
Przetworzenie odpowiedzi JSON
fetch('/api/user/1')  
    .then(status)  
    .then(parseJson)  
    .then(function(data) {  
        console.log('Odpowiedź JSON', data);  
    }).catch(function(error) {  
        console.log('Żądanie nie powiodło się', error);  
    });
Przetworzenie odpowiedzi HTML / Text

To samo możemy teraz powtórzyć dla odpowiedzi HTML, ale chcemy ją wysłać metodą POST

fetch('/api/user/1', { method: 'POST' })  
    .then(status)  
    .then(parseText)  
    .then(function(data) {  
        console.log('Odpowiedź HTML / Text', data);  
    }).catch(function(error) {  
        console.log('Żądanie nie powiodło się', error);  
    });
Wysłanie danych z formularza

Bezpośrednio pobierając nasz formularz:

fetch('/form-submit', {
	method: 'POST',
	body: new FormData(document.getElementById('nasz-formularz'))
});

Wysyłając jako parametry (w odpowiedzi otrzymujemy JSON)

fetch('/form-submit', {  
      method: 'POST',  
      headers: {  
          "Content-type": "application/x-www-form-urlencoded; charset=UTF-8"  
      },  
      body: 'foo=bar&a=b'  
  })
  .then(parseJson).then...
Przetworzenie grafiki
fetch('obrazek.jpg')
	.then(function(response) {
	  return response.blob();
	})
	.then(function(imageBlob) {
	  document.querySelector('img').src = URL.createObjectURL(imageBlob);
	});

Podsumowanie

JavaScript jest obecnie potężnym językiem który nieustannie się rozwija i zyskuje co chwila nowe funkcje. Obiekt XHR ma już swoje lata i cieszę się, że powstał jego godny następca. Tym bardziej widać dążenie do pewnej unifikacji różnych rozwiązań. Sam fakt iż w odmiennych językach client- i server-side zaczynamy pracować na obiektach Request i Response (przykładowo ukończona niedawno specyfikacja PSR-7 w PHP) jest tego najlepszym dowodem. Cała magia protokołu HTTP to właśnie żądania i odpowiedzi, zatem takie podejście jest dla programistów jak najbardziej naturalne. I aż dziwne że dopiero teraz pewnie rozwiązania zostały wprowadzone. Mnie Fetch API jak najbardziej przekonał.

Komentarze

Nie ma jeszcze żadnych komentarzy do wyświetlenia. Może chcesz zostać pierwszą osobą która podzieli się swoją opinią?

Dodaj komentarz

*
Nazwa zostanie wyświetlona wraz z komentarzem. Możesz też utworzyć nowe konto w serwisie, dzięki czemu uzyskasz dodatkową funkcjonalność.
*
Akceptowana jest ograniczona składnia Textile. Wszystkie tagi HTML zostaną usunięte.