Uvod
Externe sintakse nisu JavaScript biblioteke, već odgovarajuće specifikacije i konvencije za definisanje modula. Eksterna sintaksa može da se koristi jedino uz pomoć modul loader-a koji joj “udahnu život”. Ako modul loader podržava sintaksu to znači da unutar sebe sadrži ugradjene metode koje su prethodno zamišljene da se koriste kroz odredjenu sintaksu.
Prednost korišćenja ovih eksternih sintaksi u odnosu na native ES5 modul patern je ta što ne zagadjuje globalni domen nazivima modula i što je omogućen rad dependencies menadžera.
- Asynchronous Module Definition (AMD) kao što mu i samo ime kaže, podržava asihrono učitavanje modula što je pogodno za rad sa modulima u browser-u.
- CommonJS učitava module sinhrono i zbog toga se najčešće koristi za rad sa modulima na serverskoj strani u okruženju node.js. Iako nije planiran za rad u browser-u, uz pomoć “modul bundler-a” je moguće da prilagoditi CommonJS radu u browser-u.
- Universal module definition (UMD) je kompatibilan i sa AMD i sa CommonJS definicijom i koristi se uglavnom ukoliko ima potrebe da se isti modul učitava na serveru i u browser-u.
OBJAŠNJENJE:
CommonJS, AMD ili UMD nisu JavaScript biblioteke. To su organizacije za standardizaciju, kao što je ECMA (definiše specifikaciju jezika za JavaScript) ili W3C (definiše JavaScript web API, kao što su DOM ili DOM događaji). Cilj CommonJS ili AMD sintaksi je definisanje API-ja za rad sa modulima.
Asynchronous Module Definition – AMD
Pod AMD sintaksom se podrazumeva dogovoreni set pravila i specifikacija koje ukazuju kako treba da izgleda kod za kreiranje modula. Ali njena implementacija je moguća tek uz pomoć modul loader-a, pa je pored korišćenja same sintakse potrebno učitati odgovarajući modul loader/bundler.
AMD sintaksa
Osnova AMD sintakse je funkcija define(), koja preko prosledjenih parametara definiše sam modul i pristup drugim modulima (“dependencies”).
1 |
define(id?, dependencies?, factory); |
Pri pozivanju funkcije “define()” kroz parametre mu se prosledjuju sledeće stvari:
- id – naziv modula (“string”) bez extenzije i nije obavezan.
- dependensies – niz (opcioni parametar) popunjen sa nazivima potrebnih modula ili od relativnih putanja do svih potrebnih modula. Redosled u nizu je bitan jer definiše redosled učitavanja.
- factory – funkcija koja instancira modul ili objekat. Ukoliko modul ima dependencies oni moraju bit prosledjeni kao parametri ove funkcije. Ukoliko funkcija vraća neku vrednost (funkciju, objekat…) onda će ta vrednost biti dodeljena kao vrednost koju modul exportuje.
Primer
U ovom primeru prikazana su dva modula, prvi modul nema dependencies ali je potrebno da se javno objavi u globalni domen (eksportuje) jer je potreban za drugi modul.
calculator.js
1 2 3 4 5 |
define("calculator", function() { return { sum: function(a, b) { return a + b; } }; }); |
app.js
1 2 3 4 5 6 |
define("app", ["calculator"], function(calculator) { console.log(calculator.sum(1, 2)); // => 3 } ); |
Praktična primena u aplikaciji
Ovo je isti primer koji se koristi kroz sve članke vezane za modularno progrmiranje ali sada prilagodjen novoj sintaksi. AMD sintaksu za rad sa modulima ćemo implementirati sa require.js modul loader-om, koji smo prethodno instalirali uz pomoć “npm” package manager-a.
unos.js
Ovaj modul nema potrebu za drugim modulima tako da argument zadužen za naziv modula (ili relativnu putanju do modula) ostaje prazan:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
define([], function() { // PRIVATNA PROMENJIVA var unesenPodatak = ''; // FUNKCIJA KOJA PRIHVATA UNOS function setPodatak(noviPodatak) { unesenPodatak = noviPodatak; } // FUNKCIJA KOJA PROSEDJUJE UNOS function getpodatak() { return unesenPodatak; } // JAVNO OTKRIVANJE METODA return { setPodatak: setPodatak, getpodatak: getpodatak }; }); |
proracun.js
Ovom modulu je potrebam modul ‘unos.js’, pa je stoga dodata relativna putanja do tog modula u niz. Pored toga smo ubačeni modul prosledili kao argument funkcije.
1 2 3 4 5 6 7 8 9 10 11 |
define (['./unos'], function(unos){ function calculateScore() { // POZIV METODE IZ unos.js unos.getPodatak(); // ovde ide deo koda vezan za PRORACUN } // JAVNO OTKRIVANJE METODE return { calculateScore: calculateScore, }; }); |
app.js
Ovom modulu su potrebni moduli: ‘unos.js’ i ‘proracun.js’, pa dodajemo njihove relativne putanje u niz i prosledjujemo ih u funkciju.
1 2 3 4 5 6 7 8 9 10 11 |
define (['./unos', './proracun'], function(unos, proracun){ // "click handler" ZA UNOS PODATAKA (poziva funkciju iz modula unos.js) document.getElementById('entry').addEventListener('change', function() { unos.setPodatak(document.getElementById('entry').value); }); // "click handler" ZA STARTOVANJE PRORAČUNA (poziva funkciju iz proracun.js) document.getElementById('calculate').addEventListener('click', function() { proracun.calculateScore(); }); }); |
index.html
Fajl se koristi za prikupljanje podataka od korisnika i vraćanje rezultata ostaje skoro isti kao u predhodnim primerima osim dela vezanog za učitavanje modula.
1 |
<script data-main="js/app" src="node_modules/requirejs/require.js"></script> |
Sada umesto više uzastopno učitanih skripti imamo samo jednu koja učitava modul loader require.js, a on brine o učitavanju svih ostalih. Kroz atribut “data-main” se definiše početna skripta aplikacije, a kroz “src” atribut putanju do mesta gde je instaliran modul loader.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
<html> <head lang="en"> <meta charset="UTF-8"> <title>Modularna aplikacija</title> </head> <body> <nav class="navbar navbar-default"> <div class="container-fluid"> <div class="navbar-header"> <span class="navbar-brand">MODULARNA APLIKACIJA</span> </div> </div> </nav> <div class="form-horizontal" id="nameform"> <!-- UNOS PODATAKA: --> <div class="form-group"> <label for="entry" class="col-sm-2 control-label">Ulazni podaci</label> <div class="col-sm-2"> <input type="text" class="form-control" id="entry" size="20" placeholder="Unesite podatke" /> </div> </div> <!-- DUGME ZA STARTOVANJE APLIKACIJE (PRORAČUN): --> <div class="form-group"> <div class="col-sm-offset-2 col-sm-10"> <button class="btn btn-success" id="calculate">Proračun</button> </div> </div> </div> <!-- SEKCIJA ZA VRAĆANJE REZULTATA: --> <div class="col-sm-10" id="scores"> <h3>REZULTAT:</h3> <div id="rezultat"> <p>Proačunati iznos je: <span></span></p> </div> </div> <!-- SEKCIJA ZA UCITAVANJE MODULA --> <script data-main="js/app" src="node_modules/requirejs/require.js"></script> </body> </html> |
Prednosti i mane
AMD sintaksa rešava najveće nedostatke koje ima ES5 u radu sa modulima:
- ugradjen menadžment modula u sklopu “modul loader-a” koji zamenjuje “ručno” odredjivanje redosleda učitavanja
- globalni nampespace je “zagadjen” samo sa jednim imenom (require) umesto sa svim nazivima modula.
Za mane ovog pristupa se smatraju:
- lista zavisnosti (dependencies) u nizu mora da se slaže sa listom argumenata prosledjenih funkciji, što može da bude teško kada ima puno zavisnosti.
- sintaksa zahteva da je sve obavijeno sa define() funkcijom, što iznudjuje ekstra uvlačenje koda.
- U slučaju korišćenja modul lodera i HTTP 1.1 protokola, učitavanjem puno malih JS fajlova može da se javi problem sa perfomansama. Ovaj problem može da se prevazidje korišćenjem HTTP/2 protokola ili korišćenjem modul boundler-a (npr. browserify ili webpack) koji će da sve js fajlove skupe u jedan veliki fajl..
Smatra se da je uz AMD bolje koristiti “modul loader” nego “modul builder”, jer tada asinhroni rad dolazi do izražaja i pokazuje svoj pun potencijal. Download-uju se samo potrebni moduli umesto da se downloaduje jedan u kome su svi, što je slučaj kada se koristi modul bundler.
CommonJS
CommonJS sintaksa je prvenstveno planirana za korišćenje na serveru, za rad u sinhronom modu. Za rad na serveru je logičan izbor korišćenje SystemJS modulu loader-a, ali ukoliko želimo da ovu sintaksu koristimo u okruženju browser-a potrebno je da koristimo modul bundler (browserify ili webpack) koji može da prilagodi ovu sintaksu radu u browser-u. Za razliku od require.js gde telo modula treba da bude obavijeno sa funkcijom, ovde nema nikakvog omotača jer se smatra da je svaki .js fajl jedan modul.
Izvoz modula
Metode možemo da eksportujemo na dva načina:
- dodeljivanjem svake metode pojedinačno objektu “module.export“
12module.export.nazivMetodePodKojimSeEksportuje1 = nazivMetodeIzModula1module.export.nazivMetodePodKojimSeEksportuje2 = nazivMetodeIzModula2 - dodeljivanjem literaranog objekta sa željenim metodam objektu “module.export”
1234module.export = {nazivMetodePodKojimSeEksportuje1 : nazivMetodeIzModula1,nazivMetodePodKojimSeEksportuje2 : nazivMetodeIzModula2}
Uvoz modula
Korišćenje metoda iz drugih modula se omogućava definisanjem nove promenjive koristeći funkciju require() koja daje referencu na dati modul. U okviru funkcije require() se kroz parametar prosledjuje relativna putanja do modula koji zahtevamo.
1 |
var promenjivaKojaImaReferencuNaDrugiModul = require('./drugiModul'); |
Uveženi modul je samo “KOPIJA” eksportovane vrednosti. Kopija “proracun.js” modula unutar “main.js” je prekinula vezu sa originalom. Zbog toga čak i kad povećamo promenjivu “counter” sa metodom “increment()”, promenjiva “counter” neće se promeniti, jer promenjiva koju smo mi importovali je “disconnected copy” promenjive “counter”. Tj. sa metodom “increment()” će se povećati promenjiva “counter” u originalnom modulu, ali ne i u našoj kopiranoj verziji.
lib/proracun.js
1 2 3 4 5 6 7 8 |
var counter = 1; function increment() { counter++; } module.exports = { counter: counter, increment: increment, }; |
src/main.js
1 2 3 4 |
var proracun = require('../../lib/proracun'); console.log(proracun.counter); // 1 proracun.increment(); console.log(proracun.counter); // 1 |
Praktična primena u aplikaciji
Ovo je isti primer koji se koristi kroz sve članke vezane za modularno progrmiranje ali sada prilagodjen novoj sintaksi.
unos.js
Ovaj modul nema potrebu za drugim modulima, ali je zato potrebno eksportovati dve metode da bi bile globalno dodtupne:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
// PRIVATNA PROMENJIVA var unesenPodatak = ''; // FUNKCIJA KOJA PRIHVATA UNOS function setPodatak(noviPodatak) { unesenPodatak = noviPodatak; } // FUNKCIJA KOJA PROSEDJUJE UNOS function getpodatak() { return unesenPodatak; } module.export = { setPodatak : setPodatak, getPodatak : getPodatak } |
proracun.js
Ovom modulu je potrebam modul ‘unos.js’, pa je stoga importovan, a exportovali smo metodu koja treba da bude javno dostupna.
1 2 3 4 5 6 7 8 9 |
var unos = require(./unos); function calculateScore() { // POZIV METODE IZ unos.js unos.getPodatak(); // ovde ide deo koda vezan za PRORACUN } module.export.calculateScore = calculateScore; |
app.js
Ovom modulu su potrebni moduli: ‘unos.js’ i ‘proracun.js’.
1 2 3 4 5 6 7 8 9 10 11 12 |
var unos = require(./unos); var proracun = require(./proracun) // "click handler" ZA UNOS PODATAKA (poziva funkciju iz modula unos.js) document.getElementById('entry').addEventListener('change', function() { unos.setPodatak(document.getElementById('entry').value); }); // "click handler" ZA STARTOVANJE PRORAČUNA (poziva funkciju iz proracun.js) document.getElementById('calculate').addEventListener('click', function() { proracun.calculateScore(); }); |
index.js
U zavisnosti da li je aplikacija planirana za rad na serveru ili u browseru potrebno je da izaberemo odgovarajući alat za rad sa modulima. Od izabranog alata zavisi i učitavanje modula. Osnovna razlika izmedju index.html fajla iz AMD primera i ovog je u “SEKCIJI ZA UČITAVANJE MODULA”, stoga ću da bi izbegao ponavljanje prikazati samo taj deo:
a)Slučaj da koristimo Modul bundler (Browserify ili Webpack)
Posao modul bundler-a je da spakuje sve module u jedan fajl, stoga je dovoljno samo učitati taj fajl:
1 2 |
<!-- SEKCIJA ZA UCITAVANJE MODULA --> <script src="bundle.js"></script> |
b)Slučaj da koristimo Modul loader (SystemJS)
Ako se koristi SystemJS potrebno ga je učitati sa lokacije gde je instaliran preko npm package managera:
1 2 |
<!-- SEKCIJA ZA UCITAVANJE MODULA --> <script src="node_modules/systemjs/dist/system.js"></script> |
Pored ovoga je potrebno da konfigurišemo par stvari kroz novu inline skriptu:
1 2 3 4 5 6 7 8 |
<script> System.config({ meta: { format: 'cjs' } }) System.import('js/app.js') </script> |
U prvom delu prethodne skripte pozivamo config() metodu System objekta, i kroz meta objekat definišemo uz koji format (sintaksu) se koristi. U našem slučaju je CommonJS, pa se piše skraćenica cjs. Zatim kroz import metodu definišemo gde se prema relativnoj putanji nalazi početni modul. Ovde je prikazana samo najosnovnija konfiguracija, više o ovome pogledajte na oficijelnoj github stranici.
Prednosti i mane
Ovaj pristup kao i AMD-ov rešava problem “ručnog” menadžerisanja modula i smanjuje “zagadjenje” globalnog domena ali sa još jednostavnijom sintaksom nego AMD.
Ali nije sve idealno, pa tako i ovaj pristup ima svojih mana:
- sinhron rad nije baš najbolji za browser i obavezno je korišćenje modul bundler-a
- svaki modul je potrebno staviti u jedan fajl
- za razliku od AMD, ovde konstruktorska funkcija nije podržana
- ne podržava “cyclic dependencies”
- CommonJS ima dinamičku strukturu modula, koja se definiše tek u vreme izvršavanja koda (runtime). Tako da u nekim slučajevima nije lako videti šta se ustvari eksportuje sve dok se ne izvrši kod.