Menu

Moduły ES w JavaScript: import, export i dynamiczne ładowanie

Jak działają moduły ES w JavaScript: eksporty nazwane i domyślne, składnia import, dynamiczne import() oraz zasady, które odróżniają moduły od zwykłych skryptów.

Na tej stronie są działające edytory: edytuj, uruchamiaj i od razu zobacz wynik.

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'.
  • this na najwyższym poziomie to undefined, 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 if lub try/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 .mjs albo
  • Ustaw "type": "module" w najbliższym package.json, co sprawia, że każdy plik .js jest 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, a import to 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 __dirname lub require w module ES. Są dostępne tylko w CommonJS. W ESM użyj import.meta.url i 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 instrukcja import na to nie pozwala. Do wszystkiego, co zależy od czasu działania, używaj dynamicznego import().

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ść.

Ilustracja języków programowania w Coddy

Ucz się programowania z Coddy

ZACZNIJ