Ponad dwa lata temu opisywałem sposoby wczytywania plików JavaScript, gdzie wymieniłem kilka fajnych bibliotek do asynchronicznego doczytywania kodu.
Jakiś czas później trafiłem także na RequireJS, który ma podobną funkcjonalność i całkowicie zmienił moje podejście do pisanego kodu JavaScript…
Modularne tworzenie kodu
Wspomniany RequireJS to biblioteka, która implementuje interfejs AMD (Asynchronous module definition). Podejście to propaguje tworzenie re-używalnych (zna ktoś polski odpowiednik tego słówka? :)) kawałków kodu, które odpowiadają tylko za jedną funkcjonalność (np. slider, fader, datapicker) i łatwo je przenosić do innych projektów.
To z kolei przynosi kolejne korzyści, jak choćby:
- Uproszczenie kodu. Tworząc nawet najbardziej skomplikowany system, możesz rozbić jego poszczególne części na małe kawałki. Traktując każdą funkcjonalność jako osobny byt, być może uda Ci się nie spieszyć całego projektu :-)
- Ograniczenie zmiennych globalnych. Zmienne globalne to zło – powodują trudne do wykrycia błędy, jak choćby przypadkowe przeciążanie funkcji czy własności. Wykorzystując w projekcie AMD, używasz tylko dwóch zmiennych globalnych – require i define (o tym dalej).
- Możliwość wielokrotnego użycia kodu. Szalenie ważna cecha, jeśli cenisz swój czas i pieniądz. Tworząc konkretny moduł (np. kalendarz, zegar, kalkulator, slider) możesz go wykorzystać w dowolnie innym projekcie – wystarczy dostosować CSS i odpowiednio skonfigurować/dopisać brakujące funkcjonalności.
- Wczytujesz tylko to co potrzebujesz. Z różnych statystyk można wyczytać, że strony ważą coraz więcej. Wiadomo, Internet jest coraz szybszy, więc niektórzy zaczynają zapominać o optymalizacji. To ciency gracze, bowiem zapominają przy tym o mobilnym Internecie (który jest przyszłością) i limitach panujących w tym świecie.
AMD pozwala na wczytywanie tylko tych modułów, które są aktualnie używane. W dodatku, jak nazwa wskazuje – doczytywane są one asynchronicznie. To nowe podejście, bowiem wcześniej najczęściej łączyło się wszystkie pliki w jeden wielki i każdorazowo go wczytywało (co też ma swoje zalety i czasem się przydaje, zwłaszcza przy mniejszych serwisach).
- Wczytujesz tylko kiedy potrzebujesz. Cecha podobna do powyższej, lecz tym razem w kontekście lazy loadingu. AMD umożliwia wczytanie odpowiednich modułów tylko w razie potrzeby, np. dopiero po otworzeniu okienka modalnego możesz wczytać moduł do kalendarza, które będzie można wywołać z poziomu tego okna.
AMD/RequireJS zrewolucjonizowało podejście do pisanego kodu JavaScript. I choć moduły w JavaScript były znane i używane wiele lat wcześniej to AMD narzucił pewien standard, do którego dostosowuje się coraz więcej web-developerów i bibliotek przez nich pisanych (m. in. jQuery, Dojo, MooTools, Underscore.js, Backbone, etc).
To z kolei rokuje pisaniem lepszego kodu.
Tworzymy pierwszy moduł w JavaScript
Tak jak wcześniej wspomniałem, będziemy mieli do czynienia tylko z dwoma globalnymi funkcjami:
- require – asynchronicznie wczytuje wskazany moduł, a po tego dokonaniu wykonuje kod zawarty w funkcji zwrotnej, np.
1
2
3require(['jquery'], function(jQuery) {
jQuery(document.body).html('It\'s working!');
});Co się dzieje? Wczytujemy moduł jQuery, a następnie wypluwamy do body jakiś napis.
- define – definiuje nowy moduł, jak choćby powyższe jQuery. Nazwa pliku będzie stanowić przy okazji nazwę modułu – plusem jest lepsza organizacja kodu i tworzenie modułów o konkretnych funkcjonalnościach nadających się do wielokrotnego użytku.
1
2
3
4
5define(function() {
return {
'doSomething': ...
};
});Co się dzieje? Definiujemy nowy moduł i zwracamy obiekt z własnością doSomething.
- w przestrzeni globalnej jest jeszcze obiekt requirejs umożliwiający konfigurację biblioteki.
Mając już podstawową wiedzę o działaniu RequireJS, możesz napisać pierwszy przykład. Do tego celu musisz stworzyć szablonowy plik HTML (razem z elementami: html, head, body), folder scripts/ z pobranym require.js oraz w dokumencie załączyć bibliotekę RequireJS:
1 | <script data-main="scripts/app" src="scripts/require.js"></script> |
Tak będzie wyglądał natomiast nasz plik scripts/app.js:
1 | document.body.innerHTML = 'It\'s working!'; |
Ten brzydki przykład pokazuje pierwszą funkcjonalność RequireJS, jaką jest możliwość wczytywania pliku głównego aplikacji zaraz po wczytaniu require.js. Wystarczy dodać niestandardowy atrybut data-* wskazujący na plik rozruchowy (data-main), który w zależności od potrzeb będzie wczytywał kolejne moduły. Nie ma potrzeby dodawać rozszerzenia plików, jeśli jest nim standardowe rozszerzenie dla JavaScriptu (*.js).
RequireJS i jQuery
Kiedy już znasz działanie RequireJS, pewnie chciałbyś zacząć go używać z jakąś biblioteką, jak choćby jQuery. Aby jednak móc to zrobić, biblioteka powinna być zdefiniowana jako moduł. jQuery (i wiele innych bibliotek) już to robi – sprawdza, czy w przestrzeni globalnej widnieje funkcja define i jeśli tak jest, nie zanieczyszcza przestrzeni globalnej :-)
Zatem ponownie wczytajmy require.js w dokumencie:
1 | <script data-main="scripts/app" src="scripts/require.js"></script> |
A następnie wczytajmy jQuery i odpalmy nasz wypasiony kod:
1 2 3 4 5 6 7 8 9 10 11 12 | requirejs.config({ 'paths': { 'jquery': [ '//ajax.googleapis.com/ajax/libs/jquery/2.0.0/jquery.min', 'jquery' ] } }); require(['jquery'], function(jQuery) { jQuery(document.body).text('It\'s working!'); }); |
Dzieje się tutaj kilka rzeczy:
- Najpierw odwołujemy się do obiektu requirejs, konfigurując jego ustawienia. Ustawiamy ścieżkę, skąd ma zostać pobrane jQuery.
- Ścieżka do jQuery została przekazana dwukrotnie w tablicy. Taki zapis mówi: pobierz moduł z pierwszego adresu, a jeśli nie zadziała, pobierz z kolejnego adresu. Jeśli więc tworząc stronę zabraknie dostępu do Sieci, jQuery pobierze się z lokalnej lokalizacji.
- Na koniec wywołujemy globalną funkcję require, która powoduje wczytanie modułu (jeśli jeszcze nie jest wczytany) i wywołanie kodu z funkcji zwrotnej (który wymaga do działania wczytanego modułu).
RequireJS i zależności
Żaden z tworzonych modułów nie zaśmieca już przestrzeni globalnej. Jak zatem odwołać się z modułu A do modułu B? W tym celu wymyślono zależności pomiędzy modułami.
W naszym przykładzie pomińmy tworzenie kolejnego, takiego samego pliku HTML :-) Zabierzmy się za napisanie prostego modułu, którego celem będzie odwrócenie tekstu. Oczywiście wykorzystajmy do tego niezawodne jQuery! :D (dla lepszego pokazania współpracy kilku modułów)
scripts/jquery.object-size.js:
1 2 3 4 5 | define(['jquery'], function(jQuery) { return function(object) { return jQuery.map(object, function(n, i) { return i}).length; } }); |
Nowością w powyższym kodzie jest tablica przed definicją modułu. To właśnie w tej tablicy wskazujemy nazwy modułów, które należy najpierw wczytać, nim zostanie wykonany kod tworzonego modułu. Zależne moduły zostaną w tej samej kolejności przekazane jako argumenty funkcji definiującej nowy moduł.
Do modułów możemy się także odwoływać przez użycie funkcji require:
1 2 3 4 5 6 7 | define(['require'], function(require) { var jQuery = require('jquery'); return function(object) { return jQuery.map(object, function(n, i) { return i}).length; } }); |
Zmianie ulega również plik główny aplikacji, app.js:
1 2 3 4 5 6 7 8 9 10 11 | requirejs.config({ 'paths': { 'jquery': '//ajax.googleapis.com/ajax/libs/jquery/2.0.0/jquery.min' } }); require(['jquery', 'jquery.object-size'], function(jQuery, objectSize) { var myObject = {'a': 1, 'b': 2, 'c': 3}; jQuery(document.body).text('Object ' + JSON.stringify(myObject) + ' size: ' + objectSize(myObject)); }); |
Ponownie definiujemy ścieżkę do jQuery, a następnie wczytujemy dwa moduły: jquery oraz jquery.object-size. Pierwszym jest biblioteka jQuery, której użyliśmy tutaj do operacji DOM, drugim funkcja do obliczania wielkości obiektu.
Moduły z własną nazwą
Wcześniej wspomniałem, że każdy moduł powinien być w osobnym pliku. To najlepsze rozwiązanie, bowiem rozwijanie takiego kodu będzie proste nawet po dłuższej przerwie. Niemniej jednak pojawia się poważny problem – co z wydajnością takiego rozwiązania? Przecież każdy z tych modułów to osobne żądanie HTTP, a skoro na starcie potrzebujemy XX modułów to dlaczego by ich nie połączyć w jednym pliku?
I właśnie tutaj przydaje się nazywanie modułów. Oczywiście nie musimy tego robić ręcznie (a nawet nie powinniśmy). Mamy od tego r.js oraz obszerną instrukcję jak sobie z tym poradzić (być może omówię w kolejnym wpisie, jeśli będzie taka potrzeba).
Jeśli jednak chciałbyś z jakiegoś powodu robić to ręcznie, możesz użyć następującej składni:
1 2 3 | define('nazwa-modulu', function() { return function() {}; }); |
Współpraca ze starym kodem
Ok, spodobało Ci się to podejście i chciałbyś zastosować je w swojej firmie. Jednak co zrobić ze starym kodem, niekompatybilnym z RequireJS? Wyrzucić lata pracy?
Najlepszym rozwiązaniem jest przerobić kod na modularny – jeśli to był dobry kod, zapewne wystarczy dodać definicję i zależności modułu (2 linijki). Jeśli słaby – lepiej przepisać od nowa lub to komuś zlecić :-)
Gorzej w przypadku kodu, który nie jest naszego autorstwa. Przecież nie będziemy modyfikować komuś kodu – musielibyśmy to robić przy każdej jego aktualizacji. Rozwiązaniem jest odpowiednie skonfigurowanie RequireJS z wykorzystaniem własności shim.
W naszym nieidealnym świecie potrzebujemy użyć funkcji objectSize, którą trzymamy w pliku jquery.object-size.js:
1 2 3 | function objectSize(object) { return jQuery.map(object, function(n, i) { return i}).length; } |
Funkcja jest napisana przez innego programistę i nie masz praw do jej modyfikacji :-) Co więcej, używa ona globalnego, znajomo brzmiącego obiektu jQuery. Niemniej jednak w naszym projekcie jQuery to tylko moduł, do którego dostęp jest utrudniony (nie zaśmieca globalnej przestrzeni). Co zrobić, jak żyć?
Wystarczy właściwa konfiguracja RequireJS:
1 2 3 4 5 6 7 8 | requirejs.config({ 'paths': { 'jquery': '//ajax.googleapis.com/ajax/libs/jquery/2.0.0/jquery.min' }, 'shim': { 'jquery.object-size': ['jquery'] } }); |
By chwilę później móc użyć:
1 2 3 4 | require(['jquery.object-size'], function(objectSize) { var myObject = {'a': 1, 'b': 2, 'c': 3}; document.body.innerHTML = 'Object ' + JSON.stringify(myObject) + ' size: ' + objectSize(myObject); }); |
Problem rozwiązany. Przykład 5.
Podobnie można poradzić sobie z niedostosowanymi do RequireJS pluginami do jQuery. Przykład 6.
Co tutaj się dzieje? W konfiguracji dodajemy obiekt shim, w którym podajemy niekompatybilne z RequireJS pliki. W nazwie własności używamy nazwy pliku, natomiast jako wartość podajemy tablicę z zależnymi modułami, które staną się globalne w obrębie tego pliku (w tym przypadku jQuery).
Moduły w JavaScript
RequireJS nie jest jedyną biblioteką implementującą AMD. Masz do wyboru wiele innych opcji, m. in. curl.js, Yabble, PINF. RequireJS jest natomiast najpopularniejszy (co przekłada się na jego wsparcie w bibliotekach i dostępność modułów).
Również grupa CommonJS stworzyła standard dla modułów i paczek, a ich składnia jest dość szeroko wspierana (także przez jQuery).
Co warto jeszcze wiedzieć?
Modularne podejście do programowania nie jest żadną nowością – JavaScript co najwyżej zapożyczył sprawdzoną w innych językach funkcjonalność. W niektórych językach, jak Java, moduły są wbudowane w domyślną bibliotekę. Nawet w PHP mamy require do zaciągania kolejnych plików! :D (choć temu akurat daleko do modułów).
Co ważne, także i w ECMAScript 6 (Harmony) najprawdopodobniej pojawią się moduły. Doskonale to pokazuje w jakim kierunku zmierza świat JavaScriptu.
” re-używalnych” wielokrotnego użytku ; D ?
@DarV: bardziej mi chodziło o jednowyrazowy odpowiednik dla reusable, ale wielokrotnego użytku również może być :)
Fajny temat, akurat myślałem o nauce JavaScriptu, a ten artykuł motywuje :)
Czy pliki podane w src=”” dalej są wgrywane przy ładowaniu strony? Chodzi tylko o interpretowanie gdy będą potrzebne, tak?
@hyh: są wczytywane. Dlatego jak widzisz wyżej, wczytujemy tylko require.js i main.js, a kolejne pliki doczytywane są dopiero w momencie ich wywołania, np.:
2
require('nazwapliku', myCallback);