2 березня 2025 р.

Експорт та імпорт

Директиви експорту та імпорту мають декілька варіантів синтаксису.

В попередній статті ми вже бачили спосіб простого використання, тому давайте розглянемо ще декілька прикладів.

Експорт перед оголошенням

Будь-яке оголошення змінної, функції чи класу можна позначати попереду оператором export.

Наприклад, всі наступні експорти валідні:

// експорт масиву
export let months = ['Січ', 'Лют', 'Бер','Квіт', 'Серп', 'Вер', 'Жов', 'Лист', 'Груд'];

// експорт константи
export const MODULES_BECAME_STANDARD_YEAR = 2015;

// експорт класу
export class User {
  constructor(name) {
    this.name = name;
  }
}
Не потрібно ставити крапку з комою після експорту класу чи функції

Зверніть увагу, export перед класом чи функцією не робить її функціональним виразом. Це все ще оголошення функції (function declaration), хоч і екпортовуваної.

Більшість стилів 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('Іван');

З першого погляду, “імпортувати все” виглядає цікавим та зручним у використанні, тоді навіщо нам явно виписувати список того, що потрібно імпортувати?

На це є декілька причин.

  1. Явний список того, що потрібно імпортувати дає коротші імена: sayHi() замість say.sayHi().
  2. Явний список того, що потрібно імпортувати дає краще розуміння структури коду: що використано та в якому місці. Також дозволяє підтримувати та рефакторити код легше.
Не бійтеся імпортувати занадто багато

Сучасні інструменти збірки, такі як 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('Іван'); // Бувай, Іван!

Типовий експорт

На практиці існує два основних види модулів.

  1. Модулі, що містять бібліотеку (набір функцій), як say.js вище.
  2. Модулі, що визначають єдину сутність, тобто модуль 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

Навіщо нам це може знадобитися? Розглянемо практичний спосіб використання.

Уявімо, що нам потрібно написати “пакет” (package): директорію з багатьма модулями, що експортує певний функціонал (інструменти як 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 {
  // ...
}

У нас може виникнути дві проблеми:

  1. export User from './user.js' не спрацює. Це призведе до синтаксичної помилки.

    Для типового реекспорту нам потрібно використати export {default as User}, як в прикладі вище.

  2. 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
}

…Що робити, якщо нам потрібно щось імпортувати за певних умов? Або в певний час? Наприклад, завантажити модуль за запитом, коли він дійсно стане потрібним.

Динамічні імпорти буде розглянуто в наступній частині.

Навчальна карта

Коментарі

прочитайте це, перш ніж коментувати…
  • Якщо у вас є пропозиції, щодо покращення підручника, будь ласка, створіть обговорення на GitHub або одразу створіть запит на злиття зі змінами.
  • Якщо ви не можете зрозуміти щось у статті, спробуйте покращити її, будь ласка.
  • Щоб вставити код, використовуйте тег <code>, для кількох рядків – обгорніть їх тегом <pre>, для понад 10 рядків – використовуйте пісочницю (plnkr, jsbin, codepen…)