Uvod u AJAX (Asynchronous JavaScript And XML)
Uvod
Termin AJAX (skraćeno od "Asynchronous JavaScript And XML"), predstavlja skup tehnika koje omogućavaju asinhronu komunikaciju između klijenta i servera, ili, jednostavnije (za sam početak), u pitanju je mehanizam koji omogućava automatsko ažuriranje sadržaja delova stranice bez potrebe za ponovnim učitavanjem.
Sa pristupom koji smo opisali susrećemo se svakodnevno: chat aplikacije, dodavanje komentara koje ne prekida prikazivanje video klipova, sugestije na pretraživačima koje se automatski ažuriraju shodno promenama u polju za pretragu .... samo su neki od primera.
Pored AJAX-a (koji je nešto starija tehnologija), poslednjih godina raste popularnost i novije, modernije tehnologije Fetch API (kojoj ćemo takođe uskoro posvetiti članak), ali, za početak, zarad upoznavanja sa osnovnim principima asinhrone komunikacije sa serverom, koristićemo 'tradicionalni' AJAX.
Da bismo najbolje razumeli kako AJAX funkcioniše u praksi, kreiraćemo jednostavnu chat aplikaciju (uz poseban osvrt na back-end), i 'usput' ćemo se upoznati sa teorijom.
Nedostaci sinhronog modela
Sinhroni model komunikacije između klijenta i servera, sam po sebi je krajnje adekvatan, ali - u kontekstu web aplikacija koje zahtevaju da se sadržaj stranica osvežava periodično na neupadljiv način (recimo, chat aplikacije), ili, shodno korisničkom unosu i interakciji sa stranicom (komentari ispod video klipova, automatske sugestije pri pretrazi i sl) - sinhroni model ispoljava nedostatke (to jest, "nije baš od pomoći").
Za početak, razmotrićemo šta su osnovni zahtevi koji se postavljaju pred tipičnu chat aplikaciju:
- chat aplikacije (tipično) koriste sledeće HTML elemente:
- polje za prikaz već razmenjenih poruka (za šta sasvim dobro može poslužiti običan
<div>
element) - polje za unos nove poruke (
<input>
) - dugme za slanje nove poruke
- polje za prikaz već razmenjenih poruka (za šta sasvim dobro može poslužiti običan
- back-end chat aplikacije (tipično) čini baza podataka, sa tabelama preko kojih se beleže podaci o korisnicima i poslatim porukama
- kada korisnik unese tekst poruke u odgovarajuće polje i klikne na dugme za slanje, poruka se prosleđuje u bazu podataka
- poruka koja je prethodno poslata u bazu (i upisana u odgovarajuću tabelu), čita se iz baze i pojavljuje se na ekranu svih korisnika koji učestvuju u chat-u (zajedno sa prethodno poslatim porukama)
Kada sagledamo listu koju smo prethodno naveli (na početku odeljka), deluje da smo spomenuli sve što je neophodno, ali, "slučajno" smo prevideli jednu 'sitnicu': sve što smo naveli - neće se izvršavati automatski.
Model sinhrone komunikacije između klijenta i servera
Kao što smo već prikazali u ranijim člancima, sinhroni model komunikacije između klijenta i servera, podrazumeva sledeće korake:
- klijent šalje zahtev serveru u obliku URL-a (tj. 'web adrese')
- server prima i obrađuje zahtev i potom šalje klijentu zahtevani HTML sadržaj
- na strani klijenta, primljeni HTML sadržaj se obrađuje i prikazuje u browseru
- ukoliko je potrebno osvežiti sadržaj stranice, šalje se novi zahtev *
Naravno, sve pod uslovom da je zahtev uredno poslat, primljen i obrađen (jer, u slučaju pojave grešaka u obradi zahteva: umesto HTML-a i drugih traženih sadržaja, server će poslati klijentu odgovarajući statusni kod), a napomenimo i to da server, pored HTML-a, na zahtev klijenta šalje i sav drugi pripadajući sadržaj: CSS, JavaScript, kao i druge datoteke (najčešće slike).
Nedostaci sinhronog modela u slučaju chat aplikacije
Ukoliko bismo se oslonili (samo) na sinhroni model (pri izradi chat aplikacije), suočili bismo se sa sledećom situacijom:
- za učitavanje (novih) primljenih poruka, bilo bi potrebno ponovo učitati (tj. osvežiti) celu stranicu
- za slanje poruka, bilo bi potrebno koristiti HTML formular koji (posle klika na dugme za slanje poruke), pokreće skriptu za slanje poruke, pri čemu browser prebacuje korisnika, sa prozora chat aplikacije, na drugu stranicu
Navedene okolnosti svakako predstavljaju problem ukoliko je potrebno da chat aplikacija funkcioniše onako kako su korisnici navikli (godinama unazad) - bez "seckanja", resetovanja i ručnog učitavanja poruka.
Srećom, JavaScript (koji se pokreće u web browserima), omogućava korišćenje posebnih XMLHttpRequest
objekata, čija je svrha - razmena podataka bez potrebe za ponovnim učitavanjem stranice, a sam AJAX (kao tehnologija koju ćemo u ovom članku koristiti za automatsko ažuriranje chat aplikacije), nije ni iz daleka "previše komplikovan", i samo preostaje da se upoznamo sa tehnikalijama, i da sve povežemo u funkcionalnu celinu.
AJAX - osnovne postavke
Kao što smo na početku već nagovestili, "AJAX" je akronim * iza koga stoji nekoliko međusobno povezanih tehnika koje za cilj imaju asinhrono ažuriranje delova web stranica (čemu prethodi razmena podataka između klijenta i servera):
- preko objekta
XMLHttpRequest
šalju se 'AJAX zahtevi' (koji tipično podrazumevaju slanje upita i prijem podataka od servera) - podaci se najčešće razmenjuju u vidu JSON objekata **
- JavaScript je jezik preko koga se sve objedinjuje
Pogledajmo kako AJAX zahtevi funkcionišu u praksi ....
AJAX zahtevi - slanje i obrada
Specijalizovani XMLHttpRequest
objekti razmenjuju podatke sa serverom na sledeći način:
- preko događaja
onreadystatechange
pokreće se funkcija povratnog poziva preko koje se (praktično) obavlja komunikacija između klijenta i servera (funkcija se izvršava svaki put kada se stanje zahteva promeni) - preko polja
responseText
, JS skripta dobija odgovor od servera (u obliku običnog teksta, u obliku HTML koda, ili u obliku JSON objekta)
Prvo ćemo sagledati, u celosti, primer funkcije koja koristi XMLHttpRequest
objekat za slanje AJAX poziva i obradu odgovora servera ....
function ajaxProba() {
const xhr = new XMLHttpRequest();
xhr.onreadystatechange = function() {
if (this.readyState == 4 && this.status == 200) {
let infoDiv = document.getElementById("info_div");
infoDiv.innerHTML = xhr.responseText;
}
};
xhr.open("GET", "./obrada.php", true);
xhr.send();
}
.... posle čega ćemo detaljno analizirati delove.
Provera stanja zahteva
Neimenovana funkcija povratnog poziva, koja se povezuje sa događajem onreadystatechange
....
xhr.onreadystatechange = function() {
if (this.readyState == 4 && this.status == 200) {
let infoDiv = document.getElementById("info_div");
infoDiv.innerHTML = xhr.responseText;
}
};
.... ispituje stanje (poslatog) zahteva i statusni kod koji je primljen od servera (ukoliko je zahtev rešen).
Kada funkcija očita stanje 4
i status 200
, izvršavaju se sledeće komande (koje smo naveli u prethodnom bloku):
let infoDiv = document.getElementById("info_div");
infoDiv.innerHTML = xhr.responseText;
Ovo je (pretpostavljamo), trenutak kada će se većina čitalaca zapitati (sasvim opravdano): šta su "stanje 4" i "status 200" (i šta je tačno responseText
)?
Većina uobičajenih zahteva koji se šalju serverima, u praktičnom smislu izvršava se gotovo trenutno, ali, dok se ne završi obrada, zahtev prolazi kroz sledeća stanja:
readyState:
0 - zahtev (još uvek) nije poslat
1 - zahtev je poslat (ali nije primljen)
2 - zahtev je primljen (ali nije uzet u obradu)
3 - zahtev je uzet u obradu (ali obrada nije završena)
4 - obrada zahteva je završena
Nije teško zaključiti da je najviše pažnje potrebno posvetiti prepoznavanju trenutka u kome stanje (readyState
), dostiže vrednost 4 (odnosno, trenutka u kome je prepoznato da je zahtev obrađen), dok se četiri prethodna stanja uglavnom ne proveravaju u kodu.
Pored parametra readyState
(koji označava samo da li je obrada zahteva završena, ili je zahtev "zapeo" na nekom od prethodna četiri stadijuma), potrebno je proveriti i da li je server prepoznao traženi URL i vratio uredno formatirane podatke, ili je pri traženju podataka na serveru (možda) došlo do greške.
U pitanju su dobro poznati HTTP statusni kodovi, o kojima smo već pisali (na ovom mestu, podsetićemo se na nekoliko najuobičajenijih statusnih kodova):
-Uspešan prijem podataka:
200 - status OK - uspešno primljeni podaci
-Neuspešan prijem podataka:
403 - neodgovarajući podaci za autorizaciju
404 - traženi podatak ne postoji na serveru
503 - greška na strani servera
Što se tiče parametra responseText
, u pitanju je sav tekstualni sadržaj koji PHP skripta * na koju se XMLHttpRequest
objekat poziva (u našem slučaju, skripta obrada.php
), generiše preko komande echo
.
Povezivanje XMLHttpRequest objekta sa konkretnom PHP skriptom
Komanda open
povezuje objekat klase XMLHttpRequest
sa konkretnom PHP skriptom.
U primeru koji razmatramo, datoteka obrada.php
je PHP skripta koja će biti korišćena za obradu AJAX zahteva i pokretaće se svaki put kada se serveru uputi zahtev preko XMLHttpRequest
objekta:
xhr.open("GET", "./obrada.php", true);
Slanje XMLHttpRequest zahteva
Poslednja naredba u bloku koji smo videli na slici #1 (na kojoj je prikazan celokupan kod) ....
xhr.send();
.... predstavlja komandu preko koje se (zapravo) serveru šalje zahtev, koji sadrži parametre koji su prethodno definisani (posle čega se od servera dobija odgovor, u tekstualnom obliku, preko polja responseText
objekta xhr
).
Pre nego što se pozabavimo konkretnim primerima upotrebe AJAX-a u izradi chat aplikacije, osvrnućemo se na formate za razmenu podataka između klijenta i servera.
Formati za razmenu podataka
Do sada smo već pominjali da podaci, koji nastaju obradom na serveru, do klijenta najčešće dospevaju: ili kao neformatirani tekst, ili u obliku HTML-a, ili u JSON formatu, međutim, poslednje slovo u skraćenici AJAX, ne označava: ni HTML, ni JSON, već (jednu sasvim drugu) skraćenicu - XML.
XML - pomalo zastareo format za razmenu podataka
XML (skraćeno od "Extensible Markup Language") je format za razmenu podataka (odnosno markup jezik), sa sledećim glavnim odlikama:
- sintaksa je slična HTML-u
- opšti pristup je sličan JSON formatu
Na primer, ako bi sledeći JS objekat .....
let osoba = {
ime: "Petar",
prezime: "Petrović",
godina_rodjenja: 1979
};
.... umesto preko JSON formata, bio zapisan u formatu XML, dobili bismo sledeći zapis:
<osoba>
<ime>Petar</ime>
<prezime>Petrović</prezime>
<godina_rodjenja>1979</godina_rodjenja>
</osoba>
Vidimo da je princip veoma sličan (i razumljiv), ali, nećemo se zadržavati na XML sintaksi - iz praktičnih razloga (u današnje vreme, razmena teksta između klijenta i servera pri slanju AJAX zahteva, u najvećem broju slučajeva se obavlja (kao što smo već naveli): ili preko običnog teksta, ili preko HTML-a, ili preko JSON formata).
JSON - savremenije i praktičnije rešenje za razmenu podataka
Budući da je JSON (kako gornji naslov sugeriše), savremenije i praktičnije rešenje za razmenu podataka između klijenta i servera, posvetićemo pažnju metodama koje se koriste za pretvaranje JS objekata u JSON objekte (pri slanju AJAX zahteva), kao i metodama za dekodiranje JSON objekata u PHP-u.
Primer pripreme podataka u JavaScript-u
Sledeći kod (u JavaScript-u):
let osoba = {
ime: "Petar",
prezime: "Petrović",
godina_rodjenja: 1979
};
let poruka_paket_json = JSON.stringify(osoba);
... pripremiće JSON objekat
{
"ime": "Petar",
"prezime": "Petrović",
"godina_rodjenja": 1979
}
.... koji se potom može poslati PHP skripti, zarad dalje obrade.
Primer obrade podataka u PHP-u
Nakon što PHP skripta primi poslati JSON objekat, parsiranje se obavlja preko funkcije json_decode
....
<?php
if (!isset($_GET["p"])) {
// naredbe koje treba pozvati ako
// podaci NISU prosleđeni ....
}
$osoba = json_decode($_GET["p"], false);
?>
.... pri čemu je rezultat obrade - PHP objekat odgovarajućeg sadržaja:
$osoba->ime // Petar
$osoba->prezime // Petrović
$osoba->godina_rodjenja // 1979
Primer razmene podataka preko JSON objekata videćemo u nastavku, a sada je vreme da se posvetimo implementaciji chat aplikacije.
Primer korišćenja AJAX-a u izradi chat aplikacije
Da se podsetimo, za chat aplikaciju potrebno je osposobiti sledeće elemente:
- frontend elemente - u vidu polja za slanje novih poruka i prikaz postojećih
- backend - u vidu baze podataka sa podacima o korisnicima i poslatim porukama
- funkciju za slanje novih poruka
- funkciju za periodično ažuriranje spiska razmenjenih poruka (praktično: učitavanje novih poruka koje su poslate u međuvremenu)
Krenimo redom ....
Za početak, dodajte sledeći sadržaj unutar <body>
tagova u skripti index.php
<div id='poruke_div'>
<!-- Poruke -->
</div>
<input id='nova_poruka_tekst' type='text' />
<button onclick='slanjePoruke()'>SLANJE PORUKE</button>
(Navedene id-ove koristićemo u JS kodu.)
Čitanje poruka sa servera
Da bi skripta imala odakle da čita poruke, kreiraćemo na serveru bazu chat
, i (unutar baze), tabelu chat_poruke
, sledeće strukture i sadržine:
id | id_korisnika | korisnicko_ime | tekst_poruke | vreme
------------------------------------------------------------------------
1 | 112 | joca21 | Ćao svima! :) | 2021-06-19 18:54:14
2 | 237 | peraX | Šta ima? | 2021-06-19 18:54:19
3 | 125 | maja_bg | Poyy | 2021-06-19 18:54:25
Strukturu tabele definisaćemo preko SQL koda, a sadržaj možemo "hardkodirati" (da biste odmah mogli da isprobate funkcionalnost skripte za čitanje):
CREATE TABLE chat_poruke (
id int not null AUTO_INCREMENT PRIMARY KEY,
id_korisnika int not null,
korisnicko_ime varchar(255) not null,
tekst_poruke text not null,
vreme datetime not null
);
INSERT INTO chat_poruke
(id_korisnika, korisnicko_ime, tekst_poruke, vreme)
VALUES
(112, 'joca21', 'Ćao svima! :)', '2021-06-19 18:54:14'),
(237, 'peraX', 'Šta ima?', '2021-06-19 18:54:19'),
(125, 'maja_bg', 'Poyy', '2021-06-19 18:54:25')
Dodaćemo skriptu citanje_poruka.php
, koja ima sledeći sadržaj (skriptu povezivanje.php
kreirajte po ugledu na skripte za povezivanje kakve smo do sada već koristili):
<?php
require_once("povezivanje.php");
$upit = "SELECT * FROM chat_poruke ORDER BY id ASC";
$rezultat = mysqli_query($veza, $upit);
$odgovor = "";
while ($red = mysqli_fetch_assoc($rezultat))
{
$odgovor .= "<div class='poruka_red'>";
$odgovor .= "<b>" . $red['korisnicko_ime'] . "</b>: ";
$odgovor .= $red['tekst_poruke'] . " (<font color='#27b'>";
$odgovor .= $red['vreme'] . ")</font>";
$odgovor .= "</div>";
}
echo $odgovor;
?>
Ako pokrenete skriptu za čitanje poruka, dobićete u browseru sledeći ispis:
Vidimo da rezultat već uveliko podseća na chat aplikaciju, i sada je samo potrebno pozvati funkciju setInterval
, kojoj se u svojstvu funkcije povratnog poziva predaje funkcija koja šalje AJAX zahteve za čitanje podataka (preko skripte citanje_poruka.php
):
setInterval(citanjePoruka, 500);
function citanjePoruka() {
const xhr = new XMLHttpRequest();
xhr.onreadystatechange = function() {
if (this.readyState == 4 && this.status == 200) {
document.getElementById("poruke_div").innerHTML = xhr.responseText;
}
};
xhr.open("GET", "./citanje_poruka.php", true);
xhr.send();
}
Da pojasnimo:
- na svakih 500ms, * poziva se funkcija
citanjePoruka
- funkcija
citanjePoruka
šalje AJAX zahtev skripticitanje_poruka.php
- skripta
citanje_poruka.php
vraća formatirani spisak poruka (preko komandeecho
) - kada funkcija vezana za događaj
onreadystatechange
prepozna stanje 4 i status 200, poruke će biti ispisane unutar<div>
elementa sa id-omporuke_div
Slanje poruka
Pri slanju poruka, koristićemo nešto drugačiji mehanizam za pozivanje AJAX zahteva: radna funkcija za slanje poruka neće se pozivati periodično (preko funkcije setInterval
), već, preko dugmeta za slanje poruka.
Na primer:
<button onclick='slanjePoruke()'>SLANJE PORUKE</button>
Sadržaj funkcije slanjePoruke
je sledeći:
function slanjePoruke() {
let tekst_poruke = document.getElementById("nova_poruka_tekst").value;
tekst_poruke = tekst_poruke.trim();
tekst_poruke = tekst_poruke.replace(/"/g, "'");
tekst_poruke = encodeURIComponent(tekst_poruke);
if (tekst_poruke.length == 0) {
document.getElementById("nova_poruka_tekst").value = "";
return;
}
const xhr = new XMLHttpRequest();
xhr.onreadystatechange = function() {
if (this.readyState == 4 && this.status == 200) {
document.getElementById("nova_poruka_tekst").value = "";
}
};
let poruka_paket = {
"tekst_poruke" : tekst_poruke
};
let poruka_paket_json = JSON.stringify(poruka_paket);
xhr.open("GET", "slanje_poruke.php?p=" + poruka_paket_json, true);
xhr.send();
}
Slanje poruka PHP skriptama, podrazumeva kreiranje JSON objekta (onako kako smo prethodno videli):
let poruka_paket = {
"tekst_poruke" : tekst_poruke
};
let poruka_paket_json = JSON.stringify(poruka_paket);
Kada PHP skripta primi poslati paket, sledi dekodiranje preko sledećih funkcija:
json_decode
- kreiranje PHP objekta od primljenog JSON objektamysqli_real_escape_string
ihtmlentities
- 'sanitacija' primljenog podatka (o čemu smo već pisali u prethodnim člancima o PHP-u)trim
- uklanjanje whitespace znakova sa početka i kraja niske
JS funkcija slanjePoruke
prosleđuje novu poruku skripti dodavanje_poruke.php
(koja u celosti ima sledeći sadržaj):
<?php
include_once('povezivanje.php');
$poruka_paket = json_decode($_GET["p"], false);
$tekst_poruke = $poruka_paket->tekst_poruke;
$tekst_poruke = mysqli_real_escape_string($veza, $tekst_poruke);
$tekst_poruke = htmlentities($tekst_poruke);
$tekst_poruke = trim($tekst_poruke);
if (empty($tekst_poruke)) {
exit();
}
$datum_poruke = date("Y-m-d");
$vreme_poruke = date("H:i:s");
$vremeDB = "$datum_poruke $vreme_poruke";
// Ovoga puta, zarad preglednosti, izostavićemo delove koda
// za autorizaciju, i unapred ćemo definisati id korisnika i
// korisničko ime
$id_korisnika = 419;
$korisnicko_ime = "cane15";
$upit = "INSERT INTO chat_poruke
(id_korisnika, korisnicko_ime, tekst_poruke, vreme)
VALUES
('$id_korisnika', '$korisnicko_ime',
'$tekst_poruke', '$vremeDB')";
$rezultat = mysqli_query($veza, $upit);
if (!$rezultat)
{
echo "Greška pri slanju poruke!";
}
?>
Poslata poruka pojaviće se u browseru klijenata, posle prvog sledećeg poziva funkcije citanjePoruka
(u praktičnom smislu - naizgled istog trenutka kad je poslata).
Obrada složenijih poruka sa servera
Do sada smo se pretvaranjem teksta u JSON objekte bavili samo u kontekstu poruka koje se šalju u smeru od klijenta prema serveru (kao što ste verovatno već primetili), dok smo u 'obrnutom smeru' slali običan HTML.
Međutim, šta ako se u PHP-u koristi složeni objekat koji sadrži (i) dodatne podatke vezane za izvršavanje upita koji se upućuje bazi ....
<?php
class Odgovor {
public $tekst,
$status,
$poruka_o_gresci;
/*
tekst - poruka koja se šalje klijentu u slučaju
da nije bilo grešaka u obradi
status - true - nije bilo grešaka u obradi
- false - bilo je grešaka u obradi
$poruka_servera - poruka servera, u slučaju da dođe
do greške u obradi
*/
function __construct($tekst, $status, $poruka_o_gresci) {
$this->tekst = $tekst;
$this->status = $status;
$this->poruka_o_gresci = $poruka_o_gresci;
}
}
if ($rezultat) {
$red = mysqli_fetch_assoc($rezultat);
$odgovor = new Odgovor($red['kolona_3'], true, "");
}
else {
$poruka_servera = "Greška u obradi zahteva!";
$odgovor = new Odgovor("", false, $poruka_servera);
}
?>
.... i potrebno je takav objekat poslati JS skripti kao responseText
?
JSON objekat prvo se "pakuje" u PHP-u, preko komande json_encode
, nakon čega se "šalje" preko komadne echo
....
<?php
$poruka_za_js = json_encode($odgovor);
echo $poruka_za_js;
?>
.... posle čega se primljeni objekat može obraditi u JS-u:
let json_iz_php_a = JSON.parse(this.responseText);
let ispis = json_iz_php_a['tekst'] + "<br>" +
json_iz_php_a['poruka_o_gresci'] + "<br>" +
json_iz_php_a['status'];
document.getElementById("poruke_div").innerHTML =
Kao i u mnogim drugim situacijama do sada, JSON priskače u pomoć. :)
Kako upotpuniti chat aplikaciju?
Šta nedostaje chat aplikaciji koju smo koristili kao primer u članku?
Pre svega (veoma očigledno), sistem za autorizaciju korisnika, koji je ovoga puta (kao što smo već napomenuli), izostavljen zarad preglednosti članka.
Druga stvar (koja možda nije toliko očigledna): može se primetiti da se pri ažuriranju učitavaju sve poruke.
U praksi (u chat aplikacijama sa više (desetina) hiljada poruka), učitavanje svih poruka ne predstavlja optimalan pristup, i bolje rešenje bi bilo - učitavati samo one poruke čiji je id veći od id-a poslednje (do tada) učitane poruke.
Ako rešimo sve što smo naveli, rezultat će biti prava pravcata chat aplikacija. :)
Pored dva pomenuta unapređenja, ako je potrebno da aplikacija bude još dopadljivija, funkcionalnija i modernija, mogu se dodati i delovi koda koji bi prikazivali informacije o tome ko od korisnika je online, kao i o tome da li neko od korisnika trenutno unosi tekst u polje za slanje poruke ("korisnik_x is typing ...."), i sl.
Verujemo da će iskusniji čitaoci (koji su pri tom dobro razumeli do sada izneti sadržaj), rado prihvatiti izazov rešavanja navedenih zadataka.
Za kraj ....
Kroz kratak prikaz 'kostura' jednostavne chat aplikacije, mogli smo uvideti da je način za ažuriranje sadržaja stranice bez ponovnog učitavanja - prilično jednostavan (ili makar relativno jednostavan). :)
Za vežbu, probajte sami da kreirate (neku drugu) web aplikaciju koja koristi AJAX.
Sa naše strane, uskoro ćemo diskusiju o asinhronoj komunikaciju (između klijenta i servera), dopuniti člankom koji će za temu imati slanje asinhronih zahteva preko tehnike Fetch API ....