https://learn.javascript.ru/modules
Модуль - це просто файл. Один скрипт - це один модуль. Модулі можуть завантажувати один одного і використовувати директиви export
і import
, щоб обмінюватися функціональністю, викликати функції одного модуля з іншого:
export
відмічає змінні і функції, які повинні бути доступні поза поточного модуля.import
дозволяє імпортувати функціональність з інших модулів.
Наприклад, якщо у нас є файл sayHi.js
, який експортує функцію:
// 📁 sayHi.js
export function sayHi(user) {
alert(`Hello, ${user}!`);
}
... Тоді інший файл може імпортувати її і використовувати:
// 📁 main.js
import {sayHi} from './sayHi.js';
alert(sayHi); // function...
sayHi('John'); // Hello, John!
Так как модули поддерживают ряд специальных ключевых слов, и у них есть ряд особенностей, то необходимо явно сказать браузеру, что скрипт является модулем, при помощи атрибута <script type="module">
.
Директива import
завантажує модуль по шляху./SayHi.js
щодо поточного файлу і записує експортовану функцію sayHi
в змінну.
Так як модулі підтримують ряд спеціальних ключових слів, і у них є ряд особливостей, то необхідно явно сказати браузеру, що скрипт є модулем, за допомогою атрибута <script type ="module">
. Ось як це виглядає:
say.js
export function sayHi(user) {
return `Hello, ${user}!`;
}
index.html
<!doctype html>
<script type="module">
import {sayHi} from './say.js';
document.body.innerHTML = sayHi('John');
</script>
Браузер автоматично завантажить і запустить імпортований модуль (і ті, які він імпортує, якщо треба), а потім запустить скрипт.
Чим відрізняються модулі від «звичайних» скриптів? Є основні можливості та особливості, що працюють як в браузері, так і в серверному JavaScript.
У модулях завжди використовується режим use strict
. Наприклад, присвоювання до неоголошеної змінної викличе помилку.
<script type="module">
a = 5; // ошибка
</script>
Кожен модуль має свою власну область видимості. Іншими словами, змінні і функції, оголошені в модулі, які не видно в інших скриптах.
У наступному прикладі імпортовані 2 скрипта, і hello.js
намагається використовувати змінну user
, оголошену в user.js
. В результаті буде помилка:
hello.js
alert(user); // в этом модуле нет такой переменной (каждый модуль имеет независимые переменные)
user.js
let user = "John";
index.html
<!doctype html>
<script type="module" src="user.js"></script>
<script type="module" src="hello.js"></script>
Модулі повинні експортувати функціональність, призначену для використання ззовні. А інші модулі можуть її імпортувати. Так що нам треба імпортувати user.js
в hello.js
і взяти з нього потрібну функціональність, замість того щоб покладатися на глобальні змінні.
Правильний варіант буде наступний:
hello.js
import {user} from './user.js';
document.body.innerHTML = user; // John
user.js
export let user = "John";
index.html
<!doctype html>
<script type="module" src="hello.js"></script>
У браузері також існує незалежна область видимості для кожного скрипта <script type ="module">
:
<script type="module">
// Переменная доступна только в этом модуле
let user = "John";
</script>
<script type="module">
alert(user); // Error: user is not defined
</script>
Якщо нам потрібно зробити глобальну змінну рівня всієї сторінки, можна явно привласнити її об'єкту window
, тоді отримати значення змінної можна звернувшись до window.user
. Але це повинно бути винятком, що вимагає вагомої причини.
Якщо один і той же модуль використовується в декількох місцях, то його код виконається тільки один раз, після чого функціональність, що експортується передається всім імпортерам. Це дуже важливо для розуміння роботи модулів. Давайте подивимося приклади. По-перше, якщо при запуску модуля виникають побічні ефекти, наприклад видається повідомлення, то імпорт модуля в декількох місцях покаже його лише один раз - при першому імпорті:
// 📁 alert.js
alert("Модуль выполнен!");
// Импорт одного и того же модуля в разных файлах
// 📁 1.js
import `./alert.js`; // Модуль выполнен!
// 📁 2.js
import `./alert.js`; // (ничего не покажет)
На практиці, завдання коду модуля - це зазвичай ініціалізація, створення внутрішніх структур даних, а якщо ми хочемо, щоб щось можна було використовувати багато разів, то експортуємо це. Тепер більш просунутий приклад. Давайте уявимо, що модуль експортує об'єкт:
// 📁 admin.js
export let admin = {
name: "John"
};
Якщо модуль імпортується в декількох файлах, то код модуля буде виконаний тільки один раз, об'єкт admin
буде створений і в подальшому буде переданий всім імпортерам. Всі Імпортери отримають один-єдиний об'єкт admin
:
// 📁 1.js
import {admin} from './admin.js';
admin.name = "Pete";
// 📁 2.js
import {admin} from './admin.js';
alert(admin.name); // Pete
// Оба файла, 1.js и 2.js, импортируют один и тот же объект
// Изменения, сделанные в 1.js, будут видны в 2.js
Ще раз зауважимо - модуль виконується тільки один раз. Генерується експорт і після передається всім імпортерам, тому, якщо щось зміниться в об'єкті admin
, то інші модулі теж побачать ці зміни. Така поведінка дозволяє конфігурувати модулі при початковому імпорті. Ми можемо встановити його властивості один раз, і в подальших імпортах він буде вже налаштованим. Наприклад, модуль admin.js
надає певну функціональність, але очікує передачі облікових даних в об'єкт admin
ззовні:
// 📁 admin.js
export let admin = { };
export function sayHi() {
alert(`Ready to serve, ${admin.name}!`);
}
В init.js
, першому скрипті нашого застосування, ми встановимо admin.name
. Тоді всі це побачать, включаючи виклики, зроблені з самого admin.js
:
// 📁 init.js
import {admin} from './admin.js';
admin.name = "Pete";
Інший модуль теж побачить admin.name
:
// 📁 other.js
import {admin, sayHi} from './admin.js';
alert(admin.name); // Pete
sayHi(); // Ready to serve, Pete!
Об'єкт import.meta
містить інформацію про поточний модулі. Вміст залежить від оточення. У браузері він містить посилання на скрипт або посилання на поточну веб-сторінку, якщо модуль вбудований в HTML:
<script type="module">
alert(import.meta.url); // ссылка на html страницу для встроенного скрипта
</script>
Це незначна особливість, але для повноти картини нам потрібно згадати про це. У модулі на верхньому рівні this
не визначений (undefined). Порівняємо з не-модульними скриптами, там this
- глобальний об'єкт:
<script>
alert(this); // window
</script>
<script type="module">
alert(this); // undefined
</script>
Є й кілька інших, саме браузерних особливостей скриптів з type =" module "
в порівнянні зі звичайними скриптами.
Модулі завжди виконуються в відкладеному (deferred) режимі, так само, як скрипти з атрибутом defer
(Скрипти: async, defer). Це вірно і для зовнішніх і вбудованих скриптів-модулів. Іншими словами:
- завантаження зовнішніх модулів, таких як
<script type =" module "src ="...">
, не блокує обробку HTML. - модулі, навіть якщо завантажилися швидко, очікують повного завантаження HTML документа, і тільки потім виконуються.
- зберігається відносний порядок скриптів: скрипти, які йдуть раніше в документі, виконуються раніше.
Як побічний ефект, модулі завжди бачать повністю завантажену HTML-сторінку, включаючи елементи під ними. Наприклад:
<script type="module">
alert(typeof button); // object: скрипт может 'видеть' кнопку под ним
// так как модули являются отложенными, то скрипт начнёт выполнятся только после полной загрузки страницы
</script>
<!-- Сравните с обычным скриптом ниже: -->
<script>
alert(typeof button); // Ошибка: кнопка не определена, скрипт не видит элементы под ним
// обычные скрипты запускаются сразу, не дожидаясь полной загрузки страницы
</script>
<button id="button">Кнопка</button>
Будь ласка, зверніть увагу: другий скрипт виконається раніше, ніж перший! Тому ми побачимо спочатку undefined
, а потім object
. Це тому, що модулі починають виконуватися після повного завантаження сторінки. Звичайні скрипти запускаються відразу ж, тому повідомлення зі звичайного скрипта ми бачимо першим. При використанні модулів нам варто мати на увазі, що HTML-сторінка буде показана браузером до того, як виконаються модулі і JavaScript-додаток буде готовий до роботи. Деякі функції можуть ще не працювати. Нам слід розмістити «індикатор завантаження» або щось ще, щоб не збентежити цим відвідувача.
Для не-модульних скриптів атрибут async
працює тільки на зовнішніх скриптах. Скрипти з ним запускаються відразу по готовності, вони не чекають інші скрипти або HTML-документ. Для модулів атрибут async
працює на будь-яких скриптах. Наприклад, в скрипті нижче є async
, тому він виконається відразу після завантаження, не чекаючи інших скриптів. Скрипт виконає імпорт (завантажить ./Analytics.js
) і відразу запуститься, коли буде готовий, навіть якщо HTML документ ще не буде завантажений, або якщо інші скрипти ще завантажуються. Це дуже корисно, коли модуль ні з чим не пов'язаний, наприклад для лічильників, реклами, обробників подій.
<!-- загружаются зависимости (analytics.js) и скрипт запускается -->
<!-- модуль не ожидает загрузки документа или других тэгов <script> -->
<script async type="module">
import {counter} from './analytics.js';
counter.count();
</script>
Зовнішні скрипти з атрибутом type="module"
мають дві відмінності:
- Зовнішні скрипти з однаковим атрибутом
src
запускаються тільки один раз:
<!-- скрипт my.js загрузится и будет выполнен только один раз -->
<script type="module" src="my.js"></script>
<script type="module" src="my.js"></script>
- Зовнішній скрипт, який завантажується з іншого домену, вимагає вказівки заголовків CORS. Іншими словами, якщо модульний скрипт завантажується з іншого домену, то віддалений сервер повинен встановити заголовок
Access-Control-Allow-Origin
означає, що завантаження скрипта дозволене.
<!-- another-site.com должен указать заголовок Access-Control-Allow-Origin -->
<!-- иначе, скрипт не выполнится -->
<script type="module" src="http://another-site.com/their.js"></script>
Це забезпечує кращу безпеку за замовчуванням.
У браузері import
повинен містити відносний або абсолютний шлях до модуля. Модулі без шляху називаються «голими» (bare). Вони не дозволені в import
. Наприклад, цей import
неправильний:
import {sayHi} from 'sayHi'; // Ошибка, "голый" модуль
// путь должен быть, например './sayHi.js' или абсолютный
Інші середовища, наприклад Node.js, допускають використання «голих» модулів, без шляхів, так як в них є свої правила, як працювати з такими модулями і де їх шукати. Але браузери поки не підтримують «голі» модулі.
Старі браузери не розуміють атрибут type="module"
. Скрипти з невідомим атрибутом type
просто ігноруються. Ми можемо зробити для них «резервний» скрипт за допомогою атрибута nomodule
:
<script type="module">
alert("Работает в современных браузерах");
</script>
<script nomodule>
alert("Современные браузеры понимают оба атрибута - и type=module, и nomodule, поэтому пропускают этот тег script")
alert("Старые браузеры игнорируют скрипты с неизвестным атрибутом type=module, но выполняют этот.");
</script>
У реальному житті модулі в браузерах рідко використовуються в «сирому» вигляді. Зазвичай, ми об'єднуємо модулі разом, використовуючи спеціальний інструмент, наприклад Webpack і після викладаємо код на робочий сервер. Одна з переваг використання "складальника" - він надає більший контроль над тим, як модулі шукаються, дозволяє використовувати «голі» модулі та багато іншого «свого», наприклад CSS/HTML-модулів.
Складальник робить наступне:
- Бере «основний» модуль, який ми збираємося помістити в
<script type ="module">
в HTML. - Аналізує залежності (імпорти, імпорти імпортів і так далі)
- Збирає один файл з усіма модулями (або кілька файлів, це можна налаштувати), перезаписує вбудований
import
функцією імпорту від складальника, щоб все працювало. «Спеціальні» типи модулів, такі як HTML/CSS теж підтримуються. - У процесі можуть відбуватися й інші трансформації та оптимізації коду:
- Недосяжний код видаляється.
- Не використовувані експорти видаляються ( «tree-shaking»).
- Специфічні оператори для розробки, такі як
console
іdebugger
, видаляються. - Сучасний синтаксис JavaScript також може бути трансформований в попередній стандарт, зі схожою функціональністю, наприклад, за допомогою Babel.
- Отриманий файл можна мінімізувати (видалити пробіли, замінити назви змінних на більш короткі і т.д.).
Якщо ми використовуємо інструменти збірки, то вони об'єднують модулі разом в один або кілька файлів, і замінюють import/export
на свої виклики. Тому підсумкову збірку можна підключати і без атрибута type ="module"
, як звичайний скрипт:
<!-- Предположим, что мы собрали bundle.js, используя например утилиту Webpack -->
<script src="bundle.js"></script>
Хоча і «як є» модулі теж можна використовувати, а збирач налаштувати пізніше при необхідності.
В предыдущей главе мы видели простое использование, давайте теперь посмотрим больше примеров.
https://learn.javascript.ru/import-export
Ми можемо помітити будь-яке оголошення як експортоване, розмістивши export
перед ним, будь то змінна, функція або клас. Наприклад, всі наступні експорти допустимі:
// экспорт массива
export let months = ['Jan', 'Feb', 'Mar', 'Apr', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
// экспорт константы
export const MODULES_BECAME_STANDARD_YEAR = 2015;
// экспорт класса
export class User {
constructor(name) {
this.name = name;
}
}
Не ставиться крапка з комою після експорту класу/функції.
Зверніть увагу, що export
перед класом або функцією не робить їх функціональним виразом. Це все також оголошення функції, хоча і експортоване. Більшість посібників по стилю коду в JavaScript не рекомендують ставити крапку з комою після оголошень функцій або класів. Тому в кінці export class
і export function
не потрібна крапка з комою:
export function sayHi(user) {
alert(`Hello, ${user}!`);
} // без ; в конце
Також можна написати export
окремо. Тут ми спочатку оголошуємо, а потім експортуємо:
// 📁 say.js
function sayHi(user) {
alert(`Hello, ${user}!`);
}
function sayBye(user) {
alert(`Bye, ${user}!`);
}
export {sayHi, sayBye}; // список экспортируемых переменных
... Або, технічно, ми також можемо розташувати export
вище функцій.
Зазвичай ми маємо в своєму розпорядженні список того, що хочемо імпортувати, в фігурних дужках import {...}
, наприклад ось так:
// 📁 main.js
import {sayHi, sayBye} from './say.js';
sayHi('John'); // Hello, John!
sayBye('John'); // Bye, John!
Але якщо імпортувати потрібно багато чого, ми можемо імпортувати все відразу у вигляді об'єкта, використовуючи import * as <obj>
. наприклад:
// 📁 main.js
import * as say from './say.js';
say.sayHi('John');
say.sayBye('John');
На перший погляд «імпортувати все» виглядає дуже зручно, не треба писати зайвого, навіщо нам взагалі може знадобитися явно перераховувати список того, що потрібно імпортувати?
Для этого есть несколько причин.
- Сучасні інструменти збірки (webpack та інші) збирають модулі разом і оптимізують їх, прискорюючи завантаження і видаляючи невикористаний код. Припустимо, ми додали в наш проект сторонню бібліотеку
say.js
з безліччю функцій:
// 📁 say.js
export function sayHi() { ... }
export function sayBye() { ... }
export function becomeSilent() { ... }
Тепер, якщо з цієї бібліотеки в проекті ми використовуємо тільки одну функцію:
// 📁 main.js
import {sayHi} from './say.js';
... Тоді оптимізатор побачить, що інші функції не використовуються, і видалить інші із зібраного коду, тим самим роблячи код менше. Це називається «tree-shaking».
- Явно перераховуючи те, що хочемо імпортувати, ми отримуємо більш короткі імена функцій:
sayHi()
замістьsay.sayHi()
. - Явна перерахування імпорту робить код більш зрозумілим, дозволяє побачити, що саме і де використовується. Це спрощує підтримку і рефакторинг коду.
Ми також можемо використовувати as
, щоб імпортувати під іншими іменами. Наприклад, для стислості імпортуємо sayHi
в локальну змінну hi
, а sayBye
імпортуємо як bye
:
// 📁 main.js
import {sayHi as hi, sayBye as bye} from './say.js';
hi('John'); // Hello, John!
bye('John'); // Bye, John!
Аналогічний синтаксис існує і для export
. Давайте експортуємо функції, як hi
і bye
:
// 📁 say.js
...
export {sayHi as hi, sayBye as bye};
Тепер hi
і bye
- офіційні імена для зовнішнього коду, їх потрібно використовувати при імпорті:
// 📁 main.js
import * as say from './say.js';
say.hi('John'); // Hello, John!
say.bye('John'); // Bye, John!
На практиці модулі зустрічаються в основному одного з двох типів:
- Модуль, що містить бібліотеку або набір функцій, як
say.js
вище. - Модуль, який оголошує щось одне, наприклад модуль
user.js
експортує тількиclass User
.
Здебільшого, зручніше другий підхід, коли кожна «річ» знаходиться в своєму власному модулі. Природно, потрібно багато файлів, якщо для всього робити окремий модуль, але це не проблема. Так навіть зручніше: навігація по проекту стає простіше, особливо, якщо у файлів хороші імена, і вони структуровані по папках. Модулі надають спеціальний синтаксис export default
(«експорт за замовчуванням») для другого підходу. Ставимо export default
перед тим, що потрібно експортувати:
// 📁 user.js
export default class User { // просто добавьте "default"
constructor(name) {
this.name = name;
}
}
Зауважимо, у файлі може бути не більше одного export default
. ... І потім імпортуємо без фігурних дужок:
// 📁 main.js
import User from './user.js'; // не {User}, просто User
new User('John');
Імпорт без фігурних дужок виглядає красивіше. Звичайна помилка початківців: забувати про фігурні дужки. Запам'ятаємо: фігурні дужки необхідні в разі іменованих експортом, для export default
вони не потрібні.
Іменований експорт | Експорт за замовченням |
---|---|
export class User {...} |
export default class User {...} |
import {User} from ... |
import User from ... |
Технічно в одному модулі може бути як експорт за замовчуванням, так і іменовані експорти, але на практиці зазвичай їх не змішують. Тобто, в модулі знаходяться або іменовані експорти, або один експорт за замовчуванням. Так як в файлі може бути максимум один export default
, то експортована сутність не зобов'язана мати ім'я. Наприклад, все це - повністю коректні експорти за замовчуванням:
export default class { // у класса нет имени
constructor() { ... }
}
export default function(user) { // у функции нет имени
alert(`Hello, ${user}!`);
}
// экспортируем значение, не создавая переменную
export default ['Jan', 'Feb', 'Mar','Apr', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
Це нормально, тому що може бути тільки один export default
на файл, так що import
без фігурних дужок завжди знає, що імпортувати. Без default
такий експорт видав би помилку:
export class { // Ошибка! (необходимо имя, если это не экспорт по умолчанию)
constructor() {}
}
У деяких ситуаціях для позначення експорту за замовчуванням в якості імені використовується default
. Наприклад, щоб експортувати функцію окремо від її оголошення:
function sayHi(user) {
alert(`Hello, ${user}!`);
}
// то же самое, как если бы мы добавили "export default" перед функцией
export {sayHi as default};
Або, ще ситуація, давайте уявимо наступне: модуль user.js
експортує одну сутність «за замовчуванням» і кілька іменованих (рідкісний, але можливий випадок):
// 📁 user.js
export default class User {
constructor(name) {
this.name = name;
}
}
export function sayHi(user) {
alert(`Hello, ${user}!`);
}
Ось як імпортувати експорт за замовчуванням разом з іменованих експортом:
// 📁 main.js
import {default as User, sayHi} from './user.js';
new User('John');
І, нарешті, якщо ми імпортуємо все як об'єкт import *
, тоді його властивість default
- якраз і буде експортом за замовчуванням:
// 📁 main.js
import * as user from './user.js';
let User = user.default; // экспорт по умолчанию
new User('John');
Іменовані експорти «включають в себе» своє ім'я. Ця інформація є частиною модуля, говорить нам, що саме експортується. Іменовані експорти змушують нас використовувати правильне ім'я при імпорті:
import {User} from './user.js';
// import {MyUser} не сработает, должно быть именно имя {User}
... У той час як для експорту за замовчуванням ми вибираємо будь-яке ім'я при імпорті:
import User from './user.js'; // сработает
import MyUser from './user.js'; // тоже сработает
// можно импортировать с любым именем, и это будет работать
Так що члени команди можуть використовувати різні імена для імпорту однієї і тієї ж речі, і це не дуже добре. Зазвичай, щоб уникнути цього і дотримати одноманітність коду, є правило: імена імпортованих змінних повинні відповідати іменам файлів. Ось так:
import User from './user.js';
import LoginForm from './loginForm.js';
import func from '/path/to/func.js';
...
Проте, в деяких командах це вважають серйозним аргументом проти експорту за замовчуванням і вважають за краще використовувати іменовані експорти всюди. Навіть якщо експортується тільки одна річ, вона все одно експортується з ім'ям, без використання default
. Це також трохи спрощує реекспорт (дивіться нижче).
Синтаксис «реекспорту» export ... from ...
дозволяє імпортувати щось і тут же експортувати, можливо під іншим ім'ям, ось так:
export {sayHi} from './say.js'; // реэкспортировать sayHi
export {default as User} from './user.js'; // реэкспортировать default
Навіщо це потрібно? Розглянемо практичний приклад використання. Уявімо, що ми пишемо «пакет»: папку з безліччю модулів, з якої частина функціональності експортується назовні (інструменти на зразок NPM дозволяють нам публікувати і поширювати такі пакети), а багато модулів - просто допоміжні, для внутрішнього використання в інших модулях пакета. Структура файлів може бути такою:
auth/
index.js
user.js
helpers.js
tests/
login.js
providers/
github.js
facebook.js
...
Ми б хотіли зробити функціональність нашого пакета доступною через єдину точку входу: «головний файл» auth/index.js
. Щоб можна було використовувати її в такий спосіб:
import {login, logout} from 'auth/index.js'
Ідея в тому, що зовнішні розробники, які будуть використовувати наш пакет, не повинні розбиратися з його внутрішньою структурою, ритися в файлах всередині нашого пакета. Все, що потрібно, ми експортуємо в auth/index.js
, а решта приховуємо від зацікавлених поглядів. Так як потрібна функціональність може бути розкидана по модулях нашого пакета, ми можемо імпортувати їх в auth/index.js
і тут же експортувати назовні.
// 📁 auth/index.js
// импортировать login/logout и тут же экспортировать
import {login, logout} from './helpers.js';
export {login, logout};
// импортировать экспорт по умолчанию как User и тут же экспортировать
import User from './user.js';
export {User};
...
Тепер користувачі нашого пакета можуть писати import {login} from" auth/index.js "
. Запис export ... from ...
- це просто більш короткий варіант такого імпорту-експорту:
// 📁 auth/index.js
// импортировать login/logout и тут же экспортировать
export {login, logout} from './helpers.js';
// импортировать экспорт по умолчанию как User и тут же экспортировать
export {default as User} from './user.js';
...
https://learn.javascript.ru/import-export#reeksport-eksporta-po-umolchaniyu