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:
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.
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:
GET /products/42
|
v
HTML
Po pojawieniu się dynamicznych klientów zaczęła powstawać druga warstwa endpointów:
GET /products/42
|
v
HTML
GET /api/products/42
|
v
DATA
Browser zaczął sam decydować, co zrobić z odpowiedzią.
Mógł pobrać dane:
{
"id": 42,
"name": "Mechanical Keyboard",
"price": 129
}
i zaktualizować tylko fragment dokumentu:
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:
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:
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:
drag
|
v
request
|
v
render entire page
|
v
reload
Praktycznie bezużyteczne.
Dynamiczne pobieranie danych pozwalało robić coś zupełnie innego:
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ć:
XML
JSON
HTML
tekst
później praktycznie dowolny format binarny
Dlatego współczesny:
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:
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ć:
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:
"k"
|
v
request A
"ke"
|
v
request B
Nie ma gwarancji, że odpowiedzi wrócą w tej samej kolejności.
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:
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:
application state
|
v
DOM state
To właśnie ten problem zacznie generować kolejną falę narzędzi i architektur:
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.