W niniejszym artykule prezentuję przykłady modułów ECMAScript (ES) – co można z nimi zrobić i jakie są ich ograniczenia. Moduły ES są obsługiwane przez wszystkie przeglądarki wydane po maju 2018 roku, więc możesz założyć, że można z nich bezpiecznie korzystać w większości przypadków.
Kodowanie bez modułów ES
Przed powstaniem modułów ES wszystkie JS trzeba było importować globalnie. Każdy plik miał dostęp do wcześniej zdefiniowanych zmiennych i mógł zostawić informację dla kodu wykonywanego później. Kolejność importowania miała znaczenie, zwłaszcza że elementy zaimportowane później mogły zmienić te, które zostały zaimportowane wcześniej. Starsze importy wyglądały mniej więcej tak:
display-data.js
:
document.body.innerHTML = "lorem ipsum";
log.js
:
console.log("Some test info");
index.html
:
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>No modules</title>
<link rel="shortcut icon" href="#" />
</head>
<body>
<script src="./display-data.js"></script>
<script src="./log.js"></script>
</body>
</html>
Problemy
Podejście to jest problematyczne głównie z dwóch powodów:
I. Zanieczyszcza zakresu globalnego przez kod. Jeśli kilka plików określa tę samą wartość, będą one ze sobą kolidować i będą się na siebie wzajemnie nadpisywać: powodzenia w szukaniu i naprawianiu powstałych w związku z tym bugów. Przykład:
data-1.js
:
var data = “lorem ipsum”;
data-2.js
:
var data = “sin dolor”;
index.html
:
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>Name collision</title>
<link rel="shortcut icon" href="#" />
</head>
<body>
<script src="./data-1.js"></script>
<script src="./data-2.js"></script>
<script>
document.body.innerHTML = data;
</script>
</body>
</html>
Obchodzenie tego problemu polegało najczęściej na wykorzystywaniu natychmiastowo wywoływanego wyrażenia funkcyjnego. Pozwalało to na izolację bloków kodu i chroniło przed zanieczyszczeniem zakresu globalnego, ale jednocześnie sprawiało, że kod stawał się bardziej zagmatwany.
II. Wszelkimi zależnościami trzeba zarządzać ręcznie. Jeśli jeden plik zależy od drugiego, pliki te trzeba zaimportować w odpowiedniej kolejności. Na przykład:
log-data.js
:
console.log(data);
data.js
:
const data = ‘some data’;
display-data.js
:
document.html = data;
index.html
:
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>File order</title>
<link rel="shortcut icon" href="#" />
</head>
<body>
<script src="./log-data.js"></script>
<script src="./data.js"></script>
<script src="./display-data.js"></script>
</body>
</html>
Jak widać tutaj, część wyświetlająca dane działa, jak należy, a część z logowaniem danych – nie.
Moduły ES w akcji
Co się zmieni, gdy to samo zrobimy z modułami ES? Przede wszystkim zależności definiowane są na poziomie kodu. Jeśli więc chcesz, żeby plik wykorzystywał wartości z innego pliku, definiujesz to bezpośrednio w tym pliku. Podejście to wiele zmienia, zwłaszcza w zakresie czytania kodu: aby poznać cały kontekst stosowany przez konkretny plik, wystarczy go otworzyć i przeczytać.
Jak zatem stosować moduły ES?
data.js
:
export const data = "lorem ipsum";
display-data.js
:
import { data } from "./data.js";
document.body.innerHTML = data;
index.html
:
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>Simple modules</title>
<link rel="shortcut icon" href="#" />
</head>
<body>
<script type="module" src="./display-data.js"></script>
</body>
</html>
Główne zmiany wprowadzone w kodzie:
- dodanie
type=”module”
do importu<script>
w pliku HTML; - zastosowanie eksportowych i importowych słów kluczowych w plikach JS w celu zdefiniowania i załadowania modułów.
Import tego samego pliku przez kilka innych plików
Opisywany przykład można urozmaicić poprzez zaimportowanie tych samych plików dwukrotnie. Jako że każdy z plików musi być niezależny od tego drugiego, import zostanie dodany dwa razy – osobno dla każdego pliku. Przeglądarki zarządzają importem prawidłowo i ładują dany plik tylko raz.
data.js
:
export const data = "lorem ipsum";
display-data.js
:
import { data } from "./data.js";
document.body.innerHTML = data;
log-data.js
:
import { data } from "./data.js";
console.log(data);
index.html
:
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>Shared import</title>
<link rel="shortcut icon" href="#" />
</head>
<body>
<script type="module" src="./display-data.js"></script>
<script type="module" src="./log-data.js"></script>
</body>
</html>
Leniwe ładowanie aka lazy load
Lazy load opóźnia ładowanie aplikacji do chwili, w której kod zaczyna być potrzebny. Ta technika optymalizacji jest bardziej skomplikowana niż ładowanie wszystkiego od razu, ale pozwala na większą kontrolę nad tym, co i kiedy się ładuje. W poniższym przykładzie ładuję i wyświetlam dane z około półsekundowym opóźnieniem:
display-data.js
:
setTimeout(
() =>
import("./data.js").then(({ data }) => {
document.body.innerHTML = data;
}),
500
);
data.js
:
export const data = "lorem ipsum";
index.html
:
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>Lazy load</title>
<link rel="shortcut icon" href="#" />
</head>
<body>
<script type="module" src="./display-data.js"></script>
</body>
</html>
Czy moduł ES obejmuje wszystko, czego potrzeba w nowoczesnym JS?
Chociaż natywne moduły ES znacznie poprawiają wcześniejsze modele wprowadzania nowych elementów, brakuje im kilku cech kluczowych w nowoczesnym procesie programowania w JavaScript. Aktualnie nie można wykonać następujących działań:
- Importu typów innych niż JS. Obecnie przygotowywane są inne pliki JSON, ale minie jeszcze trochę czasu, zanim będą one obsługiwane przez przeglądarki.
- Importu zewnętrznych bibliotek tak jak w Node.js. Można by skopiować pliki podczas kompilacji i zaimportować je z lokalizacji w
node_modules
, ale proces ten wydaje się dużo bardziej skomplikowany niż zwyczajneimport “library”
. - Transpilacji. Wiele nowoczesnych kodów JS pisze się w różnych językach – na przykład w TypeScript. Nawet czysty JS wymaga transpilacji do obsługi starszych przeglądarek lub wykorzystywania najnowszych funkcji języków.
Z tych względów większość projektów wykorzystuje narzędzia typu JS bundler, czyli swego rodzaju kompilatorów, które przygotowują build na poszczególne wdrożenia. Jeśli zaciekawił Cię temat bundlerów, daj znać w komentarzu i sprawdź poniższe linki.
Linki
- Repozytorium przykładów,
- wszystkie przykłady,
- mój wideokurs na temat esbuild,
- mój wideokurs na temat webpack.
Podsumowanie
W niniejszym artykule omówiłem istotne przypadki użycia modułów ES. Kolejnym krokiem będzie skonfigurowanie JS bundlera tak, aby obejść ograniczenia modułów natywnych.