Директиви експорту та імпорту мають декілька варіантів синтаксису.
В попередній статті ми вже бачили спосіб простого використання, тому давайте розглянемо ще декілька прикладів.
Експорт перед оголошенням
Будь-яке оголошення змінної, функції чи класу можна позначати попереду оператором export
.
Наприклад, всі наступні експорти валідні:
// експорт масиву
export let months = ['Січ', 'Лют', 'Бер','Квіт', 'Серп', 'Вер', 'Жов', 'Лист', 'Груд'];
// експорт константи
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(`Привіт, ${user}!`);
} // знак ; відсутній вкінці
Експорт поза оголошенням
Також export
можна використовувати окремо.
В прикладі ми спочатку оголошуємо функцію, а потім експортуємо:
// 📁 say.js
function sayHi(user) {
alert(`Привіт, ${user}!`);
}
function sayBye(user) {
alert(`Бувай, ${user}!`);
}
export {sayHi, sayBye}; // список експортованих змінних
…Чи, технічно, ми можемо використати export
вище оголошення функції.
Імпорт *
Зазвичай, список того, що потрібно імпортувати розташовують у фігурні дужки import {...}
, як у прикладі:
// 📁 main.js
import {sayHi, sayBye} from './say.js';
sayHi('Іван'); // Привіт, Іван!
sayBye('Іван'); // Бувай, Іван!
Якщо потрібно імпортувати дуже багато сутностей, ми можемо імпортувати все, як об’єкт з використанням import * as <obj>
, наприклад:
// 📁 main.js
import * as say from './say.js';
say.sayHi('Іван');
say.sayBye('Іван');
З першого погляду, “імпортувати все” виглядає цікавим та зручним у використанні, тоді навіщо нам явно виписувати список того, що потрібно імпортувати?
На це є декілька причин.
- Явний список того, що потрібно імпортувати дає коротші імена:
sayHi()
замістьsay.sayHi()
. - Явний список того, що потрібно імпортувати дає краще розуміння структури коду: що використано та в якому місці. Також дозволяє підтримувати та рефакторити код легше.
Сучасні інструменти збірки, такі як webpack та інші, об’єднують модулі разом і оптимізують їх для прискорення завантаження. Вони також видаляють імпорти, що не використовуються.
Наприклад, якщо ви import * as library
з величезної бібліотеки коду, а потім використаєте лише кілька методів, тоді невикористані методи не будуть включені у оптимізований бандл.
Імпорт “as”
Для імпорту під іншим іменем можна використовувати as
.
Наприклад, для спрощення імпортуймо sayHi
в локальну змінну hi
та sayBye
як bye
:
// 📁 main.js
import {sayHi as hi, sayBye as bye} from './say.js';
hi('Іван'); // Привіт, Іван!
bye('Іван'); // Бувай, Іван!
Експорт “as”
Подібний синтаксис існує і для export
.
Експортуймо функцію як hi
та bye
:
// 📁 say.js
...
export {sayHi as hi, sayBye as bye};
Тепер hi
та bye
будуть використовуватися зовнішніми модулями при імпорті:
// 📁 main.js
import * as say from './say.js';
say.hi('Іван'); // Привіт, Іван!
say.bye('Іван'); // Бувай, Іван!
Типовий експорт
На практиці існує два головних типи модулів.
- Модулі, що містять бібліотеку – набір функцій, як
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');
Імпорти без фігурних дужок виглядають краще. Коли починають використовувати модулі, поширеною помилкою є невикористання фігурних дужок взагалі. Отже, import
потребує фігурних дужок для іменованих імпортів та не потребує, якщо це типовий імпорт.
Іменований експорт | Типовий експорт |
---|---|
export class User {...} |
export default class User {...} |
import {User} from ... |
import User from ... |
Технічно, ми можемо використовувати іменовані та типові експорти в одному модулі, але, на практиці, їх не прийнято змішувати. Модулі повинні мати або іменований експорт, або типовий.
Оскільки, тільки одна сутність може бути типово експортованою, вона може не мати імені.
В прикладі правильно використано типові експорти:
export default class { // відсутнє ім’я класу
constructor() { ... }
}
export default function(user) { // відсутнє ім’я функції
alert(`Привіт, ${user}!`);
}
// експортовано єдину змінну без оголошення
export default ['Jan', 'Feb', 'Mar','Apr', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
Відсутність імені є абсолютно нормальним, бо може бути тільки одне використання export default
у файлі. Таким чином import
без фігурних дужок знає, що імпортувати.
Такий експорт без default
згенерує помилку:
export class { // Помилка! (типовий експорт потребує імені)
constructor() {}
}
Ім’я “default”
В деяких ситуаціях ключове слово default
використовують для позначення типового імпорту.
Наприклад, для експорту функції окремо від її оголошення:
function sayHi(user) {
alert(`Привіт, ${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(`Привіт, ${user}!`);
}
Для імпорту типової сутності та декількох іменованих потрібно:
// 📁 main.js
import {default as User, sayHi} from './user.js';
new User('Іван');
Урешті-решт, якщо імпортувати всі *
як об’єкт, тоді властивість default
матиме значення типового імпорту:
// 📁 main.js
import * as user from './user.js';
let User = user.default; // типовий імпорт
new User('Іван');
Аргументи проти типових імпортів
Іменовані імпорти є явними. Нам потрібно точно перелічити все, що імпортуємо – це є перевагою.
Іменовані експорти змушують нас використовувати точне ім’я сутності для імпорту:
import {User} from './user.js';
// import {MyUser} не спрацює, оскільки ім’я повинно бути {User}
…В той час, як для типового імпорту нам завжди потрібно обрати ім’я:
import User from './user.js'; // спрацює
import MyUser from './user.js'; // спрацює теж
// можна навіть import Anything... і це все одно ще спрацює
Таким чином різні члени команд можуть використовувати різні імена для імпорту однакових сутностей і це не дуже добре.
Зазвичай, щоб уникати цього і тримати код узгодженим, існує правило – імпортована змінна повинна мати ім’я, що відповідає імені файлу:
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
:
// 📁 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';
...
Суттєвою різницею між export ... from
та import/export
є недоступність реекспортованих модулів всередині поточного файлу. Тому в прикладі вище всередині файлу auth/index.js
ми не зможемо використати реекспортовані функції login/logout
.
Реекспорт типового експорту
Для реекспорту типового експорту потрібна спеціальна обробка.
Скажімо, у нас є user.js
з якого ми хочемо реекспортувати клас User
:
// 📁 user.js
export default class User {
// ...
}
У нас може виникнути дві проблеми:
-
export User from './user.js'
не спрацює. Це призведе до синтаксичної помилки.Для типового реекспорту нам потрібно використати
export {default as User}
, як в прикладі вище. -
export * from './user.js'
реекспортує тільки іменовані експорти і проігнорує типові.Якщо ми хочемо реекспортувати як іменовані експорти, так і типові, тоді нам потрібні дві інструкції:
export * from './user.js'; // для реекспорту іменованих експортів export {default} from './user.js'; // для реекспорту типових експортів
Такі дивацтва для реекспорту типових експортів є однією з причин чому деякі розробники недолюблюють типові експорти та надають перевагу іменованим.
Підсумки
Ось всі типи export
, що ми розглянули в цій та попередній частинах.
Щоб перевірити себе, ви можете прочитати та згадати, що вони означають:
- Перед оголошенням класу/функції/…:
export [default] class/function/variable ...
- Окремий експорт:
export {x [as y], ...}
.
- Реекспорт:
export {x [as y], ...} from "module"
export * from "module"
(не реекспортує типові експорти).export {default [as y]} from "module"
(типовий реекспорт).
Імпорт:
- Імпорт іменованих експортів:
import {x [as y], ...} from "module"
- Імпорт типових експортів:
import x from "module"
import {default as x} from "module"
- Імпортування всього:
import * as obj from "module"
- Тільки імпорт модуля (буде виконано його код) без присвоєння в змінну:
import "module"
Інструкції import/export
можуть бути розташовані як зверху, так і знизу в скрипті.
Тому, технічно, це правильний код:
sayHi();
// ...
import {sayHi} from './say.js'; // імпорт розташовано в кінці файлу
На практиці, імпорти зазвичай розташовують на початку файлу, але це переважно для зручності.
Зверніть увагу, що інструкції імпорту та експорту не спрацюють, якщо розташовані всередині {...}
.
Умовний імпорт, як в наступному прикладі, не спрацює:
if (something) {
import {sayHi} from "./say.js"; // Error: import must be at top level
}
…Що робити, якщо нам потрібно щось імпортувати за певних умов? Або в певний час? Наприклад, завантажити модуль за запитом, коли він дійсно стане потрібним.
Динамічні імпорти буде розглянуто в наступній частині.