Rozdział 14. Żądania HTTP

> Dodaj do ulubionych

Jak wspomniałem w rozdziale 11, komunikacja na stronach WWW odbywa się za pośrednictwem protokołu HTTP. Proste żądanie HTTP może wyglądać tak:

GET /wp-content/ejs/files/fruit.txt HTTP/1.1
Host: eloquentjavascript.net
User-Agent: Jakaś przeglądarka

Jest to żądanie pliku fruit.txt do serwera znajdującego się pod adresem eloquentjavascript.net. Ponadto wiadomo, że użyty został protokół HTTP 1.1 ― wersja 1.0 również jest nadal używana i działa nieco inaczej. Wiersze Host i User-Agent mają strukturę odpowiadającą następującemu wzorcowi: słowo określające zawarte w nich informacje, średnik oraz sama informacja. Są to tzw. nagłówki HTTP. Nagłówek User-Agent informuje serwer, jaka przeglądarka (lub jaki program) wysłała żądanie. Dodatkowo zazwyczaj wysyłane są jeszcze inne nagłówki, np. określające typy dokumentów rozpoznawane przez klienta czy preferowany język.

W odpowiedzi na powyższe żądanie serwer może zwrócić następujące dane:

HTTP/1.1 200 OK
Last-Modified: Mon, 23 Jul 2007 08:41:56 GMT
Content-Length: 24
Content-Type: text/plain

jabłka, pomarańcze, banany

W pierwszym wierszu określona jest wersja protokołu HTTP oraz znajduje się informacja o stanie żądania. W tym przypadku kod stanu to 200, co oznacza, że wszystko jest w porządku, nic niezwykłego się nie zdarzyło i został przesłany plik. Dalej znajduje się kilka nagłówków określających datę ostatniej modyfikacji pliku oraz jego długość i typ (zwykły tekst). Po nagłówkach znajduje się pusty wiersz, a za nim sam plik.

Podczas gdy żądania zaczynające się od słowa GET, oznaczającego, że klient chce tylko pobrać dokument, za pomocą słowa POST można informować, że w żądaniu do serwera wysyłane są jakieś informacje, które serwer ma w jakiś sposób przetworzyć.1


Gdy użytkownik kliknie łącze, zatwierdzi formularz albo inaczej spowoduje przejście do innej strony w przeglądarce, nastąpi wysłanie żądania HTTP i usunięcie starej strony, aby załadować nowy dokument. Zazwyczaj właśnie o to chodzi, bo tak działa sieć internetowa. Czasami jednak programy JavaScript potrzebują komunikacji z serwerem bez ponownego wczytywania strony. Na przykład przycisk Załaduj w konsoli służy do ładowania plików bez opuszczania strony.

Aby to było możliwe, program musi samodzielnie sporządzić żądanie HTTP. W nowoczesnych przeglądarkach dostępny jest specjalny interfejs, który pozwala to robić. Podobnie jak jest w przypadku otwierania nowych okien, interfejs ten podlega pewnym obostrzeniom. Aby uniemożliwić skryptom działanie na szkodę użytkowników, pozwolono im wysyłać żądania HTTP tylko do tej samej domeny, w której znajduje się strona, z której pochodzą.


Obiekt XMLHttpRequest

Obiekt do tworzenia żądań HTTP w większości przeglądarek można utworzyć przy użyciu instrukcji new XMLHttpRequest(). W starszych wersjach Internet Explorera, w których pierwotnie te obiekty były używane, trzeba pisać new ActiveXObject("Msxml2.XMLHTTP") lub, w jeszcze starszych wersjach, new ActiveXObject("Microsoft.XMLHTTP"). ActiveXObject to interfejs przeglądarki Internet Explorer do różnych dodatków. Jesteśmy już przyzwyczajeni do pisania opakowań niwelujących różnice między przeglądarkami, a więc napisanie jeszcze jednego nie sprawi nam kłopotu:

function makeHttpObject() {
  try {return new XMLHttpRequest();}
  catch (error) {}
  try {return new ActiveXObject("Msxml2.XMLHTTP");}
  catch (error) {}
  try {return new ActiveXObject("Microsoft.XMLHTTP");}
  catch (error) {}

  throw new Error("Nie można utworzyć obiektu żądania HTTP.");
}

show(typeof(makeHttpObject()));

Powyższa funkcja próbuje utworzyć obiekt na trzy sposoby i wykrywa, które techniki nie zadziałały za pomocą konstrukcji trycatch. Jeśli żadna z metod nie zadziała, co może się zdarzyć w bardzo starych przeglądarkach i przy włączonych wysokich zabezpieczeniach, zgłaszany jest błąd.

A dlaczego obiekt ten nazywa się żądaniem XML HTTP ? Nazwa ta jest trochę myląca. Język XML służy do przechowywania danych tekstowych. Jego składnia zawiera elementy i atrybuty podobne do języka HTML, ale jest on bardziej uporządkowany i elastyczniejszy — do przechowywania własnych rodzajów danych można definiować własne elementy XML. Obiekty żądań HTTP mają wbudowane funkcje do obsługi dokumentów XML i dlatego właśnie mają XML w nazwie. Za ich pomocą można jednak też obsługiwać inne typy dokumentów i z mojego doświadczenia wynika, że do tych celów wykorzystuje się je równie często.


Tworzenie żądania HTTP

Mając obiekt żądania HTTP możemy utworzyć żądanie podobne do wcześniej pokazanego przykładu.

var request = makeHttpObject();
request.open("GET", "/wp-content/ejs/files/fruit.txt", false);
request.send(null);
print(request.responseText);

Metoda open służy do konfigurowania żądań. W tym przypadku utworzyliśmy żądanie GET pliku fruit.txt. Podany tu adres URL jest względny, tzn. nie zawiera części http:// ani nazwy serwera, dzięki czemu plik będzie szukany na serwerze, z którego został pobrany bieżący dokument. Trzeci parametr, false, jest opisany trochę dalej. Po wywołaniu metody open żądanie można wysłać przy użyciu metody send. Jeśli żądanie jest typu POST, to dane, które mają być wysłane do serwera (jako łańcuch) można przekazać jako argument tej metody. W przypadku żądania GET, należy przekazać wartość null.

Po wykonaniu żądania, własność responseText obiektu żądania zawiera treść otrzymanego dokumentu. Nagłówki przesłane przez serwer można znaleźć przy użyciu funkcji getResponseHeader i getAllResponseHeaders. Pierwsza znajduje konkretny nagłówek, a druga zwraca łańcuch zawierający wszystkie nagłówki. Dzięki nim można czasami zdobyć dodatkowe informacje o dokumencie.

print(request.getAllResponseHeaders());
show(request.getResponseHeader("Last-Modified"));

Jeśli trzeba dodać nagłówki do żądania wysyłanego na serwer, można użyć metody setRequestHeader. Metoda ta przyjmuje dwa argumenty: nazwę i wartość nagłówka.

Kod odpowiedzi, którym w przykładzie była liczba 200, znajduje się we własności status. Jeśli coś się nie uda, w tym kodzie znajdziemy odpowiednią informację. Na przykład kod 404 oznacza, że żądany plik nie istnieje. Własność statusText zawiera trochę bardziej zrozumiały opis zaistniałego stanu.

show(request.status);
show(request.statusText);

Aby dowiedzieć się czy żądanie zostało poprawnie spełnione, najczęściej wystarczy porównać zawartość własności status z wartością 200. Teoretycznie serwer może w niektórych przypadkach zwrócić kod 304 oznaczający, że nadal aktualna jest starsza wersja dokumentu przechowywana w pamięci podręcznej przeglądarki. Ale przeglądarki chyba chronią nas przed tym ustawiając status na 200 nawet, gdy jest to 304. Ponadto, jeśli żądanie jest wysyłane przy użyciu innego protokołu niż HTTP2, np. FTP, własność status jest bezużyteczna, ponieważ protokół ten nie korzysta z kodów stanu HTTP.


Gdy żądanie zostanie wysłane tak jak w powyższym przykładzie, metoda send zwraca wartość dopiero po zakończeniu tego żądania. Jest to dla nas wygodne, ponieważ po zakończeniu działania metody send dostępna jest własność responseText, której możemy od razu zacząć używać. Jest jednak pewien problem. Jeśli serwer jest powolny albo plik jest duży, wykonania żądanie może sporo potrwać. W tym czasie program oczekuje, a wraz z nim cała przeglądarka. Dopóki żądanie nie zostanie zakończone, użytkownik nie może nic robić, nawet nie przewinie strony. Dlatego metoda ta nadaje się do użytku w sieciach lokalnych, które są szybkie i niezawodne. Ale na stronach w wielkim internecie nie powinno się jej stosować.

Tryb asynchroniczny

Ustawienie trzeciego argumentu metody open na true powoduje, że żądanie jest wysyłane w trybie asynchronicznym. W takim przypadku metoda send zwraca wartość natychmiast, a żądanie jest wykonywane w tle.

request.open("GET", "/wp-content/ejs/files/fruit.xml", true);
request.send(null);
show(request.responseText);

Odczekaj chwilę i…

print(request.responseText);

„Odczekanie chwilę” można zaimplementować przy użyciu funkcji setTimeout lub innej podobnej, ale jest na to lepszy sposób. Obiekt żądania ma własność readyState określającą stan tego obiektu. Gdy dokument jest w całości załadowany, jej wartość wynosi 4, a wcześniej jest mniejsza3. Aby reagować na zmiany tego stanu, można ustawić własność onreadystatechange obiektu na funkcję. Funkcja ta będzie wywoływana przy każdej zmianie stanu.

request.open("GET", "/wp-content/ejs/files/fruit.xml", true);
request.send(null);
request.onreadystatechange = function() {
  if (request.readyState == 4)
    show(request.responseText.length);
};

Jeśli plik otrzymany przez obiekt żądania jest dokumentem XML, we własności responseXML tego żądania znajduje się reprezentacja tego dokumentu. Reprezentacja ta ma podobne właściwości do drzewa DOM opisanego w rozdziale 12, z tym wyjątkiem, że nie ma elementów przeznaczonych do pracy z HTML-em, takich jak style czy innerHTML. Własność responseXML zawiera obiekt dokumentu, którego własność documentElement odnosi się do zewnętrznego elementu dokumentu XML.

var catalog = request.responseXML.documentElement;
show(catalog.childNodes.length);

Takich dokumentów XML można używać do wymiany danych strukturalnych z serwerem. Ich format — elementy znajdujące się w innych elementach — pozwala na przechowywanie informacji, których przesłanie w postaci zwykłego tekstu byłoby trudne. Jednak interfejs DOM nie najlepiej nadaje się do pobierania takich informacji, a dokumenty XML często bywają bardzo rozwlekłe: Można odnieść wrażenie, że dokument fruit.xml zawiera dużo informacji, a tak naprawdę informuje, że „jabłka są czerwone, pomarańcze są pomarańczowe, a banany są żółte”.


Format JSON

Alternatywnym dla XML-a formatem wymiany danych jest format JSON opracowany przez programistów JavaScript. W formacie tym wykorzystywana jest składnia wartości JavaScript do reprezentowania „hierarchicznych” informacji w zwięzły sposób. Dokument JSON jest plikiem zawierającym obiekt lub tablicę JavaScript zawierającą dowolną liczbę obiektów, tablic, łańcuchów, liczb, wartości logicznych oraz wartości null. Jako przykład możesz obejrzeć zawartość pliku fruit.json:

request.open("GET", "/wp-content/ejs/files/fruit.json", true);
request.send(null);
request.onreadystatechange = function() {
  if (request.readyState == 4)
    print(request.responseText);
};

Taki tekst można zamienić w zwykłą wartość JavaScript przy użyciu funkcji eval. Przed wywołaniem funkcji eval należy dodać nawias, ponieważ w przeciwnym razie JavaScript może zinterpretować obiekt (znajdujący się w klamrze) jako blok kodu i zgłosić błąd.

function evalJSON(json) {
  return eval("(" + json + ")");
}
var fruit = evalJSON(request.responseText);
show(fruit);

Przekazując funkcji eval fragment tekstu należy pamiętać, że tekst ten może wykonać dowolny kod. Jako że w JavaScripcie można wysyłać żądania tylko do własnej domeny, najczęściej wie się, jaki tekst się otrzyma i nie stanowi to problemu. Jednak w innych sytuacjach może to być niebezpieczne.


Ćwiczenie 14.1

Napisz funkcję o nazwie serializeJSON przyjmującą wartość JavaScript i zwracającą łańcuch będący reprezentacją tej wartości w formacie JSON. Proste wartości, takie jak liczby i logiczne, można zwyczajnie zamienić na łańcuchy za pomocą funkcji String. Obiekty i tablice można przetworzyć przy użyciu rekurencji.

Trudności może sprawiać rozpoznawanie tablic, które są typu object. Można użyć instrukcji instanceof Array, ale ta zadziała tylko w przypadku tablic utworzonych we własnym oknie ― inne używają prototypu Array z innych okien i instanceof zwróci dla nich false. Istnieje też tania sztuczka polegająca na przekonwertowaniu własności constructor na łańcuch i sprawdzeniu, czy zawiera "function Array".

Dokonując konwersji na typ łańcuchowy należy też zająć się znakami specjalnymi. Jeśli łańcuch zostanie umieszczony w podwójnym cudzysłowie, to specjalnymi sekwencjami trzeba będzie zastąpić znaki ", \, f, b, n, t, r oraz v4.

function serializeJSON(value) {
  function isArray(value) {
    return /^s*function Array/.test(String(value.constructor));
  }

  function serializeArray(value) {
    return "[" + map(serializeJSON, value).join(", ") + "]";
  }
  function serializeObject(value) {
    var properties = [];
    forEachIn(value, function(name, value) {
      properties.push(serializeString(name) + ": " +
                      serializeJSON(value));
    });
    return "{" + properties.join(", ") + "}";
  }
  function serializeString(value) {
    var special =
      {""": "\"", "\": "\\", "f": "\f", "b": "\b",
       "n": "\n", "t": "\t", "r": "\r", "v": "\v"};
    var escaped = value.replace(/["\fbntrv]/g,
                                function(c) {return special[c];});
    return """ + escaped + """;
  }

  var type = typeof value;
  if (type == "object" && isArray(value))
    return serializeArray(value);
  else if (type == "object")
    return serializeObject(value);
  else if (type == "string")
    return serializeString(value);
  else
    return String(value);
}

print(serializeJSON(fruit));

Sztuczka użyta w funkcji serializeString jest podobna do tego, co zrobiliśmy w funkcji escapeHTML w rozdziale 10. Zastępnik dla każdego znaku jest znajdowany w obiekcie. Niektóre z nich, jak choćby "\\", wyglądają dziwnie, ponieważ każdy ukośnik wsteczny, który ma się pojawić w wyniku musi mieć towarzyszący ukośnik wsteczny.

Zwróć też uwagę, że także nazwy własności są zapisane w cudzysłowach, jako łańcuchy. W niektórych przypadkach nie jest to konieczne, ale dla nazw zawierających spacje i jakieś inne dziwne rzeczy tak. Tu zostało użyte uproszczone rozwiązanie polegające na ujęciu w cudzysłów wszystkiego.


Jeśli często wysyłamy żądania, to oczywiście nie chcemy w kółko powtarzać rytuału open, send, onreadystatechange. Możemy napisać bardzo proste opakowanie, jak poniższe:

function simpleHttpRequest(url, success, failure) {
  var request = makeHttpObject();
  request.open("GET", url, true);
  request.send(null);
  request.onreadystatechange = function() {
    if (request.readyState == 4) {
      if (request.status == 200)
        success(request.responseText);
      else if (failure)
        failure(request.status, request.statusText);
    }
  };
}

simpleHttpRequest("/wp-content/ejs/files/fruit.txt", print);

Funkcja ta pobiera podany jej adres URL i wywołuje funkcję przekazaną jako drugi argument. Trzeci argument, jeśli zostanie zdefiniowany, służy do powiadamiania o niepowodzeniu ― kod inny niż 200.

Aby móc wykonywać bardziej złożone żądania, można dodać tej funkcji jeszcze inne parametry, do określania metody (GET lub POST), opcjonalnego łańcucha, który ma być przekazany jako dane, do dodawania nagłówków itd. Gdy argumentów jest dużo, dobrym pomysłem jest przekazywanie ich w obiekcie, jak opisałem w rozdziale 9.


Niektóre strony internetowe prowadzą intensywną komunikację między programami działającymi na kliencie i na serwerze. W przypadku takich systemów dobrym pomysłem jest traktowanie niektórych żądań HTTP jako wywołań funkcji działających na serwerze. Klient wysyła żądanie pod określony adres URL identyfikujący funkcje przekazując argumenty jako parametry URL lub dane POST. Następnie serwer wywołuje funkcję i zapisuje wynik w dokumencie JSON lub XML i wysyła go w odpowiedzi. Gdy napisze się kilka funkcji pomocniczych, wywoływanie funkcji na serwerze może być prawie tak łatwe jak wywoływanie funkcji na kliencie, oczywiście z tą różnicą, że wyniki nie będą dostępne natychmiast.

Przypisy

  1. To nie są jedyne typy żądań. Istnieje też żądanie HEAD do pobierania nagłówków dokumentu, bez treści, PUT do dodawania dokumentów na serwer oraz DELETE do usuwania dokumentów. Typy te nie są używane przez przeglądarki i często nie obsługują ich serwery sieciowe, ale jeśli napisze się działające na serwerze programy do ich obsługi, to mogą być przydatne.
  2. Nie tylko część „XML” nazwy XMLHttpRequest jest myląca ― obiektu tego można używać także do wysyłania żądań przy użyciu innych protokołów niż HTTP, a więc tylko słowo Request ma rację bytu w tej nazwie.
  3. 0 (niezainicjowany) to stan obiektu przed wywołaniem na nim metody open. Wywołanie metody open zmienia go na 1 (otwarty). Wywołanie metody send powoduje zmianę na 2 (wysłany). Natomiast po odpowiedzi serwera wartość zmienia się na 3 (odbieranie). W końcu 4 oznacza „załadowany”.
  4. Znak n już znamy — jest to znak nowego wiersza. t to znak tabulatora, r to znak powrót karetki, który w niektórych systemach jest używany przed lub zamiast znaku nowego wiersza do oznaczania końca wiersza. Znaki b (backspace), v (pionowy tabulator), f (wysuw strony) są przydatne w pracy ze starymi drukarkami, natomiast w przeglądarkach nie mają zastosowania.

Autor: Marijn Haverbeke

Źródło: http://eloquentjavascript.net/1st_edition/chapter14.html

Tłumaczenie: Łukasz Piwko

Treść tej strony jest dostępna na zasadach licencji CC BY 3.0