master
grani nalazi V1 verzija naše aplikacije a na drugoj grani V2 verzija. Kada završimo V2 verziju logično je da ona bude aktivna i nalazi se na master grani.Kako ovo eleganto uraditi pomoću gita?
Kao i uvek, tajna se krije duboko u dokumentaciji. Ako zagledamo malo dublje koje su opcije git merge
komande, možemo da vidimo da ima opcija za biranje strategije.
Ako pogledamo strategiju ours
videćemo da je to ono što nam treba.
This resolves any number of heads, but the resulting tree of the merge is always that of the current branch head, effectively ignoring all changes from all other branches. It is meant to be used to supersede old development history of side branches.
Što znači da će se sve izmene sa naše grane uzeti "na slepo" i preklopiće izmene sa osnovne grane. Pa da vidimo to u praksi:
master
git checkout v2
2. Zatim će da u v2
"merge"ujemo master
ali će da zadržimo sve naše izmene.
git merge -s ours master
Ako ne želimo da imamo "commit", možemo da prosledimo opciju --no-commit
.git merge -s ours --no-commit master
3. Vratimo se na master
granu
git checkout master
4. "Merge"ujemo v2
git merge v2
Ovim smo preklopili master
granu v2
granom. Istorija master
grane je zadržana.
Zbog loše dokumentacije ili lošeg formatiranja često dolazi do greške da developeri pomešaju git merge -s ours xyz
sa git merge -X ours xyz
. Ove dve stvari uopšte nisu iste. Ako malo bolje pogledamo dokumentaciju, možemo da vidimo da se prvo ours
nalazi ispod recursive
opcije i ta je to pod-opcija, iz tog razloga i ima -X
Glavne stavke koje utiču na učitavanje stranice su resursi i javaskripta koja se izvršava. Ove probleme možemo vrlo lako identifikovati na razlicite načine, koristeći interne ili eketerne alate (možda će biti neki post o ovome kasnije). Ali ovaj posao postaje dosta teži kada se radi CD (Continuous Development), zato što je potrebno performanse testirati posle svake izmene. Dodatni problem predstavlja rad u timu, ko testira i koje su granice da se je nesto dobro?
Ovaj problem je poznat i više puta obrađen, a to je da postavimo budžet. Isto kao i kod izrade projekta, gde postoji finansijski budžet ili vremenski budžet (rok), tako treba da postoji budžet za performanse.
Definicija je vrlo slobodna i otvorena, ali možemo da kažemo da je to ograničenje za stranice koje tim ne sme da prekorači. Namerno nisam definisao na šta se odnosi ograničenje, zato što to zavisi od tima. Ograničenje može da se postavi na broj resursa, veličinu stranice, ukupnu veličina svih slika,...
Kada sam iznad definisao šta je budžet performansi, pomenuo sam da možemo da ograničimo različite stvari. Ograničenja možemo da podelimo u tri kategorije:
To su događaji koji se okidaju posle određenog vremenskog perioda prilikom učitavanja stranice. Takvi događaji su Time-To-Interactive, First Contentful Paint, domContentLoaded,...
Kada pratimo vremenske događaje bitno je da pratimo više od jednog događaja. Zato što se često dešava da se jedna stranica učitava za 5 sekundi ali se veći deo sadržaja učita za manje od jedne sekunde dok se druga stranica učitava za dve sekunde a sadržaj za jednu i po sekundu. Iako se prva stanica učitava tri sekunde duže, sam sadržaj se brže učitao.
Zato je bitno da pratimo više vremenskih događaja i da ih uparimo sa drugim metrikama.
Kao i što samo ime kaže, ovo su sirove brojke koje određuju, na primer, koliko zahteva ima ili kolika je veličina stranice. Ove brojke ne preslikavaju realno performanse, zato sto dve stranice mogu da budu iste veličine a da se jedna dosta brže učitava od druge.
Svakako, ova ograničenja igraju svoju ulogu i mogu biti korisna ako se upare sa drugim metrikama. Za razliku od drugih ograničenja, ovo je dosta lakše razumeti i odrediti. Znamo da ako naša stranica zauzima x
i mi dodamo skriptu koja zauzima y
da će naša stranica zauzimati x+y
.
Ja lično koristim ovo ograničenje kao neku vrstu fizičkog ograničenja, u smislu da cela stranica treba da se učita za x
sekundi na 3G mreži. Na primer:
Željeno vreme učitavanja stranice: 2 sekunde
Konekcija klijenta: 3G (1.6 Mbps
)
Maksimalna veličina stranice:1 400 Kb
Kako stalno ne bismo računali koliko stranica može da teži, postoji kalkulator koji će da uradi svu matematiku za nas. Kao bonus, kalkulator nam pruža mogućnost da rasporedimo resurse po tipu.
U ovom slučaju kao mera ograničenja se koriste eksterni alati, kao što su Lighthouse ili Webpagetest. Obično se koristi jedna ili više brojki iz razultata kao ograničenje.
Iako je ovo ograničenje dosta bolje od kvantitativnog ograničenja, zato što pruža celokupnu sliku stranice, ipak nije idealno rešenje. Postoji odličan članak koji pokazuje da je moguće da se napravi sajt koji ima ocenu 100 u eksternim alatima, a da zapravo sajt nije moguće koristiti.
Prošli smo kroz različite vrste ograničenja ali kada imamo više opcija uvek se postalvja pitanje koje ograničenje je pravo ograničenje? Odgovor na ovo pitanje je dosta subjektivno, ali mislim da svi objektivno možemo da se složimo da je idealno imati kombinaciju ograničenja. Zato što nijedno ograničenje neće da nam da potpunu sliku, ali ako iskoristimo kombinaciju ograničenja možemo sigurno pokriti mnogo više slučajeva.
Često akteri, dizajneri i ljudi bez inžinjerske pozadine nisu svesni da njihove odluke utiču loše na performanse. Ovo je u neku ruku razumljivo, naša uloga je da objasnimo projekt menadžerima, dizajnerima i akterima kako izmene utiču na korisničko iskustvo.
Iz tog razloga je bitno da performans budžet postane deo projekta i nađe se na početku a ne na kraju kao opcija.
Ja lično imam jedno bitnije pitanje, ustvari inspiraciju za ovaj članak, a to je kako da integrišemo ovo unutar tima? U uvodu sam pomenuo isto ovo, a to je da je lako izmeriti i naći probleme ali kako integrisati ceo ovaj proces u tim gde radi više od jednog čoveka sa konstantnim izmenama (CI/CD).
Kroz ovaj članak ću pokušati da pokrijem kako da integrišemo budžet u sklopu git procesa, tako da pri kreiranju svakog PR-a dobijemo rezultate za performanse. Naravno, da bismo ovo uspeli potreban nam je CI, konkretno za ovaj primer ću korististi CircleCI ali se isto odnosi i na druge CI-ove.
Aplikacija koju testiramo je SPA aplikacija koja je napravljena kao deo prezentacije. Homemau je aplikacija koja predstavlja dom za mačke i gde drugi ljudi mogu potražiti mačku koja će biti njihov novi kućni ljubimac. Aplikacija nema nikakvu konkretnu funkcionalnost već služi kao primer.
Kako bismo ispoštovali sve ono o čemu smo pričali, moramo da odredimo budžet. U idealnom svetu ovo se određuje na početku projekta kad i finansijski budžet. Recimo da smo se složili koji je naš budžet, šta dalje?
Ovo je na nama. Možemo da napravimo novi fajl, i moja praksa je da se on nalazi unutar package.json
. Smatram da sve što se tiče projekta treba da se nalazi tu i dosta je lakše da prolazimo kroz jedan konfiguaracioni fajl nego kroz više manjih fajlova.
"budget": {
"lighthouse": {
"performance": 90,
"accessibility": 80,
"best-practices": 80,
"seo": 80
},
}
Kako bismo se malo bolje upoznali sa CircleCI-em i terminologijom, proćiću korak po korak kroz proces testiranja.
graph LR
A[Instaliraj biblioteke]-->B[Build and deploy]
B-->C[Pokreni Lighthouse]
C-->D[Analiziraj i ažuriraj PR]
U CircleCI-u svaki od ovih koraka se zove "job" a sekvenca poslova (kao što mi imamo) je "workflow". Konfiguraciju za CircleCI čuvamo u config.yaml
koji treba da stavimo u .circleci
folder.
Drugi korak u celom ovom procesu je vrlo bitan i mnogo zavisi od našeg okruženja. Vrlo je bitno da postoji mogućnost da iz našeg CI-a pokrenemo deploy proces, zato što nam je potreban link do poslednje verzije aplikacije.
Korisno je da imamo više okruženja prilikom izrade. Obično je praksa da postoje development
, staging
i live
okruženje. Ovo je možda članak sam za sebe ali je bitno za ovaj proces.
Ono što treba da uradimo jeste da pokrenemo Lighthouse u odnosu na naš development
ili staging
server. Lighthouse pruza CLI koji možemo da iskoristimo ali potrebno nam je celokupno okruženje. Na svu sreću postoji Docker container kporras07/lighthouse-ci
koji mi možemo da iskoristimo kao neku vrstu interfejsa.
runPerf:
docker:
- image: kporras07/lighthouse-ci
steps:
- checkout
- run:
name: Run lighthouse against staging deployment
environment:
DEV_URL: https://marko.ilic.ninja/homemau
command: |
lighthouse $TEST_URL \
--chrome-flags=\"--headless\" \
--output-path=/home/chrome/reports/anonymous-"$(echo -n $CIRCLE_SHELL_ENV | md5sum | awk '{print $1}')" \
--output=json \
--output=html
- persist_to_workspace:
root: /home/chrome
paths:
- reports
Ovo što smo ovde opisali je samo treći korak našeg procesa.
CircleCI će prvo da preuzme i pokrene Docker image kporras07/lighhouse-ci
.
Zatim se izvršava samo jedan korak, kom smo dodelili ime (name
) koje kasnije vidimo unutar CircleCI-a. Postavimo promenjivu DEV_URL
koju ćemo kasnije da koristimo. Na kraju pokrenemo lighthouse
komandu kojoj prosledimo par parametara, kao što su output
parametri za tip izveštaja koji želimo.
JSON izveštaj koristimo radi lakšeg upoređivanja sa našim budžetom. Dok je HTML izveštaj radi prikaza i linkovanja u PR.
Opcija persist_to_workspace
daje mogućnost da sve iz tog kontejnera ostane dostupno pod /reports
direktorijumom unutar workspace
-a.
Ako sada pokrenemo CircleCI on će pokrenuti test, generisati izveštaje i sačuvaće ih u /reports
direkotorijumu.
Nakon generisanja izveštaja neophodno ih je uporediti sa postavljenim budžetom na početku. Zato hajde da napišemo skriptu koja će da radi poređenje i zatim postavi komentar na PR.
Za ovo ne postoji neki "gotov" ili specijalan način, pošto radimo od nule, ovu skriptu moramo sami napisati. Pošto imamo JSON fajlove mislim da je najlakše da skriptu napišemo u NodeJS-u.
Pravimo novi "job" u CI-u:
updatePr:
docker:
- image: circleci/node:11.4.0
steps:
- checkout
- restore_cache:
keys:
- node-v1-{{ checksum "package.json" }}-{{ checksum "yarn.lock" }}
- attach_workspace:
at: "."
- store_artifacts:
path: reports
destination: reports
- run:
name: "Analyze and comment to the PR"
command: ./.circleci/build-score.js budget.json reports
Pošto nam je potreban Node ja ću iskoristiti najnoviji "container" node:11.4.0
. Takođe ono što je vrlo bitno je da imamo pristup izveštajima. Kako smo u prethodnom "job"-u koristili persist_to_workspace
sada možemo isti taj folder iskoristiti tako što ćemo ga dodati pomoću attach_workspace
komande. Još jedna stvar koja nam je bitna da imamo pristup HTML izveštaju unutar PR-a. Time postižemo da uvek imamo pristup izveštaju i detaljniji pregled. CircleCI pruža artifakte, koji služe za čuvanje podataka nakon što se poslovi završe. Artifakte možemo da koristimo koristeći store_artifacts
komandu.
Kad smo sve to lepo podeseli, vreme je da pokrenemo našu skriptu. Ja ću je čuvati u .cricleci
folderu i zvaće se build-score.js
.
Ja ću ovde pokriti samo neke osnove kao kako postaviti komentar i uzeti izveštaj. Cela skripta je dostupna na Github-u.
Kako bih lakše postavljao komentar i uzeo link do artifakta koristiću circle-github-bot
ovo je konkretno bot za Github koji u pozadini koristi ništa više nego Github API. Tako da je vrlo lako uraditi ovo i za Bitbucket.
#!/usr/local/bin/node
const fs = require('fs');
const path = require('path');
const bot = require('circle-github-bot').create();
const budget = JSON.parse(fs.readFileSync(process.argv[2], 'utf8'));
const { lighthouse } = budget;
// Ucitaj sve izvestaje
const reportsFolder = process.argv[3];
const reports = {
json: [],
html: [],
};
fs.readdirSync(reportsFolder).forEach(file => {
switch (path.extname(file)) {
case '.json':
reports.json.push(JSON.parse(fs.readFileSync(path.join(reportsFolder, file), 'utf8')));
break;
case '.html':
reports.html.push(file);
break;
}
});
Sada kad imam pristup svim izveštajima i budžetu koji smo postavili, možemo vrlo lako da manipulišemo podacima, zatim da ih formatiramo i postavimo kao komentar.
Prilikom generisanja linka za artifakte koristi se funkcija artifactLink()
ali putanja koju vrati nije tačna zato što vrati punu putanju koja ima unutar sebe /home/circleci/project
. Ovo samo treba zameniti praznim stringom.
Evo primera kako izgleda moja funkcija za uzimanje svih HTML izveštaja:
const reportLinks = reports.html.map((filename, i) => {
return bot.artifactLink(`reports/${filename}`, `Report ${i + 1}`).replace('/home/circleci/project', '');
});
Evo kako izgleda kranji izveštaj:
Na početku sam pričao o različitim metrikama koje možemo da koristimo i vidimo da Lighthouse pokriva veliki deo, ali ipak ima nekih nedostataka. Recimo da hoćemo da ograničimo veličinu našeh izlaznog JS fajla.
Ako zagrebemo malo dublje u Lighthouse dokumentaciju možemo da nađemo način na koji se pišu testovi po meri. Možemo da iskoristimo Lighthouse da nam vrati neke korisne podatke, zatim da uporedimo sa našim budžetom.
Kako bismo napisali test potrebne su dve komponente:
Za početak je potrebno da definišemo podešavanja koje će Lighthouse da uzme. Nazvaćemo fajl weight-audit-config.js
module.exports = {
// Pokreni nas test uz ostale regularne (default) testove
extends: "lighthouse:default",
// Testovi koji ce da se pokrenu uz Lighthouse
// Na osnovu ovog se odredjuje ime fajla.
audits: ["weight-audit"],
// Napravi novu kategoriju `Velicina JS datoteke`
categories: {
"js-weight": {
title: "Velicina JS datoteke",
description: "Alooo bato, ajmo malo crossfit za sajt!",
auditRefs: [{ id: "weight-audit", weight: 1 }]
}
}
};
Pošto smo gore nazvali test weight-audit
tako isto moramo da nazovemo fajl koji će da pokreće test. Zato pravim fajl weight-audit.js
.
Ustvari, pre nego što napravimo taj fajl bitno je da razmislimo gde da stavimo koliki je naš budžet i možda još bitnije ime fajla. Mislim da je najbolje da se ovi podaci nalaze tamo gde i sav budžet budget.json
.
"bundleSize": 200,
"bundleName": "app.js"
Potencijalni problem ako bundleName
bude ovako definisan je što ako korisitmo neki heš prilikom generisanje fajla to neće da radi. Iz tog razloga ovde možemo da napišemo RegEx i kasnije koristimo test()
metodu.
Tako da u našem slučaju taj RegEx može da izgleda ovako js/app.*\\.js
Da se vratimo na weight-audit.js
// Zato sto je `npm link` pravio problem morao sam da navedem celu putanju
const Audit = require("/usr/local/lib/node_modules/lighthouse").Audit;
class WeightAudit extends Audit {
static get meta() {
return {
id: "weight-audit",
title: "Velicina JS datoteke",
failureTitle: `JS bundle exceeds your threshold of ${
process.env.MAX_BUNDLE_SIZE_KB
}kb`,
description: "Alooo bato, ajmo malo crossfit za sajt!",
// Sakpuljac (Getherer)
requiredArtifacts: ["devtoolsLogs"]
};
}
static async audit(artifacts, context) {
// Uzmi Devtools log
const devtoolsLogs = artifacts.devtoolsLogs["defaultPass"];
// Uzmi network tab
const networkRecords = await artifacts.requestNetworkRecords(devtoolsLogs);
// Nadji sve resurse koji su tipa `Script` i podudaraju se sa nasim imenom
const bundleRecord = networkRecords.find(record =>
record.resourceType === "Script"
&& new RegExp(process.env.BUNDLE_NAME).test(record.url)
);
// Proveri da li je test prosao
const belowThreshold = bundleRecord.transferSize <= process.env.BUNDLE_SIZE * 1024;
return {
rawValue: (bundleRecord.transferSize / 1024).toFixed(1),
// Rezultat se vraca izmedju 0 i 1, posto kod nas to moze da bude samo true (1) ili false (0)
// Prebacicu rezultat iz Boolean u Number.
score: Number(belowThreshold),
displayValue: `${bundleRecord.url} je ${(bundleRecord.transferSize / 1024).toFixed(1)}kb`
};
}
}
module.exports = WeightAudit;
Malo da objasnim o čemu se radi. Na početku definišem meta podatke kao što su ime, opis, poruku ako je pao test i, najbitnije, sakupljač podataka (gatherer).
Zatim pišemo naš test koji treba da vrati objekat sa nekim definisanim svojstvima. Bitno je da napomenem da se rezultat vraća kao vrednost od nula do jedan iako je u testu prikazana kao 0 - 100.
Sad kad smo definisali test ostalo je još da kažemo Lighthouse-u da pokrene test. To se radi tako što prilikom pokretanja lighthouse
CLI prosledimo --config-path
putanju do naše datoteke. Dobijamo sledeću komandu:
command: |
BUNDLE_NAME="$(node -p 'require("./package.json").lighthouse.bundleName')" \
BUNDLE_SIZE="$(node -p 'require("./package.json").lighthouse.bundleSize')" \
lighthouse $TEST_URL \
--port=9222 \
--chrome-flags=\"--headless\" \
--config-path=./.circleci/lighthouse/weight-audit-config.js \
--output-path=/home/chrome/reports/anonymous-"$(echo -n $CIRCLE_SHELL_ENV | md5sum | awk '{print $1}')" \
--output=json \
--output=html
Ovaj naš test će se sada pojaviti i u HTML izveštaju.
Odužio se ovaj članak, ali evo još malo kraja. Za kraj izborićemo se sa najvećim problemom, autentifikacija. Verovano se velika većina tokom čitanja ovog teksta pitala "šta je sa stranicama koje imaju autentifikaciju?"
Ovo je odlično pitanje koje je bitno da rešimo. Zato što je bitno da možemo da testiramo i delove sajta koji su iza prijave za korisnika.
Ligthouse pruža --extra-headers
opciju, ali to znači da moramo ručno da generišemo tokene. Tokeni ističu što znači test može da padne ako zaboravimo da ažuriramo token. Ovo rešenje nije prihvatljivo.
Pošto imamo pristup NodeJS-u možemo ručno da punimo localStorage
ali to neće da funkcioniše za svaki projekat, takođe može da bude poprilično naporno za održavanje.
Rešenje do kojeg sam ja došao je malo "hacky", zato što se oslanja ne redosled poziva "gatherer-a" i njegovih "hookova" da simulira kao da smo otvorili tu stranicu.
Napisaću skupljać koji ništa ne skuplja ;) već koristim beforePass
"hook" i da unutar njega podignem puppeteer, ulogujem se i onda se pokrene test.
class Auth extends Gatherer {
async beforePass(options) {
const ws = await options.driver.wsEndpoint();
const browser = await puppeteer.connect({
browserWSEndpoint: ws,
});
const page = await browser.newPage();
await page.goto(process.env.TEST_URL);
await page.click('input[name=username]');
await page.keyboard.type(process.env.ADMIN_USER);
await page.click('input[name=password]');
await page.keyboard.type(process.env.ADMIN_PASSWORD);
await page.click('button[type="submit"]');
await page.waitForSelector('#logout');
browser.disconnect();
return {};
}
}
1.6 / 8 * 2 = 0.4 Mb = 400 Kb
]]>Kada se jQuery pojavio, još davne 2006. godine, ja još nisam ni počeo da učim veb tehnologije. U tom trenutku je jQuery bio san svakog developera, zato što je nastao kao rešenje jednog mnogo velikog problema - IE6. Internet Explorer 6 je bilo nešto najbliže paklu što smo videli u vebu. Ja sam imao malo dodirnih tačaka sa njim ali sam upoznat sa skoro svim "hakovima" koji su nastali kao rezultat tog veb pregledača.
jQuery je igrao jednu važnu i veliku ulogu, a to je standardizacija. Omogućio je da se developeri osećaju sigurnije zato što je kod radio na IE7 (2007. godina) a i, posebno, na IE6.
Na našu sreću IE6 je "mrtav". U međuvremenu, javaskripta je dobila dosta novina i vredno se radi na usavršavanju javaskripte i stoga korišćenje jQuery-a u nema smisla niti prednosti.
Tako da jQuery jeste odlična biblioteka, za vreme kada je bilo potrebna. Danas, mislim da svi možemo da se složimo da smo prerasli jQuery. Ali svakako, hvala mu za pomoć kada je to bilo potrebno.
Ovo nije toliko jQuery problem, već problem sa ljudima koji ga koriste.
Novi developeri koji kreću sa javaskriptom obično počinju sa jQuery-jem i potpuno se oslanjaju na njega. Iako sve ono za šta koriste u jQuery-ju je moguće podjednako lako pa čak i brže uraditi u vanila javaskripti. Gore sam pomenuo da je jQuery nastao kao biblioteka da pomogne developerima u najtežem periodu podrške veb pregledača, i iz tog razloga sa sobom nosi mnogo koda koji se nikad ne koristi.
Za takve developere koristim izraz full jQuery developer™.
Poruka od mene za full jQuery developere je da se oslobode i probaju vanilla javaskript, jer skoro pa sve što pruža jQuery možemo lako implementirati:
$()
selektor === querySelectorAll()
addClass()
, removeClass()
=== classList
on(event)
=== addEventListener()
je svuda podržanDa ne bih pisao šta sve može, pogledajte na youmightnotneedjquery.
Ono što je po mom mišljenju najviše ubilo jQuery jeste pojava "pametnih" telefona koji imaju pristup internetu. Prvi telefoni su bili 10, a možda čak i 100 puta sporiji nego računari u to vreme. jQuery zbog velike količine koda (150-230kB) predstavljao je usko grlo za telefone.
Mislim da je bitno da napomenem da jQuery-a zauzima dosta veliki deo koda. Neretko viđam da je jQuery uključen samo radi nekog plugina, recimo u primeru ispod bxslider
.
Nemam ništa protiv jQuery, čak sam mu mnogo zahvalan iako mene nije zahvatio taj najteži period veb pregledača. Pomogao mi je dosta prilikom pravljenja veb stranica za IE9. Ipak, moje mišljenje je da smo ga odavno prerasli i da više nema smisla koristiti ga jer postoji toliko novih biblioteka koje su odgovor za današnje probleme (SPA, dinamički sadržaj). Verovatno će kroz 10 godina Vue, Angular i React biti zastarele biblioteke kao jQuery danas, što ne znači da su one loše, već da su samo bile odgovor na aktuelne probleme koji možda u budućnosti neće postojati.
]]>Recimo da hoćemo iskoristiti naš primer od ranije, dodavanje elemenata u DOM. Postoji par načina da se to implementira
innerHTML
createElement()
i appendChild()
insertAdjacentHTML()
Svaki test će da doda 1000
DOM (li
) elemenata. Postavka je sledeća
<ul id="list">
<!-- Ubaci stvari ovde -->
</ul>
Testovi izgledaju ovako. Za prvi slučaj:
const list = document.getElementById('list');
const start = performance.now();
let html = '';
for(let i = 0; i < 1000; i++) {
html += '<li>' + i + '</li>';
}
list.innerHTML = html;
const end = performance.now();
console.log('innerHTML ' + (end - start) + 'ms');
Ako napišemo kôd za svaki od slučajeva, dobijamo sledeći rezultat:
Slučaj | Vreme |
---|---|
innerHTML | 3.4549999982118607ms |
appendChild | 4.28000000101747ms |
insertAdjacentHTML | 2.8750000019499566ms |
Zahvaljujući performans APIu smo lako videli koja implementacija je najbrža.
Možda se pitate zašto nam je potreban još jedan intefejs, kada imamo Date.now()
. Postojeći Date
interfejs vraća vreme u milisekundama što je već dovoljno precizno.
const start = Date.now();
longToDoTask();
const duration = Date.now() - start;
U ovom slučaju su milisekunde dosta neprecizna jedinica. Ono što performanse API koristi su isto milisekunde, ali na drugačiji način. Metoda performance.now()
vraća DOMHighResTimeStamp
, što garantuje preciznost u 5 mikrosekundi.
Još jedna, verovatno mnogo bitnija razlika je, to što se vrednost performance.now()
metode konstantno povećava, nezavisno od sata na računaru. To ustvari i jeste glavni problem kod Date.now()
metode. Ako tokom testiranja izmenimo vreme na računaru, prilikom uzimanja krajnjeg vremena naše merenje neće biti tačno.
Ako ne želimo da radimo testove lokalno ili pak želimo da podelimo testove sa nekim da i on može testirati, možemo da koristimo eksterne alate za testiranje. Jedan on najstarijih i sigurno najpoznatijih je JSPerf. Iako je par puta bio nedostupan zbog problema servera i finansijskih problem, ostao je sigurno jedan on najboljih.
Evo kako na JSPerfu izgleda naš primer, https://jsperf.com/inesering-dom. Rezultati su isti, samo je sada merena jedinica broj operacija u sekundi.
Slučaj | Ops/s |
---|---|
innerHTML | 19,201 |
appendChild | 53,707 |
insertAdjacentHTML | 57,434 |
Kad već pričamo o merenju performansi, mislim da je bitno pomenuti biblioteku koja predstavlja zlatan standard za merenje. Biblioteka Benchmark.js nam pruža značajno veću fleksibilnost nego kada sami koristimo Performans API, zato što se kod izvršava više puta, sve dok se ne dođe do minimalnog vremena, potom se to pokrene više puta kako bismo dobili prosek.
Ideja privatnosti je da samo određeni članovi iz određene oblasti mogu da pristupaju promenjivama, dok je pristup ostalim članovima van te oblasti zabranjen.
Nakon onoga što smo naučili, nije tako teško postići privatnost u Javaskripti, zar ne?
function foo() {
var privatno = 'tajna';
console.log(privatno);
}
foo(); // 'tajna'
privatno; // Reference error
Ovim smo efektivno sakrili promenjivu privatno
, jer zbog oblasti važenja ova promenjiva samo postoji unutar funkcije foo
.
Kraj? Ne bih rekao. Problem je u tome što ova promenjiva živi samo dok i funkcija živi i ako želimo da radimo još nešto sa njom to neće biti moguće.
Ako se malo prisetimo kako radi leksička oblast, a to je tako što pita na gore da li neko ima tu referencu, shvatamo da upravo taj mehanizam možemo iskoristiti.
function foo() {
var privatno = 'tajna';
function bar() {
console.log(privatno);
}
bar();
}
foo(); // 'tajna'
Iz ovog primera vidimo da unutrašnja funkcija ima pristup članovima "iznad" nje. Ovo je odlično ali ne rešava naš problem zato što mi nemamo pristup već se ovo izvrši jednom i zatim obriše. Očigledno, neophodno nam je drugačije rešenje.
Rešenje za ovaj problem je Closure, vrlo česta reč u kombinaciji sa Javaskript-om i razgovorom za posao. Veliki broj ljudi smatraju da je ovo neophodno znati i često se forsira kao eliminaciono pitanje na razgovoru za posao. Videćemo da ovo uopšte nije strašno. Sada kad znamo kako radi oblast definisanosti, Closure je logična stvar.
Dakle, šta je closure? Funkcija koja vraća fukciju... Možda zvuči suviše prosto ali jeste upravo to.
function foo() {
var privatno = 'tajna';
function bar() {
console.log(privatno);
}
return bar();
}
var bar = foo();
bar(); // tajna
bar(); // tajna
Ovim smo dobili primer pristup funkciji foo
van nje. Kako ovo radi?
Kada mi pozovemo funkciju foo
ona nama vrati referencu na funkciju bar
. Samim tim što je vratila referencu na nešto unutar nje skupljač smeća (garbage collector) neće obrisati funkciju foo
sve dok postoji referenca. Sve funkcije unutar funkcije foo
znaju za njene promenjive, samim tim mogu i de pristupe njenom sadrzaju.
Recimo da imamo ovaj kod:
for (var i = 0; i < 17; i++) {
setTimeout(function () {
console.log(i);
}, 1000);
}
Ovaj kod sam iskoristio zato što se često javlja u projektima i predstavlja problem. Ovaj kod da loguje 17 puta broj 17. Čudno?
no-loop-func
error. Ovo je vrlo čest probelem i javlja se zbog nepoznavanja kako closure radi.
Ono što sigurno možemo da zaključimo je da se setTimeout
pozvao posle petlje, to ima smisla. Zato što petlja ne čeka da se izvrši setTimeout
već ide dalje. Šta više, sve setTimeout
funkcije unutar sebe referenciraju na istu promenjivu, i u ovom slučaju je to stvarno referenca a ne kopija. Što znači, kad dođe vreme da se izvrši setTimeout
to je posle petlje, a naša vrednost je već povećana.
i < 17
, što znači da petlja treba da stane kad je broj i = 16
. To jeste istina, ali kada petlja stane i = 17
zato što se i++
izvršilo u prethodnom koraku.
Ono što nama treba ješte da postoji kopija i
u svakoj iteraciji, čime postižemo da je i
jedinstveno i ne zavisi od petlje. Sad kad znamo šta je Closure i kako radi hajde da probamo to rešenje.
for (var i = 0; i < 17; i++) {
(function() {
setTimeout(function () {
console.log(i);
}, 1000);
})()
}
Da skratim muke pokretanja ovog koda, ovo neće raditi. Vidimo da iako imamo leksičku oblast važenja pomoću IIFE ovo ne rešava problem. Neki već sad vide rešenje, a problem je u tome što je naša oblast prazna. Ona i dalje referencira istu promenjivu.
for (var i = 0; i < 17; i++) {
(function() {
var j = i;
setTimeout(function () {
console.log(j);
}, 1000);
})()
}
Napokon, ovo radi i dobijamo izlaz brojeva od 0
do 16
. Ono što je bilo bitno je da prilikom svake iteracije mi unutar funkcije napravimo novu kopiju za promenjivu i
. Ovaj trenutni kod možemo da poboljšamo tako što i
prosledimo kao vrednost funkcije. Ovo će raditi zato što JS prosleđuje primitivne promenljive po vrednosti a ne po referenci.
for (var i = 0; i < 17; i++) {
(function(j) {
setTimeout(function () {
console.log(j);
}, 1000);
})(i)
}
Iako je rešenje gore potpuno validno i tačno, moguće je značajno poboljšati kod. Ono što je bilo potrebno da bismo rešili problem je da za svaku iteraciju napravimo blok oblast važenja, što već znamo da uradimo zahvaljujući ES6 (živ bio i veliki porasooo!!!).
for (let i = 0; i < 17; i++) {
setTimeout(function () {
console.log(i);
}, 1000);
}
Ako otvorimo specifikaciju i pogledamo definiciju
for ( LexicalDeclaration Expressionopt ; Expressionopt )
U drugom koraku kaže Let loopEnv be NewDeclarativeEnvironment(oldEnv).
, što znači da za svaki korak u petlji se definiše nova leksička oblast koja je ulančana sa starom.
Pravljenje nove oblasti i ulančavanje sa starom je nekad bila vrlo skupa operacija. Ako pokrenemo dve petlje sa potpuno istim sadržajem a jedina razlika je da jedna koristi var
a druga pre
možemo da vidimo da danas daju jednake rezultate. Ali u ranijim implementacijama var
petlja je bila preko dva puta brža.
UserAgent | Petlja sa var |
Petlja sa let |
---|---|---|
Chrome 48.0.2564 | 5,891,754 ops/sec | 11,683,983 ops/sec |
Testovi su napisani i testirani na JSPerf. Takođe, ovaj bug je bio prijavljen Chrome timu.
const nickname = req.body.nick;
const formData = {
name: 'John',
}
if (nickname) {
formData['nickname'] = nickname;
}
Ovo je najčešći način za dodavanje svojstva objektu. Na ovaj način činimo da kôd bude manje čitljiv i definicija objekta (strukture) se dešava na više mesta.
Ako iskoristimo pametno "spread" operator možemo značajno da popravimo kôd.
const nickname = req.body.nick;
const formData = {
name: 'John',
...nickname && {nickname: nickname}
}
Ovim smo primetno skratili naš kôd i definicija strukture je na jednom mestu.
0
ili neki drugi broj koji je "false". Uslov može ovako da izgleda ...typeof zetoni === "number" && !isNaN(zetoni) && {zetoni},
Prvo da se upoznamo sa &&
operatorom. Ako zagledamo malo u specifikaciju &&
operatora, možemo da vidimo da će vrednost biti jedna od vrednosti operatora.
&&
ne mora da bude boolean
tipa.
Ukoliko leva strana nije istinita, desna strana se neće evaluirati i rezultat operacije biće "false". I suprotno, desna strana će se evaluirati samo ako je leva strana istinita. Ovo znači da će naš "spread" operator dobiti levu vrednost false, ili desnu vrednost - objekat.
Ovo je malo naprednije objašnjenje šta se stvarno dešava i oslanja se na poznavanje ECMA specifikacije. Gore nije objašnjeno šta se desi kada se nađe "number" ili "boolean" vrednost pored "spread" operatora.
Šta može da se nađe sa desne strane "spread" operatora?
null
ili undefined
Boolean
i Number
Object
- Ovo je već poznato ponašanjeKada pogledamo specifikaciju za "spread" operator vidimo da se pozove CopyDataProperties
operator. Gledajući šta radi CopyDataProperties
operator, možemo da vidimo da u koraku tri ako se nađe null
ili undefined
, operator neće uraditi ništa.
const obj = {
...null,
...undefined
}
console.log(obj) // {}
Ovim smo pokrili prvi slučaj, sada ostaje još drugi slučaj kad se nađu drugi tipovi koji nisu Object
.
Ako pogledamo četvrtu stavku CopyDataProperties
operatora, vidimo da se nad from
(desna strana) zove ToObject
. Što znači da će svaki tip biti obmotan u odgovarajući tip objekta. Ono što je bitno da znate je, da ovi objekti, Boolean
i Number
, sadrže nešto što se zovu interni slotovi. Ovim slotovima ne može da se pristupi već predstavljaju interno stanje objekta na nivou "engine"a. Slotovi u objektu se nazivaju između duplih uglastih zagrada [[imeSlota]]
.
console.log(new Number(17));
console.log(new Boolean(true));
/*
Number {23}
__proto__: Number
[[PrimitiveValue]]: 23
Boolean {true}
__proto__: Boolean
[[PrimitiveValue]]: true
*/
Primitivnim vrednostima ne možemo obično da pristupimo, već koristimo valueOf()
.
console.log(new Number(17).valueOf());
console.log(new Boolean(true).valueOf());
/*
17
true
*/
Ovo nama ide sve u prilog zato što objekat bez svojstva znači da CopyDataProperties
operator nema šta da kopira.
Pokrili smo sve slučajeve, tako da smo sigurni koja god vrednost da dođe kao uslov neće da napravi problem objektu.
]]>Proći ću kroz jedan konkretan i stabilan način za korišćenje SVG-a unutar HTML-a, tačnije kako napraviti sistem ikonica pomoću SVG-a. Debeda da li koristiti SVG ili font za ikonice je već mnogo puta vođena i moj savet je da koristite SVG. Ovaj članak obuhvata i dizajnere zato što su oni dužni da urade samu pripremu SVG-a.
Kao i kod štampe, pre korišćenja vektorske slike, potrebno je uraditi grafičku pripremu. Iako je najlakše samo izvesti ikonicu iz našeg omiljenog alata (Illustrator, Sketch, Inscape itd.), uz malo truda, možemo mnogo olakšati developerima i sprečiti razne probleme na koje oni mogu naići.
Prilikom pravljenja ikonice, svaka ikonica treba da ima svoj zaseban "artboard" tj. radnu površinu. Ovim se osiguravamo da ikonica sadrži tačno ono što joj je potrebno i nema sakrivenih putanja u pozadini.
Oblik same ikonice nije bitan. Ono što jeste bitno je da "artboard" u kome se nalazi ikonica bude kvadratnog oblika. Kada se ikonica nalazi u kvadratnoj radnoj površini to dosta olakšava rad sa ikonicom, jer možemo biti sigurni da su sve ikonice jednake visine i širine.
Tačne dimenzije radne površine nisu definisane, ali postoji nepisano pravilo da to bude 16x16
ili 20x20
piksela zato što se ikonice najčešće koriste u toj izvornoj veličini.
Ovo je možda i najčešća greška kod dizajnera, a to je da se ikonica prostire od ivice do ivice "artboarda".
Kada veb pregledač prikazuje SVG ikonicu on koristi "anti-aliasing", ali često taj jedan dodatni piksel koji doda AA neće da se prikaže van našeg viewBox
a.
Ovaj problem je dosta vidljiv u Firefox i Edge pregledačima i, kada se javi, to izgleda ovako:
Kako bismo izbegli ovaj problem, pridržavajte se pravila da ikonica ima 0.5px
ili 1px
prostora sa svake strane.
Kada smo napravili novu radnu površinu, a zatim napravili našu ikonicu i umanjili je za jedan piksel kao što je gore rečeno, tek tada smo spremni da izvezemo ikonicu iz našeg omiljenog alata. Ovaj proces će zavisiti od alata do alata, ja ću konkretno pokriti dva najkorišćenija alata, Sketch i Illustrator.
Prilikom izvoza iz Sketch-a može doći do poznatog problema da Sketch ubaci nepotreban markup koji će da nam poveća veličinu datoteke. Osim same veličine datoteke ovakav markup može da napravim problem prilikom korišćenja ikonice.
Evo kako izgleda loša ikonica iz Sketch-a:
<?xml version="1.0" encoding="UTF-8"?>
<svg width="100px" height="94px" viewBox="0 0 100 94" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 54.1 (76490) - https://sketchapp.com -->
<title>logo-m</title>
<desc>Created with Sketch.</desc>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Artboard" transform="translate(0.000000, -3.000000)" fill="#1A1A1A" fill-rule="nonzero">
<g id="logo-m" transform="translate(0.000000, 3.000000)">
<path d="..." id="Shape"></path>
</g>
</g>
</g>
</svg>
Možemo primetiti da postoji veliki broj grupa <g>
i ono što često pravi problem je transform
koji ustvari ničemu ne služi. Ako pogledamo malo bliže, videćemo da jedan drugog poništavaju. Ovaj primer zna biti mnogo lošiji, sa mnogo više grupa.
Zašto se ovo tačno dešava ne mogu reći sa sigurnošću, ali sam zaključio da se dešava kada izvozimo "shape" a ne "artboard", što bismo svakako trebali da radimo.
Pravi način da izvezemo ikonicu je da izaberemo "artboard" a zatim opciju "Make Exportable", dok za format izaberemo "SVG".
U Illustrator-u je ovaj proces dosta jednostavniji i Illustrator nema probleme sa grupama i transformacijama. Idemo na File > Export > Export As...
i jedina bitna stvar je da izaberemo u dnu "Use Artboards".
Kad smo uspešno izvezli ikonice vreme je da dodatno sačuvamo prostor tako što ih optimizujemo.
SVG datoteke obično sadrže dosta redundantnih i beskorisnih informacija. Često su to metapodaci, komentari, skriveni elementi i druge stvari koje se mogu sigurno ukloniti ili pretvoriti bez uticaja na rezultat SVG-a.
Trenutni zlatni standard je SVGO koji je konkretno NodeJS alat, ali dolazi u različitim oblicima.
Komandna linija:
Unutar foldera gde su nam ikonice pokrenemo svgo *.svg
i ovo će nam optimizovati sve ikonice.
Ostali alati:
Ikonica u sebi ne bi trebala da ima definisanu boju. Ako ikonica ima "hardkodiranu" boju unutar sebe, nećemo moći da izmenimo boju ikonice preko CSS-a, što znači da gubimo kontrolu nad bojama. Zato je uvek dobro da sklonimo boje.
Postoje dva načina da se dodeli boja u SVG-u:
fill
.style
blok).Bitno je da naš SVG ne sadrži ni jedan fill
atribut, niti style
blok.
fill
attribut putanjama koje su pune crne boje (#000000
). Zato se generalno preporučuju da ikonice budu potpuno crne. Za sketch ovo ne važi, već moramo ručno obrisati.
Krećemo u developerski deo posla. Ako niste developer ili jednostavno niste zainteresovani šta sve developeri treba da urade da bi prikazali ikonice na sajtu, ovaj deo nije za vas.
Sistem ikonica pravimo pomoću "sprajta" (ne to nije sok). Sprajt je jedna velika slika koja sadrži sve moguće sličice tako da se znaju kordinate svake sličice i na taj način se vrši mapiranje. Ova tehnika postoji još od 70ih godina, kada je Atari, jedan od prvih, implementirao ovo.
Malo preciznija i slobodnija definicija u našem slučaju bi bila mapa simbola, zato što je mapa ustvari jedan SVG koji ima definisane simbole.
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
<path d="M16.5 18.8c-.1-.8-1-.9-1.5-.8-.5-2 1.3-5 1.4-5.8.1-.8-.5-5.2-.6-7.5-.1-2.3-3.4-4.9-4-4.5-.6.6.8 1.4.1 2.2-.7.7-.9 1.2-1.4 1.9-1.3 2 2.3 2 2.3 2S10 8.5 9.4 9.5c-.6 1-.8 2-1.5 2.9-1.6 2.1-1.2 5.7-1.2 5.7s-3-1-1.3-4.4c1.2-2.6 1.9-6.4-.9-5.4-1.2.4.1 1.2.3 1.5.8.9.1 2.3-.3 3.1-1.2 2.2-1.7 4.6.9 6 1.2.6 2.7.8 4.3.8h4c1.3.1 2.8-.1 2.8-.9z"/>
</svg>
To izgleda ovako:
Vrlo je jednostavno dodati ikonice na mapu, kao što sam rekao, SVG mapa je SVG sa simbolima.
<svg xmlns="http://www.w3.org/2000/svg">
<symbol id="macka" viewBox="0 0 20 20">
<path d="M16.5 18.8c-.1-.8-1-.9-1.5-.8-.5-2 1.3-5 1.4-5.8.1-.8-.5-5.2-.6-7.5-.1-2.3-3.4-4.9-4-4.5-.6.6.8 1.4.1 2.2-.7.7-.9 1.2-1.4 1.9-1.3 2 2.3 2 2.3 2S10 8.5 9.4 9.5c-.6 1-.8 2-1.5 2.9-1.6 2.1-1.2 5.7-1.2 5.7s-3-1-1.3-4.4c1.2-2.6 1.9-6.4-.9-5.4-1.2.4.1 1.2.3 1.5.8.9.1 2.3-.3 3.1-1.2 2.2-1.7 4.6.9 6 1.2.6 2.7.8 4.3.8h4c1.3.1 2.8-.1 2.8-.9z"/>
</symbol>
</svg>
Možda izgleda kao da se svašta desilo, ali je ustvari prilično jednostavno. Samo treba da prebacimo <svg viewBox="...">
u <symbol id="..." viewBox="...">
.
Ovim smo SVG ikonicu prebacili u simbol i dodelili mu jedinstven selektor (id). Ovo treba uraditi za svaku ikonicu. Kako to može biti mukotrpan proces, postoje alati za koji to mogu uraditi umesto nas.
Ikonice se dodaju pomoću <use>
elementa, ali postoje dva načina da referenciramo sprajt:
Eksterni sprajt
Kao što ime kaže, sprajt se nalazi u eksternoj datoteci, recimo ikonice.svg
<svg>
<use xlink:href="/putanja/do/ikonice.svg#macka"></use>
</svg>
Ovim smo uzeli ikonice.svg
i iz tog SVG-a i uzeli simbol sa ID-em macka
. Ovaj pristup zahteva jedan dodatni HTTP zahtev da pribavi taj fajl. Obično taj fajl nije toliko veliki pa zato preporučujem interni sprajt.
Interni sprajt
Umesto da idemo po fajl, možemo staviti SVG sa simbolima negde na početku stranice, a zatim da koristimo ikonice iz istog.
<!DOCTYPE html>
<html>
<head>
<title>Nas sajt</title>
</head>
<body>
<!-- Nas interni sprajt sa ikonicama -->
<svg style="display: none">
<symbol id="macka" viewbox="0 0 20 20">
<path d="M16.5 18.8c-.1-.8-1-.9-1.5-.8-.5-2 1.3-5 1.4-5.8.1-.8-.5-5.2-.6-7.5-.1-2.3-3.4-4.9-4-4.5-.6.6.8 1.4.1 2.2-.7.7-.9 1.2-1.4 1.9-1.3 2 2.3 2 2.3 2S10 8.5 9.4 9.5c-.6 1-.8 2-1.5 2.9-1.6 2.1-1.2 5.7-1.2 5.7s-3-1-1.3-4.4c1.2-2.6 1.9-6.4-.9-5.4-1.2.4.1 1.2.3 1.5.8.9.1 2.3-.3 3.1-1.2 2.2-1.7 4.6.9 6 1.2.6 2.7.8 4.3.8h4c1.3.1 2.8-.1 2.8-.9z" />
</symbol>
</svg>
<!-- Ovde neki HTML -->
<svg><use xlink:href="#macka"></use></svg>
</body>
</html>
Razlike
Osim što je drugačiji stil pisanja, postoji velika razlika između ova dva načina. Jedna od glavnih je podrška pregledača:
Chrome | Edge | Firefox | IE | Opera | Safari | |
---|---|---|---|---|---|---|
Eksterni sprajt | 36 | Ne | 15 | Ne | 23 | 7.1 |
Interni sprajt | 36 | 9 | 15 | 12 | 23 | 7.1 |
Više na Can I Use.
Imajte na umu da interni sprajt nije moguće keširati, dok će se eksterni sprajt keširati od strane pregledača.
Sad kad smo dodali SVG ikonice i naučili različite načine kako da ih uključimo u HTML, vreme je da pređemo na CSS. SVG ikonice koje su deo mape možemo posebno da stilizujemo kao da su ustvari posebno uključene i tu leži sama moć mape, mogućnosti internog SVG-a bez ponavljanja.
.icon {
fill: dodgerBlue;
height: 30px;
}
Ako dodamo klasu .icon
na našu ikonicu <svg class="icon"><use xlink:href="#macka"></use></svg>
dobijamo sledeći razultat.
Kako stalno ne bismo pisali fill
, height
i duplirali kod za color
ovo može lepše da se standardizuje.
.icon {
fill: currentColor;
width: 1em;
height: 1em;
vertical-align: middle;
overflow: hidden;
}
Ovim će se naša ikonica ponašati kao i font.
Kako bismo razumeli proces leksiranja, najbolje je da postavimo primer. Ako posmatramo sledeći izraz var m = 17
, na prvi pogled on izgleda kao jedan izraz ali ovaj proces može da se podeli u dva koraka:
var m
Deklaracija promenjive unutar oblasti definisanostim = 17
Dodela vrednosti promenjivoj, 17
.Tokom procesa komajliranja, tačnije u procesu leksiranja, kompajler će da odluči kako i gde su definisane sve promenjive. Ovo je onaj isti mehanizam koji izaziva podizanje (hosting) o kome sam pričao u funkcijama u Javaskriptu.
Vrlo je važno odvojiti deklaraciju od dodele vrednosti zato što se te dve stvari dešavaju u potpuno različitom trnutku u vremenu. Ako iskoristimo sledeći primer:
console.log(m)
var m = 17
Šta će ovo da ispiše? Postoje tri mogućnosti:
Tačan odgovor je undefined, upravo iz razloga što se definicija promenjive dešava tokom leksičke (kopajlerske) faze. Dok se dodela vrednosti dešava tokom dinamičke faze.
Kada smo razjasnili da Javaskript nije interpreterski jezik, već se kompajluje i kombinacija je statičkog i dinamičkog izvršavanja, možemo da pređemo na oblast definisanosti.
Javaskripta ima dosta problema što se tiče oblasti definisanosti, pa zato sam i odlučio da sve to opišem ovde, ali ono što je možda i najveća inicijalna greška Javaskripte je da je zamišljena da leksička oblast definisanosti bude nad funkcijom, iako većina jezika imaju oblast definisanosti nad blokom (blokom se smatraju { }
zagrade). Na žalost, još veći problem je što uprkos inicijalne ideje da oblast definisanosti bude nad funkcijama, videćemo da to nije tačno i da je moguće prevariti taj mehanizam.
Kao što sam već rekao, ideja je da oblast definisanosti bude nad funkcijma, a evo i par primera:
var m = 17 // Globalna oblast
function foo() {
var ninja = "tajna" // Foo oblast
}
console.log(m) // 17
console.log(ninja) // ReferenceError: ninja is not defined
Ono što ovde vidimo je da je var m
definisano u globalnoj oblasti, što znači da je dostupno bilo gde u kodu, dok je sa druge strane var ninja
definisano unutar funkcije što znači da je oblast važenje te promenjive samo unutar te funkcije. Ovaj primer je "idealan" primer gde se sve lepo izvršava, ali hajde da vidimo kako izgleda tok izvršenja.
Recimo da imamo sledeći primer:
var m = 17
var bar = 'bam'
function foo(m) {
var bam = 'hi'
m = 9
n = 'fun'
}
Šta će ovde da se desi? Prvo će se izvršiti korak leksike, gde će kompajler da prođe i nađe sve definicije novih promenjivih.
Kompajler:
Linija 1: Nova promenjivam
, definiši je na globalnom nivou.
Linija 2: Nova promenjivabar
, definiši je na globalnom nivou.
Linija 4: Nova funkcijafoo
, definiši je na globalnom nivou.
Linija 4: Nova promenjivam
, definiši je na nivou funkcijefoo
.
Linija 5: Nova promenjivabam
, definiši je na nivou funkcijefoo
.
Ovim je završena leksička faza i sve promenjive su definisane.
Zatim dolazi druga faza kompajliranja a to je dodela vrednosti.
Interpreter:
Linija 1: Global jel imašm
? Da. Dodeli mu ovu vrednost.
Linija 2: Global jel imašbar
? Da. Dodeli mu ovu vrednost.
Linija 5: Foo jel imašbam
? Da. Dodeli mu ovu vrednost.
Linija 6: Foo jel imašm
? Ne. Global jel imašm
? Da. Dodeli mu ovu vrednost.
Linija 7: Foo jel imašn
? Ne. Global jel imašn
? Ne... Šta onda?
Ovo je ono gde nastaje problem a to je da će global da kaže:
Nemam ni ja tu promenjivu, hajde da ti je napravim... Izvoli!
Šta se sada deslio? Došlo je do kršenja pravila oblasti važenja unutar funkcije, naša promenjiva n
je postala globalna iako se nalazi unutar funkcije.
Ovo je lako rešivo tako što koristimo strict
režim.
Ovo je prvi problem, gde vidimo da oblast važenja nad funkcijom više ne važi.
Na početku sam rekao da je ideja bila da oblast važenja bude nad funkcijama. To se promenilo u verziji ES3 kad je uveden try catch
blok.
Ono što mali broj ljudi zna je da catch
ima svoju oblast važenja.
try {
throw new Error()
} catch(e) {
console.log(e) // Error
}
console.log(e)
Ako se vratimo na prethodnu priču, ovo će i biti logično, zato što catch
gledamo kao funkciju, što znači da je parametar funkcije lokalna promenjiva u odnosu na funkciju.
Ja lično ovo ne smatram kao "varanje" oblasti važenja zato što, ako posmatramo na pretohdno opisan način, onda ovo ima smisla. Ali postoje načini da se prevari oblast važenja.
Postoje dva načina da se prevari oblast važenja u Javaskripti ali oba ova načina treba izbegavati i ne preporučujem da se ikad koriste.
eval()
- Funkcija koja je češće poznatija kao evil (zlo) i to iz više razloga, je funkcija koja izvršava string kao Javaskript kod. Često se koristi kao primer za pravljenje digitrona gde konstruišemo string od brojeva i operatora zatim to prosledimo eval
u na izvršenje.
eval("2+3-1") // 4
Problem je što eval
izvršava bilo šta što mu pošaljemo, eval("alert('cao')")
- ovo će da izbaci alert. Često je eval
bio problem za XSS. Toliko je loše da ima posvećenu sekciju na MDN-u, Do not ever use eval!
Kakve veze ima sa oblašću važenja? I te kako ima veze, zato što eval
pravi svoju oblast važenja:
var m = 17
function foo(str) {
eval(str) // NIKAD OVO
console.log(m)
}
foo('var m = 42');
Verovatno može da se zaključi izlaz ovog, a to je 42... Ovo se dešava zato što eval
menja leksičku oblast važenja i izgleda kao da je var m = 42
bilo tu tokom kompajlovanja programa. Ovde takođe postoji problem optimizacije zato što poziva interpreter i nema optimizacije na nivou kompajlera.
with
ključna reč se retko viđa, ja je iskreno nikad nisam video u produkciji, a videćemo da je to dobro. Ali može da se nađe u golfjs takmičenjima ili 1 kB, 13 kB Javaskript takmičenjima.
with
nam omogućava da skratimo kod tako što prosledimo objekat za koji može da se evaluira oblast važenja:
var obj = {
a: 17,
b: 42
}
obj.a = obj.a + obj.b
obj.b = obj.b - obj.a
// Ovo može kraće da se napiše bez `obj`-a.
with (obj) {
a = a + b
b = b - a
c = 1
}
console.log(obj.c) // undefined
with
se u ovom slučaju ponaša tako što pravi svoju leksičku oblast definisanosti. To znači kao i do sada, ako nešto kad naiđe na vrednost prvo pita lokalnu oblast, što je parametar koji smo prosledili obj
, ako ne može tu da nađe onda ide dalje globalno. I tu se javlja isti problem kao kad ne koristimo ključnu reč var
, napraviće se globalna promenjiva.
Znači da će c
biti globalno, a ne deo obj
-a kao što smo možda očekivali.
Dolaskom EcmaScript 2015 (ES6) dobili smo potpuno novu Javaskriptu, sa ozbiljnim promenama od kojih su dve nove ključne reči let
i const
.
let
i const
su ključne reči za deklaraciju promenjivih koji rešavaju probleme var
-a. Dodavanjem let
-a i const
-a Javaskripta je promenila svoju inicijalnu ideju definisanja oblasti, pošto nove ključne reči imaju oblast važenja na nivou bloka. Takođe promenjive se više ne podižu (hojstuju), već ostaju gde su deklarisane.
for (let i = 0; i < 17; i++) {
// Neki kod
}
console.log(i); // Greška
Ovim smo rešili većinu problema, ali takođe ove reči treba gledati i sa strane optimizacije.
Možda je na prvi pogled vrlo čudno kako zamenom var
-a let
-om možemo da dobijemo bilo kakve dobitke u pogledu performansi. Ali ako zađemo malo dublje u rad Javaskripte u pozadini, možemo da vidimo i zašto.
Iako želim da napišem poseban članak kako radi Javaskript u pozadini, evo ukratko pregleda. Skupljač smeća (garbage colletor) je zadužen da se brine o memoriji, pošto ako stalno pravimo objekte i nikad ih ne brišemo kad tad će naša aplikacija da ostane bez memorije. Zato skupljač smeća kada "vidi" da se promenjiva više ne koristi on će da ukloni tu promenjivu i ono na šta ona pokazuje.
Pošto var
ima samo oblast važenja nad funkciom skupljač smeća može da očisti te promenjive samo ako su u funkciji inače su uvek globalne. Zato let
i const
koji imaju oblast važenja u odnosu na blok i biće obrisani kad se zatvori blok, pružaju optimizaciju.
var uslov = true
if (uslov) {
var korsnik1 = { //... }
} else {
var korsnik2 = { //... }
}
U ovom primeru korisnik1
i korisnik2
se nalaze u globalnoj oblasti i neće biti obrisani sve dok se ne završi program, dok ako ovo napišemo koristeći let
ili const
:
let uslov = true
if (uslov) {
let korsnik1 = { //... }
} // korisnik1 Može da se obriše
else {
let korsnik2 = { //... }
} // korisnik2 Može da se obriše
Možda ovaj članak deluje kao dug, ali postoji dosta problema i rešenja što se tiče oblasti definisanosti u Javaskripti. Potrebno je dobro vladati i znati koja će promenjiva gde da se javi kako bismo izbegli greške.
Možda ova priča zvuči kao nepotrebna zato što koristeći use strict
, let
i const
nama su rešeni svi problemi. Na žalost to nije tako i ovaj članak je uvod za jedan dosta važniji a to je Šta je this
u Javaskripti?
setInterval()
, koja se koristi za za periodično pozivanje neke druge funkcije. Sa druge strane, postoji funkcija setTimeout()
, koja poziva neku drugu funkciju posle određenog vremena.Primer (setInterval()
):
function hello() {
console.log('Sta radis?');
}
setInterval(hello, 1000);
Na svakih 1000 milisekundi (1 sekunda) će se pozvati funkcija hello()
, koja će u konzoli ispisati 'Sta radis?'
.
Primer (setTimeout()
):
function hello() {
console.log('Dobro dosao');
}
setTimeout(hello, 1000);
Posle 1000 milisekundi (1 sekunda) će se pozvati funkcija hello()
, koja će u konzoli ispisati 'Dobro dosao'
.
Iz opisa možemo zaključiti da se setInterval()
koristi za događaje koji se periodično ponavljaju, dok se setTimeout()
koristi za odlaganje nekog događaja, što je delimično tačno.
Ako setTimeout()
poziva funkciju u kojoj je definisan (rekurzija) dobijamo isti efekat koji dobijamo i pomoću setInterval()
funkcije.
function hello() {
console.log('Sta radis?');
setTimeout(hello(), 1000);
}
Iako u ovom primeru nema razlike, ona definitivno postoji. Razika će se pokazati u slučaju kada naša funkcija radi nešto vremenski zahtevno.
Ako funkcija, koja se izvršava, radi neki zahtevan posao ili jednostavno radi AJAX poziv, i ima duže vreme izvršavanja nego što je postavljeni interval, dolazi do zagušenja. setInterval()
će probati da održi svoj interval, gde dolazimo do problema da se javlja dupli poziv iako ne treba. Taj problem možemo da rešimo ako umesto setInterval()
iskoristimo setTimeout()
. Ali prvo da vidimo na primeru kako se ponašaju.
let runs = 0;
let execTime = 0;
const startTime = new Date().getTime();
function hello() {
if (++runs === 10) {
clearInterval(timer);
}
const now = new Date().getTime();
execTime = now - startTime;
// Trosi neko vreme neko vreme
for (let i = 0; i < 100000; i++) {
document.querySelector('body > a');
}
let exec = new Date().getTime() - now;
console.log(`#${runs} poziv je poceo u ${execTime}ms i trajao je ${exec}ms`);
};
const timer = setInterval(hello, 1000);
Pokretanjem gore navedenog koda, dobijamo sledeći izlaz:
#1 poziv je poceo u 1000ms i trajao je 529ms
#2 poziv je poceo u 2001ms i trajao je 566ms
#3 poziv je poceo u 3000ms i trajao je 554ms
#4 poziv je poceo u 4001ms i trajao je 553ms
#5 poziv je poceo u 5002ms i trajao je 546ms
#6 poziv je poceo u 6001ms i trajao je 640ms
#7 poziv je poceo u 7003ms i trajao je 730ms
#8 poziv je poceo u 8002ms i trajao je 680ms
#9 poziv je poceo u 9003ms i trajao je 607ms
#10 poziv je poceo u 10003ms i trajao je 623ms
Ovde nema nikakvog iznenađenja. Funkciji treba neko vreme da se izvrši ali setInterval()
odžava svoj interval.
Ono što možemo da primetimo je da početak poziva funkcije nije toliko precizan (kasni 2-3 ms). To je iz razloga zato što tajmeri u Javaskripti nisu toliko pouzdani. Moguće je da se sinhronizuju sa Date
objektom.
Ako zamenimo setInterval()
sa setTimeout()
dobijamo sledeći slučaj
let runs = 0;
let execTime = 0;
const startTime = new Date().getTime();
function hello() {
const now = new Date().getTime();
execTime = now - startTime;
// Trosi neko vreme neko vreme
for (var i = 0; i < 100000; i++) {
document.querySelector('body > a');
}
console.log(`#${runs + 1} poziv je poceo u ${execTime}ms i trajao je ${new Date().getTime() - now}ms`);
if (++runs < 10) {
setTimeout(hello, 1000);
}
};
setTimeout(hello, 1000);
Što će nam dati sledeći rezultat:
#1 poziv je poceo u 1002ms i trajao je 565ms
#2 poziv je poceo u 2570ms i trajao je 601ms
#3 poziv je poceo u 4173ms i trajao je 533ms
#4 poziv je poceo u 5709ms i trajao je 509ms
#5 poziv je poceo u 7219ms i trajao je 642ms
#6 poziv je poceo u 8863ms i trajao je 742ms
#7 poziv je poceo u 10607ms i trajao je 848ms
#8 poziv je poceo u 12455ms i trajao je 651ms
#9 poziv je poceo u 14107ms i trajao je 564ms
#10 poziv je poceo u 15673ms i trajao je 673ms
Ovde takođe nema nikakvog iznenađenja. Pošto već znamo da setTimeout()
ne prati nikakav interval, već sačeka da se završi funcija onda čeka odrđeno vreme pre sledećeg poziva.
Ok, sve je ovo već poznato,ali koja je reazlika u "realnom" svetu:
Recimo da radimo neki file upload i da se obrada fajla radi na backend serveru. Mi uploadujemo fajl, a zatim moramo konstanto da proveravamo da li je obrada završena. Ako koristimo setInterval()
da radimo GET
zahtev na svaku sekundu, ono što se može desiti jeste da je server zauzet i ako server ne vrati odgovor u roku od našeg intervala (jedna sekunda), funkcija setInterval()
neće mariti — u suštini, ona ni ne zna, time će da pošalje još jedan AJAX zahtev. Dok kod setTimeout()
rekurzije, mi prvo sačekamo odgovor i ako treba nakon toga pošaljemo još jedan AJAX zahtev.
Kada setInterval()
preskoči interval šta se desi? Haos!!!
Ono što se desi zavisi malo od brzine računara kao i od veb pregledača koji korisnik koristi. Postoje dve mogućnosti:
Kao i sve u veb svetu, ponašanje je u zavisnosti od veb pregledača. Firefox je jedinsteven, zato što jedino on ima ovo prvo (1) ponašanje. Doduše ovo nije potpuno tačno, zapravo se ponaša kao kombinacija prvog i drugog, evo kako:
U slučaju da FF propusti samo jedan otkucaj onda će se ponašati na prvi (1) način, u slučaju da propusti više od jednog otkucaja, prebaciće se na drugi (2) način.
Svi ostali veb pregledači imaju drugo (2) ponašanje.
Ako Vam je imalo stalo do tajminga ne koristite setInterval()
već iskoristite rekurziju i setTimeout()
. Za nijansu je lakše koristiti setInterval()
ali može dovoesti do grešaka i raznih problema koji nisu očigledni.
setTimeout
i setInterval
na jednu sekundu kada se pokreće na kartici koja nije aktivna (u pozadini).
Uporediću ulogu developera koji pravi CSS i softverskog inženjera koji pravi softver. Vrlo suludo zvuči ali postoji dosta sličnosti.
UML (Unified Modeling Language) - objedinjeni vizuelni jezik za
poslovno i softversko modelovanje u svim fazama razvoja i za sve
tipove sistema, kao i za generalno modelovanje kojim se definišu
statičke strukture i dinamičko ponašanje.
Oni koji nisu upoznati sa terminom, ovo je osnovni korak svakog softverskog inženjera, vizuelno prikazivanje objektnog modela. Na žalost veb developeri ne potpadaju u kategoriju softverskih inženjera, tako da oni ne mogu da koriste ovaj UML dijagram? Pa nije baš tako.
Ono što je zajedničko za softver i sajt jeste da je potrebno planiranje. Zvuči smešno? Ovo zvuči smešno u oba sveta ako se radi o malim, ne skalabilnim aplikacijama.
Smešno bi bilo crtati UML dijagram za kalkulator koji sabira brojeve, više vremena bi se utrošilo u crtanje no u programiranje samog kalkulatora. To isto važi i za veb, neće niko da planira sajt koji ima 3-4 vrlo proste stranice i to je to.
Ceo ovaj članak zvuči suludo ali kada otkucate 20 000 linija u jednom CSS fajlu, shvatićete da ste se i sami izgubili u svom kodu.
Ono što smo sigurno bar jednom uradili je slepo kopiranje. To izgleda nekako ovako:
Floatovi nemaju visinu? Aha, odemo do google-a, potražimo rešenje.
Našli smo rešenje clearfix, iskopiramo kod u sred koda.
Par meseci kasnije desi se isto, zaboravimo klasu clearfix, napravimo novo rešenje za floatove, sad u klasi cf.
Upravo smo napravili dupli kod, koji je odmah teži za održavanje. Videćemo kako da rešimo te probleme.
Lepo je kad neko ima inspiraciju odmah uskoči u HTML i CSS i počne da radi bez dizajna. Dizajn treba gledati kao pod-UML dijagram.
UML dijagram treba konstruisati iz dizajna. Gde su jedinstveni stilovi, objekti (komponente), a polja objekta njeni pod elementi.
Evo jednog primera koji se često javlja, "avatar box".
Kao što vidimo gore imamo tri različite komponente, ustvari, ove tri komponente su iste i možemo da ih obuhvatimo jednom klasom.
Klase će se zvati .avatar-box
, kao podelemente saržaće sliku, naslov i opis. Slika je sa leve strane, naslov i opis sa desne, to je normalan izgled.
U slučaju da želimo okrugli avatar, u imenu klase dodamo modifikator --okrugla
. Klase sada izgledaju ovako .avatar-box .avatar-box--okrugla
gde će u CSS-u .avatar__slika
imati border-radius: 50%
.
Isti taj princip primenimo za dobijanje trećeg primera, dodamo modifikator --blok
. Klase sada izgledaju ovako .avatar-box .avatar-box--blok
gde će u CSS-u .avatar__slika
imati display: block
i tekst će biti centriran.
Pokazali smo da dobro planiranje može da nam uštedi vreme i prostor.
Naučili smo da tri vizuelno različita ali strukturno slična elementa možemo spojiti u jedan i tako izbeći pisanje više klasa.
Šta više, za primer iznad mogli smo dodati modifikatore za veličinu tako da imamo avatar sa velikom, malom i srednjom slikom.
Moja preporuka je kada imate dizajn da dobro sagledate koje su to komponente i na koji ćete ih način struktuirati, ako su strukture slične, verovatno ih možete obuhvatiti jednom klasom.
]]>Deklaracija:
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, Serializable
Karakteristike:
Funkcije:
void add(int index, Object element)
boolean add(Object o)
void clear()
int lastIndexOf(Object o)
Object[] toArray()
int indexOf(Object o)
Primer:
ArrayList<String> korisnici = new ArrayList();
korisnici.add("Marko");
korisnici.add("Pera");
korisnici.add("Mika");
for ( int i = 0; i < korisnici.size(); i++ ) {
System.out.println( korisnici.get(i) );
}
// Iterator
Iterator it = korisnici.iterator();
while ( it.hasNext() ) {
System.out.println( it.next() );
}
Deklaracija:
public class LinkedList<E> extends AbstractSequentialList<E> implements List<E>, Deque<E>, Cloneable, Serializable
Karakteristike:
ArrayList
zato što nema potrebe za shiftovanje elemenata.Funkcije:
void add(int index, Object element)
void addFirst(Object o)
void addLast(Object o)
boolean add(Object o)
boolean contains(Object o)
boolean remove(Object o)
int indexOf(Object o)
int lastIndexOf(Object o)
Object getFirst()
Object getLast()
Primer:
LinkedList<String> korisnici = new LinkedList();
korisnici.add("Marko");
korisnici.add("Pera");
korisnici.add("Mika");
for ( int i = 0; i < korisnici.size(); i++ ) {
System.out.println( korisnici.get(i) );
}
// Iterator
Iterator it = korisnici.iterator();
while ( it.hasNext() ) {
System.out.println( it.next() );
}
Ima tri vrsta listi, neke razlike:
ArrayList
- je implementirana kao dinamički niz.LinkedList
- je implementirana kao dvostruka lančana lista, ima mnogo bolje performanse na dodavanju i brisanju elemenata, ali i loši performanse prilikom get()
i set()
metode.Vector
- kao ArrayList
samo je sinhronizovana.
Slika preuzeta sa: programcreek.com
Deklaracija:
public class HashSet<E> extends AbstractSet<E> implements Set<E>, Cloneable, Serializable
Karakteristike:
hashCode
Funkcije:
boolean add(Object o)
boolean contains(Object o)
boolean remove(Object o)
void clear()
Primer:
HashSet<String> korisnici = new HashSet();
korisnici.add("Marko");
korisnici.add("Pera");
korisnici.add("Mika");
korisnici.add("Marko"); // Nece da se pojavi
// Iterator
Iterator it = korisnici.iterator();
while ( it.hasNext() ) {
System.out.println( it.next() );
}
/*
Output:
Pera
Mika
Marko
*/
Napomena:
equals()
metoda mora da se prekolopi i hashCode()
. Važi i suprotno!equals()
za izračunavanje hashCode()
!Deklaracija:
public class TreeSet<E> extends AbstractSet<E> implements NavigableSet<E>, Cloneable, Serializable
Karakteristike:
HashSet
.Funkcije:
boolean add(Object o)
Object first()
Object last()
boolean contains(Object o)
boolean remove(Object o)
void clear()
Primer:
TreeSet<String> korisnici = new TreeSet();
korisnici.add("Marko");
korisnici.add("Pera");
korisnici.add("Marko"); // Nece da se pojavi
korisnici.add("Aca");
// Iterator
Iterator it = korisnici.iterator();
while ( it.hasNext() ) {
System.out.println( it.next() );
}
/*
Output:
Aca
Marko
Pera
*/
HashSet
je znatno brža strukturna od TreeSet
-a. Zato što pruža konstantno vreme osnovnim operacijama (add()
, remove()
,size()
i contains()
). Dok je TreeSet
implementirano kao stablo samim tim su sve operacije log(n)
.HashSet
nije uređena kolekcija.HashSet
i zatim da se prebaci u TreeSet
za sortiranu kolekciju.Mape sadrže dva polja ključ i vrednost. Mapa sadrži samo jedinstvene ključeve. Par ključ i vrednost se nazivaju entry.
Deklaracija:
public class TreeMap<K,V> extends AbstractMap<K,V> implements NavigableMap<K,V>, Cloneable, Serializable
Karakteristike:
Funkcije:
Object put(K key, V value)
boolean containsKey(Object key)
boolean containsValue(Object value)
Object firstKey()
Object lastKey()
Object get(Object key)
Object remove(Object key)
Set entrySet()
int size()
Primer:
TreeMap<Integer, String> korisnici = new TreeMap();
korisnici.put(40, "Aca");
korisnici.put(20, "Pera");
korisnici.put(10, "Marko");
korisnici.put(30, "Marko"); // Pojavljuje se, ima jedinstven kljuc
for ( Map.Entry<Integer, String> entry : korisnici.entrySet() ) {
System.out.println( entry.getKey() + " => " + entry.getValue() );
}
// Drugi nacin za iteraciju, iteracija po kljucevima
Iterator<Integer> itr = korisnici.keySet().iterator();
while (itr.hasNext()) {
Integer key = itr.next(); // Ovo vraca kljuc (10, 20, 30...)
String user = korisnici.get(key);
System.out.println( key + " => " + user );
}
/*
Output:
10 => Marko
20 => Pera
30 => Marko
40 => Aca
*/
Ima četiri tipa mapa, evo i neke razlike:
HashMap
- je implementirana kao heš tabela, nije uređena kolekcija.TreeMap
- je implementirana kao crno-crveno stablo, uređena kolekcija po ključu.LinkedHashMap
- sadržava redosled upisivanja.Hashtable
- sinhronizovana HashMap
-a.Veza koja kaže da objekat sadrži referencu drugog objekta.
U ovom primeru znači da klasa Automibil
sadži instancu (referencu) klase Putnik
. Ovaj tip veze je slab. Objekti mogu da žive nezavisno od
Veza koja kaže da objekat sadrži referencu drugog objekta.
U ovom primeru znači da klasa Automibil
sadži instancu (referencu) klase Putnik
. Ovaj tip veze je slab. Objekti mogu da žive nezavisno od veze.
Primer koda:
public class Automobil {
private Putnik putnik;
public void setPutni(Putnik p) {
this.putnik = p;
}
public Putnik getPutnik() { return putnik; }
}
Agregacija je slična asocijaciji, ustavri agregacija je specijalan slučaj asocijacije. Takođe ovaj tip veze je slab i objekti mogu da postoje nezavisno. Međutim ovde su objekti važan deo objekta koji ih agregira.
U ovom primeru znači da klasa Automobil
sadrži više instanci klase Guma
.
Primer koda:
public class Automobil {
private ArrayList<Guma> gume;
public void setGuma(Guma g) {
gume.add(g);
}
public ArrayList<Guma> getGume() {
return gume;
}
}
Dijaman na strani klase znači da ona agregira (sadrži) drugu klasu. Veza se kreira tako što se vuče od strane koju hoćemo da agregiramo do strane koja agregira.
Kompozicija je takođe specijalan slučaj asocijacije. Dok su agregacija i asocijacija slabi tipovi, kompozicija je jak tip veze. Objekti klase koja je agregirana ne mogu postojati ako ne postoji objekat klase koja ih agregira.
U ovom primeru znači da se klasa Automobil
sačinjena od Šasije. Ako se automobil obriše briše se i šasija zato što šasija bez automobila ne postoji. Automobil je dužan da napravi šasiju.
Primer koda:
public class Automobil {
private Sasija sasija;
public Automobil() {
this.sasija = new Sasija();
}
}
Ovde je jaka veza ilustrovana tako što se prilikom konstrukcije objekta, kreira i šasija koja se vezuje za automobil.
Generalizacija ili specijalizacija je verovatno najjednostavniji tip veze u UML-u, zato što generalizacija predstavlja ono što je u objektno orijentisanom programiranju poznato kao nasleđivanje.
U ovom primeru klase Limuzina
, Karavan
, Hatchback
nasleđuju klasu Automobil
Primer koda:
public class Limuzina extends Automobil {}
public class Karavan extends Automobil {}
public class Hatchback extends Automobil {}
Realizacija ili apstraktna generalizacija je isto što i generalicazija samo što se nasleđuje apstraktna klasa (interfejs).
U ovom primeru IFabrika
je interfejs i implementiraju ga klase Fabrika Zastava
i Fabrika Fiat
.
Primer koda:
public interface IFabrika {}
public class Fabrika_Zastava implements IFabrika {}
public class Fabrika_Fiat implements IFabrika {}
]]>Šta je tooltip?
Tooltip ili infotip ili hint je komponenta koja u kombinaciji sa kursorom služi da nam pruži dodatne informacije.
Kada korisnik kurosorm pređe preko objekta, pojavljuje se tooltip sa informacijama o stavci preko koje je prešao kursorom.
Treba imati na umu da tooltip nije isto što i popover. Popover može da sadrži i multimedijalni sadržaj, dok tooltip samo sadrži tekst.
Veb pregledač ima već ugrađeni tooltip. On se postavlja tako što postavite atribut title
na neki element.
<span title="Ovo je tooltip">Pređi preko mene</span>
Nažalost, ovaj tooltip nije moguće stilski promeniti, pa je zato pogodno napraviti naš tooltip.
Ovo je verzija koju najčešće viđamo kao rešenje za tooltip pomoću CSS-a
U HTML-u nam je tooltip predstavljen preko jednog elementa, gde tooltip kontrolišemo preko atributa.
<span data-tooltip="Ovo je tooltip">Pređi preko mene</span>
Korisnički navedeni atributi moraju da počinju sa data-
Klasa nam uopšte nije potrebna već ćemo selekciju vršiti preko selektora atributa.
[data-tooltip] {
position: relative;
cursor: pointer;
}
Selektujemo sve elemente koji imaju atribut data-tooltip
. Dodelićemo mu poziciju koja će biti relative
zato što će unutar sebe imati apsolutne elemente. Takođe smo dodali opciju cursor
kako bismo naznačili korisniku kada pređe kursorom preko elementa da postoji neka dodatna informacija.
Unutar tooltip elementa moramo da dodamo pseudo element :before
, koji će sadržati sam tekst tooltipa.
On treba da bude apsolutne pozicije zato što će plutati iznad teksta. Sadržaj ćemo uzeti iz samog atributa, pomoću attr()
'funkcije' u CSS-u.
[data-tooltip]::before {
content: attr(data-tooltip);
position: absolute;
bottom: 100%; /* Gurnemo ga do vrha */
left: 50%; /* Stavimo na pola */
transform: translate(-50%, -4px); /* Centriraj vertikalno i pomeri na gore 4px */
padding: .5em 1em;
border-radius: 4px;
background-color: rgba(20, 20, 20, 0.9);
color: #fff;
white-space: nowrap;
z-index: 7;
}
Ovim smo sada dobili jedan vrlo lep tooltip.
Preostalo nam je da napravimo animaciju. Iskoristićemo dva svojstva u CSS-u visibility
i opacity
.
Preko svojstva visibility
ćamo sakriti tooltip tako da ne smeta elementima iznad, zato što ako koristimo samo svojstvo opacity
onda ako postoji element na mestu našeg tooltipa koji treba da se pojavi, korisnik neće moći da dođe do tog elementa, pošto se tooltip nalazi ispred njega. opacity
svojstvo će nam služiti samo radi animacije.
[data-tooltip]::before {
visibility: hidden;
opacity: 0;
transition: opacity .3s ease-in-out;
}
Dodamo :hover
događaj, kad se pređe preko [data-tooltip]
da se pojavi :before
.
[data-tooltip]:hover::before {
visibility: visible;
opacity: 1;
}
Dobili smo traženi rezultat.
Pozicija
Ovo je najprostiji primer gde se tooltip uvek pojavljuje iznad samog sadržaja ali lako možemo nadograditi ovaj tooltip da se sadržaj pojavljuje sa bilo koje strane.
Ja ću napraviti samo jedan primer, vrlo je lako uraditi za ostale pozicije.
Prvo moramo uvesti dodatni atribut koji određuje poziciju, na primer data-tooltip-position
.
<span data-tooltip="Ovo je tooltip" data-tooltip-position="desno">Pređi preko mene</span>
U CSS-u izaberemo element koji ima data-tooltip-position
i čija je vrednost desno
.
[data-tooltip-position="desno"]:before {
bottom: 50%; /* Pola visine */
left: 100%; /* Gurnemo skroz desno */
transform: translate(4px, 50%); /* Centriraj horizontalno i pomeri na desno 4px */
}
Ostale
Moguće je dodati još neke opcije kao što su:
Ovaj pristup iako vrlo jednostavan i elegantan ima jedan nedostatak, a to je da ako njegov roditelj ima overflow: hidden
tooltip se neće videti.
<div class="overflow">
<span data-tooltip="Ovo je tooltip">Pređi preko mene</span>
</div>
.overflow {
overflow: hidden;
border: 1px solid Tomato;
padding: 20px;
}
Ovaj problem sa ovakvom HTML struktorom nije moguće rešiti.
Ova verzija nije konkretno rešenje već će nam pomoći da dođemo do krajnjeg rešenja.
Kako bi implementirali ovu verziju potrebno je da izmenimo malo strukturu.
<span>Pređi preko mene
<span class="tooltip">Ovo je tooltip</span>
</span>
.tooltip
će biti absolutno pozicioniran.
.tooltip {
position: absolute;
background-color: DodgerBlue;
}
Sada da probamo naš primer:
<div class="overflow">
<span>Pređi preko mene
<span class="tooltip">Ovo je tooltip</span>
</span>
</div>
Hmmm, nekako čudno tooltip nije više isečen, o čemu se radi?
Ovde je tooltip relativan u odnosu na neki drugi element a ne na onaj koji ga ograničava.
Jedan veliki nedostatak ovog pristupa, koji ga ustvari čini ne primenljivim, je to što u slučaju da koristimo svojstva za pozicioniranje (top, right, left i bottom
) dobićemo potpuno neočekivane rezultate, zato što je tooltip relativan na neki drugi element (često body
).
Probaću da modifikujem verziju 1.1 i napravim nešto funkcionalno. Način na koji možemo da rešimo problem pomeranja samog tooltipa ako nije relativan na ono što želimo jeste da ga obmotamo jednim apsolutnim divom i sam tooltip pomeramo sada relativno. Možda zvuči komplikovano, ali uopšte nije ukoliko se razume šta se ustvari dešava.
Postaviću novu strukturu:
<span>Pređi preko mene!
<span class="tooltip-wrapper">
<span class="tooltip">Ovo je tooltip</span>
</span>
</span>
.tooltip-wrapper
će imati istu ulogu kao .tooltip
u verziji 1.1, biće samo apsolutno pozicioniran.
.tooltip-wrapper {
position: absolute;
}
Dok će unutar njega postojati još jedan element koji će biti relativan, upravo zbog pomenutog problema nemogućnosti pomeranja.
.tooltip {
position: relative;
right: 100%; /* Sada pomeramo tooltip */
bottom: 2em;
padding: .5em 1em;
border-radius: 4px;
background-color: rgba(20, 20, 20, 0.9);
color: #fff;
}
Dobijemo upravo ono što smo tražili
Dodaću na .tooltip-wrapper
-u plavu boju kako bismo lakše shvatili gde se on ustvari nalazi.
Ovaj pristup, iako pruža rešenje, meni lično nije sintaksno lep, struktura je komplikovana.
U cilju poboljšanja verzije 2.0 napravio sam verziju 2.1 koja ima malo uprošćenu strukturu. Rešenje je kombinacija rešenja 2.0 i 1.0.
<span>Pređi preko mene!
<span class="tooltip-wrapper" data-tooltip="Ovo je tooltip"></span>
</span>
Kao u verziji 1.0 ću iskoristiti pseudo elemente da dodam sam tooltip, samo što ga sada dodajem u naš .tooltip-wrapper
koji je apsolutan.
.tooltip-wrapper {
position: absolute;
}
.tooltip-wrapper::before {
content: attr(data-tooltip); /* Kao u verziji 1.0 */
position: relative;
right: 100%;
bottom: 2em;
padding: .5em 1em;
border-radius: 4px;
background-color: rgba(20, 20, 20, 0.9);
color: #fff;
}
Kranji rezultat:
Prošli smo kroz par načina pravljenja tooltip-a u CSS-u, neki su bili dobri, neki loši, ali smo kroz svaki valjda naučili nešto.
Tooltip je, takođe, moguće napraviti preko javaskripte, pisaću i o tome vrlo uskoro, sa željom da uporedim oba pristupa što se tiče preformansi.
]]>Evo primera jedne proste navigacije:
<ul id="nav">
<li>Početna</li>
<li>O meni</li>
<li>Kontakt</li>
</ul>
Tokom razvoja aplikacije želite da dodate "aktiv" klasu, koja će da naznači krajnjem korisniku na kojoj se stranici nalazi.
<ul id="nav">
<li class="trenutna">Početna</li>
<li>O meni</li>
<li>Kontakt</li>
</ul>
Naravno, to dodate u CSS, recimo ovako:
.trenutna {
color: #FF0000;
text-decoration: underline;
}
... Osvežite stranicu ... ne radi... ???
Nervnozno prolazite kroz CSS gore-dole i uporno ne primećujete ovo:
ul#nav li {
color: #343F44;
text-decoration: none;
}
Ovo izgleda normalno zato što se nalazi pre našeg dodatka .trenutna
. Ali na žalost to nije tako jednostavno.
Svaka CSS definicija ima svoju težinu koja je određena brojem tipova selektora unutar definicije.
h1, a, h5, p, body,
...).klasa
)#neki-id
)style="color: red;"
)Svaki od njih dodaje određenu težinu definiciji selektora:
Ako postoji ovakav selektor:
.clasa > li > a
možemo lako da izračunamo težinu:
Jedna klasa = 10
Dva elementa = 1 + 1
Ukupna težina = 12
Malo prostiji način računanja jeste da zamislimo četvorocifreni broj, gde su jedinice broj html elemenata u selektoru, desetice broj klasa u selektoru, stotice broj ID-jeva u selektoru i hiljade ako postoji inline stil.
Recimo imamo ovakav selektor:
nav#nav > ul.lista > li.active > a
Težina je:
Pseudo selektori takođe utiču na težinu selektora. Oni se ubrajaju u desetice kao i klase.
.nav li:first-child
Težina ovog selektora je: 0021
Pseudo selektore ne treba pomešati sa pseudo elementima!
Pseudo elementi takođe utiču na težinu selektora, ali kao element. Znači dodaje se kao jedinica.
.lista > .element::before
Težina ovog selektora je: 0021
:not()
selektor:not()
selektor je izuzetak. On se ne računa kao selektor, već se računa ono unutar njega.
.lista:not()
Težina ovog selektora je: 0010
Međutim, ako stavimo klasu, id ili element unutar :not()
selektora, ti elementi će se računati.
.lista:not(.numerisana)
Težina ovog selektora je: 0020
!important
deklaracija!important
igra vrlo važnu ulogu ali ne na nivou selektora, nego na nivou vrednosti.
.nav { color: #F00 !important; }
U ovom primeru color
ima najveću moguću vrednost i nije je moguće preklopiti niti jednom težinom selektora.
Način na koji možemo da predefinišemo !important
deklaraciju je:
!important
deklaracije uz veću težinu selektora..nav li { color: red !important; } /* ovo vazi */
.nav { color: green !important; }
!important
deklaracije samo kasnije.nav { color: red !important; }
.nav { color: green !important; }
]]>Želim da napravim "pametne" linkove koji će pokazivati da li je protokol http
ili https
. Ako je https
da promeni boju linka u zeleno.
Razmišljamo kako je ovo moguće? Ok, hajde da krenemo od rešenja u javaskripti.
Prvo rešenje koje sam smislio je da koristimo regex
, prođemo kroz tekst i nadjemo linkove.
var links = document.getElementsByTagName('a'); // Nadjemo sve linkove
var href;
for (var i = 0; i < links.length; i++){ // Moguce je koristis for..of za nodelistu
href = links[i].getAttribute('href'); // Izvučemo href atribut
if(href.match(/https:/)){ //proverimo da li atribut počinje sa https
links[i].className += " siguran"; // ako jeste onda dodamo klasu siguran
}
}
Ovo je jedan od načina za rešavanje ovakvog problem. Naravno u jQuery-u bi ovo bilo bar dva puta lakše.
Način na koji smo rešili ovaj problem u JS-u je tako što smo našli (selektovali) sve linkove čiji atribut href
počinje sa https
.
Ovo je, ustvari, moguće uraditi i u CSS-u.
U CSS-u postoji selektor atributa i on postoji još od verzije CSS2
. Tako da je ovaj selektor podržan na svim većim pregledačima.
Atribut selektori se pišu sa kockastim zagradam, ako ste ikad koristili jQuery i selektovali atribut, verovatno ste upoznati.
Između kockastih zagrada [ ]
navedemo ime atributa,a moguće je navesti i vrednost
[attr]{}
Selektuje elemente koji imaju atribut attr
.
<div attr="test">Imam atribut attr.</div>
Možemo da selektujemo atribute sa određenom vrednošću
[attr="novi"]{} /* moguće je napisati bez navodnika */
<div attr="test">Ja nisam selektovan :(</div>
<div attr="novi">Ja sam selektovan :D</div>
Ok, ovo sve je lepo ali kako će nam ovo pomoći? U ovoj formi ne još, ali uvešćemo džokerske znakove.
Džoker znakovi su specijalni znakovi koji stoje na mestu nepoznatih znakova u tekstualnoj vrednosti i prikladni su za pronalaženje više stavki sa sličnim, ali ne identičnim podacima.
Dodavanjem džoker znakova u selektor atributa možemo dobiti pametan selektor
Dodavanjem *
ispred znaka =
, selektujemo sve elemente sa atributom koji sadrži datu reč.
Ovo poklapa cele reči i parcijalne (u sklopu drugih reči)!
[attr*="beograd"]{}
[attr*="beli grad"]{}
<div attr="beli grad">Ja sam selektovan zato sto imam reč grad :D</div>
<div attr="beograd">Ja sam selektovan zato sto unutar moje reči beo(grad) ima grad :D</div>
Dodavanjem ~
ispred znaka =
, selektujemo sve elemente sa atributom koji u svojoj listi vrednosti sadrži traženi niz karaktera.
[attr~="grad"]{}
<div attr="beli grad">Ja sam selektovan zato sto u mojoj listi atributa imam grad :D</div>
<div attr="lep beograd">Ja nisam selektovan zato što u listi nemam grad kako posebnu reč :(</div>
Dodavanjem $
ispred znaka =
, selektujemo sve elemente sa atributom cija se vrednost završava traženim nizom karaktera.
[attr$="ad"]{}
<div attr="beli">Ja nisam selektovan zato što mi se vrednost atributa ne završava sa AD :(</div>
<div attr="lep beograd">Ja sam selektovan zato što mi se vrednost atribut završava sa AD :D</div>
Dodavanjem ^
ispred znaka jednako, selektujemo sve elemente sa atributom čija vrednost počinje datim nizom karaktera.
Ovo je upravo ono što nam treba za rešavanje problema.
[attr^="lep"]{}
<div attr="beli lepi">Ja nisam selektovan zato što mi se vrednost atributa ne počinje sa LEP :(</div>
<div attr="lep beograd">Ja sam selektovan zato što mi se vrednost atribut počinje sa LEP :D</div>
Sada kad smo naučili džoker znakove unutar selektora atributa, možemo da rešimo prethodni primer.
Moramo da selektujemo sve linkove koji počinju sa https
.
a[href^="https"]{
color: green;
}
<a href="https://google.com">Ja sam zelen :D</a>
<a href="http://google.com">Ja nisam siguran :(</a>
Ovo je jedan od korisnih slučajeva, možemo preko pseudo selektora da dodamo ikonicu katanca na sigurne linkove i mnogo još.
Pokazaću vam još jedan koristan primer, iako ih ima mnogo.
Pre par godina sam hteo da napravim da korisnik po izgledu dugmeta zna koji tip fajla skida. Prvobitna ideja mi je bila
da se to odradi preko php
-a ali sam se setio dobrog starog selektora atributa.
Evo kako je to izgledalo:
a[href$=".zip"] { color: #D766CE; }
a[href$=".pdf"] { color: #E41D24 }
a[href$=".xls"], a[href$=".xlsx"] { color: #207347 }
a[href$=".doc"], a[href$=".docx"] { color: #2A5699 }
<a href="http://primer.com/materijali/kako_biti_pametan.pdf">Crvena slova</a>
<a href="http://primer.com/materijali/lista_zelja.xlsx">Zelena slova</a>
...
Videli smo kako je moguće preko CSS-a rešiti neke dinamične probleme, za koje smo se ranije uvek okretali ka javaskripti ili drugima.
Ja sam ovde samo prošao kroz dva korisna primera, a ima ih mnogo, i sâm sam smišljao dosta korisnih primena.
Probajte sami i igrajte sa selektorima.
]]>