jak raz na zawsze naprawić nieprzyjemne problemy z zależnościami kołowymi w JavaScript & TypeScript
-
index.js
wymagaAbstractNode.js
- moduł ładujący rozpoczyna ładowanie
AbstractNode.js
i uruchamia kod modułu. Pierwszą rzeczą, którą napotka, jest polecenie require (import) doLeaf
- , więc moduł ładujący zaczyna ładować plik
Leaf.js
. Co z kolei zaczyna się od wymoguAbstractnode.js
. -
AbstractNode.js
jest już ładowany i jest natychmiast zwracany z pamięci podręcznej modułu. Ponieważ jednak moduł ten nie przekroczył jeszcze pierwszej linii (WymaganieLeaf
), instrukcje wprowadzające klasęAbstractNode
nie zostały jeszcze wykonane! - tak więc Klasa
Leaf
próbuje rozszerzyć wartośćundefined
, a nie poprawną klasę. Który wyrzuca pokazany powyżej wyjątek runtime. Bum!
Fix próba 1
więc okazuje się, że nasza zależność okrągła powoduje nieprzyjemny problem. Jeśli jednak przyjrzymy się bliżej, dość łatwo jest określić, jaka powinna być kolejność załadunku:
- załaduj klasę
AbstractNode
najpierw - załaduj klasę
Node
iLeaf
po tym.
innymi słowy, zdefiniujmy najpierw klasę AbstractNode
, a następnie wymagajmy jej Leaf
i Node
. To powinno zadziałać, ponieważ Leaf
i Node
nie muszą być jeszcze znane podczas definiowania klasy AbstractNode
. Tak długo, jak są zdefiniowane przed wywołaniem AbstractNode.from
po raz pierwszy, powinno być dobrze. Wypróbujmy więc następującą zmianę:
okazuje się, że z tym rozwiązaniem jest kilka problemów:
po pierwsze, to jest brzydkie i nie skaluje się. W przypadku dużej bazy kodu spowoduje to losowe przenoszenie importu, dopóki coś nie zadziała. Co często jest tylko tymczasowe, ponieważ niewielka refaktoryzacja lub zmiana instrukcji importu w przyszłości może subtelnie dostosować kolejność ładowania modułu, przywracając problem.
po drugie, to, czy to działa, zależy w dużym stopniu od bundlera modułów. Na przykład w codesandbox, gdy łączymy naszą aplikację z Parcel (lub Webpack lub Rollup), To rozwiązanie nie działa. Jednak podczas uruchamiania tego lokalnie z węzłem.Moduły JS i commonJS to obejście może działać dobrze.
unikanie problemu
więc najwyraźniej tego problemu nie można łatwo naprawić. Można było tego uniknąć? Odpowiedź brzmi: tak, istnieje kilka sposobów, aby uniknąć problemu. Po pierwsze, mogliśmy zachować kod w jednym pliku. Jak pokazano w naszym początkowym przykładzie, w ten sposób możemy rozwiązać problem, ponieważ daje to pełną kontrolę nad kolejnością, w jakiej działa kod inicjujący moduł.
po drugie, niektórzy ludzie użyją powyższego problemu jako argumentu do złożenia oświadczeń takich jak ” nie należy używać klas “lub”nie używaj dziedziczenia”. Jest to jednak zbyt duże uproszczenie problemu. Chociaż zgadzam się, że programiści często uciekają się do dziedziczenia zbyt szybko, w przypadku niektórych problemów jest to po prostu idealne i może przynieść wielkie korzyści pod względem struktury kodu, ponownego użycia lub wydajności. Ale co najważniejsze, ten problem nie ogranicza się do dziedziczenia klas. Dokładnie ten sam problem można wprowadzić, gdy zmienne modułu są zależne od zmiennych i funkcji, które działają podczas inicjalizacji modułu!
moglibyśmy przeorganizować nasz kod w taki sposób, że podzielimy klasę AbstractNode
na mniejsze części, tak że AbstractNode
nie ma zależności od Node
lub Leaf
. W tym piaskownicy metoda from
została wyciągnięta z klasy AbstractNode
i umieszczona w oddzielnym pliku. To rozwiązuje problem, ale teraz nasz projekt i API mają inną strukturę. W dużych projektach może być bardzo trudno określić, jak wykonać ten trik, a nawet niemożliwe! Wyobraź sobie na przykład, co by się stało, gdyby metoda print
zależała od Node
lub Leaf
w następnej iteracji naszej aplikacji…
Bonus: dodatkowy brzydki trik, którego użyłem wcześniej: zwróć klasy bazowe z funkcji i wykorzystaj podnoszenie funkcji, aby załadować rzeczy we właściwej kolejności. Nawet nie wiem, jak to właściwie wyjaśnić.
wzór modułu wewnętrznego na ratunek!
wielokrotnie walczyłem z tym problemem w wielu projektach kilka przykładów to moja praca w Mendix, MobX, mobx-state-tree i kilka osobistych projektów. W pewnym momencie, kilka lat temu napisałem nawet skrypt, aby połączyć wszystkie pliki źródłowe i usunąć wszystkie instrukcje importu. Poor-mans moduł bundler tylko po to, aby opanować kolejność ładowania modułu.
jednak po kilkukrotnym rozwiązaniu tego problemu pojawił się wzór. Taki, który daje pełną kontrolę nad zamówieniem ładowania modułu, bez konieczności restrukturyzacji projektu lub ciągnięcia dziwnych hacków! Ten wzór działa idealnie ze wszystkimi narzędziami, które wypróbowałem (Rollup, Webpack, Parcel, Node).
sednem tego wzorca jest wprowadzenie pliku index.js
i internal.js
. Zasady gry są następujące:
- moduł
internal.js
importuje i eksportuje wszystko z każdego modułu lokalnego w projekcie - każdy inny moduł w projekcie importuje tylko z pliku
internal.js
, a nigdy bezpośrednio z innych plików w projekcie. - plik
index.js
jest głównym punktem wejścia i importuje i eksportuje wszystko zinternal.js
, które chcesz ujawnić światu zewnętrznemu. Pamiętaj, że ten krok ma znaczenie tylko wtedy, gdy publikujesz bibliotekę używaną przez inne osoby. Pominęliśmy ten krok w naszym przykładzie.
zauważ, że powyższe reguły dotyczą tylko naszych lokalnych zależności. Import modułów zewnętrznych pozostaje taki, jaki jest. W końcu nie są oni zaangażowani w nasze cyrkulacyjne problemy zależności. Jeśli zastosujemy tę strategię do naszej aplikacji demonstracyjnej, nasz kod będzie wyglądał tak:
kiedy zastosujesz ten wzór po raz pierwszy, może to być bardzo wymyślne. Ale ma kilka bardzo ważnych korzyści!
- przede wszystkim rozwiązaliśmy nasz problem! Jak pokazano tutaj Nasza aplikacja jest szczęśliwie działa ponownie.
- to rozwiązuje nasz problem: mamy teraz pełną kontrolę nad kolejnością ładowania modułu. Niezależnie od tego, jakie jest zamówienie importu w
internal.js
, będzie naszym zamówieniem ładowania modułu. (Możesz sprawdzić poniższy obrazek lub ponownie przeczytać wyjaśnienie kolejności modułów powyżej, aby zobaczyć, dlaczego tak jest) - nie musimy stosować refaktoryzacji, których nie chcemy. Nie jesteśmy też zmuszani do używania brzydkich sztuczek, takich jak przenoszenie instrukcji require na dół pliku. Nie musimy naruszać architektury, API czy struktury semantycznej naszej bazy kodu.
- Bonus: polecenia importu staną się znacznie mniejsze, ponieważ będziemy importować rzeczy z plików less. Na przykład
AbstractNode.js
ma teraz tylko polecenie import, gdzie wcześniej miało dwa. - Bonus: dzięki
index.js
mamy jedno źródło prawdy, dając drobnoziarnistą kontrolę nad tym, co ujawniamy światu zewnętrznemu.