WEBES ALKALMAZÁSFEJLESZTÉS 1. Horváth Győző Egyetemi adjunktus 1117 Budapest, Pázmány Péter sétány 1/C, 2.404 Tel: (1) 372-2500/1816
Tartalom 2 AJAX elve AJAX technológiai alapjai AJAX jquery-vel JSON AJAX és MVC Megfontolások az AJAX-szal kapcsolatban Aszinkronitás és promise-ok További CodeIgniter funkcionalitások
3 AJAX elve
AJAX 4 Elmúlt 10 év legnagyobb hatású webes technológiája Modern web alkalmazások világának alapozása Keretrendszerek fejlődésének egyik mozgatórugója
Probléma felvetése 5 Hagyományos oldalak Hiperhivatkozások, adatok küldése Teljes oldal újratöltődik Ezt kell figyelembe venni az alkalmazás folyamatlogikájában
AJAX 8 Aszinkron Javascript és XML Nem új technológia Technológiák együttese, amelyek szabványosak és kiforrottak XHTML és HTML megjelenítés CSS megjelenítés DOM dinamikus felhasználói felület és interakció XMLHttpRequest aszinkron adatátvitel a kliens és a szerver között XML/XSLT adatcsere és manipuláció Javascript mindezeket összefogja
9 Kommunikáció a szerverrel
10 Kliens-szerver aktivitás
11 Kliens-szerver aktivitás
AJAX hívás 12 XMLHttpRequest objektum url meghívása GET és POST adatok küldése válasz feldolgozása sima szöveg HTML XML JSON
Korlátozások 13 Biztonsági okokból nem engedélyezett különböző domainek között ajax kommunikáció Kivéve JSONP (JSON with Padding): script elem dinamikus beszúrásával idegen tartalom futtatása CORS (Cross-Origin Resource Sharing) Nincsen szerver push itt sem: ajax kéréseket mindig a kliens kezdeményezi
14 AJAX jquery-vel
AJAX jquery-vel 15 Natív kód esetén bonyolultabb dolgunk van Böngészők közötti különbséget is át kell hidalni jquery-ben egy egyszerű interfész került kialakításra, ahol paramétereken kell beállítanunk a kérés és a válasz feldolgozásának adatait
load metódus 16 Kiválasztott jquery objektum HTML törzseként olvassa be a paraméterként megadott url-től a tartalmat (GET) A) Load $("#hirek1").load("hirlista.html"); // Lekéri a hirlista.html oldalt, és a kapott válasszal lecseréli a #hirek elem tartalmát B) Paraméterek küldése // A második paraméterben GET paramétereket definiálhatunk. Természetesen nem csak html oldalakat, hanem bármilyen URL-t meghívhatunk, így dinamikusan generált oldalakat is, csupán a válaszul kapott tartalom típusa számíthat $("#hirek2").load("hirlista.php", { honnan: 0, mennyit: 10
load metódus 17 C) Callback függvény definiálása // Callback függvény definiálása, a harmadik paraméterben átadott függvényt meghívja, amint letöltődött a hirlista.html - így ekkor fog megjelenni a figyelmeztető ablak! $("#hirek3").load("hirlista.php", { honnan: 0, mennyit: 10 }, function() { alert('betöltöttem a hírlistát!'); D) Oldaltöredék betöltése $("hirek4").load("hirlista.php #hirlista"); // A választ a jquery feldolgozza, mielőtt beillesztené, és csak a #hirlista elemet (annak tartalmával együtt) fogja visszamásolni a #hirek4-be
Alacsonyabb szintű AJAX metódusok 18 Probléma nem GET, hanem POST metódus HTML feldolgozása beírás előtt nem HTML a válasz Megoldás $.get: GET küldés $.post: POST küldés $.getjson: a válasz objektum-literálként értelmeződik $.getscript: a választ kódként értelmezi és futtatja
Alacsonyabb szintű AJAX metódusok 19 A) Egyszerű HTTP kérés GET paraméterekkel $.get("hirlista.php", { honnan: 0, mennyit: 10 }, function(adat) { console.log(adat); // => '<div>első hír<div>bla</div></div>' // A paraméterlista opcionális: $.get("hirlista.php", function(adat){ console.log(adat); // => '<div>első hír<div>bla</div></div>' B) Egyszerű HTTP kérés POST paraméterekkel $.post("hirlista.php", { honnan: 0, mennyit: 10 }, function(adat){ console.log(adat); // => '<div>első hír<div>bla</div></div>'
Alacsonyabb szintű AJAX metódusok 20 C) Egyszerű HTTP kérés egyszerre GET és POST paraméterekkel $.post("hirlista.php?honnan=0", { mennyit: 10 }, function(adat){ console.log(adat); // => '<div>első hír<div>bla</div></div>' D) JSON típusú adat lekérése $.getjson("hirlista.php", { formatum: 'json' }, function(adat){ console.log(adat); // => [{ cim: 'Első hír', tartalom: 'Bla' }] E) Dinamikus szkript-letöltés $.getscript("jquery_ui.js", function() { console.log("a jquery UI sikeresen betöltve!");
Űrlap küldése AJAX-szal 21 serializearray metódus a formon $('#registration form').submit(function(e) { // A form elküldésének megakadályozása e.preventdefault(); var $this = $(this); $.post('regisztracio.php', $this.serializearray(), function(data) { $this.html(data);
Összetett AJAX kérések: $.ajax() 22 $.ajax a legalacsonyabb szintű jquery absztrakció AJAXhoz objektumliterállal a paraméterezhető átláthatóbb, jobban bővíthető kód az eredmény érdemes ezt használni ld. a jquery dokumentációt http://api.jquery.com/jquery.ajax/
$.ajax() példák 23 $.ajax({ url: 'hirlista.php', data: { mettol: 0 }, type: 'POST', datatype: 'html', cache: true, // a címzett URL // átadandó paraméterek // a data-ban átadott paraméterek típusa (GET vagy POST) // a válasz adattípusának beállítása, így fogja értelmezni a böngésző a kapott adatot. Értéke lehet: text (egyszerű sztring), html (egyszerű sztring, de, ha van benne <script> tag, akkor az abban levő kód lefut), json (objektum literált kapunk), script (a kapott szöveg JS kódként lesz értelmezve és azonnal lefut), jsonp vagy xml. Ha nem adunk meg datatypeot, akkor a jquery megpróbálja kitalálni, hogy milyen adattípusról is van szó! // logikai érték, false-ra állítva nem cacheli a böngésző a kérést (alapértéke a true) beforesend: function() { console.log('metódus, mely az Ajax kérés előtt fut le!'); return false; // a return false ebben a metódusban megakadályozza az Ajax kérést! }, success: function(data) { console.log('sikeres kérés. A kapott válasz: ', data); }, error: function(request, status) { console.log('hiba történt a küldés során. A hiba leírása: ', status); }, complete: function(request, status) { console.log('az error vagy success után, de mindenképpen lefut a kérés végén'); }
Globális AJAX beállítások 24 Központi AJAX eseménykezelők beállítása több XHR kérés közös funkcionalitása globális hibakezelés $.ajaxsetup(): bármelyik alapérték felüldefiniálható, eseménykezelők is Globális AJAX események regisztrálása külön ajaxstart, ajaxsend, ajaxsuccess, ajaxerror, ajaxcomplete, ajaxstop
$.ajaxsetup példa 25 // Töltő animáció megjelenítése var loading = function(status) { status? $("#loader").show() : $("#loader").hide(); } // Alapértékek beállítása, az itt definiált eseménykezelők lefutnak minden Ajax kérés esetén $.ajaxsetup({ cache: false, complete: function() { loading(false); }, error: function(request, status, errorthrown) { console.log('hiba: ', status); } loading(true); $.post('http://www.google.hu', {engedjbe: true // A töltés eltűnik úgy is, hogy hibával tér vissza az Ajax kérés. Ha nem kezelnénk le, akkor a töltő animáció ott ragadna az oldalon, a felhasználó pedig nem értené, hogy miért tölt folyamatosan a weboldal!
Globális AJAX események 26 $("#loading").bind("ajaxsend", function(){ $(this).show(); }).bind("ajaxcomplete", function(){ $(this).hide();
27 JSON
JSON 28 JavaScript Object Notation JavaScript JSON.parse(), JSON.stringify() PHP json_encode(), json_decode() jquery AJAX $.getjson()
JSON példa 29 A) JSON sztringek értelmezése var vombatobjektum = { "tipus": "Csupaszorrú", "nev": "Aladár" }, // egyszerű objektum literál vombatjson = '{ "tipus": "Csupaszorrú", "nev": "Aladár" }'; // egyszerű objektum literál sztring alakban vombatobjektum; // => { tipus: "Csupaszorrú", nev: "Aladár" } var vombat = $.parsejson(vombatjson); // a JSON sztring értelmezése vombat; // => { tipus: "Csupaszorrú", nev: "Aladár" } vombat.tipus; // => "Csupaszorrú" var osszetett = '[1,2,{ "almastatusza": "ép" }, true]'; $.parsejson(osszetett); // => [1,2,{ almastatusza: "ép" }, true]
JSON példa 30 { } "dolgozok": [ { "nev": "Hajni", "munkakor": "Esztergályos", "aktiv": true }, { "nev": "Bulcsu", "munkakor": "Termékmenedzser", "aktiv": true }, { "nev": "János", "munkakor": "Főnök", "aktiv": false } ]
JSON példa 31 C) Példa PHP és JavaScript közötti adatcserére // Egyszerű PHP szkript // vombatgenerator.php kezdete ====> $vombat = array( 'tipus' => 'Csupaszorrú', 'nev' => 'Aladár' ); echo json_encode($vombat); // vombatgenerator.php vége ====> // JavaScriptben ezt az adatot egy Ajax függvénnyel kérdezhetjük le, pl. a getjson-el $.getjson("vombatgenerator.php", function(vombat) { console.log(vombat); // => { tipus: "Csupaszorrú", nev: "Aladár" }
32 AJAX és MVC
AJAX és MVC 33 Az AJAX-os hívás a szerver szempontjából ugyanolyan HTTP hívás, mint bármi más AJAX-os hívásnál meg kell adni egy vezérlőt és annak egy metódusát külön metódus ajaxos kérések kiszolgálására ugyanaz a metódus normál és ajaxos kérésekre pl. ha egy oldalt ajaxszal és nélküle is használnánk routing és remap használata
AJAX és MVC 34 Honnan tudjuk, hogy egy hívás AJAX-os volt? Speciális url (pl. # a kérésben) Speciális HTTP fejléc a kérésben Pl. request library <?php class Request { function is_ajax() { return (isset($_server['http_x_requested_with']) && ($_SERVER['HTTP_X_REQUESTED_WITH'] == 'XMLHttpRequest')); } }
AJAX és MVC 35 Az AJAX válasz nézetben van HTML Szöveg JSON XML Egyszerűbb esetekben szerveroldalon nem kell megkülönböztetni: kliens dolgozza fel a HTML választ load metódus
36 AJAX-os megfontolások
Megfontolandó gondolatok 37 Technikai szempont, biztonság, korlátozások Architekturális szempont Ergonómiai szempont Vissza/frissít gomb
38 Aszinkronitás
Fogalmak 39 Szinkron/Aszinkron kód Callback Események Promise
Aszinkron kód 40 // Aszinkron hívás késleltetett metódussal: // a settimeout 1000ms-al a deklaráció után a Queue-ba // teszi az elsõ paraméterben található függvényt settimeout(function() { console.log('aszinkron hívás történt!'); }, 1000); console.log('az Aszinkron hívás elõtt lefut, hiszen' + 'ez a parancs már a Stack-ben van!'); // Aszinkron hívás user inputra: // a callback függvény akkor kerül be a Queue-ba, ha // a felhasználó rákattint a gombra document.getelementbyid('supabutton').addeventlistener( 'click', function() { console.log('aszinkron hívás történt!'); } );
Aszinkron kód 41 function maybe() { console.log(" execute async, maybe?"); } function proveit() { settimeout(maybe, 0); console.log("hey, you just invoked me, and this is crazy "); console.log("but I'll queue you up"); return "and you'll "; } proveit();
Technológiai háttér 42 Heap Adatok, függvények, visszatérési értékek Nem rendezett Stack Futás alatt álló függvények blokkol Queue Feldolgozásra váró függvények Event loop
Call Stack 43 function h(z) { // Print stack trace console.log(new Error().stack); // (A) } function g(y) { h(y + 1); // (B) } function f(x) { g(x + 1); // (C) } f(3); // (D) return; // (E)
44 Event loop
Aszinkron műveletek 45 Időzítők Bizonyos DOM változások AJAX Node.js process.nexttick() (Node.js) readfile Stb.
Aszinkronitás kezelése 46 Események Callback
Események 47 var req = new XMLHttpRequest(); req.open('get', url); req.onload = function() { if (req.status == 200) { processdata(req.response); } else { console.log('error', req.statustext); } }; req.onerror = function() { console.log('network Error'); }; req.send(); // Add request to task queue
Callback 48 fs.readfile('usernames.csv', function (err, data) { if (err) throw err; console.log(data);
CPS 49 Continuiation-passing style //Szinkron function loadavatarimage(id) { var profile = loadprofile(id); return loadimage(profile.avatarurl); } //Callback (nem feltétlen aszinkron) function loadavatarimage(id, callback) { loadprofile(id, function (profile) { loadimage(profile.avatarurl, callback); }
CPS 50 //CPS (szinkron) function f() { g(function (result) { console.log(result); } function g(success) { h(success); } function h(success) { success(123); }
Direkt hívással 51 function f() { console.log(g()); } function g() { return h(); } function h() { return 123; }
Callback hell/pyramid of doom 52 var dialog = new Dialog(), maprenderer = new MapRenderer(dialog); $('#homebutton').on('click', function() { dialog.open(function() { maprenderer.render(); $.getjson('/gethomeinfo', function(home) { GMaps.geocode({ address: home.address, callback: function(position) { maprenderer.addaddress(home, position); }
Függvények kiemelése 53 var HomeDialog = function() { this.dialog = new Dialog(); this.maprenderer = new MapRenderer(this.dialog); }; HomeDialog.prototype.open = function() { var render = this.render.bind(this); this.dialog.open.call(this.dialog, render); }; HomeDialog.prototype.render = function() { this.maprenderer.render(); this.gethomedata(this.renderhome); }; HomeDialog.prototype.getHomeData = function(callback) { $.getjson('/gethome', (function(homeinfo) { this.home = homeinfo; callback(); }).bind(this); }; HomeDialog.prototype.renderHome = function() { GMaps.geocode({ address: this.home.address, callback: this.renderposition.bind(this) }; HomeDialog.prototype.renderPosition = function(position) { maprenderer.addaddress(this.home, position); }; var homedialog = new HomeDialog(); $('#homebutton').on('click', homedialog.open.bind(homedialog));
Promise 54 A promise egy olyan művelet eredményét reprezentálja, aminek az értéke a jelenben nem ismert. Success/error
readfile promise-szal és nélküle 55 fs.readfile('usernames.csv', function (err, data) { if (err) throw err; console.log(data); readfilepromisified('usernames.csv').then(function(data) { console.log('got data: ', data); }, function() { console.log('error happened!');
readfile példa bővítve 56 readfilepromisified('usernames.csv').then(function(data) { var validusernames = data.split("\n"); // Õrizzük meg azokat a neveket, amik nem üresek validusernames = validusernames.filter(function(username) { return username.trim()!== ''; return fs.writefile('usernames.csv', validusernames.join("\n")); }).then(function() { console.log('successfully fixed the usernames!');
Visszatérési érték továbbadása 57 readfilepromisified('usernames.csv').then(filtervalidusernames).then(function(validusernames) { fs.writefile('usernames.csv', validusernames); }).then(function() { console.log('successfully fixed the usernames!'); function filtervalidusernames(data) { var validusernames = data.split("\n"); // Õrizzük meg azokat a neveket, amik nem üresek return validusernames = validusernames.filter(function(username) { return username.trim()!== ''; }
Hibakezelés 58 readfilepromisified('usernames.csv').then(filtervalidusernames).then(writeusernames).then(showsuccessmessage).catch(function() { alert('something bad happened! :( ');
Promise-szá alakítás 59 var readfilepromisified = function(filename) { return new Promise(function(resolve, reject) { fs.readfile(filename, function (err, data) { if (err) reject(); else resolve(data); };
Promise létrehozása és kezelése 60 var promise = new Promise( function (resolve, reject) { // (A)... if (...) { resolve(value); // success } else { reject(reason); // failure } promise.then( function (value) { /* fulfillment */ }, function (reason) { /* rejection */ } ); promise.then( function (value) { /* fulfillment */ } ); promise.catch( function (reason) { /* rejection */ } );
61 httpget() function httpget(url) { return new Promise( function (resolve, reject) { var request = new XMLHttpRequest(); } request.onreadystatechange = function () { if (this.status === 200) { // Success resolve(this.response); } else { // Something went wrong (404 etc.) reject(new Error(this.statusText)); } } request.onerror = function () { reject(new Error( 'XMLHttpRequest Error: '+this.statustext)); }; request.open('get', url); request.send(); httpget('http://example.com/file.txt').then( function (value) { console.log('contents: ' + value); }, function (reason) { console.error('something went wrong', reason);
delay() 62 function delay(ms) { return new Promise(function (resolve, reject) { settimeout(resolve, ms); // (A) } // Using delay(): delay(5000).then(function () { // (B) console.log('5 seconds have passed!')