Moduł to plik z własnym zakresem
Zanim pojawiły się moduły ES, każdy znacznik <script> wrzucał swoje zmienne do globalnej przestrzeni nazw, a kolejność ładowania decydowała o tym, co widzi co. Moduły ES to naprawiają, bo każdy plik ma własny zakres. Nic nie wycieka na zewnątrz, dopóki jawnie tego nie wyeksportujesz przez export. Nic nie wchodzi do środka, dopóki jawnie tego nie zaimportujesz przez import.
Dwa pliki, jeden eksportuje, drugi importuje:
add i multiply żyją w math.js. Stają się widoczne w main.js tylko dzięki import. Nic innego z math.js, czy to funkcje pomocnicze, stałe, czy cokolwiek innego, nie jest dostępne z zewnątrz.
Wynikają z tego dwie zasady, które warto przyswoić wcześnie:
- Moduły automatycznie działają w strict mode. Nie potrzebujesz
'use strict'. thisna najwyższym poziomie toundefined, a nie obiekt globalny.
Eksporty nazwane: eksportuj na bieżąco
Najczęstsza forma. Dopisz export przed dowolnym function, class, const lub let, a stanie się on częścią publicznego interfejsu modułu:
Nazwy w nawiasach klamrowych muszą dokładnie odpowiadać eksportowanym nazwom: import { circlearea } by się nie powiódł. Jeśli nazwa koliduje z czymś, co już masz, zmień ją przy imporcie przez as:
Eksporty możesz też wymienić na dole pliku zamiast przy każdej deklaracji. Niektórzy wolą to, bo daje wyraźną sekcję „publicznego API”:
Oba style dają ten sam wynik. Wybierz jeden i trzymaj się go w obrębie projektu.
Eksporty domyślne: jeden na moduł
Moduł może też mieć jeden eksport domyślny. Eksporty domyślne są dla plików, które naprawdę mają jedną główną rzecz: komponent, klasę, obiekt konfiguracji:
Zwróć uwagę na trzy rzeczy:
- Po stronie importu nie ma nawiasów klamrowych wokół
log. - Zaimportowana nazwa może być dowolna.
import shout from './logger.js'zadziała identycznie. - Na plik przypada tylko jeden eksport domyślny. Spróbuj dodać drugi, a plik się nie sparsuje.
Eksporty nazwane i domyślne mogą współistnieć:
Najpierw domyślny, potem nazwane w nawiasach klamrowych. Ta kolejność jest stała.
Którego używać? Eksporty nazwane łatwiej refaktoryzować: zmiana nazwy w całym projekcie to jedno znajdź i zamień, bo każdy import używa tej samej nazwy. Eksporty domyślne są elastyczne, ale pozwalają każdemu wywołującemu wybrać inną nazwę, co utrudnia wyszukiwanie przez grep. Większość nowoczesnych przewodników stylu skłania się ku eksportom nazwanym, a domyślne rezerwuje dla modułów, które naprawdę mają jedno zadanie.
Import wszystkiego, reeksport, efekty uboczne
Jeszcze kilka kształtów importu, które spotkasz.
Pobranie wszystkich eksportów nazwanych do jednego obiektu przestrzeni nazw:
Reeksport z innego modułu bez importowania do bieżącego zakresu:
W ten sposób punkty wejścia bibliotek zbierają kawałki z wewnętrznych plików w jeden publiczny interfejs.
I na koniec import bez żadnych wiązań, dla modułów, których jedynym zadaniem są efekty uboczne (polyfille, CSS-in-JS, rejestrowanie handlerów):
Plik wykonuje się raz, nic nie jest importowane z nazwy.
Importy są statyczne i żywe
Dwie właściwości import, które czasem zaskakują.
Statyczne. Deklaracje import są rozwiązywane, zanim wykona się jakikolwiek twój kod. Nie możesz umieścić ich w if, w funkcji ani w try. Ścieżka musi być literałem stringa, a nie zmienną. Dzięki temu narzędzia mogą analizować importy bez uruchamiania kodu: bundlery, systemy sprawdzania typów i tree-shaking opierają się właśnie na tym.
// Not allowed - SyntaxError.
if (userWantsFancy) {
import { fancy } from './fancy.js';
}
Jeśli potrzebujesz ładowania warunkowego, użyj import() (o tym za chwilę).
Żywe. Zaimportowane wiązanie to referencja tylko do odczytu, prowadząca z powrotem do eksportu, a nie jego migawka. Jeśli moduł eksportujący przypisze nową wartość, importujący zobaczą nową wartość:
Po stronie konsumenta nie możesz też ponownie przypisać importu: count = 5 w main.js rzuciłoby błąd. Importy to widoki tylko do odczytu.
Dynamiczne import() do ładowania na żądanie
Gdy musisz zdecydować w czasie działania, czy załadować moduł (ciężkie funkcje, code splitting według tras, warunkowe polyfille), użyj import() jako funkcji. Zwraca ona obietnicę, która rozwiązuje się do eksportów modułu:
Ponieważ to zwykłe wywołanie funkcji, możesz:
- Czekać na nie przez await wewnątrz funkcji
async. - Przekazać zmienną jako ścieżkę.
- Używać go wewnątrz
iflubtry/catch.
Destrukturyzacja rozwiązanego obiektu działa tak samo jak przy statycznym imporcie:
Przy destrukturyzacji default to klucz eksportu domyślnego. Zmień jego nazwę na dowolną.
Praktyczne zastosowania to code splitting (wysyłaj bibliotekę wykresów dopiero wtedy, gdy użytkownik kliknie „pokaż wykres”), polyfille zależne od wykrycia funkcji i wtyczki odkrywane w czasie działania.
Uruchamianie modułów ES: przeglądarki i Node
Składnia jest wszędzie taka sama, zmienia się tylko to, jak środowisko uruchomieniowe znajduje i ładuje plik.
W przeglądarce oznacz skrypt wejściowy jako moduł:
<script type="module" src="./main.js"></script>
Z type="module" przeglądarka respektuje import/export, uruchamia kod w strict mode i odkłada wykonanie do czasu sparsowania HTML. Ścieżki muszą być względne (./, ../) albo być bezwzględnymi adresami URL: gołe specyfikatory, takie jak import 'lodash', nie działają bez import mapy lub bundlera.
W Node są dwa sposoby, by włączyć moduły:
- Nadaj plikowi rozszerzenie
.mjsalbo - Ustaw
"type": "module"w najbliższympackage.json, co sprawia, że każdy plik.jsjest modułem.
Node wymaga też pełnych ścieżek z rozszerzeniami: import './utils.js', a nie import './utils'.
// package.json
{
"type": "module",
"main": "./index.js"
}
Oba środowiska wymagają jawnych rozszerzeń w natywnym ESM. Bundlery (Vite, webpack, esbuild) rozwiążą ścieżki bez rozszerzeń za ciebie podczas developmentu. To wygodne, ale poleganie na tym oznacza, że twój kod źródłowy nie uruchomi się bez etapu budowania.
Typowe pułapki
Kilka rzeczy, na których ludzie się potykają:
- Brak
type="module"w przeglądarce. Bez niego<script>działa jak klasyczny skrypt, aimportto błąd składni. - Brak rozszerzeń plików w Node.
import './utils'się nie powiedzie,import './utils.js'zadziała. Bundlery to ukrywają, natywne środowiska nie. - Oczekiwanie
__dirnamelubrequirew module ES. Są dostępne tylko w CommonJS. W ESM użyjimport.meta.urli przekonwertuj go, gdy potrzebujesz ścieżki. - Cykliczne importy, które sięgają po wartości, zanim będą gotowe. Dwa moduły importujące się nawzajem są dozwolone, ale odczyt eksportu, który nie został jeszcze przypisany, daje
undefined. Ułóż kod tak, żeby cykl nie był wykorzystywany podczas inicjalizacji, albo go rozbij. - Próby warunkowego
import. Statyczna instrukcjaimportna to nie pozwala. Do wszystkiego, co zależy od czasu działania, używaj dynamicznegoimport().
Dalej: CommonJS vs ESM
Moduły ES to standard, ale mnóstwo kodu Node w praktyce wciąż używa CommonJS: require, module.exports i innego zestawu zasad dotyczących tego, kiedy kod się wykonuje. Znajomość obu systemów i tego, jak ze sobą współpracują, to temat następnej strony.
Najczęściej zadawane pytania
Jak używać modułów ES w JavaScript?
Eksportuj wartości z jednego pliku przez export lub export default, a potem wczytaj je w innym pliku przez import. W przeglądarce załaduj plik wejściowy przez <script type="module" src="main.js"></script>. W Node użyj rozszerzenia .mjs albo ustaw "type": "module" w package.json.
Czym różnią się eksporty domyślne od nazwanych?
Moduł może mieć wiele eksportów nazwanych (export function foo() {}), ale tylko jeden eksport domyślny (export default ...). Eksporty nazwane trzeba importować pod dokładnie tą samą nazwą w nawiasach klamrowych: import { foo } from './x.js'. Eksport domyślny można zaimportować pod dowolną nazwą: import whatever from './x.js'.
Czym jest dynamiczne import() w JavaScript?
import() wywołane jak funkcja zwraca obietnicę, która rozwiązuje się do eksportów modułu. W przeciwieństwie do statycznej instrukcji import działa w chwili wywołania, więc możesz ładować kod warunkowo albo na żądanie. W ten sposób realizuje się code splitting i lazy loading.
Czy w ścieżkach importu trzeba podawać rozszerzenie pliku?
W natywnych modułach ES, czyli w przeglądarkach i w loaderze ESM w Node, tak. Musisz pisać ./utils.js, a nie ./utils. Bundlery, takie jak Vite i webpack, są bardziej wyrozumiałe i same rozwiążą ścieżki bez rozszerzeń, ale poleganie na tym sprawia, że kod traci przenośność.