Moduły w JS
Trudno wyobrazić sobie aplikację, która składa się z jednego pliku. Trudno wyobrazić sobie rozwijanie takiego kodu i jego późniejsze utrzymywanie. Na szczęście Javascript obsługuje moduły, ale nie zawsze tak było.
From this article you will learn:
Czym są moduły w inżynierii oprogramowania?
Jak i dlaczego powstał standard CommonJS?
Jak działają ES Modules?
Kiedy można użyć zmiennej w specyfikatorze modułu?
Jakie są różnice pomiędzy CommonJS a ES Modules?
Żyjesz samotnie pośrodku niczego i masz marzenie, a jednocześnie zadanie - zbudować dom, swoje miejsce do życia. Początkowo Twój plan jest prosty ma nie padać Ci na głowę. Pomimo że oglądanie rozgwieżdżonego nieba jest seksi, to już spanie w deszczu przypomina raczej nurkowanie niż wypoczynek. Stawiasz zatem cztery słupy, a z liści znalezionych w pobliżu budujesz dach.
Okazuje się jednak, że projekt trzeba wciąż rozwijać. Dobudować ściany, wstawić okna, utwardzić posadzkę, dobudować kominek. Twój mały domek w zasadzie spełnia już wszystkie Twoje oczekiwania. Sytuacja się jednak zmienia - poznajesz swoją drugą połówkę - która zgadza się z Tobą zamieszkać. Zrobiło się ciasno. Czas dobudować kolejne moduły - sypialnię, kuchnię, salon, łazienkę. To, co kiedyś było całym Twoim domkiem, teraz stało się jego sienią.
Im więcej czasu upływa, tym więcej modułów potrzebujemy. Pokoje dla dzieci, komórka, gabinet, garaż, biblioteka…
Powyższy opis nie jest historią mojego życia. Chociaż bibliotekę we własnym domu pewnie chciałbym mieć. Przykład, który przedstawiłem to analogia do aplikacji, które tworzymy jako programiści. Początkowo nasza aplikacja najczęściej obejmuje jeden plik, który ma jedno lub kilka prostych zadań. Z czasem jednak zaczynamy rozdzielać funkcjonalności do osobnych części, z jednej strony po to, aby ogarnąć chaos, który zaczyna powstawać, z drugiej aby odseparować odpowiedzialności i pogrupować je ze sobą odpowiednio. Każda nowa część, która jest wydzielana stanowi pod kątem funkcjonalnym samodzielny moduł. Najczęściej moduł ogranicza plik, z którego udostępnione na zewnątrz są kluczone funkcjonalności.
W rozumieniu inżynierii oprogramowania moduły to rozszerzenia do naszego programu, dostarczające określone funkcjonalności. Mogą być wielokrotnie wykorzystywane w różnych jego częściach, bez konieczności dostosowywania ich w zależności od miejsca zastosowania. Ponadto pozwalają nam na lepszą organizację kodu i samej aplikacji. Dodatkowo przy dobrym ustrukturyzowaniu - nasz kod będzie mniej podatny na błędy.
W zasadzie proste i logiczne. Spójrzmy jednak na to z perspektywy języka Javascirpt.
Marzenie mam
Mamy rok 2008. Wyobrażam sobie, że Kevin Dangoor podobnie jak wielu innych programistów w tamtych czasach irytował się na sposób dodawania kolejnych zależności do stron internetowych, które tworzył. Zakładam również, że nie mógł zrozumieć jak główny język wykorzystywany przez przeglądarki może nie posiadać zorganizowanego systemu do zarządzania zależnościami. Pozwól, że przypomnę, iż pod koniec pierwszej dekady XXI wieku JS nie działał jeszcze samodzielnie po stronie serwera, a najpopularniejszymi dostępnymi silnikami Javascriptu na komputerach osobistych były przeglądarki internetowe. Nie oznacza to, że JSa nie można było w ogóle wykorzystywać w pracach serwerowych. Wystarczy spojrzeć chociażby na projekt Helma (projekt napisany w Javie, istniejący na długo przed rokiem 2008, który pozwalał na użycie Javascriptu po stronie serwera - był to jednak raczej ciekawy framework niż rozwiązanie globalne dla JSa). Jeśli cofniemy się jeszcze dalej w czasie, to w 1996 roku Netscape udostępnił w ich oprogramowaniu serwerowym możliwość wykorzystania języka Javascript. W 1996! czyli już rok po pojawieniu się tego języka na rynku.
Wspomniany Kevin Dangoor pracował wówczas jako inżynier dla Mozilla Corporation. W styczniu 2009 roku zainicjował projekt, który nazwał ServerJS. Opisując go następująco:
What I’m describing here is not a technical problem. It’s a matter of people getting together and making a decision to step forward and start building up something bigger and cooler together.
Kevin Dangoor, 2009
Chciał opracować koncept, który będzie można wykorzystywać na różnych platformach i który stałby się standardową biblioteką dla całego świata Javascriptu. Wizja była czymś wyjątkowym jak na tamte czasy. W swojej koncepcji mówił o standaryzacji podejścia do podłączania modułów, utworzeniu standardowych interfejsów do rozwiązywania popularnych problemów czy utworzeniu miejsca, z którego łatwo można pobierać różne pakiety (jak dzisiejszy NPM, swoją drogą wówczas jeszcze nie istniał - zamiast tego dostępne były inne rozwiązania jak np. JSAN - JavaScript Archive Network).
Doprowadziło to do przemianowania ServerJS na CommonJS, aby podkreślić szersze zastosowanie projektu. Warto wspomnieć, że ECMA International nigdy oficjalnie nie zaimplementowała CommonJSa do standardu języka, ale niektórzy członkowie TC39 uczestniczyli w jego tworzeniu.
W między czasie rozwijał się projekt NodeJS, który wykorzystał powstający standard. A marzenie stało się rzeczywistością.
Składnia CommonJS
Każdy plik traktowany jest jako osobny moduł. CommonJS umieszcza każdy moduł wewnątrz funkcji require i zwraca obiekt module.exports. Obiekt ten znajduje się wewnętrz modułu. W tym obiekcie powinien znaleźć się kod, który zagwarantuje dostępność funkcjonalności na takim poziomie, aby inne części naszej aplikacji mogły z niego korzystać.
Warto podkreślić, że modułem może być jakikolwiek kod JS. Nie musimy do module.exports zwracać konkretnych danych czy funkcji - decyzja należy do nas. Moduły CommonJS ładowane są synchronicznie, co oznacza, że musimy zachować kolejność dodawania modułów, które są od siebie zależne.
Pomimo sukcesu CommonJS nie został nigdy zaimplementowany do przeglądarek. Spróbujmy zatem zrozumieć dlaczego.
Dużo zmian
ECMA International co roku, poczynając od 2015 roku, wydaje nową wersję standardu języka ECMAScript. 2015 był przełomowy. Do standardu wprowadzono bardzo dużo nowości - wśród nich były również ES Modules. ES Modules to koncept dodający możliwość tworzenia modułów w ramach kodu JS. Przed tą datą nie było oficjalnego rozwiązania, które obsługiwałoby moduły w przeglądarce. Skłamałbym, gdyby stwierdził, że nie istniało nic, co rozwiązywało ten problem - tutaj należy oddać hołd bibliotece RequireJS, która pełnymi garściami czerpała z konceptu modułów AMD (Asynchronous Module Definition), jednocześnie starając się utrzymać ducha CommonJS.
ES Modules zakłada, że mamy dwa sposoby na eksportowanie kodu z naszych modułów: w sposób domyślny za pomocą export default lub w sposób nazwany export {}. Dostęp do modułów możemy uzyskać za pomocą import, jak w poniższym przykładzie:
Eksport domyślny działa podobnie do exports.module. Nie ma znaczenia jaką nazwę ma zmienna / funkcja, którą eksportujemy jako default. Z jednego pliku możesz wyeksportować tylko jeden default. Podczas importu możemy wskazać dowolną nazwę pod jaką będziemy chcieli używać naszego modułu. W zasadzie można to zapisać na dwa sposoby:
Jeśli w ramach naszego pliku HTML załączamy kod js, który wykorzystuje moduły, powinniśmy dodać do znacznika script, odpowiedzialnego za jego załadowanie, atrybut type="module". Musimy dokonać tej zmiany, aby móc korzystać z tej funkcjonalności. W przeciwnym wypadku aplikacja nam nie zadziała. W aplikacji backendowej wystarczy zmienić rozszerzenie pliku na mjs
Co się dzieje pod maską?
Tworząc aplikację w sposób modułowy budujemy graf zależności. W takim grafie połączenia pomiędzy poszczególnymi zależnościami są tworzone na podstawie wyrażeń import. Import pozwala określić przeglądarce lub serwerowi jaki kod powinien być załadowany i w jakiej kolejności. Tutaj bardzo ważna uwaga - przeglądarka nie może wykorzystać kolejnych plików modułów w ich pierwotnej formie. Zanim będą one możliwe do użycia zostaną one zmapowane do struktur, które nazywają się Module Records. Każda taka struktura zawiera komplet informacji o module. Po powstaniu Module Records, mogą one być wykorzystane do utworzenia instancji modułów. Każda instancja modułu składa się z dwóch części: stanu i kodu.
Dopiero tak przygotowane moduły będą używane przez przeglądarkę.
Przygotowywanie modułów składa się z 3 faz (ładowania, instalacji, uruchomienia), z których każda może być wykonywana oddzielnie. Wszystkie zależności do modułu są ładowane równolegle, bez przerw pomiędzy ładowaniem kolejnych elementów. Tak zbudowany mechanizm domyślnie będzie działał asynchronicznie. Warto dodać, że same fazy niekoniecznie muszą być asynchroniczne - mogą zostać uruchomione synchronicznie.
Co nas różni?
CommonJS nie dzieli ładowania modułów na fazy, ponieważ z założenia pracuje na plikach, które dostępne są dużo szybciej niż w przypadku przeglądarki. W przeglądarce mamy dodatkowy element ograniczający, jakim jest Internet. W przypadku CommonJSa Node może zablokować główny wątek i zaczekać na doczytania pliku. Oznacza to, że finalnie przechodzimy przez całe drzewo i w określonej kolejności ładujemy, tworzymy instancje i wykonujemy kod zależności przed zwróceniem całego modułu.
Przez różny sposób odczytywania kodu w specyfikatoach modułów w CommonJS możemy wykorzystywać zmienne. Przykładowo:
Warto dodać, że w ES Modules istnieją również dynamiczne moduły, które zezwalają na użycie zmiennych w import, ale import traktowane jest wówczas jako funkcja, którą wywołujemy z odpowiednią ścieżką. Dynamiczny moduł tworzy nowy graf zależności, który analizowany jest w odseparowaniu od głównego grafu.
Natura CommonJSa jest synchroniczna, natomiast ES Modules asynchroniczna. Najnowszy Node wspiera oba podejścia. W przeglądarce nadal możemy wykorzystywać tylko ES Modules.
Podsumowanie
Jak widzicie moduły w świecie JSa były potrzebne. Bez traktowania naszych aplikacji jako zbioru samodzielnych grup funkcjonalności doszlibyśmy do ściany, a dalszy ich rozwój stałby się wręcz niemożliwy. O modułach rozmawia się od samego początku istnienia języka JS, ale dopiero ostatnie 10 lat przyniosło duże zmiany i konkretne rozwiązania.
Jeśli mówimy o modułach warto wspomnieć również o narzędziach takich jak module bundlery (np. webpack), które pomagają rozwiązać najbardziej klasyczne problemy z modułami, w szczególności w przeglądarkach. Myślę, że warto temu poświęcić więcej miejsca - dlatego pozwolę sobie na tym zakończyć temat.
Podejście do naszego kodu w sposób modułowy daje nam sporo swobody i wolności, tak jak dom składający się z wielu pomieszczeń. Trudno spać, gotować, wypoczywać i załatwiać potrzeby fizjologiczne w jednym pomieszczeniu, tak jak trudno stworzyć jedną funkcję, która wykona wszystko co nasz program ma robić.
Comments (1)
Another_On3
16 stycznia 2023 o 06:03Brakuje kontentu :/