View on GitHub

info

Материјали за курс из УВИТ-а на Математичком факултету Универзитета у Београду

УВИТ - Програмски језик ЈаваСкрипт

Владимир Филиповић

Модули

Модуларни концепт програмирања је тренутно један од најзаступљенијих концепата у модерном ЈаваСкрипт програмирању. Заснива се на разбијању једне велике апликације на мање, енкапсулиране, независне делове кода – модуле. Циљ модуларности јесте да се смањи комплексност према принципу “подели па владај”.

Најзначајније предности модуларног програмирања су:

У зависности од верзије ЈаваСкрипта постоје различити приступи реализацији модуларног програмирања, па самим тим и различите синтаксе које омогућавају модуларно програмирање:

Проблем учитавања модула није тривијалан (модули зависе од других модула, треба се ефикасно реализовати, итд.) па се за решавање овог проблема користе посебни програми тзв. алати за учитавање модула (енгл. module loader) и алати за увезивање модула (енгл. module bundler).

Нативни ES5 модули

У време писања ЈаваСкрипта, модуларно програмирање није било планирано као начин програмирања, тако да ЈаваСкипт све до верзије ES6 (јиш се означава и са ES2015) нема уграђену подршку за модуле. У оквиру нативног ES5 се користе разни обрасци (енгл. pattern) који имају сличности са модуларним програмирањем, али који немају баш све карактеристике “чистокрвног модуларног шаблона”.

Ефекат модула код којих је енкапсулиран њихов код је у овом случају могу постићи на да начина: коришћењем функцијских израза који се одмах изршавају или коришћењем конструктора.

У оба случаја, основна идеја је иста: оно што треба да буде доступно спољашњости се враћа из функције помоћу резервисане речи return. Овај једноставан шаблон се може примењивати било где и не захтева коришћење додатних библиотека. Надаље, у оквиру једне датотке је, ако је то потребно, могуће дефинисати више модула.

Поред наведених предности овај приступ ипак има и својих мана:

Модули преко функцијских израза који се одмах извршавају

Функцијски изрази који се одмах извршавају (енгл. Immediately-Invoked Function Expressions - IIFE (изговара се “ифи”) ), су функцијски изрази који се извршавају одмах након стварања. Користе се функцијски изрази, а не декларације функција, зато што декларисана функција не може бити одмах позвана у истој наредби док функцијски израз може.

Пример. Следећа наредба ( function(){} )(); представља функцијски израз који се извршава одмах по креирању. █

Функцијски изрази који се одмах извршавају се користе за модуларно програмирање, јер сав код унутар овакве функције остаје приватан.

Коришћењем функцијских израза који се одмах извршавају може да се изабере које податке/функције ће да се задрже као приватни, а које податке/функције ће бити објављене. Јавно доступни ће бити они делови кода који се врате са резервисаном речи return. На овај на начин се може у глобалном простору имена проследити све типове података.

Пример. Илуструје модуларно програмирање преко функцијских израза који се одмах извршавају. Наиме, потребно је извршити прорачунавање (у овом случају квадрирање) над датим аргументом. При томе, вредност аргумента је енкапсулирана у модулу, исто као и функција која врши прорачунавање.

У датотеци vrednost.js је у посебан модул издвојено енкапсулирање вредности за podatakKojiSeCuva. Сам модул је, преко функцијских израза који се одмах извршавају, изложио методе за приступ getPodatak() и за промену setPodatak().

const vrednost = function () {
    // ovo je privatan podatak
    let podatakKojiSeCuva = '';

    // funkcija za postavljanje vrednosti
    function setPodatak(noviPodatak) {
        podatakKojiSeCuva = noviPodatak;
    }

    // funkcija za ocitavanje vrednosti
    function getPodatak() {
        return podatakKojiSeCuva;
    }

    // publikovanje "javnih "funkcija
    return {
        setPodatak: setPodatak,
        getPodatak: getPodatak
    };
}();

У модулу који садржи датотека proracun.js је изложена функција izracunajKvadrat().

const proracun = function () {
    function izracunajKvadrat() {
        // pozvan je metod iz vrednost.js
        let x = vrednost.getPodatak();
        // ovde ide deo koda vezan za proracun
        return x * x;
    }

    // publikovanje "javne" funkcije
    return {
        izracunajKvadrat: izracunajKvadrat,
    };
}();

Да би било обезбеђено учитавање модула, користиће се спољашњи АPI веб прегледача. Извршавање почиње од датотеке index.html. Овде се, на почетку извршавања, потребно извршити учитавање модула. Редослед учитавања модула зависи од функционалности апликације, што је најтежи део за програмера наручито код великих и комплексних апликација. У овом примеру се прво учитава vrednost.js јер се његове методе користе и у оквиру proracun.js.

<html>

<head lang="en">
    <meta charset="UTF-8">
    <title>Modularna aplikacija</title>
</head>

<body>
    <!-- ucitavanje modula -->
    <script src="vrednost.js" type="text/javascript"></script>
    <script src="proracun.js" type="text/javascript"></script>
    <!-- skripta koja koristi publikovane elemente modula -->
    <script type="text/javascript">
        let argument = 10;
        vrednost.setPodatak(argument);
        console.log(proracun.izracunajKvadrat());
    </script>
</body>

</html>

Као што видимо, Дакле, учитавање и “регистрација” модула је реализовано извршавањем скрипти које садрже програмски код модула.

Кад се веб страна погледа у веб преглдачу, ако је све како треба, као резултат рада ће у конзоли прегледача бити уписан број 100. █

Модули преко конструктора

Конструктор омогућава прављење више објеката на основу конструкторске функције.

Пример. Илуструје модуларно програмирање преко конструктора. И овде је задатак исти као у претходном примеру: потребно је извршити прорачунавање (квадрирање) над датим аргументом. При томе, вредност аргумента је енкапсулирана у модулу, исто као и функција која врши прорачунавање.

Измене кода у односу на претходни пример су мале - тело функција остаје непромењено а прилагођавају се следећи елементи:

У датотеци vrednost.js је у посебан модул издвојено енкапсулирање вредности за podatakKojiSeCuva.

const Vrednost = function() {
    // ovo je privatan podatak
    let podatakKojiSeCuva = '';

    // funkcija za postavljanje vrednosti
    function setPodatak(noviPodatak) {
        podatakKojiSeCuva = noviPodatak;
    }

    // funkcija za ocitavanje vrednosti
    function getPodatak() {
        return podatakKojiSeCuva;
    }

    // publikovanje "javnih "funkcija
    return {
        setPodatak: setPodatak,
        getPodatak: getPodatak
    };
};

У модулу који садржи датотека proracun.js је преко конструктора изложена функција izracunajKvadrat().

const Proracun = function () {
    function izracunajKvadrat() {
        // pozvan je metod iz vrednost.js
        let x = vrednost.getPodatak();
        // ovde ide deo koda vezan za proracun
        return x * x;
    }

    // publikovanje "javne" funkcije
    return {
        izracunajKvadrat: izracunajKvadrat,
    };
};

Као и у претходном примеру, да би било обезбеђено учитавање модула, користиће се спољашњи АPI веб прегледача. И овдe извршавање почиње од датотеке index.html. И у овом примеру је редослед учитавања модула експлицитно дат редоследом <script> елемената: прво учитава vrednost.js, па потом proracun.js.

<html>

<head lang="en">
    <meta charset="UTF-8">
    <title>Modularna aplikacija</title>
</head>

<body>
    <!-- ucitavanje modula -->
    <script src="vrednost.js" type="text/javascript"></script>
    <script src="proracun.js" type="text/javascript"></script>
    <!-- skripta koja koristi publikovane elemente modula -->
    <script type="text/javascript">
        let argument = 10;
        let  vrednost = new Vrednost();
        vrednost.setPodatak(argument);
        let proracun = new Proracun();
        console.log(proracun.izracunajKvadrat());
    </script>
</body>

</html>

Као резултат рада ЈаваСкрипт модуларног кода, у конзоли прегледача ће бити уписан број 100. Исто као у претходном примеру, евентуална промена редоследа учитавања тј. промена редоследа <script> елемената би довела до грешке у извршавању. █

ES5 модули преко спољних библиотека

Управо због недостатка подршке за модуле у ES5, креиране су спољашње синтаксе које језику дају недостајућу функционалност:

Асинхрона дефиниција модула, као што јој и само име каже, подржава асихроно учитавање модула што је погодно за рад са модулима у прегледачу.

CommonJS учитава модуле синхроно и због тога се најчешће користи за рад са модулима на серверској страни у окружењу node.js. Иако није планиран за рад наклијентској страни, тј. унутар прегледача, уз помоћ алата за увезивање модула је могуће прилагодити CommonJS раду у веб прегледачу.

Универзална дефиниција модула је компатибилна и са AMD и са CommonJS дефиницијом и користи се углавном уколико има потребе да се исти модул учитава на серверу и у веб прегледачу.

Асинхрона дефиниција модула

Под AMD синтаксом се подразумева договорени скуп правила и спецификација које указују како треба да изгледа код за креирање модула.

Основа AMD синтаксе је функција define(), која преко прослеђених аргумената дефинише сам модул и зависности од других модула.

Функција define() има три параметра:

Треба истаћи да је имплементација асинхорне дефиниције модула могућа тек уз помоћ алати за учитавање модула, па је поред коришћења саме синтаксе потребно учитати одговарајући алат за учитавње модула - овде ће се за ту сврху користити require.js.

У случају када се као алат за учитавње модула користи require.js библиотека, програмеру је на располагању функција requirejs(), која омогућава извршавање програмског кода који зависи од модула. Функција requirejs() има два параметра:

Пример. Илуструје модуларно програмирање преко асинхроне дефиниције модула. Опет је задатак исти као у претходним примерима: потребно је извршити прорачунавање (квадрирање) над датим аргументом. При томе, вредност аргумента треба енкапсулирана у модулу, а исто тако и функција која врши израчунавање.

С обзиром да ће полазна тачка извршавања ЈаваСкрипт кода бити веб страна, онда је библиотека require.js довучена и смештена у исти директоријум у ком се налазе све остале датотеке, а алат за учитавање модула бива покренут коришћењем елемента script са атрибутом src који има вредност путање до те библиотеке и са атрибутом data-main који има вредност путање до ЈаваСкрипт датотеке од које почиње учитавање модула. За разлику од претходних случајева, овде процес учитавања реализује библиотека require.js, па нема потребе да програмер ручно подешава редослед учитавања модула, нити може доћи до грешке зато што је тај редослед погрешан.

Mодул vrednost.js не зависи од других модула, тако да аргумент задужен за назив модула (или релативну путању до модула) остаје празан:

define([], function() {
    // ovo je privatan podatak
    let podatakKojiSeCuva = '';

    // funkcija za postavljanje vrednosti
    function setPodatak(noviPodatak) {
        podatakKojiSeCuva = noviPodatak;
    }

    // funkcija za ocitavanje vrednosti
    function getPodatak() {
        return podatakKojiSeCuva;
    }

    // publikovanje "javnih "funkcija
    return {
        setPodatak: setPodatak,
        getPodatak: getPodatak
    };
});

Mодулу proracun.js је потребам модул vrednost,js, па је стога додата релативна путања до тог модула у низ. Поред тога, убачени модул je прослеђен као аргумент функције:

define(['./vrednost'], function (vrednost) {
    function izracunajKvadrat() {
        // pozvan je metod iz vrednost.js
        let x = vrednost.getPodatak();
        // ovde ide deo koda vezan za proracun
        return x * x;
    }

    // publikovanje "javne" funkcije
    return {
        izracunajKvadratAMD: izracunajKvadrat
    };
});

У горњем модулу је одлучено да према спољашњости буде изложена функција izracunajKvadrat(), при чему ће јој се из спошашњости приступати преко имена izracunajKvadratAMD().

Mодул index.js зависи и од модула vrednost,js и од модула proracun.js, па су аргумент који садржи низ зависности додате путање до оба модула и функција повратног позива има два аргумента:

define(['./vrednost', './proracun'], function (vrednost, proracun) {
    function pokreni() {
        let argument = 10;
        vrednost.setPodatak(argument);
        console.log(proracun.izracunajKvadratAMD());
    };
    // publikovanje "javne" funkcije
    return {
        pokreniAMD: pokreni,
    };
});

У претходном скрипту је, дакле, нанправљен модул, у њему направљена функција pokreni() и та функција је изложена за коришћење под именом pokreniAMD().

Извршавање програмског кода почиње од датотеке index.html. Сада, уместо више узастопно учитаних скрипти, имамо само једну која учитава require.js, а он брине о учитавању свих осталих модула. Обезбеђивање да се учитају зависни модули пре извршења дате ЈаваСкрипт функције, постигнуто је помоћу функције requrejs():

<html>

<head lang="en">
    <meta charset="UTF-8">
    <title>Modularna aplikacija</title>
</head>

<body>
    <!-- ucitavanje modula -->
    <script data-main="index.js" src="./require.js"></script>

    <!-- skripta koja koristi publikovane elemente modula -->
    <script type="text/javascript">
        // poziv publikovane funkcije
        requirejs(["index"], function (index) { 
            index.pokreniAMD();
        });

    </script>
</body>

</html>

Ако је све како треба, приликом прегледа ове веб стране у конзоли прегледача ће бити уписан број 100. █

AMD синтакса решава највеће недостатке које у раду са модулима исказује нативни ES5 приступ:

Међутим, и овај приступ има одређене мане:

Сматра се да је уз AMD боље користити алате за учитавање него алате за увезивање, јер тада асинхрони рад долази до изражаја и показује свој пун потенцијал. Наиме, при учитавању се довлаче само потребни модули, док се при увезивању довлачи једна велика датотека који обихвата све модуле.

CommonJS модули

CommonJS синтакса је првенствено планирана за коришћење на серверу, за рад у синхроном моду. За рад на серверу је логичан избор коришћење алата за учитавање модула SystemJS.

Уколико има потребе да се CommonJS синтаксу користи у окружењу веб прегледача, потребно је да се користи алате за увезивање (browserify или webpack) који може да прилагоди ову синтаксу раду у прегледачу. За разлику од AMD где тело модула треба да буде обавијено функцијом, код CommonJS нема никаквог омотача јер се сматра да је свака ЈаваСкрипт датотека један модул.

Извоз метода се може извршити на два начина:

  1. додељивањем сваке методе појединачно објекту module.export:
module.export.imeMetodeZaSpoljasnost1 = imeMetodeIzModula1
module.export.imeMetodeZaSpoljasnost2 = imeMetodeIzModula2
  1. додељивањем објекта са жељеним методаaм објекту module.export:
module.export = {
  imeMetodeZaSpoljasnost1 : imeMetodeIzModula1,
  imeMetodeZaSpoljasnost2 : imeMetodeIzModula2
}

Увоз модула, тј. коришћење метода из других модула се омогућава дефинисањем нове промењиве и доделом њене вредности користећи функцију require(). Функција require() за параметар има ниску - релативну путању до модула који се захтева, а као резулата враће референцу на захтевани модул.

var promenjivaKojaReferiseNaDrugiModul = require('./drugiModul');

Важно је истаћи да је увезени модул само копија извезене вредности, тј. копија модула. Копија враћена са require() је прекинула везу са оригиналом.

Пример. Илуструје модуларно програмирање преко CommonJS. Опет је задатак исти као у претходним примерима: потребно је извршити прорачунавање (квадрирање) над датим аргументом. При томе, вредност аргумента треба енкапсулирана у модулу, а исто тако и функција која врши израчунавање. Међутим, за разлику од ранијих ситуација, у овом примеру се код извршава на серверској страни.

Модул vrednost.js извози функције getPodatak() и setPodatak(), а вредност променљиве podatakKojiSeCuva остаје сакривена:

// ovo je privatan podatak
let podatakKojiSeCuva = '';

// funkcija za postavljanje vrednosti
function setPodatak(noviPodatak) {
    podatakKojiSeCuva = noviPodatak;
}

// funkcija za ocitavanje vrednosti
function getPodatak() {
    return podatakKojiSeCuva;
}

// publikovanje "javnih "funkcija
exports.setPodatak = setPodatak;
exports.getPodatak = getPodatak;

Модул proracun.js захтева функционалност модула vrednost.js на који реферише променљива vrednost, користи функцију getPodatak() из тог модула, те креира и ивози функцију за квадрирање, под именом izracunajKvadratCommonJS():

const vrednost = require("./vrednost");

function izracunajKvadrat() {
    // pozvan je metod iz vrednost.js
    let x = vrednost.getPodatak();
    // ovde ide deo koda vezan za proracun
    return x * x;
}

exports.izracunajKvadratCommonJS = izracunajKvadrat;

Датотека index.js је улазна тачка за извршење програма:

const vrednost = require("./vrednost");
const proracun = require("./proracun");

const argument = 10;
vrednost.setPodatak(argument);
console.log(proracun.izracunajKvadratCommonJS());

Приликом извршавања овог скрипта у окружењу node, на конзоли терминала ће бити прикан број 100. █

CommonJS приступ као и AMD решава проблем “ручног” управљања редоследом учитавања модула и смањује “загађење” глобалног опсега дефинисаности и то са још једноставнијом синтаксом него код AMD.

Међутим, овај приступ има и својих мана:

ES6 модули

Уз ЕS6 стиже подршка за модуларни систем уграђена и у сам језик – ECMAScript 6 модули. Тиме је интегрисана подршка за модуларно програмирање:

ЕS6 модули имају статичку структуру. Пошто је структура модула непромењива, обично је довољно да се прегледа код да би се схватило шта се где увози. Ово није случај код динамичке структуре која карактерише CommonJS, где је често потребно да се програмски код изврши да би се видело шта се увози. Стога, код овог приступа, евентуалне грешке могу да се открију и у време prevo]ewa (са алатом за увезивање модула), јер се све радње које се односе на увоз и извоз модула одређују у тренутку увезивања модула.

Сви увезени елементи су непромењиви из модула који их је увезао. Свака операција доделе вредности увеженом елементу би проузроковала грешку (изузетак типа TypeError).

ЕS6 се не прави копију својства већ дели везу на то својство. Ово важи чак и за дељење примитивних својстава (својстава која су типа број или ниска).

Стандардни извоз се постиже помоћу резервисане речи export - њеним постављањем испред декларације променљиве/функције.

Пример. Илуструје станардни извоз.

export var foo = 1;
export var foo = function () {};
export var bar;
export let foo = 2;
export let bar;
export const foo = 3;
export function foo () {}
export class foo {}

Именовани извоз је врста извоза којом на крају модула дефинишу елементе који се извозе. Назив је потекао из чињенице да се за дефинисање елемената који се извозе користе њихова имена. Неком извезеном елементу се може дати друго име коришћењем кључне речи as.

Пример. Илуструје именовани извоз.

export {};
export {foo};
export {foo, bar};
export {foo as bar};

Препорука је да се при извозу неког модула дефинише део модула који се подразумевано извози. Подразумевани извоз се дефинише са кључном речи default. У једном модулу је дозвољен само један подразумевани извоз. Извезени део се у другом модулу користи под именом default.

Пример. Илуструје подразумевани извоз.

export default 42;
export default {};
export default [];
export default (1 + 2);
export default foo;
export default function () {}
export default class {}
export default function foo () {}
export default class foo {}

Подразумевани извоз може да се комбинује са именованмм извозом уз коришћење кључне речи as.

Пример. Илуструје комбиновање подразумеваног и именованог извоза.

export {foo as default};
export {foo as default, bar};

Уколико модул има приличан број “разбацаних” елемената који се извозе, препорука је да се на врху модула јасно и прегледено дефинише API као што следи:

Пример. Илуструје извоз API-ја.

// calculator.js
const api = { add, subtract, multiply, divide };
  function add(a,b) {...}
  function subtract(a,b) {...}
  function m ultiply(a,b) {...}
  function divide(a,b) {...}
  function somePrivateHelper() {...}
export default api;

Увоз модула се реализује коришћењем резервисане речи import, иза које следи списак функија које се увозе (евентуално и њихових алијса), затим кључна реч from, па ниска која садржи путању до датотеке са модулом.

Пример. Илуструје увоз функција из модула.

import { foo } from "foo";

Пример. Илуструје именовани увоз функција из модула.

import {} from "foo";
import {bar} from "foo";
import {bar, baz} from "foo";
import {bar as baz} from "foo";
import {bar as baz, xyz} from "foo";

Увоз подразумеваних вредности се врши једноставним позивањем имена модула или позивањем преко имена.

Пример. Илуструје подразумевани увоз из модула.

import foo from "foo";
import {default as foo} from "foo";

Увоз целог простора имена модула, још се зове и глобални увоз, се користи уколико треба да се у једној наредби учитају сви извезени елементи једног модула. Овакав глоблани увоз косити синтаксу * as.

Пример. Илуструје подразумевани увоз из модула.

import * as bar from "foo"

Након оваквог глобалног увоза доступни су сви елементи из модула foo у модулу под “новим” именом bar:

bar.x;
bar.baz();

Увоз модула има следеће карактеристике:

Пример. Илуструје ефекат дизања променљиве приликом увоза.

Због дизања увезене функције, позивање те функције неће избацивати грешку:

foo();
import { foo } from "foo";

Нова ES6 синтакса још увек није подржана од стране свих прегледача, па је потребно користи алате (нпр. Babel) да би се транспилирала у ES5. Тај сценарио је прихватљив и за извршавање у оквиру прегледача и на серверској страни, само што захтева инсталацију додатних библиотека.

Поред тога, могуће је извршити ЕS6 модуларни код и на платформи node без инсталације додатних библиотека. За успешно покретање ЕS6 модуларног кода на node платформи и без додатних бибблиотека-транспилатора (у овом тренутку) потребно је да:

Пример. Илуструје модуларно програмирање преко ES6. Потребно је извршити прорачунавање (квадрирање) над датим аргументом. При томе, вредност аргумента треба енкапсулирана у модулу, а исто тако и функција која врши израчунавање. У овом примеру ће се порграмски код извршавати на серверској страни.

Модул vrednost.mjs коришћењем наредбе export извози функције getPodatak() и setPodatak():

// ovo je privatan podatak
let podatakKojiSeCuva = '';

const _setPodatak = function (noviPodatak) {
    podatakKojiSeCuva = noviPodatak;
};
export { _setPodatak as setPodatak };

const _getPodatak = function() {
    return podatakKojiSeCuva;
};
export { _getPodatak as getPodatak };

Модул proracun.mjs увози функцију getPodatak() из модула vrednost.mjs те креира и ивози функцију за квадрирање, под именом izracunajKvadratES6():

import { getPodatak } from "./vrednost";

function izracunajKvadrat() {
    // pozvan je metod iz vrednost.js
    let x = getPodatak();
    // ovde ide deo koda vezan za proracun
    return x * x;
}

export const izracunajKvadratES6 = izracunajKvadrat;

Датотека index.mjs је улазна тачка за извршење програма:

import { setPodatak } from "./vrednost";
import { izracunajKvadratES6 } from "./proracun";
 
let argument = 10;
setPodatak(argument);
console.log(izracunajKvadratES6());

Ту се увози функција setPodatak() из модула vrednost.mjs и функција izracunajKvadratES6() из модула proracun.mjs, а потом се те две увезене функције користе за постављање вредности аргумента и за израчуб+навање.

Покретањем скрипте уз ддодатне параметре у node окружењу, тј. изршењем наредбе:

 node --experimental-modules index.mjs

добија се следећи резултат:

(node:14480) ExperimentalWarning: The ESM module loader is experimental.
100

Дакле, као и у свим претходним ситуацијама, на конзоли се приказује број 100. █

Алати за учитавање и за увезивање модула

Уколико се програмер одлучи за модуларни начин програмирања апликације, он ће се сусрести са проблемом организовања учитавања потребних модула, као и модула од којих су они зависни (енгл. dependinces). Алати за учитавање (енгл. modul loader) и за увезивање модула (енгл. modul bundler) су непоходна подршка програмеру за једноставнији и ефикаснији рад са модулима.

Алати за учитавање модула

Алати за учитавање модула омогућавају динамичко учитавање модула водећи рачуна о распореду учитавања, како би се зависни модули раније учитали. Најпознатији алати за учитавање модула су:

Алати за увезивање модула

Алати за увезивање модула решавају проблем тако што компајлирају све модуле у једну датотеку према одређеном реду, водећи рачуна о томе да неки модул од којег зависи други буде учитан на време. Најпознатији алати за увезивање модула:

Литература

  1. Haverbeke M.: Eloquent JavaScript

  2. JavaScript - Mozzila Developer Network (MDN)

  3. Живановић, Д.: Веб програмирање - ЈаваСкрипт

  4. Copes F.: Complete JavaScript Handbook