JavaScript: dobre stvari

http://mbranko.github.io/webkurs

Ovo je deo web kursa

Literatura

  • JavaScript Programmers Reference
  • JavaScript: The Good Parts
  • JavaScript Guide od Mozilla Developers Network.
  • Code Academy sa interaktivnim JavaScript lekcijama.
  • Khan Academy ima puno informacija o crtanju i animaciji.

Sadržaj

  1. Kaskade
  2. Callback
  3. Moduli
  4. Obećanja
  5. Web Storage
  6. Efikasno učitavanje
  7. CORS

#1: cascade

Cascade

Mogućnost da pozovemo više metoda prosleđujući im isti objekat:

str.replace("k", "R").toUpperCase().substr(0,4);

Cascade: primer


​var userController = {
  currentUser: "",
  findUser: function (userEmail) {
    var arrayLength = usersData.length, i;
    for (i = arrayLength - 1; i >= 0; i--) {
      if (usersData[i].email === userEmail) {
        this.currentUser = usersData[i];
        break;
      }
    }
    return this;
  },
  formatName: function () {
    if (this.currentUser) {
      this.currentUser.fullName = this.currentUser.firstName + " " + this.currentUser.lastName;
    }
    return this;
  },
  createLayout: function () {
    if (this.currentUser) {
      this.currentUser.viewData = "

Member: " + this.currentUser.fullName + "

"​ + "

ID: " + this.currentUser.id + "

" + "

Email: " + this.currentUser.email + "

"; } return this; }, };

Svaka funkcija vraća this.

Cascade: primer

Primer pozivanja


​userController.findUser("test2@test2.com").formatName().createLayout().displayUser();
          

#2: callback

Callback funkcija

  • koncept iz funkcionalnog programiranja
  • funkcija kao parametar funkcije

$("#btn_1").click(function() {
  alert("Btn 1 Clicked");
});          
          

var friends = ["Mike", "Stacy", "Andy", "Rick"];
​
friends.forEach(function (eachName, index){
  console.log(index + 1 + ". " + eachName); // 1. Mike, 2. Stacy, 3. Andy, 4. Rick​
});         
          

Callback funkcija

  • prilikom poziva funkcije prosleđujemo definiciju druge funkcije
  • ne izvršava se prilikom poziva

  • callback funkcija je closure

Callback tip #1: anonimne i imenovane funkcije

primer imenovane callback funkcije


​var allUserData = [];
​
​function logStuff(userData) {
  if (typeof userData === "string")
    console.log(userData);
  else if (typeof userData === "object")
    for (var item in userData)
      console.log(item + ": " + userData[item]);
}
​
​function getInput(options, callback) {
  allUserData.push(options);
  callback(options);
}
​
​getInput ({name:"Rich", speciality:"JavaScript"}, logStuff);
​          

Callback funkcija prima parametar od funkcije koja je obuhvata.

Callback tip #2: prenos parametara

callback može pristupiti globalnim promenljivima


​​var defaultOptions = { ... };
​
​function logStuff(userData) {
  if (!userData)
    userData = defaultOptions;
  ...
}
          

Callback tip #3: provera da li je callback

pre poziva može se proveriti da li je u pitanju funkcija


​​function getInput(options, callback) {
  allUserData.push(options);
​  if (typeof callback === "function")
    callback(options);
}
          

Callback tip #4: pazi na this

Kada je callback metoda koja koristi this:


​​// definiši objekat sa metodom
​// metodu ćemo kasnije proslediti kao callback
​var clientData = {
  id: 094545,
  fullName: "Not Set",
  setUserName: function (firstName, lastName)  {
    this.fullName = firstName + " " + lastName;
  }
}
​
​function getUserInput(firstName, lastName, callback)  {
  callback (firstName, lastName);
}
          

Kada se pozove setUserName, this se ne odnosi na objekat clientData nego na window objekat u web čitaču, jer je getUserInput globalna funkcija. U globalnoj funkciji this pokazuje na window.

Callback tip #4: pazi na this

Funkcije call i apply mogu da postave this unutar funkcije i proslede parametre funkciji. Obe funkcije primaju novu vrednost za this kao prvi parametar.


​​// dodali smo novi parametar ovde: callbackObj
​function getUserInput(firstName, lastName, callback, callbackObj)  {
  callback.apply(callbackObj, [firstName, lastName]);
}
          

Drugi parametar za apply je niz koji će se proslediti funkciji kao njeni parametri.

Naredni parametri za call su parametri koji će se direktno proslediti funkciji.

Callback tip #5: više callback funkcija odjednom

Možemo proslediti više callback funkcija odjednom prilikom poziva.


​​function successCallback() { ... }
​
​function completeCallback() { ... }
​
​function errorCallback() { ... }
​
$.ajax({
    url: "http://fiddle.jshell.net/favicon.png",
    success: successCallback,
    complete: completeCallback,
    error: errorCallback
});
          

Primer koristi jQuery ajax funkciju.

Callback Hell

Callback poziva callback poziva callback poziva...


var p_client = new Db('integration_tests_20', new Server("127.0.0.1", 27017, {}), {'pk':CustomPKFactory});
p_client.open(function(err, p_client) {
    p_client.dropDatabase(function(err, done) {
        p_client.createCollection('test_custom_key', function(err, collection) {
            collection.insert({'a':1}, function(err, docs) {
                collection.find({'_id':new ObjectID("aaaaaaaaaaaa")}, function(err, cursor) {
                    cursor.toArray(function(err, items) {
                        test.assertEquals(1, items.length);
​
                        // Let's close the db​
                        p_client.close();
                    });
                });
            });
        });
    });
});
          

Primer iz MongoDB drajvera za Node.js.

Callback Hell

Dva pristupa rešavanju ovog problema:

  1. Dati ime funkciji i proslediti samo ime funkcije kao callback, umesto pisanja anonimne funkcije unutar poziva.
  2. Podeliti kod u module, tako da se odgovarajući modul importuje u veću aplikaciju.

#3: moduli

JavaScript nema module

  • Java ima pakete
  • C# ima namespaces
  • JavaScript ima samo globalni opseg i lokalni opseg
  • nema nivoa između!

Koncept modula

  • smeštanje srodnih funkcija u poseban fajl
  • sprečavanje kolizije imena
  • pakovanje u module
  • online katalog i distribucija modula (NPM)

Funkcija kao namespace

Funkcija je jedina stvar u JavaScriptu koja pravi novi opseg. Pogledajmo primer...


​var names = ["Sunday", "Monday", "Tuesday", "Wednesday",
             "Thursday", "Friday", "Saturday"];

function dayName(number) {
  return names[number];
}

console.log(dayName(1)); // → Monday
          

Funkcija kao namespace

... kada se malo preradi:


​var dayName = function() {
  var names = ["Sunday", "Monday", "Tuesday", "Wednesday",
               "Thursday", "Friday", "Saturday"];
  return function(number) {
    return names[number];
  };
}();

console.log(dayName(3)); // → Wednesday
          

names je sada lokalna promenljiva. Ova funkcija se kreira i odmah poziva, a njen rezultat je funkcija koja se smešta u promenljivu dayName. Ovde može biti hiljade linija koda sa puno lokalnih promenljivih; one bi bile vidljive samo u našoj funkciji ali ne i spolja.

Funkcija kao namespace

Sada hoćemo da vratimo dve funkcije! Moramo ih spakovati u objekat:


​var weekDay = function() {
  var names = ["Sunday", "Monday", "Tuesday", "Wednesday",
               "Thursday", "Friday", "Saturday"];
  return {
    name: function(number) { return names[number]; },
    number: function(name) { return names.indexOf(name); }
  };
}();

console.log(weekDay.name(weekDay.number("Sunday"))); // → Sunday
          

Za veće module, sakupljanje svih vrednosti u objekat na kraju funkcije može biti nečitko. Želimo da eksportovane funkcije definišemo na zgodnijem mestu.

Funkcija kao namespace

Deklarišemo objekat i dodajemo osobine u njega kad god imamo nešto za eksport.


​(function(exports) {
  var names = ["Sunday", "Monday", "Tuesday", "Wednesday",
               "Thursday", "Friday", "Saturday"];

  exports.name = function(number) {
    return names[number];
  };
  exports.number = function(name) {
    return names.indexOf(name);
  };
})(this.weekDay = {});

console.log(weekDay.name(weekDay.number("Saturday"))); // → Saturday
          

Samo jedna promenljiva u globalnom opsegu - weekDay. Ali šta ako dva modula koriste isto ime za globalnu promenljivu?

Funkcija kao namespace

Deklarišemo objekat i dodajemo osobine u njega kad god imamo nešto za eksport.


​(function(exports) {
  var names = ["Sunday", "Monday", "Tuesday", "Wednesday",
               "Thursday", "Friday", "Saturday"];

  exports.name = function(number) {
    return names[number];
  };
  exports.number = function(name) {
    return names.indexOf(name);
  };
})(this.weekDay = {});

console.log(weekDay.name(weekDay.number("Saturday"))); // → Saturday
          

Samo jedna promenljiva u globalnom opsegu - weekDay. Ali šta ako dva modula koriste isto ime za globalnu promenljivu?

Sudari u globalnom opsegu

  • hoćemo da napravimo funkciju require koja će, za dato ime modula, da ga učita iz fajla ili sa weba i vratiti odgovarajuću vrednost
  • rešava prethodni problem i sprečava da koristimo modul bez eksplicitnog uvoženja
  • moramo biti u stanju da učitani string izvršimo kao JavaScript kod u funkciji require

Izvršavanje koda iz stringa

Prost način: upotreba eval.


​function evalAndReturnX(code) {
  eval(code);
  return x;
}

console.log(evalAndReturnX("var x = 2")); // → 2
          

Za nevolju, eval će rezultate izvršavanja upisati u tekući opseg.

Izvršavanje koda iz stringa

Bolji način: pomoću Function konstruktora. On prima dva argumenta: string sa nazivima parametara funkcije razdvojenih zarezima i string sa telom funkcije.


​var plusOne = new Function("n", "return n + 1;");
console.log(plusOne(4)); // → 5
          

Pri kraju smo: umotaćemo kod modula u funkciju, i ta funkcija postaje opseg za naš modul.

Funkcija require

Minimalna implementacija za require:


​function require(name) {
  var code = new Function("exports", readFile(name));
  var exports = {};
  code(exports);
  return exports;
}

console.log(require("weekDay").name(1)); // → Monday
          

Pošto funkcija require umotava kod u funkciju, nema potrebe da to radimo u modulu.

Primer modula za require

Sada fajl sa modulom može ovako da izgleda:


​var names = ["Sunday", "Monday", "Tuesday", "Wednesday",
             "Thursday", "Friday", "Saturday"];

exports.name = function(number) {
  return names[number];
};
exports.number = function(name) {
  return names.indexOf(name);
};
          

Tipičan modul

Tipičan modul će na vrhu učitati module koji su mu potrebni:


​var weekDay = require("weekDay");
var today = require("today");

console.log(weekDay.name(today.dayNumber()));
          
  • Ovakav require će izvršiti kod modula svaki put kad se učitava.
  • Nije moguće eksportovati ništa osim exports objekta (npr. funkciju).

CommonJS

Moduli će dobiti promenljivu module koja je objekat sa osobinom exports. Ova osobina inicijalno pokazuje na {} koji je kreirao require.


​function require(name) {
  if (name in require.cache)
    return require.cache[name];

  var code = new Function("exports, module", readFile(name));
  var exports = {}, module = {exports: exports};
  code(exports, module);

  require.cache[name] = module.exports;
  return module.exports;
}
require.cache = Object.create(null);
          
  • ovakav stil rada sa modulima zove se CommonJS
  • koristi ga i Node.js

AMD

  • učitavanje modula sa weba može biti sporo...
  • ...rešenje: umotaj modul u funkciju tako da se u pozadini obavi
    1. učitavanje drugih potrebnih modula
    2. inicijalizacija modula pozivom te funkcije
  • to se naziva AMD (Asynchronous Module Definition)

Upotreba AMD modula


​define(["weekDay", "today"], function(weekDay, today) {
  console.log(weekDay.name(today.dayNumber()));
});
          

Funkcija define prima niz sa nazivima modula i zatim funkciju koja prima po jedan parametar za svaki modul. Učitaće module u pozadini dok stranica radi. Kada su svi moduli učitani pozvaće datu funkciju koja će obaviti inicijalizaciju.

Upotreba AMD modula

Nova verzija našeg modula:


​define([], function() {
  var names = ["Sunday", "Monday", "Tuesday", "Wednesday",
               "Thursday", "Friday", "Saturday"];
  return {
    name: function(number) { return names[number]; },
    number: function(name) { return names.indexOf(name); }
  };
});
          

Funkcija define prima niz sa nazivima modula i zatim funkciju koja prima po jedan parametar za svaki modul. Učitaće module u pozadini dok stranica radi. Kada su svi moduli učitani pozvaće datu funkciju koja će obaviti inicijalizaciju.

Implementacija AMD

Funkcija getModule će učitati modul ili ga izvući iz keša.


​var defineCache = Object.create(null);
var currentMod = null;

function getModule(name) {
  if (name in defineCache)
    return defineCache[name];

  var module = {exports: null,
                loaded: false,
                onLoad: []};
  defineCache[name] = module;
  backgroundReadFile(name, function(code) {
    currentMod = module;
    new Function("", code)();
  });
  return module;
}
          

Funkcija backgroundReadFile nije jednostavna.

Implementacija AMD

Funkcija define će pozvati inicijalizaciju modula kada prikupi sve potrebne module.


​function define(depNames, moduleFunction) {
  var myMod = currentMod;
  var deps = depNames.map(getModule);

  deps.forEach(function(mod) {
    if (!mod.loaded)
      mod.onLoad.push(whenDepsLoaded);
  });

  function whenDepsLoaded() {
    if (!deps.every(function(m) { return m.loaded; }))
      return;

    var args = deps.map(function(m) { return m.exports; });
    var exports = moduleFunction.apply(null, args);
    if (myMod) {
      myMod.exports = exports;
      myMod.loaded = true;
      myMod.onLoad.forEach(function(f) { f(); });
    }
  }
  whenDepsLoaded();
}
          

RequireJS

Funkcija define će učitati modul ili ga izvući iz keša pomoću getModule. Njen zadatak je da se moduleFunction (funkcija koja sadrži kod modula) pozove kada su učitani svi potrebni moduli. Zato definiše whenDepsLoaded koja se dodaje na kraj onLoad niza svih trenutno nedostajućih modula. Ova funkcija se odmah vraća ako ima još neučitanih modula. Tako će se posao obaviti samo jednom, kada se učita i poslednji modul. Poziva se i direktno iz define ako tekući modul nema potrebnih modula.

Kada su svi potrebni moduli dostupni, whenDepsLoaded poziva funkciju koja obmotava modul dajući joj sve tražene module kao parametre.

RequireJS radi na ovaj način.

#4: obećanja

Promise objekti

Implementirani u posebnim bibliotekama od ranije, npr:

Podrška u web čitačima:

  • Chrome 32
  • Firefox 29
  • Safari 8
  • Microsoft Edge

Ili ovaj polyfill za starije čitače.

Uvedeni u ECMAScript 6.

Promise definicija

Promise je proxy za vrednost koju ne moramo znati u vreme kreiranja. Može biti u tri stanja:

  • pending: inicijalno stanje, niti ispunjeno niti odbijeno
  • fulfilled: operacija je uspešno završena
  • rejected: operacija nije uspešno završena

  • settled: fulfilled ili rejected

Primer


​var img1 = document.querySelector('.img-1');

img1.addEventListener('load', function() {
  // ...
});

img1.addEventListener('error', function() {
  // ...
});
          

Događaj može da se desi pre nego što počnemo da ga osluškujemo!

Primer

Da probamo da iskoristimo osobinu complete za slike:


​var img1 = document.querySelector('.img-1');

function loaded() {
  // ...
}

if (img1.complete) {
  loaded();
}
else {
  img1.addEventListener('load', loaded);
}

img1.addEventListener('error', function() {
  // ...
});
          

Ne hvata slike koje su proizvele grešku pre nego što smo počeli da slušamo.

Ako treba da obradimo više slika...

Primer

Idealno nam treba nešto ovakvo:


​img1.callThisIfLoadedOrWhenLoaded(function() {
  // loaded
}).orIfFailedCallThis(function() {
  // failed
});

whenAllTheseHaveLoaded([img1, img2]).callThis(function() {
  // all loaded
}).orIfSomeFailedCallThis(function() {
  // one or more failed
});
          

Ovaj problem rešavaju promise objekti.

Primer

Kada bi img element imao ready metodu koja vraća promise:


​img1.ready().then(function() {
  // loaded
}, function() {
  // failed
});

Promise.all([img1.ready(), img2.ready()]).then(function() {
  // all loaded
}, function() {
  // one or more failed
});
          

Primer

Promise - slično osluškivanju događaja osim:

  1. Promise može biti uspešan ili neuspešan jednom. Ne može dva puta, niti se može menjati status iz uspešnog u neuspešno ili obrnuto.
  2. Ako je promise bio uspešan ili neuspešan, a kasnije smo dodali callback funkcije, odgovarajući callback će biti pozvan iako se događaj desio pre toga.

Promise: kreiranje

Ovako se kreira promise:


​var promise = new Promise(function(resolve, reject) {
  // uradi neki posao

  if (/* sve je u redu? */) {
    resolve("Radi!");
  } else {
    reject(Error("Ne radi!"));
  }
});
          

Promise: korišćenje

A ovako se koristi:


​promise.then(function(result) {
  console.log(result); // "Radi!"
}, function(err) {
  console.log(err); // Error: "Ne radi!"
});
          

then()

then prima dva parametra

  1. callback za uspešan ishod
  2. callback za neuspešan ishod

Primer

Naša stranica bi trebalo da:

  1. pokrene spinner da prikaže učitavanje
  2. učita JSON za knjigu (što nam daje naslov i URI-je za svako poglavlje)
  3. postavi naslov stranice
  4. učita sva poglavlja
  5. doda knjigu na stranicu
  6. zaustavi spinner

U slučaju greške treba obavestiti korisnika i zaustaviti spinner.

Promisify XMLHttpRequest

​function get(url) {
  return new Promise(function(resolve, reject) {
    var req = new XMLHttpRequest();
    req.open('GET', url);

    req.onload = function() {
      if (req.status == 200)
        // uspešan ishod
        resolve(req.response);
      else
        // neuspešan ishod
        reject(Error(req.statusText));
    };

    // mrežne greške
    req.onerror = function() {
      reject(Error("Network Error"));
    };

    // pošalji zahtev
    req.send();
  });
}

Promisify XMLHttpRequest

Korišćenje prethodno napravljenog promise:

​
get('story.json').then(function(response) {
  console.log("Success!", response);
}, function(error) {
  console.error("Failed!", error);
})

Ulančavanje then

Rezultat učitavanja će biti JSON tekst koji treba parsirati:

​
get('story.json').then(function(response) {
  return JSON.parse(response);
}).then(function(response) {
  console.log("Yey JSON!", response);
})

Pošto JSON.parse prima jedan parametar, to može i kraće:

​
get('story.json').then(JSON.parse).then(function(response) {
  console.log("Yey JSON!", response);
})

Ulančavanje then

Ako callback vrati

  • gotovu vrednost: sledeći then će je preuzeti
  • promise: sledeći then će je sačekati
​
getJSON('story.json').then(function(story) {
  return getJSON(story.chapterUrls[0]);
}).then(function(chapter1) {
  console.log("Got chapter 1!", chapter1);
})

Hvatanje greške

then prima dva parametra:

​
get('story.json').then(function(response) {
  console.log("Success!", response);
}, function(error) {
  console.log("Failed!", error);
})

Može i pomoću catch:

​
get('story.json').then(function(response) {
  console.log("Success!", response);
}).catch(function(error) {
  console.log("Failed!", error);
})

Ali to nije baš isto!

Hvatanje greške

Kada se koristi catch:

​
get('story.json').then(function(response) {
  console.log("Success!", response);
}).catch(function(error) {
  console.log("Failed!", error);
})

je ekvivalentno sa:

​
get('story.json').then(function(response) {
  console.log("Success!", response);
}).then(undefined, function(error) {
  console.log("Failed!", error);
})

Hvatanje greške

Sa then(f1, f2), biće pozvana ili f1 ili f2, nikada obe.

Sa then(f1).catch(f2), biće pozvane obe i ako f1 proizvede grešku jer su to posebni koraci u lancu.

Izuzeci i promise

Negativan ishod za promise dobija se

  • eksplicitno, pozivanjem reject callback-a
  • bacanjem izuzetka u konstruktoru promise-a, kao u primeru:
​
var jsonPromise = new Promise(function(resolve, reject) {
  // JSON.parse baca grešku ako tekst nije pravilan JSON
  // tako da se ovo implicitno reject-uje
  resolve(JSON.parse("This ain't JSON"));
});

jsonPromise.then(function(data) {
  // nikad se neće desiti
  console.log("It worked!", data);
}).catch(function(err) {
  // ovo će se desiti
  console.log("It failed!", err);
})
          

Izuzeci i promise

Isto važi i za greške koje nastanu u callbacku za then:

​
get('/').then(JSON.parse).then(function() {
  // Ovo se neće desiti, '/' je HTML strana, ne JSON
  // pa će JSON.parse baciti izuzetak
  console.log("It worked!", data);
}).catch(function(err) {
  // ovo će se desiti
  console.log("It failed!", err);
})
          

Primer

U primeru sa učitavanjem knjige:

​
getJSON('book.json').then(function(book) {
  return getJSON(book.chapterUrls[0]);
}).then(function(chapter1) {
  addHtmlToPage(chapter1.html);
}).catch(function() {
  addTextToPage("Failed to show chapter");
}).then(function() {
  document.querySelector('.spinner').style.display = 'none';
})
          

Ako getJSON(book.chapterUrls[0]) vrati grešku, preskaču sve svi then callbacks, i prelazi na catch callback. Spinner će se isključiti u oba slučaja.

Primer

Prethodni primer je asinhrona varijanta sledeće ideje:

​
try {
  var story = getJSONSync('story.json');
  var chapter1 = getJSONSync(story.chapterUrls[0]);
  addHtmlToPage(chapter1.html);
} catch (e) {
  addTextToPage("Failed to show chapter");
}
document.querySelector('.spinner').style.display = 'none'
          

Primer

Ako koristimo catch samo da zabeležimo grešku ali nastavljamo rad:

​
function getJSON(url) {
  return get(url).then(JSON.parse).catch(function(err) {
    console.log("getJSON failed for", url, err);
    throw err;
  });
}
          

Ponovo bacimo grešku.

Paralelizam i sekvenca

Počnimo od sinhrone varijante koja učitava sva poglavlja:

​
try {
  var story = getJSONSync('story.json');
  addHtmlToPage(story.heading);

  story.chapterUrls.forEach(function(chapterUrl) {
    var chapter = getJSONSync(chapterUrl);
    addHtmlToPage(chapter.html);
  });

  addTextToPage("All done");
}
catch (err) {
  addTextToPage("Argh, broken: " + err.message);
}

document.querySelector('.spinner').style.display = 'none'
          

Sinhrono izvršavanje će blokirati web čitač dok traje download.

Paralelizam i sekvenca

Počnimo od sinhrone varijante koja učitava sva poglavlja:

​
try {
  var story = getJSONSync('story.json');
  addHtmlToPage(story.heading);

  story.chapterUrls.forEach(function(chapterUrl) {
    var chapter = getJSONSync(chapterUrl);
    addHtmlToPage(chapter.html);
  });

  addTextToPage("All done");
}
catch (err) {
  addTextToPage("Argh, broken: " + err.message);
}

document.querySelector('.spinner').style.display = 'none'
          

Sinhrono izvršavanje će blokirati web čitač dok traje download.

Paralelizam i sekvenca

Asinhrona varijanta bi trebalo da izgleda ovako:

​
getJSON('story.json').then(function(story) {
  addHtmlToPage(story.heading);

  // TODO: za svaki url u story.chapterUrls, dobavi i prikaži ga
}).then(function() {
  // završili smo
  addTextToPage("All done");
}).catch(function(err) {
  // uhvati usputne greške
  addTextToPage("Argh, broken: " + err.message);
}).then(function() {
  // uvek
  document.querySelector('.spinner').style.display = 'none';
})
          

Paralelizam i sekvenca

Ali kako da prođemo kroz poglavlja i učitavamo ih u pravom redosledu? Ovo neće raditi:

​
story.chapterUrls.forEach(function(chapterUrl) {
  // dobavi poglavlje
  getJSON(chapterUrl).then(function(chapter) {
    // dodaj ga na stranicu
    addHtmlToPage(chapter.html);
  });
})
          

forEach nije async-aware, tj. ne vodi računa o završetku operacije za svaki element sekvence. Poglavlja će se pojavljivati redosledu dobavljanja umesto u pravom redosledu.

Paralelizam i sekvenca

Treba da pretvorimo chapterUrls u listu promisa.

​
// počni od promisa koji je uvek uspešan
var sequence = Promise.resolve();

// iteracija kroz chapterUrls
story.chapterUrls.forEach(function(chapterUrl) {
  // dodaj ove akcije na kraj sekvence
  sequence = sequence.then(function() {
    return getJSON(chapterUrl);
  }).then(function(chapter) {
    addHtmlToPage(chapter.html);
  });
})
          

Paralelizam i sekvenca

Ovo može i pomoću Array.reduce:

​
// iteracija kroz chapterUrls
story.chapterUrls.reduce(function(sequence, chapterUrl) {
  // dodaj ove akcije na kraj sekvence
  return sequence.then(function() {
    return getJSON(chapterUrl);
  }).then(function(chapter) {
    addHtmlToPage(chapter.html);
  });
}, Promise.resolve())
          

Ne treba nam posebna promenljiva.

Paralelizam i sekvenca

Rešenje:

​getJSON('story.json').then(function(story) {
  addHtmlToPage(story.heading);

  return story.chapterUrls.reduce(function(sequence, chapterUrl) {
    // kada je završio promise prethodnog poglavlja
    return sequence.then(function() {
      // dobavi sledeće poglavlje
      return getJSON(chapterUrl);
    }).then(function(chapter) {
      // i dodaj ga na stranicu
      addHtmlToPage(chapter.html);
    });
  }, Promise.resolve());
}).then(function() {
  // sve je gotovo
  addTextToPage("All done");
}).catch(function(err) {
  // uhvati greške usput
  addTextToPage("Argh, broken: " + err.message);
}).then(function() {
  // uvek skloni spinner na kraju
  document.querySelector('.spinner').style.display = 'none';
})

Promise.all

Zašto da radimo download sekvencijalno? Postoji API:

​
Promise.all(arrayOfPromises).then(function(arrayOfResults) {
  //...
})
          

Promise.all prima niz promisa i kreira promise koji je ispunjen kada se svi uspešno završe. Dobija se niz rezultata u redosledu koji odgovara redosledu promisa.

Paralelizam i sekvenca

Promise.all primenjeno na naš problem:

​getJSON('story.json').then(function(story) {
  addHtmlToPage(story.heading);

  // kreiraj niz promisa i čekaj na sve njih
  return Promise.all(
    // mapiraj chapterUrls niz na niz JSON promisa
    story.chapterUrls.map(getJSON)
  );
}).then(function(chapters) {
  // sada imamo JSON-e u pravom redosledu, iteriramo kroz njih
  chapters.forEach(function(chapter) {
    // i dodajemo u stranicu
    addHtmlToPage(chapter.html);
  });
  addTextToPage("All done");
}).catch(function(err) {
  // uhvati greške usput
  addTextToPage("Argh, broken: " + err.message);
}).then(function() {
  document.querySelector('.spinner').style.display = 'none';
})

Paralelizam i sekvenca

Kada stigne poglavlje 1 možemo ga dodati na stranicu.

Kada stigne poglavlje 3 ne možemo ga dodati na stranicu jer nije još stiglo poglavlje 2.

Kada stigne poglavlje 2 možemo dodati poglavlja 2 i 3 na stranicu.

Dobavićemo sva poglavlja paralelno, ali ćemo kreirati sekvencu za dodavanje u stranicu.

Paralelizam i sekvenca

Paralelni download, sekvencijalno dodavanje u stranicu:

​getJSON('story.json').then(function(story) {
  addHtmlToPage(story.heading);

  // Mapiraj niz chapterUrls na niz JSON promisa. Oni će se dobavljati paralelno.
  return story.chapterUrls.map(getJSON)
    .reduce(function(sequence, chapterPromise) {
      // pomoću reduce ćemo ulančati promise, dodajući sadržaj na stranicu za svako poglavlje
      return sequence.then(function() {
        // sačekaj na sve u sekvenci, onda sačekaj na poglavlje
        return chapterPromise;
      }).then(function(chapter) {
        addHtmlToPage(chapter.html);
      });
    }, Promise.resolve());
}).then(function() {
  addTextToPage("All done");
}).catch(function(err) {
  addTextToPage("Argh, broken: " + err.message);
}).then(function() {
  document.querySelector('.spinner').style.display = 'none';
})

#5: web storage

HTML5 Web Storage

  • jednostavna baza podataka u web čitaču
  • čuva parove ključ-vrednost
  • do 10MB po domenu
  • podrška u savremenim čitačima

Vrste web storage

  • local storage: trajno čuva podatke
  • session storage: čuva podatke u toku sesije; gubi se po zatvaranju čitača

Primer: forma

Forma za unos podataka

​
<section>
  <form onsubmit="javascript:setSettings()">
    <label>Select your BG color: </label>
    <input id="favcolor" type="color" value="#ffffff" />
    <label>Select Font Size: </label>
    <input id="fontwt" type="number" max="14" min="10" value="13" />
    <input type="submit" value="Save" />
    <input onclick="clearSettings()" type="reset" value="Clear" />
  </form>
</section> 
          

Primer: test podrške u web čitaču

Test podrške za local storage:

​
function setSettings() {
  if ('localStorage' in window && window['localStorage'] !== null) {
    // postoji podrška za local storage
  } else {
    alert('Nije moguće sačuvati podešavanja jer Vaš čitač nema local storage');
  }
}
          

Primer: test pomoću Modernizr

Čitkije pomoću Modernizr:

​
<script type="text/javascript" src="modernizr.min.js"></script>
          
​
if (Modernizr.localstorage) {
  // postoji podrška za local storage
} else {
  alert('Nije moguće sačuvati podešavanja jer Vaš čitač nema local storage');
}
          

Čuvanje podataka: setItem

Dodat kod za čuvanje podataka:

​
function setSettings() {
  if (Modernizr.localstorage) {
    try {
      var favcolor = document.getElementById('favcolor').value;
      var fontwt = document.getElementById('fontwt').value;
      localStorage.setItem('bgcolor', favcolor);
      localStorage.fontweight = fontwt;
    } catch (e) {
      if (e == QUOTA_EXCEEDED_ERR) { // premašio 10 MB
        alert('Quota exceeded!');
      }
    }
  } else {
    alert('Nije moguće sačuvati podešavanja jer Vaš čitač nema local storage');
  }
}
          

Čitanje podataka: getItem

Čitanje iz local storage:

​
function applySettings() {
  if (localStorage.length != 0) {
    document.body.style.backgroundColor = localStorage.getItem('bgcolor');
    document.body.style.fontSize = localStorage.fontweight + 'px';
    document.getElementById('favcolor').value = localStorage.bgcolor;
    document.getElementById('fontwt').value = localStorage.fontweight;
  } else {
    document.body.style.backgroundColor = '#FFFFFF';
    document.body.style.fontSize = '13px'
    document.getElementById('favcolor').value = '#FFFFFF';
    document.getElementById('fontwt').value = '13';
  }
}
          

length vraća broj sačuvanih elemenata.

Uklanjanje podataka: removeItem

Uklanjanje iz local storage:

​
function clearSettings() {
  localStorage.removeItem("bgcolor");
  localStorage.removeItem("fontweight");
  document.body.style.backgroundColor = '#FFFFFF';
  document.body.style.fontSize = '13px'
  document.getElementById('favcolor').value = '#FFFFFF';
  document.getElementById('fontwt').value = '13';
}
          

Storage događaji

Prilikom upisa ili brisanja, poseban događaj će se desiti za window objekat. Možemo dodati osluškivanje za taj događaj.

​
window.addEventListener('storage', storageEventHandler, false);
function storageEventHandler(event) {
  applySettings();
}
          

Event ima sledeće atribute:

  • key: osobina koja je promenjena
  • newValue: nova vrednost
  • oldValue: stara vrednost
  • url: pun URL gde se događaj desio
  • storageArea: localStorage ili sessionStorage objekat

Događaj će se desiti samo u drugim prozorima - ne i u prozoru koji ga je izazvao. Desiće se samo ako je došlo do promene u podacima.

#6: efikasno učitavanje

Efikasno učitavanje skriptova

Kako web čitači učitavaju sadržaj:

  • Parsira HTML dokument redom. Elementi se dodaju u DOM kako se parsiraju. Kako se dodaju u DOM tako se učitavaju (slike, stilovi, skriptovi, itd).
  • Čitači mogu učitavati više resursa istovremeno.
  • Download eksternog JavaScript fajla će blokirati druga dobavljanja, jer bi skript mogao da promeni DOM, ili da preusmeri čitač na drugu adresu. Čitač neće početi druga paralelna dobavljanja pre nego što se skript preuzme, parsira i izvrši.

Skript na kraju fajla

Skript je zgodno staviti na sam kraj HTML fajla, neposredno ispred </body> taga. To garantuje da će čitač parsirati ceo fajl i da je DOM spreman pre nego što mu skript pristupi.

​
<!DOCTYPE html>
<html>
  <body>
    <p id="myParagraph">This is my paragraph! 
      <span class="hideme">Lorem ipsum</span> 
      dolor sit amet.
    </p>
    <p class="hideme">Another paragraph!</p>
    <script>
      var myPar = document.getElementById("myParagraph");
      myPar.innerText = "I have changed the content!";
    </script>
  </body>
</html>
          

Skript na početku fajla

Ako stavimo skript na početak body počeće da se izvršava pre nego što je dokument u celosti parsiran što može dovesti do greške.

​
<!DOCTYPE html>
<html>
  <body>
    <script>
      var myPar = document.getElementById("myParagraph");
      myPar.innerText = "I have changed the content!";
    </script>
    <p id="myParagraph">This is my paragraph! 
      <span class="hideme">Lorem ipsum</span> 
      dolor sit amet.
    </p>
    </p>
    <p class="hideme">Another paragraph!</p>
  </body>
</html>
          

Tip #1: učitaj skriptove na kraju dokumenta

  • ceo dokument je parsiran
  • učitavanje drugih sadržaja (slike, itd) je već počelo
  • učitavanje i izvršavanje skripta neće blokirati druge stvari (osim drugih skriptova)

Šta ako imamo jednostavan dokument koga puno menja skript?

  • prikazaće se osnovni (neuređeni) sadržaj
  • korisnik neće moći da radi jer je izvršavanje skripta blokiralo čitač
  • konačni sadržaj će se prikazati posle pauze
  • => napravi loading screen

Tip #2: combine, minify, gzip

  • često imamo veliki broj JavaScript fajlova
  • veliki broj HTTP zahteva, dugotrajno učitavanje
  • => priprema skriptova za objavljivanje: kombinuj sve u jedan fajl

  • u JavaScript kodu ima puno whitespace znakova
  • => ukloni sve nepotrebne znakove

  • i nakon toga u pitanju je običan tekst
  • => kompresuj GZip algoritmom koji čitači prepoznaju

Tip #3: dinamički učitaj skriptove u head

​
var defaultManifest = [
  "scripts/js-lib.js",
  "scripts/js-objects.js",
  "scripts/third-party/omniture.js",
  "http://big.cdn.com/useful-library.js"
]

function loadManifest(arrManifest) {
  var i, arrManifestLength = arrManifest.length;
  for (i = 0; i < arrManifestLength; i++) {
    var newScript = document.createElement("script");
    newScript.src = arrManifest[i];
    document.getElementsByTagName("head")[0].appendChild(newScript);
  }
}
          

Programski kreiramo script tag unutar head.

Tip #3: dinamički učitaj skriptove u head

​
<!DOCTYPE html>
<html>
  <head>
    <script src="scripts/js-loader.js"></script>
    <script>
      loadManifest(defaultManifest);
    </script>
  </head>
  <body>
    <h1>Hello World!</h1>
  </body>
</html>
          

Sada se skriptovi učitavaju nakon što se programski kreiraju script čvorovi ali to ne blokira učitavanje stranice i drugih elemenata.

Tip #3: dinamički učitaj skriptove u head

  • ceo dokument je parsiran
  • učitavanje drugih sadržaja (slike, itd) je već počelo
  • učitavanje i izvršavanje skripta neće blokirati druge stvari (osim drugih skriptova)

Šta ako imamo jednostavan dokument koga puno menja skript?

  • prikazaće se osnovni (neuređeni) sadržaj
  • korisnik neće moći da radi jer je izvršavanje skripta blokiralo čitač
  • konačni sadržaj će se prikazati posle pauze
  • => napravi loading screen

#7: CORS

Cross-Origin Resource Sharing

Same Origin Policy

Iz bezbednosnih razloga skriptovi su ograničeni samo na pristup podacima koji imaju isto poreklo (origin). Poreklo je definisano u RFC 6454 kao kombinacija

  • URI šema: http ili https
  • hostname
  • port

Same Origin Policy

Primer: http://www.example.com/dir/page.html

URL status razlog
http://www.example.com/dir/page2.html OK isti protokol, host i port
http://www.example.com/dir2/other.html OK isti protokol, host i port
http://username:password@www.example.com/dir2/other.html OK isti protokol, host i port
http://www.example.com:81/dir/other.html NOK različit port
https://www.example.com/dir/other.html NOK različit protokol
http://en.example.com/dir/other.html NOK različit host
http://example.com/dir/other.html NOK različit host
http://v2.www.example.com/dir/other.html NOK različit host
http://www.example.com:80/dir/other.html ??? zavisi od browsera

CORS ideja

CORS (Cross-Origin Resource Sharing) pruža mogućnost da sajt A dopusti pristup svojim podacima skriptovima sa sajta B.

Server A treba da doda CORS zaglavlja u svoj HTTP odgovor.

Podrška za CORS u web čitačima:

  • Chrome 3+
  • Firefox 3.5+
  • Safari 4+
  • Opera 12+
  • Internet Explorer 8+
  • Microsoft Edge

Tekuće stanje je na http://caniuse.com/#search=cors.

Priprema XMLHttRequest zahteva

​
function createCORSRequest(method, url) {
  var xhr = new XMLHttpRequest();
  if ("withCredentials" in xhr) { // da li je XMLHttpRequest2 objekat
    xhr.open(method, url, true);
  } else if (typeof XDomainRequest != "undefined") {
    // XDomainRequest: specijalan slučaj za MSIE
    xhr = new XDomainRequest();
    xhr.open(method, url);
  } else {
    xhr = null; // CORS nije podržan
  }
  return xhr;
}

var xhr = createCORSRequest('GET', url);
if (!xhr) {
  throw new Error('CORS not supported');
}
          

Priprema XMLHttpRequest zahteva

Treba definisati callback za onload i onerror.

​
xhr.onload = function() {
  var responseText = xhr.responseText;
  console.log(responseText);
  // obradi odgovor
};

xhr.onerror = function() {
  console.log('Greška!');
};
          

Priprema XMLHttpRequest zahteva

Standardni CORS zahtevi ne podrazumevaju cookies. Za uključivanje cookies treba:

​
xhr.withCredentials = true;
          

Server sa svoje strane mora da omogući credentials odgovarajućim zaglavljem:

​
Access-Control-Allow-Credentials: true
          

Ovi cookies neće biti dostupni u JavaScriptu zbog SOP.

Slanje CORS zahteva

Kada je sve pripremljeno:

​
xhr.send();
          

Ako zahtev ima telo, ono se prosleđuje kao parametar send.

CORS na serveru

Najveći posao će obaviti web čitač i server. Čitač će dodati zaglavlja i po potrebi slati dodatne zahteve.

Prosti CORS zahtevi

Prosti CORS zahtevi imaju:

  • GET, HEAD ili POST
  • Accept
  • Accept-Language
  • Content-Language
  • Last-Event-ID
  • Content-Type koji mora biti
    • application/x-www-form-urlencoded
    • multipart/form-data
    • text/plain

Prosti CORS zahtev

​
var url = 'http://api.alice.com/cors';
var xhr = createCORSRequest('GET', url);
xhr.send();
          
​
GET /cors HTTP/1.1
Origin: http://api.bob.com
Host: api.alice.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...
          

Origin zaglavlje je dodao čitač i ne može se uticati na njega!

Odgovor servera

​
Access-Control-Allow-Origin: http://api.bob.com
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: FooBar
Content-Type: text/html; charset=utf-8
...
          
  • Access-Control-Allow-Origin (obavezan): mora biti uključen u sve CORS odgovore; sadrži ili adresu iz zahteva ili * (za sve domene)
  • Access-Control-Allow-Credentials (opcioni): naznačava da treba uključiti cookies u zahteve
  • Access-Control-Expose-Headers (opcioni): dodatna zaglavlja koja su dostupna klijentu (pored standardnih Cache-Control, Content-Language, Content-Type, Expires, Last-Modified, Pragma)

Složeni zahtevi

Složeni zahtevi su potrebni ako šaljemo druge metode kao PUT i DELETE ili ako prenosimo JSON podatke.

​
var url = 'http://api.alice.com/cors';
var xhr = createCORSRequest('PUT', url);
xhr.setRequestHeader('X-Custom-Header', 'value');
xhr.send();
          

Čitač prvo šalje probni zahtev (OPTIONS) da proveri da li ima dozvolu da šalje pravi zahtev. Posle pozitivnog odgovora šalje pravi zahtev. Ovo se odvija transparentno za JavaScript program. Probni zahtev se može keširati da se ne bi slao više puta.

Probni zahtev

​
OPTIONS /cors HTTP/1.1
Origin: http://api.bob.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: X-Custom-Header
Host: api.alice.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...
          
  • Access-Control-Request-Method: metoda pravog zahteva
  • Access-Control-Request-Headers: lista posebnih zaglavlja u zahtevu, razdvojenih zarezom

Odgovor na probni zahtev

​
Access-Control-Allow-Origin: http://api.bob.com
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: X-Custom-Header
Content-Type: text/html; charset=utf-8
          
  • Access-Control-Allow-Origin: kao kod prostog zahteva
  • Access-Control-Allow-Methods: lista dozvoljenih HTTP metoda
  • Access-Control-Allow-Headers: lista dozvoljenih zaglavlja u zahtevu
  • Access-Control-Allow-Credentials: kao kod prostog zahteva
  • Access-Control-Max-Age: omogućava keširanje ovog odgovora dati broj sekundi

Stvarni zahtev

​
PUT /cors HTTP/1.1
Origin: http://api.bob.com
Host: api.alice.com
X-Custom-Header: value
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...
          

Odgovor na stvarni zahtev

​
Access-Control-Allow-Origin: http://api.bob.com
Content-Type: text/html; charset=utf-8
          

Ako se izostave CORS zaglavlja, signalizira se neispravan zahtev.

Koja zaglavlja staviti u odgovor?

Kraj dela

← Početak dela

⇐ Početak kursa