Moduł10 - Zajęcia 19 - Zapytania HTTP

Protokół HTTP

Zanim użytkownik zobaczy na ekranie zawartość strony, przeglądarka wysyła do serwera żądanie otrzymania tej właśnie treści. Plik HTML, obrazy, style i skrypty pochodzą z serwera za pośrednictwem protokołu HTTP - zestawu reguł i konwencji wykorzystywanych podczas przesyłania danych przez sieć.

HyperText Transfer Protocol (protokół przesyłania hipertekstu) jest szeroko rozpowszechnionym protokołem przesyłania różnych typów zasobów internetowych: html, css, javascript, obrazów, audio i wideo itp.

HTTP bazuje na strukturze klient-serwer oraz modelu żądanie-odpowiedź, w którym aplikacja kliencka inicjuje połączenie, tworzy żądanie i wysyła je do serwera, po czym serwer przetwarza to żądanie, generuje odpowiedź i odsyła ją do klienta. Komunikacja między nimi odbywa się poprzez szereg naprzemiennych żądań HTTP i odpowiedzi HTTP.

Żądanie odbywa się w kilku etapach:

  • Zapytanie DNS - wyszukiwanie najbliższego serwera DNS, aby przetłumaczyć adres (na przykład google.com) na jego numeryczną reprezentację, adres IP (74.125.87.99).
  • Połączenie - nawiązanie połączenia z serwerem z wykorzystaniem otrzymanego adresu IP.
  • Wysyłanie danych - przekazywanie pakietów od klienta do serwera.
  • Oczekiwanie na odpowiedź - oczekiwanie na dotarcie pakietów danych do serwera, serwer je przetworzy i odpowiedź wróci.
  • Odbieranie danych - pakiety dotarły, możesz odbierać z nich dane.

Protokół HTTPS

HyperText Transfer Protocol Secure to dodatek do protokołu HTTP, w którym wszystkie wiadomości między klientem a serwerem są szyfrowane w celu zwiększenia bezpieczeństwa. Zapewnia ochronę przed atakami podsłuchowymi. Dane są przesyłane protokołami kryptograficznymi SSL lub TLS.

Podczas komunikacji przez zwykłe połączenie HTTP wszystkie dane są przesyłane w postaci tekstu i mogą być odczytane przez każdego, kto uzyskał dostęp do połączenia między klientem a serwerem. Jeśli użytkownicy robią zakupy online i wypełniają formularz zamówienia zawierający informacje o karcie kredytowej, ich dane finansowe są znacznie łatwiejsze do kradzieży, jeśli są przesyłane jako tekst. Dzięki HTTPS dane zostaną zaszyfrowane i haker nie będzie mógł ich odszyfrować, ponieważ odszyfrowanie wymaga dostępu do klucza prywatnego przechowywanego na serwerze.

Protokół HTTPS zapewnia, że informacje o klientach, takie jak numery kart kredytowych, są szyfrowane i nie można ich przechwycić w formie odszyfrowanej. Odwiedzający mogą potwierdzić, że strona jest bezpieczna, patrząc na ikonę po lewej stronie paska adresu, zabezpieczone połączenia są oznaczone ikoną kłódki.

REST API

Serwer to komputer ze specjalnym oprogramowaniem. Backend to program znajdujący się na serwerze, który może przetwarzać przychodzące żądania HTTP i posiada zestaw gotowych akcji dla określonych żądań.

API (interfejs programowania aplikacji) - zestaw dobrze zdefiniowanych reguł komunikacji pomiędzy różnymi komponentami oprogramowania. Interfejs opisuje, o co możesz poprosić program i jaki będzie wynik.

REST (representational state transfer) - to styl architektury backendu oparty na zestawie zasad opisujących sposób definiowania i adresowania zasobów sieciowych.

REST API - backend zbudowany na zasadzie REST. Służy jako warstwa między aplikacją internetową a bazą danych. Posiada standardowy interfejs dostępu do zasobów. Działa jak strona internetowa, wysyłamy żądanie HTTP od klienta do serwera i w odpowiedzi zamiast strony HTML otrzymujemy dane w formacie JSON.

Format żądania

Usługa REST wymaga od klienta złożenia żądania dodania, pobrania lub zmodyfikowania danych. Żądanie może składać się z następujących części.

Entry Description
Metoda HTTP Określa, jaką operację wykonać.
Nagłówki HTTP Umożliwia klientowi przesyłanie informacji serwisowych dotyczących żądania.
Ścieżka Ścieżka do zasobu. Dostępne ścieżki są opisane w dokumentacji backendu.
Ciało Blok żądania zawierający dane.

Metody HTTP

Istnieje kilka podstawowych metod HTTP do pracy z usługą REST.

Method Description
POST Utwórz nowy zasób
GET Uzyskaj zestaw zasobów lub jeden zasób
PUT Zaktualizuj istniejący lub utwórz nowy zasób
PATCH Zaktualizuj istniejący zasób
DELETE Usuń zasób

Nagłówki HTTP

Nagłówki zawierają informacje serwisowe związane z treścią żądania. Na przykład typ treści, którą klient może przetworzyć w odpowiedzi z serwera (nagłówek Accept) lub opisujący typ zasobu, który klient wysyła do serwera lub serwer wysyła do klienta (nagłówek Content-Type).

                Accept: text/html
                Content-Type: application/json
            

Typy MIME - opcje typów treści. Używane do wskazania treści żądania i odpowiedzi, składają się z typu i podtypu oddzielonych ukośnikiem /. Na przykład plik tekstowy zawierający HTML zostanie opisany jako typ text/html. Jeśli plik zawiera CSS, zostanie opisany jako text/css. Dane JSON będą opisane jako application/json. Jeśli klient oczekuje text/css, ale odbiera application/json, nie będzie w stanie rozpoznać i przetworzyć treści odpowiedzi.

Ścieżki

Żądania muszą zawierać ścieżkę do zasobu, na którym wykonywana jest operacja. Dostępne ścieżki (punkty końcowe, zasoby) są opisane w dokumentacji backendu.

            GET https://bookstore.com/api/orders
            Accept: application/json
          

Taka ścieżka wyraźnie wskazuje zasób, nawet jeśli nigdy wcześniej go nie widziałeś, ponieważ jest hierarchiczny i opisowy. Realizujemy żądanie pobrania zbioru zamówień.

W celu uzyskania jednego elementu kolekcji do zasobu dodawany jest jego identyfikator. Rozważmy żądanie odczytania jednego zamówienia o identyfikatorze 289.

            GET https://bookstore.com/api/orders/289
            Accept: application/json
          

Ostatnia część ścieżki nazywana jest parametrem dynamicznym i jest opisana w dokumentacji jako /zasób/:parametr. Zasób pozostaje niezmieniony, jest to ścieżka do całej kolekcji, a wartość parametru zmienia się dla każdego z jej elementów.

Kody odpowiedzi

Serwer wysyła odpowiedź na żądanie klienta, która zawiera kod statusu informujący klienta o wyniku operacji. Kody podzielone są na grupy.

Group Description
1XX W celach informacyjnych
2XX Kody sukcesu
3XX Opisują wszystko związane z przekierowaniem (redirect)
4XX Wskazują błędy po stronie klientów
5XX Wskazują błędy po stronie serwera

Nie trzeba pamiętać wszystkich kodów z każdej grupy, wystarczy znać te najczęstsze. Resztę można zawsze znaleźć w odniesienie do kodów HTTP.

Code Description
200 (OK) Standardowa odpowiedź na udane żądania HTTP.
201 (Created) Standardowa odpowiedź na żądanie HTTP, które zakończyło się pomyślnym utworzeniem zasobu.
400 (Bad Request) Nie można przetworzyć żądania z powodu nieprawidłowej składni żądania lub innego błędu klienta.
401 (Unauthorized) Aby uzyskać dostęp do zasobu, wymagana jest autoryzacja.
403 (Forbidden) Klient nie ma uprawnień dostępu do tego zasobu.
404 (Not Found) W tej chwili nie znaleziono zasobu. Być może został usunięty lub jeszcze nie istnieje.
500 (Internal Server Error) Ogólna odpowiedź na nieoczekiwaną awarię serwera, chyba że dostępne są bardziej szczegółowe informacje.

Żądanie-Odpowiedź

Załóżmy, że mamy aplikację, która umożliwia przeglądanie, tworzenie, edytowanie i usuwanie klientów i zamówień z małej księgarni z backendem hostowanym pod adresem bookstore.com/api. Wykorzystując zdobytą wiedzę, za pomocą pseudokodu opisujemy proces żądanie-odpowiedź do backendu.

Jeśli chcemy uzyskać dane o wszystkich klientach, żądanie GET będzie wyglądać tak.

            GET bookstore.com/api/customers
            Accept: application/json
          

Na co serwer wyśle nam taką odpowiedź.

            Status: 200 OK
            Content-Type: application/json
            Body: dane JSON dla wszystkich klientów
          

Aby otrzymać dane jednego klienta, wskazujemy jego identyfikator, określając żądanie GET.

            GET bookstore.com/api/customers/289
            Accept: application/json
          

Na co serwer wyśle nam taką odpowiedź.

            Status: 200 OK
            Content-Type: application/json
            Body: dane JSON o kliencie
          

W celu dodania nowego klienta wykonujemy żądanie POST.

            POST bookstore.com/api/customers
            Content-Type: application/json
            Body: { "username": "Mango", "email": "mango@gmail.com" }
          

Serwer dodaje unikalny identyfikator i jako wynik zwraca cały obiekt.

            Status: 201 Created
            Content-type: application/json
            Body: { "id": 18674, "username": "Mango", "email": "mango@gmail.com" }
          

AJAX

AJAX (Asynchronous JavaScript and XML) - metoda ściągania lub wysyłania danych, a następnie aktualizowania interfejsu o te dane, bez konieczności przeładowywania strony. Skraca to czas odpowiedzi i sprawia, że strona internetowa jest bardziej interaktywna. Proces ten można zobaczyć na przykładzie ładowania danych.

  1. Na stronie internetowej ma miejsce zdarzenie (strona ładuje się, kliknięty jest przycisk "Pokaż więcej", przesyłany formularz itp.).
  2. Na kliencie za pomocą JavaScript w odpowiedzi na to zdarzenie zostanie wykonana funkcja do pracy z serwerem, na którym tworzone i wysyłane jest żądanie HTTP.
  3. Serwer odbiera i przetwarza żądanie HTTP, odsyłając w odpowiedzi dane w formacie JSON.
  4. Na kliencie za pomocą JavaScript przetwarzana jest odpowiedź z serwera, odczytywane są dane i aktualizowany interfejs.

Mimo to, że w nazwie technologii występuje XML, we współczesnej sieci został zastąpiony przez JSON, a nazwę pozostawiono w hołdzie pamięci. AJAX oznacza dowolną komunikację z serwerem bez przeładowywania strony.

Fetch API

Zbudowany w przeglądarce interfejs dostępny w obiekcie window, który zawiera zestaw właściwości i metod do wysyłania, odbierania i przetwarzania zasobów z serwera. Metoda fetch() zapewnia nowoczesny interfejs do wysyłania żądań do serwera i jest zbudowana na obietnicach.

            fetch(url, options)
          
  • url - ścieżka do danych na backendzie, które należy pobrać, utworzyć lub zmodyfikować. Wymagany argument.
  • options - obiekt ustawień żądania: metoda (domyślnie GET), nagłówki, ciało itp. Argument opcjonalny.

Będziemy robić żądania do JSONPlaceholder API - publicznego API REST do szybkiego prototypowania, który zapewnia kolekcję fałszywych użytkowników w zasobie /users.

            fetch("https://jsonplaceholder.typicode.com/users")
            .then(response => {
                // Response handling
            })
            .then(data => {
                // Data handling
            })
            .catch(error => {
                // Error handling
            });
          

Sprawdzanie odpowiedzi

Wartość obietnicy, która zwraca metodę fetch(), to obiekt z informacją serwisową o stanie odpowiedzi serwera. Instancja klasy Response wyposażona w różne metody i właściwości. W zależności od typu odbieranej zawartości używana jest inna metoda konwersji treści odpowiedzi na dane.

  • json() - parsuje dane w formacie JSON.
  • text() - analizuje dane w formacie zwykłego tekstu, na przykład .csv (dane tabelaryczne).
  • blob() - analizuje dane opisujące plik, takie jak obraz, audio lub wideo.

Pierwsza metoda then() sprawdza status odpowiedzi i konwertuje dane do prawidłowego formatu lub jawnie zgłasza błąd, żeby obsłużyć nieudane żądanie HTTP w bloku catch().

            fetch("https://jsonplaceholder.typicode.com/users")
            .then(response => {
                if (!response.ok) {
                    throw new Error(response.status);
                }
                return response.json();
            })
            .then(data => {
                // Data handling
            })
            .catch(error => {
                // Error handling
            });
          

Jest to konieczne, aby fetch() poprawnie zareagowała na status kod 404, który technicznie nie jest błędem, ale nie jest pomyślnym wynikiem dla klienta.

Karta Network

W narzędziach programistycznych karta Network wyświetla wszystkie żądania HTTP wykonywane na stronie. Wybranie filtra XHR, spowoduje pozostawienie tylko żądań do backendu. Po kliknięciu przycisku w przykładzie, po chwili żądanie zostanie wyświetlone na liście. Wybierając go, możesz wyświetlić informacje służbowe i treść odpowiedzi na podkartach Headers, Preview i Response.

Praca z publicznym REST API

Każdy backend jest wyjątkowy, są ich tysiące. Z drugiej strony API REST są zbudowane na standardowej architekturze. Oznacza to, że możesz zrozumieć zasadę ich działania, po czym wystarczy zapoznać się z dokumentacją backendu, którego potrzebujesz.

Kontynuujemy pracę z JSONPlaceholder API. Z dokumentacji pobieramy adres URL zasobu, aby zażądać informacji o zbiorze fałszywych użytkowników

            https://jsonplaceholder.typicode.com/users
          

Ta ścieżka składa się z następujących części:

  • https://jsonplaceholder.typicode.com - to punkt końcowy, podstawowy adres URL, punkt wejścia API.
  • /users - zasób, do którego uzyskujemy dostęp.

Klikając przycisk "Fetch users" wykonamy żądanie GET i na podstawie otrzymanych danych renderujemy listę użytkowników.

Przykład -------------------------------

JavaScript:                
const fetchUsersBtn = document.querySelector(".btn");
const userList = document.querySelector(".user-list");

fetchUsersBtn.addEventListener("click", () => {
  fetchUsers()
    .then((users) => renderUsers(users))
    .catch((error) => console.log(error));
});

function fetchUsers() {
  return fetch("https://jsonplaceholder.typicode.com/users").then(
    (response) => {
      if (!response.ok) {
        throw new Error(response.status);
      }
      return response.json();
    }
  );
}

function renderUsers(users) {
  const markup = users
    .map((user) => {
      return `<li>
          <p><b>Name</b>: ${user.name}</p>
          <p><b>Email</b>: ${user.email}</p>
          <p><b>Company</b>: ${user.company.name}</p>
        </li>`;
    })
    .join("");
  userList.insertAdjacentHTML("beforeend", markup);
}
            

    PrzykładEND -------------------------------

    Zauważ, że funkcja fetchUsers() zwraca obietnicę, więc do wyniku wywołania dodajemy łańcuch metodthen() i catch().

    Błąd początkującego

    Przyjrzyjmy się częstym błędom podczas pracy z kodem asynchronicznym - próbie użycia danych żądania HTTP poza wywołaniem zwrotnym metody then(). Początkujący próbuje zapisać „wynik fetch" do zmiennej zewnętrznej i użyć jej w dalszej części kodu natychmiast po wywołaniu metody fetch().

                let globalVariable; // undefined
    
                // Initializing data fetching
                fetch("https://jsonplaceholder.typicode.com/users")
                .then(response => response.json())
                .then(users => {
                    console.log("users inside then callback: ", users);
    
                    // Writing the result to a global variable
                    globalVariable = users;
    
                    // Everything is ok here, the data is in the variable
                    console.log("globalVariable inside fetch callback: ", globalVariable);
                });
    
                // No async data here
                console.log("globalVariable outside fetch: ", globalVariable); // undefined
              

    Poza wywołaniem zwrotnym metody then() nie ma danych, ponieważ ostatni console.log() zostanie wykonany przed nadejściem odpowiedzi z serwera. W tym czasie zmienna globalVariable nadal zawiera undefined. Wartość spełnionej obietnicy jest dostępna tylko w wywołaniu zwrotnym metody then().

    Parametry ciągu zapytania

    Parametry zapytania pozwalają określić dodatkowe kryteria dla backendu. Na przykład, ile elementów kolekcji chcemy otrzymać w żądaniu, być może musimy dodać sortowanie po jakiejś właściwości obiektu, ograniczyć wybór itp. Lista parametrów zapytania, ich nazwy i możliwe wartości zależą od backendu i są opisane w dokumentacji.

    Znak ? wskazuje początek parametrów zapytania. Każdy parametr jest parą nazwa=wartość. Znak & jest używany do wskazania znaczącego „AND", oddzielając parametry w ciągu zapytania.

                const url = "https://jsonplaceholder.typicode.com/users?_limit=7&_sort=name";
              

    Takie żądanie GET zwróci tablicę siedmiu użytkowników (w sumie 10) posortowanych według nazwy (pole name) w kolejności alfabetycznej. Podkreślenia w nazwach parametrów są specyficzne dla tego backendu, nie jest to jakiś standard.

    Przykład -------------------------------

    const fetchUsersBtn_s3a7 = document.querySelector('button#btn-s3a7');
    const userList_s3a7 = document.querySelector('ul#user-list-s3a7');
    
    fetchUsersBtn_s3a7.addEventListener('click', () => {
      fetchUsers_s3a7()
        .then(users => renderUsers_s3a7(users))
        .catch(error => onError(error));
    });
    
    function onError(error) {
      Notify.failure(`${error}`, optionsNotify);
      console.log(error);
    }
    
    function fetchUsers_s3a7() {
      return fetch(
        'https://jsonplaceholder.typicode.com/users?_limit=7&_sort=name'
      ).then(response => {
        if (!response.ok) {
          throw new Error(response.status);
        }
        return response.json();
      });
    }
    
    function renderUsers_s3a7(users) {
      const markup = users
        .map(user => {
          return `
              <li>
                <p><b>Name</b>: ${user.name}</p>
                <p><b>Email</b>: ${user.email}</p>
                <p><b>Company</b>: ${user.company.name}</p>
              </li>
          `;
        })
        .join('');
      userList_s3a7.insertAdjacentHTML('beforeend', markup);
    }
                

      PrzykładEND -------------------------------

      Klasa URLSearchParams

      Parametrów może być wiele i nie jest wygodnie komponować z nich jedną długą linię, zarówno dla czytelności, jak i późniejszej edycji. Podczas tworzenia ciągu parametrów tworzona jest instancja klasy URLSearchParams i inicjowana za pomocą obiektu. Wynikiem będzie specjalny obiekt (iterator) z metodami, który w konwersji ciągu znaków zwraca wynik wywołania metody toString() - ciąg znaków.

                  const searchParams = new URLSearchParams({
                    _limit: 5,
                    _sort: "name",
                  });
      
                  console.log(searchParams.toString()); // "_limit=5&_sort=name"
      
                  const url = `https://jsonplaceholder.typicode.com/users?${searchParams}`;
                  console.log(url); // "https://jsonplaceholder.typicode.com/users?_limit=5&_sort=name"
                

      W postaci ciągu właściwości obiektu stają się parametrami i ich wartościami. Parametry zostaną oddzielone znakiem &. Podczas interpolacji wartości w ciągach szablonu jest on niejawnie konwertowany na ciąg, więc nie trzeba wywoływać metody toString() podczas konstruowania adresu URL. Nie zapomnij wskazać początku ciągu zapytania znakiem ?.

      Przykład -------------------------------

      const fetchUsersBtn_s3a8 = document.querySelector('button#btn-s3a8');
      const userList_s3a8 = document.querySelector('ul#user-list-s3a8');
      
      fetchUsersBtn_s3a8.addEventListener('click', () => {
        fetchUsers_s3a8()
          .then(users => renderUsers_s3a8(users))
          .catch(error => onError(error));
      });
      
      function onError(error) {
        Notify.failure(`${error}`, optionsNotify);
        console.log(error);
      }
      const searchParams_s3a8 = new URLSearchParams({
        _limit: 5,
        _sort: 'name',
      });
      const url_s3a8 = `https://jsonplaceholder.typicode.com/users?${searchParams_s3a8}`;
      function fetchUsers_s3a8() {
        return fetch(url_s3a8).then(response => {
          if (!response.ok) {
            throw new Error(response.status);
          }
          return response.json();
        });
      }
      
      function renderUsers_s3a8(users) {
        const markup = users
          .map((user, index) => {
            return `
                <li>
                  <p><b>${index + 1} Name</b>: ${user.name}</p>
                  <p><b>Email</b>: ${user.email}</p>
                  <p><b>Company</b>: ${user.company.name}</p>
                </li>
            `;
          })
          .join('');
        userList_s3a8.insertAdjacentHTML('beforeend', markup);
      }
                  

        PrzykładEND -------------------------------

        Nagłówki HTTP

        Klasa Headers umożliwia wykonywanie różnych akcji w nagłówkach żądań i odpowiedzi HTTP. Działania te obejmują pobieranie, konfigurowanie, dodawanie i usuwanie nagłówków.

                    const headers = new Headers({
                      "Content-Type": "application/json",
                      "X-Custom-Header": "custom value",
                    });
        
                    headers.append("Content-Type", "text/bash");
                    headers.append("X-Custom-Header", "custom value");
                    headers.has("Content-Type"); // true
                    headers.get("Content-Type"); // "text/bash"
                    headers.set("Content-Type", "application/json");
                    headers.delete("X-Custom-Header");
                

        W praktyce prosty literał obiektowy z właściwościami jest zwykle używany do tworzenia nagłówków żądań. W takim przypadku nie będzie żadnych metod, które często nie są wymagane.

                    const headers = {
                      "Content-Type": "application/json",
                      "X-Custom-Header": "custom value",
                    };
                  

        Żądanie używające nagłówków wyglądałoby tak.

                    fetch("https://jsonplaceholder.typicode.com/users", {
                      headers: {
                        Accept: "application/json",
                      },
                    }).then(response => {
                      // ...
                    });
                  

        Nowoczesne przeglądarki domyślnie dodają wiele nagłówków, w zależności od operacji i treści żądania, więc nie ma potrzeby jawnego określania standardowych nagłówków.

        Żądania międzydomenowe

        Domyślnie żądanie HTTP można wykonać tylko w bieżącej witrynie. Podczas próby wysłania żądania do innej domeny, portu lub protokołu, czyli wykonania żądania międzydomenowego, przeglądarka wyświetla błąd. Odbywa się to ze względów bezpieczeństwa, a uprawnienia są konfigurowane na backendzie. Jeśli backend nie obsługuje żądań międzydomenowych, programista front-end nie będzie mógł nic z tym zrobić w swoim kodzie.

        Ta polityka przeglądarki nazywa się CORS i oznacza Cross-Origin Resource Sharing, gdzie Origin to domena, port lub protokół. Dosłowne tłumaczenie brzmi jak „współdzielenie zasobów między różnymi źródłami".

        Z każdym żądaniem przeglądarka sama dodaje nagłówek HTTP Origin, który wskazuje adres strony internetowej, która chce wysłać żądanie HTTP. Na przykład, jeśli wysyłamy żądanie fetch ze strony https://my-site.com/about do https://my-api.com/users, nagłówki będą wyglądać następująco.

                    GET /users
                    Host: my-api.com
                    Origin: https://my-site.com
                  

        Serwer sprawdza nagłówek Origin i, jeśli obsługuje żądania międzydomenowe, dodaje do odpowiedzi specjalny nagłówek HTTP Access-Control-Allow-Origin.

                    # Private API
                    Access-Control-Allow-Origin: https://my-site.com
        
                    # Public API
                    Access-Control-Allow-Origin: *
                  

        Wartością tego nagłówka będzie rozwiązane źródło (Origin). W naszym przypadku powinna to być jedna strona https://my-site.com, jeśli backend jest prywatny, lub znak specjalny *, jeśli backend jest publiczny, umożliwia wysyłanie żądań do każdego.

        Oznacza to, że przeglądarka jest rodzajem pośrednika między kodem JavaScript a backendem. Dodaje nagłówek Origin z poprawną wartością do każdego żądania i sprawdza nagłówek Access-Control-Allow-Origin w odpowiedzi. Jeśli nagłówek jest obecny i jego wartość jest zgodna, oryginalne żądanie zostanie wykonane, a kod JavaScript otrzyma swój wynik, w przeciwnym razie wystąpi błąd CORS.