Ostatnio w pracy przygotowałem i prowadziłem małą prezentację dotyczącą frameworka RequireJS upraszczającego ładowanie plików i modułów. Postanowiłem więc podzielić się tym materiałem również tutaj na blogu. Uprzedzam, że nie będzie to post dla zaawansowanych a jedynie absolutne podstawy RequireJS - coś w rodzaju tutoriala “jak zacząć”.

Przejdźmy zatem do rzeczy.

Podstawy RequireJS - wprowadzenie

RequireJS jest frameworkiem JavaScript stworzonym do efektywnego zarządzania zależnościami pomiędzy modułami. Pozwala tworzyć kod szybszy i lepszy jakościowo.

Wszystko czego potrzeba aby zacząć pracę z RequireJS znaleźć można na stronie requirejs.org, w dziale download (tutaj link) lub pobrać bezpośrednio do projektu w Visual Studio jako paczkę NuGet (polecenie “Install-Package RequireJS”). Tak na prawdę cały framework mieści się w jednym pliku “require.js”; jeśli pobraliśmy go za pomocą NuGet’a, dodatkowo do solucji dodawany jest plik “r.js”, który pozwala na uruchomienie optymalizatora.

Aby rozpocząć pracę z RequireJS, do kodu strony HTML należy dodać jedynie deklarację skryptu “require.js” tak jak to jest pokazane poniżej:

<script data-main="scripts/main" src="/scripts/require.js"></script>

Jak widać, deklaracja posiada dodatkowy atrybut “data-main”, którym wskazuje się skrypt będący punktem startowym dla danego widoku. O tym jak taki plik powinien wyglądać napiszę w dalszej części posta, zanim jednak przejdę do deklarowania modułów za pomocą RequireJS, przybliżę/przypomnę czym jest moduł w JavaScript.

Wzorzec modułu w JavaScript

Jako, że nie ma w JavaScript żadnej skladni pozwalającej na tworzenie pakietów, stworzony został wzorzec modułu, który pozwala na odpowiednie ustrukturyzowanie i odseparowanie dużej ilości kodu.

Pierwszym krokiem przy tworzeniu modułu jest zdefiniowanie “namespace’u” czyli zasymulowanie przestrzeni nazw znanych z innych języków programowania (w JavaScript nie ma odpowiedniej składni). Robi się to, deklarując pojedynczą zmienną globalną będącą “korzeniem” wszystkich przestrzeni nazw, a następnie tworząc odpowiednie kontenery na obiekty (konkretne “namespace’y”). Spójrzmy na przykład (plik “init.js”):

var APP = window.APP || {}; // create namespace for our app
APP.modules = {}; // namespace for modules

W pierwszej linii definiowana jest wspomniana zmienna globalna, a następnie dodaje się do niej kontener na moduły - w ten sposób powstaje przestrzeń nazw “APP.modules”.

Mając przestrzeń nazw możemy utworzyć kolejne moduły. Na początek moduł “mod” (w osobnym pliku “mod.js”):

// create module in 'APP.modules' namespace
APP.modules.mod = (function () {

    function showModule() {
        alert('hello from module');
    }

    return {
        showModule: showModule
    };
} ());

Jak widać, do utworzenia modułu wykorzystuje się funkcje natychmiastowe (cała funkcja modułu owinięta jest w nawiasy, a zaraz po klamrze zamykającej mamy nawiasy okrągłe). Dzięki temu funkcja modułu wykonywana jest natychmiast po zdefiniowaniu i wszystkie użyte w niej zmienne pozostają w zakresie lokalnym, a do właściwości “APP.modules.mod” przypisywany jest tylko zwracany przez nią obiekt a nie funkcja (wyrażenie “APP.modules.mod()” nie będzie prawidłowe).

Użycie modułu w innym module

Poniżej spójrzmy na drugi moduł (plik “show.js”), wykorzystujący ten pokazany powyżej:

// another module which uses "mod"
APP.modules.show = (function () {
    // declaration of dependecies
    var mod = APP.modules.mod;

    function useModule() {
        mod.showModule(); // use module
    }

    return {
        useModule: useModule
    };
} ());

W linii czwartej widzimy przypisanie właściwości “APP.modules.mod” do zmiennej lokalnej “mod”. W ten sposób powinno się zawsze definiować wszystkie zależności do innych modułów i w dalszej części modułu używać już zmiennych lokalnych. Dzięki temu informujemy użytkowników jakie funkcjonalności są wymagane do poprawnego działania modułu. Ponadto wykorzystywanie zmiennych lokalnych jest zawsze szybsze niż wykorzystanie zmiennych globalnych a szczególnie właściwości zmiennych globalnych (jak w naszym przypadku).

Uruchomienie aplikacji

Mamy już wszystkie potrzebne moduły naszej aplikacji, wykorzystajmy je więc w aplikacji - najpierw deklaracja skryptów:

<script src="/Scripts/init.js"></script>
<script src="/Scripts/mod.js"></script>
<script src="/Scripts/show.js"></script>

Należy tutaj zwrócić uwagę, że kolejność ładowania plików jest istotna ponieważ poszczególne moduły od siebie zależą.

Na koniec użycie modułu “APP.modules.show”:

<input type="button" onclick="APP.modules.show.useModule()" value="Click!"/>

Oczywiście “w prawdziwym życiu” nie robimy tego tak brzydko ;)

RequireJS - zróbmy to lepiej

W poprzednim paragrafie utworzyliśmy dwa moduły, przy czym jeden zależał od drugiego - to spowodowało, że musieliśmy pamiętać o odpowiedniej kolejności ładowania skryptów zawierających poszczególne moduły. Ponadto zastosowaliśmy wzorzec “przestrzeni nazw” co mimo wszystko zaśmieca globalną przestrzeń zmiennych.

Instalacja i punkt początkowy

Rozwiązaniem powyższych problemów może być użycie RequireJS. Aby zobaczyć jak tego dokonać, wróćmy do początku tego posta i zastąpmy deklarację trzech powyższych plików JavaScript jedną deklaracją wskazującą na plik “require.js” (tak jak już pokazywałem):

<script data-main="scripts/main" src="/scripts/require.js"></script>

Jak już wspominałem, atrybut “data-main” wskazuje na punkt początkowy aplikacji - w powyższym przypadku jest to plik “main.js” w folderze “scripts” (rozszerzenie *.js jest jak widać pominięte - RequireJS sam będzie wiedzieć o co chodzi). Zajrzyjmy zatem do implementacji w “main.js”:

require(['show'], function (show) {
    var input = document.getElementsByTagName('input');
    input[0].onclick = show.useModule;
});

Już pierwsza linia jest bardzo ciekawa - mamy tutaj po prostu wywołanie funkcji “require()” należącej do frameworka RequireJS. Użycie jej powoduje załadowanie zależności (modułów) podanych jako pierwszy parametr (przekazujemy tablicę z nazwami modułów) i wstrzyknięcie ich do parametrów funkcji “callback” będącej drugim parametrem tej funkcji. Tak więc w powyższym przykładzie mówimy: kod funkcji “callback” wymaga modułu “show” więc wstrzyknij go do parametru wywołania - parametr ten może być później użyty w funkcji.

Definicja modułu

Jednak aby to wszystko fajnie działało, moduł “show” musi być również zdefiniowany za pomocą RequireJS. Zajrzyjmy zatem do jego implementacji w pliku “show.js”:

define('show', ['mod'], function(mod) {
    function useModule() {
        mod.showModule(); // use dependent module
    }

    return {
        useModule: useModule
    };
});

Tym razem użyta została funkcja “define()” frameworka RequireJS - służy ona do definiowania modułów, kod zdefiniowany za jej pomocą jest ładowany na żądanie, kiedy zostanie wskazany jako zależność.

Pierwszy parametr wywołania tej funkcji to nazwa modułu - w tym przypadku “show” (jest on opcjonalny, jeśli go nie podamy moduł będzie rozpoznawany na podstawie nazwy pliku w jakim się znajduje). Kolejny parametr to, analogicznie do poprzedniego przykładu, lista zależności danego modułu. Ostatni parametr to funkcja “callback” wywoływana gdy wszystkie podane zależności zostaną załadowane (są one wstrzykiwane do poszczególnych parametrów wywołania funkcji). Jak widać, ciało tej funkcji jest analogiczne do funkcji natychmiastowej ze standardowego modułu. Natomiast jeśli użyjemy tego modułu jako zależności, do kodu zależnego przekazany zostanie obiekt zwracany przez tę funkcję “callback”.

Brakująca zależność

Pora na ostatni moduł - “mod”:

define('mod', function() {
    function showModule() {
        alert('hello from module!');
    }

    return {
        showModule: showModule
    };
});

Jak widać, tutaj również zdefiniowaliśmy konkretną nazwę modułu, jednak nie zależy on już od żadnego innego modułu, a więc lista zależności jest pominięta a funkcja “callback” wywołana jest bez parametrów.

Tak zdefiniowane moduły tworzą łańcuch zależności - na starcie ładowana jest funkcja “main”, która z kolei wymaga modułu “show”, jest on więc również ładowany. Jednak, jako że “show” wymaga “mod”, on również jest ładowny na stronie. Plusem w tym przypadku jest to, że nie musimy przejmować się kolejnością ładowania plików - wystarczy tylko, że zadbamy o wskazanie odpowiednich zależności w poszczególnych modułach. Oprócz tego w naszym kodzie nie zdefiniowaliśmy ani jednej zmiennej globalnej, nie zaśmiecamy więc tej przestrzeni.

Dodatkowym benefitem z korzystania z RequireJS jest to, że w przypadku braku jakiejkolwiek zależności całość kodu się nie wykona i zwrócony zostanie odpowiedni błąd mówiący dokładnie o tym czego brakuje - na pewno znacznie ułatwia to debugowanie.

Zewnętrzne biblioteki

Tworząc aplikację w JavaScript, często zachodzi potrzeba użycia zewnętrznych bibliotek, które przecież wcale nie są zdefiniowane za pomocą RequireJS. Na szczęście jest możliwe i to zachowując konwencję zależności definiowanych podczas ładowania kodu.

Załóżmy, że wcześniej napisaliśmy super przydatną funkcję, która kosztowała nas masę pracy i która jest współdzielona z inną aplikacją więc nie możemy jej za bardzo zmienić pod RequireJS. Jej kod wygląda następująco:

showTest = function () {
    alert('test!!');
}

Niezbędna dodatkowa konfiguracja

RequireJS pozwala na wstrzyknięcie takiej funkcji jako jakiejkolwiek innej zależności zdefiniowanej za pomocą “require()” czy “define()”. Wymaga to tylko odrobiny konfiguracji - spójrzmy jak można to zrobić w na przykład w “main.js” (przy okazji pokazuję jak wstrzykiwać bibliotekę jQuery):

require.config({
    paths: {
        'jQuery': 'jquery-1.7.1' // assign proper jquery version for 'jQuery' name
    },
    shim: {
        'jQuery': {
            exports: '$' // if someone use 'jQuery' name, use global '$' variable as module value
        },
        'test': {
            exports: 'showTest' // if someone use 'test' name, use 'showTest' as module value
        }
    }
});

require(['jQuery', 'test'], function ($, showTest) {
    // jquery is available
    var h2 = $('h2').text();
    alert(h2);

    // our function is also available
    showTest();
});

W pierwszej linii widzimy jak można zmieniać konfigurację RequireJS - wystarczy do funkcji “require.config()” przekazać obiekt zawierający odpowiednie informacje. W naszym przypadku obiekt zawiera dwie właściwości: “paths”, “shim”. Pierwsza z nich pozwala na przypisanie skróconej nazwy do rzeczywistej ścieżki do danego pliku - w przykładzie do nazwy “jQuery” przypisujemy konkretny plik “jquery-1.7.1” (rozszerzenie jak widać pomijamy). Druga z nich pozwala zdefiniować, jaka zmienna danego pakietu ma zostać przekazana do modułu - na przykład dla jquery, jeśli użytkownik na liście zależności poda nazwę “jQuery”, wówczas do zmiennej funkcji “callback” przekazana zostanie wartość zmiennej “$”; tak samo w przypadku naszej super funkcji, jeśli ktoś poda jako zależność nazwę “test”, wówczas do zmiennej przekazana zostanie wartość zmiennej “showTest”.

Od linii piętnastej widzimy już właściwą definicję modułu “main” z zadeklarowanymi zależnościami - kod funkcji “callback” pokazuje, że rzeczywiście do zmiennych funkcji wstrzyknięte są odpowiednie wartości wynikające z konfiguracji.

Jako, że tematem dzisiejszego wpisu są jedynie podstawy RequireJS, nie będe pisał więcej o opcjach jego konfiguracji. Więcej na ten temat można znaleźć tutaj.

Podsumowanie

Mam nadzieję, że ten “tutorial” będzie pomocny i podstawy RequireJS są już dla Was jasne - pokazałem tutaj tylko “podstawowe podstawy” a sam framework ma bardzo duże możliwości jeśli chodzi o zarządzanie zależnościami w JavaScript. Zachęcam jednocześnie do zapoznania się z nim bliżej bo może okazać się bardzo przydatny.