Uvod
AJAX - Asynchronous JavaScript And XML - predstavlja skup tehnika preko kojih je moguće uspostaviti asinhronu komunikaciju između klijenta i servera, ili jednostavnije, u pitanju je mehanizam koji omogućava automatsko ažuriranje sadržaja delova stranice (određenih HTML elemenata) - bez potrebe za ponovnim učitavanjem.
Sa ovakvim pristupom susrećemo se svakodnevno: chat aplikacije, dodavanje komentara koje ne prekida prikazivanje video klipa, 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 veliku popularnost ima i Fetch API (novija i modernija tehnologija kojoj ćemo takođe uskoro posvetiti članak), ali ćemo za početak koristiti AJAX za upoznavanje sa osnovnim principima asinhrone komunikacije sa serverom.
Da bismo najbolje razumeli sve što smo naveli, kreiraćemo pojednostavljeni backend za chat aplikaciju.
Nedostaci sinhronog modela komunikacije između klijenta i servera
Sinhroni model komunikacije između klijenta i servera, sam po sebi, krajnje je adekvatan, ali, u kontekstu web aplikacija koje zahtevaju neupadljivo osvežavanje sadržaja periodično (chat aplikacije), ili, shodno korisničkom unosu (komentari ispod video klipova, automatske sugestije pri pretrazi), ovakav način razmene podataka neće nam mnogo pomoći.
Za početak, razmotrimo šta su osnovni zahtevi koji se postavljaju pred chat aplikacije:
- chat aplikacija koristi polje za prikaz već razmenjenih poruka (sasvim dobro za ovo može poslužiti običan
<div>
element), kao i polje za unos nove poruke (<input>
) i dugme za slanje novih poruka - backend ovakvog sistema čini baza podataka, sa tabelama koje beleže podatke 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 poslata u bazu, čita se iz baze i pojavljuje na ekranu svih korisnika koji učestvuju u chat-u
Kada sagledamo gornju listu, reklo bi se da smo spomenuli sve što je neophodno, ali, "prevideli" smo jednu 'sitnicu' - sve što smo naveli neće se izvršavati automatski.
Model sinhrone komunikacije između klijenta i servera
Kao što smo već spominjali u ranijim člancima, komunikacija između klijenta i servera (ono što, od sada, nazivamo sinhronom komunikacijom), odvija se po sledećem principu:
- klijent šalje zahtev serveru u obliku URL-a (web adrese)
- server prima i obraćuje zahtev i potom šalje klijentu zahtevani HTML sadržaj
- browser na strani klijenta prikazuje primeljeni HTML sadržaj
- ukoliko je potrebno osvežiti sadržaj stranice, mora se poslati novi zahtev za učitavanje
Naravno, sve pod uslovom da je zahtev uredno poslat, primljen i obrađen (u slučaju 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 će, pored HTML-a, server (na zahtev klijenta) poslati i sav pripadajući sadržaj - CSS, JavaScript, kao i druge datoteke (prevashodno slike).
Nedostaci sinhronog modela u slučaju chat aplikacije
Shodno onome što smo naveli u prošlom odeljku, nije teško zaključiti da će, u slučaju naše (za sada hipotetičke) chat aplikacije koja (za sada) ne koristi AJAX:
- za učitavanje (novih) primeljnih poruka, biti potrebno da ponovo učitavamo (osvežimo) stranicu
- za slanje poruka, biti potrebno da koristio HTML formular koji (posle klika na dugme za slanje poruke), pokreće skriptu za slanje poruke, pri čemu nas browser odvodi sa prozora chat aplikacije
.... a to svakako predstavlja problem ukoliko želimo da chat aplikacija funkcioniše onako kako smo navikli - bez "seckanja", resetovanja i ručnog učitavanja poruka.
Srećom, web browseri omogućavaju korišćenje posebnih XMLHttpRequest
objekata preko JavaScript-a, čija je svrha razmena podataka bez potrebe za ponovnim učitavanjem stranice, a AJAX kao tehnologija koju ćemo (u ovom članku) koristiti za automatsko ažuriranje chat aplikacije, ni iz daleka nije previše komplikovana, pa nam samo preostaje da se sa njom upoznamo i da sve povežemo u funkcionalnu celinu.
Pre nego što se posvetimo AJAX zahtevima, napomenimo za svaki slučaj da AJAX nije programski jezik, kako se ponekad misli (iako pretpostavljamo da je do sada već postalo očigledno).
Ovaj termin, koji je 2005. 'skovao' američki programer Džesi Džejms Geret, označava skup različitih tehnika koje za cilj imaju asinhronu razmenu podataka između klijenta i servera:
- Objekat
XMLHttpRequest
- za slanje zahteva - JSON - za razmenu podataka
- Javascript - preko koga se sve objedinjuje
Pogledajmo kako sve to izgleda u praksi ....
AJAX zahtevi - slanje i obrada
Specijalizovani XMLHttpRequest
objekti, podatke sa serverom razmenjuju na sledeći način:
- preko polja
onreadystatechange
, definiše se funkcija povratnog poziva koja će se izvršavati kada se stanje zahteva promeni - preko polja
responseText
, JS skripta dobija odgovor od servera (u tekstualnom formatu, kao HTML, ili JSON)
Prvo ćemo sagledati u celosti jednu funkciju koja koristi XMLHttpRequest
objekat za slanje AJAX poziva i obradu odgovora servera ....
function ajaxProba() {
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function() {
if (this.readyState == 4 && this.status == 200) {
document.getElementById("info_div").innerHTML = xhr.responseText;
}
};
xhr.open("GET", "obrada.php", true);
xhr.send();
}
.... posle čega ćemo detaljno analizirati delove.
Provera stanja zahteva
Neimenovana funkcija povratnog poziva, prispisana polju onreadystatechange
....
xhr.onreadystatechange = function() {
if (this.readyState == 4 && this.status == 200) {
document.getElementById("info_div").innerHTML = xhr.responseText;
}
};
.... ispituje stanje zahteva i statusni kod koji je primljen od servera (ukoliko je zahtev rešen).
Kada ova metoda očita stanje 4
i status 200
, izvršiće se komanda:
document.getElementById("info_div").innerHTML = xhr.responseText;
Ovo je, naravno, trenutak kada će se većina čitalaca (sasvim opravdano) zapitati: šta su "stanje 4" i "status 200" (a naravno, zanima nas i šta je tačno responseText).
Većina uobičajenih zahteva poslatih serveru, biće izvršeni gotovo trenutno u praktičnom smislu, ali, dok se ne završi obrada, zahtev prolazi kroz sledeća stanja:
<?php
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
?>
Dakle, najviše nas zanima da li je, u obradi zahteva, stanje (readyState) 'poraslo' do 4 (najprostije rečeno - da li je zahtev obrađen), dok četiri prethodna stanja uglavnom nećemo proveravati u kodu.
Pored parametra readyState
(koji označava samo da li je obraada zahteva završena, ili je zahtev "zapeo" na nekom od prethodna četiri stadijuma), zanima nas i da li server vraća uredno formatirane podatke, ili je pri slanju podataka (možda) došlo do greške.
U pitanju su dobro poznati statusni kodovi o kojima smo već pisali, pa ćemo se podsetiti nekih od najuobičajenijih:
<?php
status:
// podaci su uspešno primljeni:
200 - status OK - uspešno primljeni podaci
// podaci nisu primljeni:
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 će PHP skripta ("obrada.php"), generisati preko komande echo.
Povezivanje XMLHttpRequest objekta sa konkretnom PHP skriptom
Komanda open
povezuje XMLHttpRequest
objekat sa konkretnom PHP skriptom.
U ovom slučaju, datoteka obrada.php
je PHP skripta koja će biti korišćena za obradu AJAX zahteva i pokretaće se svaki put kada serveru uputimo zahtev preko XMLHttpRequest
objekta:
xhr.open("GET", "obrada.php", true);
Slanje XMLHttpRequest zahteva
Poslednja naredba u bloku koji smo videli je ....
xhr.send();
.... i to je komanda preko koje se (zapravo) šalje zahtev serveru, koji sadrži parametre koje smo prethodno definisali (posle čega odgovor od servera dobijamo, 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.
XML - format za razmenu podataka
Do sada smo već spominjali da se podaci koji nastaju obradom na serveru, vraćaju klijentu u obliku teksta, u HTML i JSON formatu, međutim, poslednje slovo u skraćenici AJAX, ne označava ni HTML, ni JSON, već (jednu drugu) skraćenicu - XML.
Extensible Markup Language (XML) je format za razmenu podataka (odnosno - markup jezik), koji koristi sintaksu sličnu HTML-u za definisanje objekata putem teksta.
Pristup je vrlo sličan onom koji smo videli kod JSON objekata (o čemu smo već pisali u članku o JSON objektima).
Na primer, ako bismo sledeći JS objekat .....
var osoba = {
ime: "Petar",
prezime: "Petrović",
godina_rodjenja: 1979
};
.... umesto preko JSON formata, zapisali u XML formatu, 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 se na XML sintaksi nećemo zadržavati, budući da se u današnje vreme razmena teksta između klijenta i servera (kod slanja AJAX zahteva), u najvećem broju slučajeva obavlja, ili preko običnog HTML-a, ili preko JSON formata.
JSON - savremenije i praktičnije rešenje za razmenu podataka
Budući da je JSON (kako naš naslov sugeriše), savremenije i praktičnije rešenje za razmenu podataka između klijenta i servera, posvetićemo pažnju metodama za pretvaranje JS objekata pri slanju AJAX zahteva i dekodirnaju JSON objekata u PHP-u.
Sledeći kod:
var osoba = {
ime: "Petar",
prezime: "Petrović",
godina_rodjenja: 1979
};
var 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 na dalju obradu.
Kada u PHP skripti primimo poslati JSON objekat, parsiraćemo ga preko komande json_decode
....
$osoba = json_decode($_GET["p"], false);
.... čime se kreira PHP objekat, čijim poljima možemo pristupati na uobičajen način:
<?php
$osoba->ime // Petar
$osoba->prezime // Petrović
$osoba->godina_rodjenja // 1979
?>
Primer ovakve razmene podataka 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 još jednom, za chat aplikaciju potrebno je da osposobimo 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 (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'></div>
<input id='nova_poruka_tekst' type='text' />
<button onclick='slanjePoruke()'>SLANJE PORUKE</button>
(To će biti id-ovi na koje ćemo se pozivati u JS kodu.)
Čitanje poruka sa servera
Da bismo imali odakle da čitamo poruke, kreirajte na serveru bazu chat
i u njoj tabelu chat_poruke
, sledeće strukture:
id | id_korisnika | korisnicko_ime | tekst_poruke | vreme
------------------------------------------------------------------------
1 | 112 | joca21 | Ćao svima! :) | 2021-06-19 18:54:17
2 | 237 | peraX | Šta ima? | 2021-06-19 18:54:19
3 | 125 | maja_bg | Poyy | 2021-06-19 18:54:22
Sadržaj možete "hardkodirati" (da biste odmah mogli da isprobate funkcionalnost skripte za čitanje), preko sledećeg SQL koda:
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:17'),
(237, 'peraX', 'Šta ima?', '2021-06-19 18:54:19'),
(125, 'maja_bg', 'Poyy', '2021-06-19 18:54:22')
.... a možete naravno na brzaka implementirati i nekakakv priručni mehanizam za slanje poruka (pre nego što to zajedno uradimo 'zvanično').
Dodaćemo sada skriptu citanje_poruka.php
, koja ima sledeći sadržaj (skriptu povezivanje.php
kreiraćete 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 ovakvu skriptu samostalno, dobićete u browseru sledeći ispis:
Vidimo da rezultat već uveliko podseća na chat aplikaciju i sve što je potrebno uraditi sada, je da pozovemo funkciju setInterval
, kojoj ćemo kao callback funkciju predati funkciju koja šalje AJAX zahteve za čitanje podataka (preko skripte citanje_poruka.php):
setInterval(citanjePoruka, 500);
function citanjePoruka() {
var 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
onreadystatechange
prepozna stanje 4 i status 200, poruke će biti ispisane u div elementu sa id-omporuke_div
Slanje poruka
Za slanje poruka, koristiémo malo drugačiji mehanizam pozivanja AJAX zahteva: 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() {
var 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;
}
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function() {
if (this.readyState == 4 && this.status == 200) {
document.getElementById("nova_poruka_tekst").value = "";
}
};
var poruka_paket =
{
"tekst_poruke" : tekst_poruke
};
var 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) ....
var poruka_paket =
{
"tekst_poruke" : tekst_poruke
};
var poruka_paket_json = JSON.stringify(poruka_paket);
.... koji se potom u PHP-u dekodira 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
Funkcija slanjePoruke
, proslediće 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 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 broweru klijenata posle prvog sledećg poziva funkcije citanjePoruka
(u praktičnom smislu - naizgled istog trenutka kad je poslata).
Obrada složenijih poruka sa servera
Primetili ste da smo se do sada (namerno), pretvaranjem teksta u JSON objekat bavili samo u smeru slanja poruke od klijenta na server, dok smo u obrnutom smeri slali samo običan HTML.
Šta ako u PHP-u koristimo složeni objekat koji sadrži sve podatke vezane za izvršavanje upita upućenog 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);
}
?>
(Primećujemo da se nigde ne koristi komanda echo.)
.... i želimo da takav objekat pošaljemo JS skripti kao responseText
?
Možemo "spakovati" JSON objekat u PHP-u, preko komande json_encode
....
<?php
$poruka_za_js = json_encode($odgovor);
echo $poruka_za_js;
?>
.... i potom ga 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 ostalim situacijama do sada, JSON priskače u pomoć.
Kako upotpuniti chat aplikaciju?
Šta nedostaje chat aplikaciji koju smo koristili kao primer u ovom članku?
Pre svega (veoma očigledno), sistem za autorizaciju korisnika, koji smo (kao što smo već napomenuli), ovoga puta izsotavili zarad preglednosti.
Druga stvar (koja možda nije toliko očigledna), primećujemo da se pri ažuriranju učitavaju sve poruke. U praksi (u chat aplikaciji sa više stotina, ili čak hiljada poruka), takav pristup nije najoptimalniji i bolje rešenje bi bilo da se učitavaju samo one poruke čiji je id veći od id-a poslednje (do tada) učitane poruke.
Kada to rešimo - imaćemo pravu chat aplikaciju.
Pored navedenog, ako želimo da aplikacija bude još dopadljivija, funkcionalnija i modernija, možemo dodati i delove koda koji bi prikazivali informacije o tome ko od korisnika je online, i o tome da li neko od korisnika trenutno unosi tekst u polje za poruku ("korisnik_x is typing ....").
Verujemo da će se naši čitaoci koji su dobro razumeli do sada izneti sadržaj, rado pozabaviti rešavanjem ovakvih zadataka.
Zaključak
Videli smo u ovom kratkom prikazu 'kostura' jednostavne chat aplikacije, da je način za ažuriranje sadržaja stranice bez ponovnog učitavanja prilično jednostavan.
Za vežbu, probajte sami da kreirate (neku drugu) web aplikaciju koja koristi AJAX.
U nedostatku bolje ideje, to može biti aplikacija koja prikazuje sadržaj muzičkih albuma (pri čemu možete sami kreirati odgovarajuću bazu sa izvođačima, albumima i pesmama), ili recimo aplikacija za rezervaciju mesta u bioskopskoj sali.
Sa naše strane, uskoro ćemo priču o asinhronoj komunikaciju između klijenta i servera (kao što smo već nagovestili), dopuniti člankom o tome kako se asinhroni zahtevi mogu prosleđivati preko Fetch API-ja.