Konrad Kowalski (rootsher)Principal Platform & Reliability Architect110011101100110111100110001110101010110101011110

AJAX: moment, w którym strona zaczęła zachowywać się jak aplikacja

data
kategoria
Frontend
czytanie
3 min / 574 słów

Klasyczny web miał prosty model: użytkownik wykonuje akcję, browser wysyła request, serwer renderuje nowy dokument, browser zastępuje stary dokument nowym.

Problem pojawił się wtedy, gdy interakcje zaczęły być zbyt małe, żeby uzasadniać pełną nawigację.

Zmiana sortowania tabeli, autocomplete, aktualizacja koszyka czy odświeżenie fragmentu dashboardu nie potrzebują nowego dokumentu. Potrzebują tylko nowych danych.

I właśnie tutaj pojawił się AJAX.

Request bez nawigacji

Kluczowa zmiana była prosta:

text
before:

interaction
  |
  v
navigation
  |
  v
new document


after:

interaction
  |
  v
background request
  |
  v
update fragment of current document

Browser po raz pierwszy na dużą skalę zaczął utrzymywać aktualny dokument i jednocześnie komunikować się z serwerem.

Technicznie fundamentem był XMLHttpRequest.

js
const xhr = new XMLHttpRequest();

xhr.open("GET", "/api/search?q=keyboard");

xhr.onload = () => {
  const results = JSON.parse(xhr.responseText);
  renderResults(results);
};

xhr.send();

Dzisiaj taki kod wygląda archaicznie, ale architektonicznie zmienił web bardzo mocno.

Request przestał oznaczać navigation.

Serwer przestał zwracać tylko dokumenty

W klasycznym modelu endpoint odpowiadał zwykle czymś takim:

text
GET /products/42
  |
  v
HTML

Po pojawieniu się dynamicznych klientów zaczęła powstawać druga warstwa endpointów:

text
GET /products/42
  |
  v
HTML

GET /api/products/42
  |
  v
DATA

Browser zaczął sam decydować, co zrobić z odpowiedzią.

Mógł pobrać dane:

json
{
  "id": 42,
  "name": "Mechanical Keyboard",
  "price": 129
}

i zaktualizować tylko fragment dokumentu:

js
priceElement.textContent = "$129";

To pozornie niewielka zmiana.

W praktyce oznaczała narodziny nowego problemu:

część logiki prezentacji zaczęła żyć po stronie klienta.

Document state zaczyna zamieniać się w application state

W klasycznej aplikacji stan był często zakodowany w URL-u, sesji i bazie.

Nowy dokument był po prostu kolejnym snapshotem tego stanu.

Po AJAX-ie browser zaczął przechowywać informacje, których serwer nie musiał już od razu reprezentować jako pełnego dokumentu.

Na przykład:

js
let selectedCategory = "keyboards";
let currentPage = 3;
let sort = "price";
let query = "mechanical";

Teraz UI może zmieniać się wielokrotnie bez navigation.

To daje dużo lepszą interaktywność, ale tworzy problem synchronizacji:

text
server state
  ^
  v
client state
  ^
  v
DOM

Wcześniej DOM był głównie efektem odpowiedzi serwera.

Teraz sam staje się mutable representation stanu aplikacji.

Google Maps pokazało, po co to wszystko

Jednym z historycznie najważniejszych przykładów był Google Maps.

Wyobraźmy sobie mapę działającą według klasycznego modelu dokumentowego.

Użytkownik przeciąga mapę o 50 pikseli:

text
drag
  |
  v
request
  |
  v
render entire page
  |
  v
reload

Praktycznie bezużyteczne.

Dynamiczne pobieranie danych pozwalało robić coś zupełnie innego:

text
drag map
  |
  v
calculate new viewport
  |
  v
request missing data
  |
  v
update map

Dokument pozostaje ten sam.

Zmienia się jedynie jego zawartość i stan.

To jest fundamentalny krok w stronę SPA, ale jeszcze nie SPA.

AJAX nie oznaczał od razu JSON API

Sama nazwa rozwija się jako:

Asynchronous JavaScript and XML

I faktycznie XML był wtedy istotnym formatem wymiany danych.

Ale AJAX nigdy nie był tak naprawdę o XML-u.

Sedno mechanizmu brzmiało:

JavaScript może wykonać request bez nawigacji dokumentu i wykorzystać odpowiedź do zmiany aktualnego UI.

Odpowiedzią mogło być:

text
XML
JSON
HTML
tekst
później praktycznie dowolny format binarny

Dlatego współczesny:

js
const response = await fetch("/api/products");
const products = await response.json();

jest koncepcyjnie potomkiem dokładnie tej samej zmiany.

Ciekawostka

XMLHttpRequest nie pojawił się początkowo jako otwarty standard zaprojektowany dla nowoczesnych aplikacji webowych.

Mechanizm wyrósł z implementacji Microsoftu związanej z Outlook Web Access i był początkowo dostępny przez ActiveX.

Dopiero później podobne możliwości zostały zaimplementowane natywnie przez inne przeglądarki i ustandaryzowane.

To dobry przykład częstego schematu ewolucji platformy webowej:

text
vendor-specific capability
  |
  v
real application use case
  |
  v
cross-browser adoption
  |
  v
standardization

Wiele współczesnych Web APIs przeszło podobną drogę.

Cena interaktywności

AJAX rozwiązał problem pełnych reloadów, ale nie za darmo.

Kiedy aplikacja samodzielnie modyfikuje dokument, musi wiedzieć:

text
jaki fragment UI jest aktualny
jakie dane zostały już pobrane
co zrobić, gdy request się nie powiedzie
co zrobić, gdy dwa requesty wrócą w innej kolejności
jak odtworzyć stan po refreshu
jak powiązać URL z aktualnym widokiem

Rozważ prosty autocomplete:

text
"k"
  |
  v
request A

"ke"
  |
  v
request B

Nie ma gwarancji, że odpowiedzi wrócą w tej samej kolejności.

text
request B -> 80 ms
request A -> 300 ms

Jeśli klient bezmyślnie wyrenderuje ostatnią odpowiedź, wynik dla "k" może nadpisać nowszy wynik dla "ke".

Nagle frontend ma problemy znane wcześniej głównie systemom rozproszonym:

asynchroniczność, kolejność zdarzeń i stale data.

Od manipulacji DOM-em do aplikacji

Pierwsze aplikacje AJAX-owe często wyglądały mniej więcej tak:

js
fetchData(data => {
  document.getElementById("price").innerHTML = data.price;
  document.getElementById("stock").innerHTML = data.stock;
  document.getElementById("title").innerHTML = data.title;
});

Przy kilku elementach działa to dobrze.

Przy setkach zależnych elementów zaczyna się robić trudniej.

Kod musi ręcznie utrzymywać zgodność między:

text
application state
  |
  v
DOM state

To właśnie ten problem zacznie generować kolejną falę narzędzi i architektur:

text
jQuery     - upraszcza manipulację DOM-em i requesty
Backbone   - próbuje uporządkować model aplikacji
AngularJS  - wiąże stan z widokiem
React      - opisuj UI jako funkcję stanu, zamiast ręcznie synchronizować DOM

Ale zanim do tego dojdziemy, wydarzy się ważniejsza zmiana.

Skoro aplikacja może pobierać dane bez przeładowania dokumentu, to dlaczego serwer miałby w ogóle renderować kolejne widoki?

Browser może przecież dostać aplikację raz, a później sam zarządzać routingiem, stanem i renderowaniem.

Tak zaczyna się era Single Page Applications i Client-Side Renderingu.