webdevlpr

Moduli olaksavaju odrzavanje koda jednostavnijim. Jedan modul je jedna skripta, odnosno fajl. Glavna ideja iza modula je da cuvamo relevantan kod u jednoj skripti, umjesto da sve bude zajedno. Kada je potrebno, te skripte mozemo importovati u druge skripte.

Osnovna primjena

U primjeru ispod cemo eksportovati klasu i dvije funkcije iz macka.js u glavnu skriptu gdje ce se one moci koristiti. Da bi to radilo potrebno je da:

  1. Fajl koji se eksportuje koristi export \ default export
  2. Fajl koji se importuje import (uz from sa apsolutnim ili relativnim putem)
  3. HTML fajlu bude naznaceno da se radi o skripti koja koristi module sa atributom type='module' (ukoliko ne koristite alat poput webpack-a).
// macka.js
// export default ...
export default class Macka {
constructor(ime, vrsta) {
this.ime = ime;
this.vrsta = vrsta;
}
}

// export ...
export function prikaziIme(macka) {
console.log(`Ja se zovem ${macka.ime}.`);
}

export function prikaziVrstu(macka) {
console.log(`${macka.ime} pripada ${macka.vrsta} vrsti.`);
}

// ili
// export default Macka;
// export {prikaziIme as prikaziImeMacke, prikaziVrstu};
// Odvojeni import/export se moze navesti na vrhu ili dnu skripte
// main.js:
import Macka, {prikaziIme, prikaziVrstu as prikaziVrstuMacke} from '/macka.js';

let macka = new Macka('Lana', 'Munchkin');
prikaziVrstuMacke(macka); // Lana pripada Munchkin vrsti.
prikaziIme(macka); // Ja se zovem Lana.
<!-- index.html: -->
<script type="module" src="main.js"></script>
<!-- Da bi pruzili alternativu starijim pretrazivacima: -->
<script nomodule src="fallback.js"></scripts>

type="module" se ponasa kao defer atribut. Skripte sa tim atributom se preuzimaju paralelno sa ostalim izvorima i izvrsavaju nakon sto je HTML dokument rasclanjen. Glavna razlika izmedju defer i type="module" atributa je ta sto drugi atribut oznacava da se skripta tretira kao modul zbog cega su kljucne rijeci poput import i export dostupne. Da bi se ove skripte izvrsile odmah, dovoljno je dodati atribut async. On obicno ne radi inline, osim ako se radi o skripti tipa modul.

Import iz primjera iznad mozemo skratiti kako smo sve sto je naznaceno za eksportovanje importovali u glavni .js fajl. Zato koristimo zvjezdicu * u sledecem primjeru. Ona sve dodaje u jedan objekat. Mana je sto su nazivi zbunjujuci i nepregledni samo da bi linija importovanja bila kraca.

import * as macka from './macka.js';

let macor = new macka.default('Artur', 'Evropska kratkodlaka macka');
macka.prikaziIme(macor); // Ja se zovem Artur.

console.log(macka);

U konzoli cemo dobiti:

× Module
   default: (...)
   prikaziIme: (...)
   prikaziVrstu: (...)
   Symbol(Symbol.toStringTag): 'Module'
   ...

Sa druge strane webpack i slicni alati obicno optimizuju module kako bi ubrzali ucitiavanje tako sto uklone importe koji se ne koriste.

Podrazumijevani eksport

Moguce je eksportovati bilo koji dio koda skripte: varijablu, funkciju, niz, klasu itd. Medjutim u fajlu samo jedan moze biti naznacen sa default. On se importuje bez viticastih zagrada i njen naziv je nezavisan, tj. ne mora biti identican kao u fajlu iz kog se eksportuje.

// macka.js
export default class Macka {}

// main.js
import M from '/macka.js';
let macka = new M('Lana', 'Munchkin');

Kako default export ima nezavisno ime jer mozemo imati samo jedan takav po fajlu — mozemo ga izostaviti i recimo eksportovati kao anonimnu funkciju, niz, vrijednost bez kreiranje varijable itd.

Prema tome mozemo razlikovati dva eksporta:
defaultni eksport: export default {ime: 'Lana'} i
imenovani eksport: export class Macka {}.

Priznajem da se default export ipak moze napisati sa viticastim zagradama, ali postoji mali trik — potrebno je naznaciti as default.

// export.js
class Macka {}
export {Macka as default};

// import.js
import {default as Macka};

Dalje izvozenje

export {} from ... je sintaksa koja vise podsjeca na import kakav smo koristili iznad. Kada zelimo sa jednog fajla imati dostupne sve funkcionalnosti drugih modula za dalji izvoz koristimo zvjezdicu ili viticaste zagrade kao u primjeru ispod.

export {default, prikaziIme, prikaziVrstu} from './macka.js';

Dalje izvozenje sa zvjezdicom * ne ukljucuje default eksport i njega je potrebno navesti u zasebnoj liniji sa export {default} from "./fajl.js"

Ovakvo dalje izvozenje uskracuje trenutni fajl za pristup navedenim funkcijama, ono ih samo usmjerava dalje. Da bi modul koristili u fajlu koji ih dalje izvozi, potrebno je prvo importovati i potom eksportovati.

import Macka, {prikaziIme, prikaziVrstu} from './macka.js';
export {Macka as default, prikaziIme, prikaziVrstu};

Dalje izozenje se obicno koristi kada objavljujemo paket na npm-u (Node Package Manager). U licnom projektu ovo izaziva problem sa funkcijama poput "Go to defenition", "Go to implementation", "Go to reference" u VS Code editoru (bar u mom slucaju).

Dinamicno importovanje

Sa sintaksom o kojoj smo diskutovali do sada nije moguce importovati u nekom bloku koda, samo na najvisem nivou opsega. Da bi stavili import u okviru nekog bloka kao sto je uslovni, pomoci ce nam jos jedna opcija koju nismo pominjali: import('./fajl.js')

let nazivFajla = 'macka.js';

if (nazivFajla) {
// dodjeljivanje vrijednosti destrukturisanjem
let {default: Macka, prikaziIme} = await import(`./${nazivFajla}`);

let macor = new Macka('Artur', 'Evropskoj');
prikaziIme(macor); // Ja se zovem Artur.
}

import('./fajl.js') vraca promise, iz tog razloga ispred stoji kljucna rijec await. Iako je ona uglavnom dopustena unutar async funkcije, u modulima radi i na najvisem nivou opsega izvan nje.

Dinamicno importovanje radi i u obicnim skriptama te nije potrebno dodavati type="module".

Neke karakteristike modula

Moduli su uvijek u 'use strict' rezimu. To znaci da bi akcija poput dodjeljivanja vrijednosti nedeklarisanoj varijabli dala gresku. Takodje, u tom rezimu this na opsegu najviseg nivoa ima vrijednost undefined umjesto window.

<script type="module">
ime = Vanja; // Uncaught ReferenceError: ime is not defined
console.log(this) // undefined
</script>

Opseg na nivou modula

Opseg radi na nivou modula, odnosno svaki modul za sebe ima nezavisne varijable i funkcije najviseg nivoa koje nisu vidljive u opsegu drugog modula.
Recimo da imamo dvije skripte i u drugoj zelimo pozvati funkciju iz prve:

// funkcija.js:
let pozdravi = (ime) => console.log(`Dobrodosao, ${ime}!`);

// pozivanjeFunkcije.js:
pozdravi('Marko');

index.html:

<script src="funkcija.js"></script>
<script src="pozivanjeFunkcije.js"></script>

Rezultat koda je ispis Dobrodosao, Marko! u konzoli, ali sa modulima to ne bi bio slucaj. Ako script tagu gdje se nalazi funkcija dodamo type='module' atribut, rezultat je greska: pozdravi is not defined. Druga skripta ne vidi funkciju jer je "zarobljena" unutar opsega svog modula.

Umjesto toga, module mozemo ukljuciti sa par izmjena.

// funkcija.js:
let pozdravi = (ime) => console.log(`Dobrodosao, ${ime}!`);
export {pozdravi};

// pozivanjeFunkcije.js:
import {pozdravi} from './funkcija.js';
pozdravi('Marko');

I u index.html fajlu ostavimo samo pozivanjeFunkcije.js koja ce samostalno importovati funkciju od koje ovisi:

<script type="module" src="pozivanjeFunkcije.js"></script>