Lekcja 19: Moduły w JavaScript (ES6 Modules)
W miarę rozrastania się aplikacji JavaScript, utrzymanie całego kodu w jednym pliku staje się niepraktyczne i trudne do zarządzania. Moduły pozwalają na dzielenie kodu na mniejsze, reużywalne części, które można importować i eksportować między różnymi plikami. ES6 wprowadziło natywny system modułów, znany jako ES Modules (ESM).
Moduły ES6 pomagają w organizacji kodu, hermetyzacji (zmienne i funkcje zdefiniowane w module nie są domyślnie globalne) i zarządzaniu zależnościami.
Podstawy ES Modules
System modułów ES6 opiera się na dwóch głównych słowach kluczowych: export
i import
.
export
: Używane w pliku modułu do oznaczenia zmiennych, funkcji lub klas, które mają być dostępne na zewnątrz (czyli możliwe do zaimportowania w innych modułach).import
: Używane w innym pliku do załadowania funkcjonalności wyeksportowanej z innego modułu.
Aby przeglądarka traktowała plik JavaScript jako moduł, musi on być załadowany za pomocą atrybutu type="module"
w tagu <script>
.
<!-- Ładowanie głównego pliku aplikacji jako modułu -->
<script type="module" src="main.js"></script>
Ważne cechy modułów ES6:
- Tryb ścisły (Strict Mode): Kod w modułach ES6 jest automatycznie wykonywany w trybie ścisłym (
'use strict';
), bez potrzeby jawnego deklarowania. - Zakres modułu: Zmienne i funkcje zadeklarowane w module mają zakres lokalny dla tego modułu. Nie zanieczyszczają globalnego zakresu (np. obiektu
window
). - Ładowanie asynchroniczne: Moduły są ładowane asynchronicznie przez przeglądarkę.
- Jednokrotne wykonanie: Każdy moduł jest pobierany i wykonywany tylko raz, nawet jeśli jest importowany w wielu miejscach.
Eksportowanie (export
)
Istnieją dwa główne rodzaje eksportów:
1. Eksporty nazwane (Named Exports)
Pozwalają na wyeksportowanie wielu wartości (zmiennych, funkcji, klas) z jednego modułu pod ich oryginalnymi nazwami.
// Plik: utils.js
// Eksportowanie istniejących deklaracji
export const PI = 3.14159;
export function dodaj(a, b) {
return a + b;
}
export class Uzytkownik {
constructor(imie) {
this.imie = imie;
}
przywitaj() {
console.log(`Witaj, ${this.imie}!`);
}
}
// Można też eksportować na końcu pliku
const mnoz = (a, b) => a * b; // Prywatna funkcja modułu
const wersja = "1.0";
// Lista eksportowanych elementów
// export { mnoz, wersja }; // Błąd: mnoz nie jest zdefiniowane w tym zakresie, jeśli jest const
// Poprawny sposób eksportowania zadeklarowanych wcześniej zmiennych/funkcji
function odejmij(a, b) {
return a - b;
}
const autor = "KursJS";
export { odejmij, autor };
// Można też zmienić nazwę podczas eksportu
function dziel(a, b) {
if (b === 0) throw new Error("Dzielenie przez zero");
return a / b;
}
export { dziel as podziel }; // Eksportowane jako 'podziel'
2. Eksport domyślny (Default Export)
Każdy moduł może mieć co najwyżej jeden eksport domyślny. Jest on używany, gdy moduł dostarcza jedną główną funkcjonalność.
// Plik: kalkulator.js
// Eksport domyślny klasy
export default class Kalkulator {
dodaj(a, b) {
return a + b;
}
odejmij(a, b) {
return a - b;
}
}
// Można też eksportować domyślnie funkcję anonimową lub wartość
// export default function(a, b) { return a * b; };
// export default "Jakaś wartość";
// Moduł może mieć jednocześnie eksport domyślny i eksporty nazwane
export const nazwaKalkulatora = "Prosty Kalkulator";
Importowanie (import
)
Aby użyć funkcjonalności wyeksportowanej z innego modułu, używamy instrukcji import
. Istnieje kilka sposobów importowania:
1. Importowanie nazwanych eksportów
Importujemy konkretne wartości, podając ich nazwy w nawiasach klamrowych {}
.
// Plik: main.js
// Importowanie konkretnych nazwanych eksportów z utils.js
import { PI, dodaj, Uzytkownik, podziel, autor } from './utils.js';
// Ścieżka musi być względna lub bezwzględna, zazwyczaj zaczyna się od ./ lub ../
console.log("Wartość PI:", PI);
console.log("Wynik dodawania:", dodaj(5, 3));
const user = new Uzytkownik("Anna");
user.przywitaj();
console.log("Wynik dzielenia:", podziel(10, 2));
console.log("Autor modułu utils:", autor);
// Importowanie z aliasem (zmiana nazwy)
import { dodaj as suma } from './utils.js';
console.log("Wynik sumy (alias):", suma(10, 20));
// Importowanie wszystkiego jako obiekt (namespace import)
import * as Utils from './utils.js';
console.log("PI z namespace:", Utils.PI);
console.log("Dodawanie z namespace:", Utils.dodaj(1, 2));
const user2 = new Utils.Uzytkownik("Piotr");
user2.przywitaj();
2. Importowanie eksportu domyślnego
Importując eksport domyślny, możemy nadać mu dowolną nazwę (bez nawiasów klamrowych).
// Plik: main.js (kontynuacja)
// Importowanie eksportu domyślnego z kalkulator.js
import MojKalkulator from './kalkulator.js';
// Nazwa 'MojKalkulator' jest dowolna, mogłaby być np. 'Calc'
const calc = new MojKalkulator();
console.log("Wynik kalkulatora (domyślny export):", calc.dodaj(100, 50));
// Importowanie jednocześnie eksportu domyślnego i nazwanych
import SuperKalkulator, { nazwaKalkulatora } from './kalkulator.js';
const superCalc = new SuperKalkulator();
console.log("Nazwa kalkulatora:", nazwaKalkulatora);
console.log("Odejmowanie super kalkulatorem:", superCalc.odejmij(10, 3));
3. Import dynamiczny (Dynamic Import)
Standardowe instrukcje import
muszą znajdować się na najwyższym poziomie modułu (nie wewnątrz funkcji, pętli czy warunków). Czasami jednak chcemy załadować moduł warunkowo lub na żądanie (np. po kliknięciu przycisku). Służy do tego import dynamiczny, który używa składni podobnej do funkcji: import('ścieżka/do/modułu.js')
.
Dynamiczny import()
zwraca obietnicę (Promise), która rozwiązuje się do obiektu modułu (zawierającego wszystkie jego eksporty, w tym default
).
// Plik: main.js (kontynuacja)
document.getElementById('ladujModulBtn').addEventListener('click', async () => {
console.log("Próba dynamicznego załadowania modułu...");
try {
// Dynamiczny import zwraca Promise
const modulLogowania = await import('./logger.js');
// Domyślny export jest dostępny pod kluczem 'default'
modulLogowania.default("Moduł logger.js załadowany dynamicznie!");
// Nazwane exporty są dostępne bezpośrednio
modulLogowania.logError("To jest dynamiczny błąd!");
// Można też użyć destrukturyzacji
// const { default: log, logError } = await import('./logger.js');
// log("Załadowano przez destrukturyzację");
// logError("Błąd przez destrukturyzację");
} catch (error) {
console.error("Nie udało się załadować modułu dynamicznie:", error);
}
});
// Przykładowy plik logger.js
/*
// logger.js
export default function log(message) {
console.log(`[LOG] ${new Date().toISOString()}: ${message}`);
}
export function logError(message) {
console.error(`[ERROR] ${new Date().toISOString()}: ${message}`);
}
*/
Dynamiczne importy są przydatne do optymalizacji ładowania aplikacji (code splitting), gdzie ładujemy tylko te części kodu, które są aktualnie potrzebne.
Zadanie praktyczne
Stwórz dwa pliki:
matematyka.js
: Zdefiniuj i wyeksportuj (jako eksporty nazwane) dwie funkcje:potega(liczba, wykladnik)
(obliczającą potęgę) ilosowa(min, max)
(zwracającą losową liczbę całkowitą z podanego zakresu).app.js
: Zaimportuj obie funkcje zmatematyka.js
. Wywołajpotega(2, 10)
ilosowa(1, 100)
, a wyniki wyświetl w konsoli. Pamiętaj, aby załadowaćapp.js
w HTML za pomocą<script type="module">
.
Pokaż rozwiązanie
Plik: matematyka.js
// matematyka.js
export function potega(liczba, wykladnik) {
return Math.pow(liczba, wykladnik);
}
export function losowa(min, max) {
min = Math.ceil(min);
max = Math.floor(max);
return Math.floor(Math.random() * (max - min + 1)) + min;
}
Plik: app.js
// app.js
import { potega, losowa } from './matematyka.js';
const wynikPotegi = potega(2, 10);
console.log(`2 do potęgi 10 = ${wynikPotegi}`); // 1024
const liczbaLosowa = losowa(1, 100);
console.log(`Losowa liczba z zakresu 1-100: ${liczbaLosowa}`);
Plik: index.html (fragment)
<!DOCTYPE html>
<html>
<head>
<title>Test Modułów</title>
</head>
<body>
<h1>Testowanie modułów JavaScript</h1>
<!-- Ładowanie app.js jako modułu -->
<script type="module" src="app.js"></script>
</body>
</html>
Zadanie do samodzielnego wykonania
Stwórz moduł konfiguracja.js
, który będzie eksportował domyślnie obiekt konfiguracyjny, np. { apiUrl: "https://api.example.com", jezyk: "pl", motyw: "ciemny" }
. Stwórz również nazwany eksport WERSJA_API = "v2"
.
W pliku main.js
zaimportuj zarówno domyślny obiekt konfiguracyjny (pod nazwą config
), jak i nazwany eksport WERSJA_API
. Wyświetl w konsoli wartości config.apiUrl
i WERSJA_API
.
FAQ - Moduły w JavaScript (ES6 Modules)
Jaka jest różnica między ES Modules a CommonJS (używanym w Node.js)?
CommonJS (require
/module.exports
) jest systemem modułów używanym historycznie w Node.js. Jest synchroniczny. ES Modules (import
/export
) to standardowy system modułów JavaScript, jest asynchroniczny i wspierany zarówno w przeglądarkach, jak i nowoczesnych wersjach Node.js. Składnia i sposób działania są różne.
Czy mogę używać `import` i `export` w zwykłych skryptach (bez `type="module"`)?
Nie, składnia import
i export
jest dozwolona tylko w plikach traktowanych jako moduły ES6 (czyli ładowanych z type="module"
w przeglądarce lub będących modułami ES w Node.js).
Czy ścieżki w `import` muszą mieć rozszerzenie `.js`?
W przeglądarkach zazwyczaj tak, ścieżka do modułu powinna być pełna, włącznie z rozszerzeniem (np. ./utils.js
). W środowisku Node.js i niektórych narzędziach budujących (bundlerach) rozszerzenie może być czasami pomijane, ale jawne podawanie jest bezpieczniejszą praktyką.
Co jeśli zaimportuję ten sam moduł dwa razy?
Moduł zostanie pobrany i wykonany tylko raz. Kolejne importy tego samego modułu (nawet w różnych częściach aplikacji) będą odnosić się do tej samej, już istniejącej instancji modułu i jego eksportów.
Czy mogę modyfikować zaimportowane wartości?
Zaimportowane wartości (zarówno nazwane, jak i domyślne) są traktowane jako "żywe" powiązania (live bindings), ale są tylko do odczytu. Próba przypisania nowej wartości do zaimportowanej zmiennej (np. import { PI } from './math.js'; PI = 3;
) spowoduje błąd TypeError
.
Do czego służą bundlery modułów (np. Webpack, Rollup, Parcel)?
Bundlery to narzędzia, które przetwarzają kod źródłowy aplikacji (w tym moduły ES6, CommonJS, style CSS, obrazy itp.) i łączą go w jeden lub kilka zoptymalizowanych plików (tzw. "bundles"), gotowych do wdrożenia w przeglądarce. Umożliwiają korzystanie z modułów i innych nowoczesnych funkcji nawet w starszych przeglądarkach oraz optymalizują rozmiar i ładowanie aplikacji.