Інкапсуляція коду за допомогою модулів

Одним з найбільш схильних до помилок і заплутаних аспектів JavaScript вже давно є shared everything-підхід до завантаження коду. В той час як інші мови мають такі поняття як пакети, JavaScript відстав і все, що визначається в кожному файлі поділяє єдину глобальну область видимості. Тоді, коли веб-додатки стали більш складними, а частота використання JavaScript зросла, shared everything-підхід почав виявляти проблеми з конфліктами імен, проблеми безпеки та багато іншого. Однією з цілей ECMAScript 6 було вирішення цієї проблеми і наведення порядку в додатках JavaScript. Ось тут і з’являються модулі.

Що таке модулі?

Модулі — це JavaScript файли, які завантажуються в іншому режимі (на відміну від скриптів, які завантажуються звичайним для JavaScript чином). Причина необхідності цього режиму в тому, що файли модулів мають дуже відмінну семантику порівняно з файлами скриптів:

  1. Код модуля автоматично виконується в строгому режимі і немає ніякого способу, щоб відмовитися від строгого режиму.
  2. Змінні, створені на вищому рівні модуля, не додаються автоматично до загальної глобальної області видимості. Вони існують лише в межах області видимості модуля.
  3. Значення this в вищому рівні модуля — undefined.
  4. Модулі не дозволяють використання коментарів в стилі HTML у коді (можливість, яка діє від початку існування браузера).
  5. Модулі повинні експортувати будь-що, що має бути доступним за межами модуля.
  6. Модулі можуть імпортувати зв'язування з інших модулів.

Відмінності можуть здаватися незначними на перший погляд, але вони істотним чином змінюють спосіб завантаження та виконання JavaScript, про що я буду розповідати в цьому розділі. Реальною силою модулів є можливість експортувати та імпортувати тільки ті зв'язування, які вам потрібні, а не все, що є у файлі. Добре розуміння експорту та імпорту має фундаментальне значення для розуміння того, як модулі відрізняються від скриптів.

Основи експортування

Ви можете використовувати ключове слово export, щоб надавати частини опублікованого коду іншим модулям. В найпростішому випадку ви можете поставити export на початку оголошення будь-якої змінної, функції або оголошення класу, щоб експортувати їх з модуля. Наприклад:

// експортуємо дані
export var color = "red";
export let name = "Nicholas";
export const magicNumber = 7;

// експортуємо функцію
export function sum(num1, num2) {
   return num1 + num1;
}

// експортуємо клас
export class Rectangle {
   constructor(length, width) {
       this.length = length;
       this.width = width;
   }
}

// це приватна функція модуля
function subtract(num1, num2) {
   return num1 - num2;
}

// визначаємо функцію …
function multiply(num1, num2) {
   return num1 * num2;
}

// … експортуємо пізніше
export { multiply };

Є декілька речей, на які треба звернути увагу в цьому прикладі. По-перше, кожне оголошення з ключовим словом export є точно таким, яким воно було би без нього. Кожна експортована функції або клас також мають ім’я; це тому, що оголошення експортованої функції й класу потребує ім’я. Ви не зможете експортувати анонімні функції або класи, використовуючи цей синтаксис, принаймні без використання ключового слова default (яке буде детально розглянуто в розділі «Значення за замовчуванням у модулях»).

Далі, розглянемо функцію multiply(), що не експортується, коли вона визначена. Це працює, тому що вам не потрібно завжди експортувати оголошення: ви можете також експортувати посилання. Нарешті, зверніть увагу, що цей приклад не експортує функцію subtract(). Ця функція не буде доступна за межами цього модуля, тому що будь-які змінні, функції або класи, які явно не експортуються залишаються приватними для модуля.

Основи імпортування

Якщо ви маєте модуль з експортами, ви можете використовувати експортований функціонал в іншому модулі за допомогою ключового слова import. Дві частини оператора import — це ідентифікатори, які імпортуються та модуль, звідки ті ідентифікатори мають буду імпортовані. Це загальна форма оператора:

import { identifier1, identifier2 } from "./example.js";

Фігурні дужки після import позначають зв’язування, які треба імпортувати з вказаного модуля. Ключове слово from використане, щоб позначити модуль, з якого будуть імпортовані вказані зв’язування. Модуль визначається з використанням рядка, який відображає шлях до модуля (так званий специфікатор модуля). Браузери використовують той самий формат шляху, який ви могли б передати елементу <script>, що означає — ви повинні включати розширення файлу. Node.js, з іншого боку, дотримується свого звичного способу розрізнення між локальними файлами і пакетами, ґрунтуючись на префіксах файлової системи. Наприклад, example — пакет, а ./example.js — локальний файл.

I> Список зв’язувань для імпорту виглядає подібно до деструктурованого об’єкту, але не є ним.

За імпортування зв'язувань з модуля, воно поводиться так, ніби його було визначено за допомогою const. Це означає, що ви не можете визначити іншу змінну з тим же ім'ям (в тому числі імпорту іншого зв'язування з тим же ім'ям), використовуйте ідентифікатор до оголошення import або змініть його значення.

Імпортування одиничного зв'язування

Припустимо, що перший приклад в розділі «Основи експортування» знаходиться в модулі з ім'ям example.js. Ви можете імпортувати і використовувати зв'язування з цього модуля кількома шляхами. Наприклад, ви можете просто імпортувати одне зв'язування:

// імпортуємо тільки одне
import { sum } from "./example.js";

console.log(sum(1, 2));     // 3

sum = 1;        // помилка

Незважаючи на те, що example.js експортує більше, ніж просто одну функцію, цей приклад імпортує тільки функцію sum(). При спробі присвоїти нове значення sum, результатом буде помилка, оскільки ви не можете перевизначити імпортовані зв'язування.

W> Переконайтеся в тому, щоб включити /, ./, чи ../ на початку файлу який ви імпортуєте для кращої сумісності в різних браузерах і Node.js.

Імпортування кількох зв’язувань

Якщо ви хочете імпортувати кілька зв’язувань з модуля, ви можете явно перерахувати їх таким чином:

// множинний імпорт
import { sum, multiply, magicNumber } from "./example.js";
console.log(sum(1, magicNumber));   // 8
console.log(multiply(1, 2));        // 2

Тут, з модуля імпортуються три зв'язування: sum, multiply та magicNumber. Потім вони використовуються так, наче вони були визначені локально.

Імпортування всього з модуля

Є також особливий випадок, який дозволяє імпортувати весь модуль як єдиний об'єкт. Все експортоване буде доступно у цьому об'єкті в якості властивостей. Наприклад:

// імпортуємо все
import * as example from "./example.js";
console.log(example.sum(1,
       example.magicNumber));          // 8
console.log(example.multiply(1, 2));    // 2

У цьому коді всі експортовані зв'язування з example.js завантажуються в об'єкт під назвою example. Іменовані експорти (функція sum() , функція multiple() і magicNumber) потім доступні як властивості в example. Цей формат імпорту називається імпорт простору імен, оскільки об'єкт example не існує всередині файлу example.js і замість цього створюється для використання в якості об'єкта простору імен для всіх експортованих членів example.js.

Однак, майте на увазі, що незалежно від того, скільки разів ви використовуєте модуль в операторі import, модуль буде виконуватися тільки один раз. Після того, як код імпорту модуля виконаний, інстанційований модуль зберігається в пам'яті і повторно використовуватися завжди, коли import знову буде звертатися до нього. Зверніть увагу на таке:

import { sum } from "./example.js";
import { multiply } from "./example.js";
import { magicNumber } from "./example.js";

Незважаючи на те, що є три оголошення import в даному модулі, код з example.js буде виконуватися тільки один раз. Якщо інші модулі в тому ж самому додатку повинні були імпортувати зв'язування з example.js, ці модулі будуть використовувати один і той же екземпляр модуля, що використовує цей код.

Обмеження синтаксису модуля

Важливим обмеженням як export так і import є те, що вони повинні бути використані за межами інших операторів і функцій. Наприклад, цей код буде давати синтаксичну помилку:

if (flag) {
 export flag;    // синтаксична помилка
}

Вираз export всередині оператору if, не дозволяється. Експорт не може бути умовним або зроблений динамічно будь-яким чином. Однією з причин цього синтаксису модуля, є можливість дозволити рушію JavaScript визначити, що буде експортовано статично. Таким чином, ви можете використовувати export тільки на найвищому рівні модуля.

Крім того, ви не можете використовувати import всередині виразу; ви можете використовувати його тільки на найвищому рівні. Це означає, що цей код також спричинить синтаксичну помилку:

function tryImport() {
 import flag from "./example.js";    // синтаксична помилка
}

Ви не можете динамічно імпортувати зв'язування з тієї ж причини з якої ви не можете динамічно їх експортувати. Ключові слова export та import створені для того, щоб бути статичними, тому інструменти, такі як текстові редактори можуть легко сказати, яка інформація доступна з модуля.

Тонкий момент в імпортуванні зв'язування

В ECMAScript 6 вираз import створює доступні лише для читання зв'язування до змінних, функцій і класів, а не просто посилання на оригінальні зв'язування як звичайні змінні. Навіть незважаючи на те, що модуль який імпортує зв'язування не може змінити його значення, модуль, який експортує цей ідентифікатор може. Наприклад, припустимо, що ви хочете використовувати цей модуль:

export var name = "Nicholas";
export function setName(newName) {
   name = newName;
}

При імпорті цих двох зв'язувань, функція setName() може змінити значення name:

import { name, setName } from "./example.js";

console.log(name);       // "Nicholas"
setName("Greg");
console.log(name);       // "Greg"

name = "Nicholas";       // помилка

Виклик setName("Greg") повертається до модуля, з якого експортувалась setName() і виконується там, встановлюючи name в "Greg". Зверніть увагу, ця зміна автоматично відображається на імпортованому зв'язуванні name. Це тому, що name є локальним ім'ям для ідентифікатора name, який експортується. name використане в коді вище і name, використане в модулі з якого було імпортоване — це не одне й те ж саме.

Перейменування імпортів і експортів

Іноді, ви можете не захотіти використовувати оригінальне ім'я змінної, функції або класу яку ви імпортували з модуля. На щастя, ви можете змінити назву експорту як при експорті, так і при імпорті.

У першому випадку, припустимо, що у вас є функція, яку ви хотіли б експортувати з іншим ім'ям. Ви можете використовувати ключове слово as, щоб вказати ім'я з яким функція повинна бути відома за межами модуля:

function sum(num1, num2) {
   return num1 + num2;
}

export { sum as add };

Тут, функція sum() (sum — це локальне ім’я) експортована як add() (add — це експортоване ім’я). Це означає, що коли інший модуль хоче імпортувати цю функцію, він має використовувати ім'я add:

import { add } from "./example.js";

Якщо модуль під час імпортування функції хоче використовувати для неї інше ім’я, він також може використовувати as:

import { add as sum } from "./example.js";
console.log(typeof add);            // "undefined"
console.log(sum(1, 2));             // 3

Цей код імпортує функцію add() використовуючи імпортоване ім’я та перейменовує її на sum() (локальне ім’я). Це означає, що в цьому модулі більше немає ідентифікатора з ім’ям add.

Значення за замовчуванням в модулях

Синтаксис модуля дійсно оптимізовано для експорту та імпорту значень за замовчуванням з модулів, оскільки ця модель була досить поширеним явищем в інших системах модулів, як от CommonJS (інший специфікації для використання JavaScript поза браузером). Значення за замовчуванням для модуля є однією змінною, функцією або класом, визначеним за допомогою ключового слова default, і ви можете встановити тільки один експорт за замовчуванням для кожного модуля. Використання ключового слова default з декількома експортами є синтаксичної помилкою.

Експорт значень за замовчуванням

Ось простий приклад, як використовується ключове слово default:

export default function(num1, num2) {
   return num1 + num2;
}

Цей модуль за замовчуванням експортує функцію. Ключове слово default вказує на те, що цей експорт є експортом за замовчуванням, а функція не потребує імені, оскільки сама функція є модулем.

Крім того, можна вказати ідентифікатор як експорт за замовчуванням, помістивши його після export default, наприклад:

function sum(num1, num2) {
   return num1 + num2;
}

export default sum;

Тут функція sum() визначена першою, а потім експортована в якості значення за замовчуванням для модуля. Ви можете вибрати цей підхід, якщо необхідно обчислити значення за замовчуванням.

Третій спосіб вказати ідентифікатор експорту як значення за замовчуванням є використання синтаксису перейменування, який виглядає наступним чином:


function sum(num1, num2) {
 return num1 + num2;
}

export { sum as default };

Ідентифікатор default має особливе значення в перейменуванні експорту і вказує, що значення має бути значенням за замовчуванням для модуля. Оскільки default — це ключове слово в JavaScript, воно не може бути використане для змінної, функції або ім’я класу (але воно може бути використане в якості ім’я властивості). Таким чином, використання default для перейменування експорту є окремим випадком, щоб створити узгодженість з тим, як визначається структура експорту не за замовчуванням. Цей синтаксис корисний, якщо ви хочете використовувати один екземпляр export щоб визначити відразу кілька експортів, в тому числі за замовчуванням.

Імпорт значень за замовчуванням

Ви можете імпортувати значення за замовчуванням з модуля, використовуючи такий синтаксис:

// імпорт за замовчуванням
import sum from "./example.js";

console.log(sum(1, 2));     // 3

Цей оператор імпорту імпортує значення за замовчуванням з модуля example.js. Зверніть увагу, що фігурні дужки не використовуються, на відміну від того, що ви бачили в імпорті не за замовчуванням. Локальне ім’я sum використовується для позначення будь-якої функції за замовчуванням яку експортує модуль. Цей синтаксис є найчистішим, і творці ECMAScript 6 очікують, що він буде домінуючою формою імпорту в Інтернеті, що дозволяє вам використовувати вже існуючий об'єкт.

Для модулів, які експортують, як значення за замовчуванням, так і один або кілька зв'язувань не за замовчуванням, ви можете імпортувати всі експортовані зв'язування за допомогою одного оператора. Наприклад, припустимо, що у вас є такий модуль:

export let color = "red";

export default function(num1, num2) {
   return num1 + num2;
}

Ви можете імпортувати як color, так і функцію за замовчуванням, використовуючи оператор import наступним чином:

import sum, { color } from "./example.js";

console.log(sum(1, 2));     // 3
console.log(color);         // "red"

Кома відділяє локальне ім'я за замовчуванням від імен не за замовчуванням, які також оточені фігурними дужками. Майте на увазі, що за значення за замовчуванням повинне йти перед іменами не за замовчуванням в виразі import.

Як і при експорті за замовчуванням, ви можете імпортувати значення за замовчуванням також з синтаксисом перейменування:

// еквівалентно попередньому прикладу
import { default as sum, color } from "example";

console.log(sum(1, 2));     // 3
console.log(color);         // "red"

У цьому коді, експорт за замовчуванням (default) перейменовується в sum і додатковий експорт color також імпортується. Цей приклад еквівалентний попередньому прикладу.

Переекспортування зв’язувань

Може статись так, що вам потрібно буде переекспортувати щось, що ваш модуль імпортував (наприклад, якщо ви створюєте бібліотеку з кількох невеликих модулів). Ви можете зробити це, використовуючи шаблони вже розглянуті в цьому розділі, такі як:

import { sum } from "./example.js";
export { sum }

Це працює, але є також один оператор, який може зробити те ж саме:

export { sum } from "./example.js";

Ця форма export шукає в зазначеному модулі оголошення sum, а потім експортує його. Звичайно, ви можете також вибрати для експорту інше ім'я:

export { sum as add } from "./example.js";

Тут sum імпортоване з "./example.js", а потім експортоване як add.

Якщо ви хочете експортувати все з іншого модуля, то ви можете використовувати шаблон *:

export * from "./example.js";

При експорті всього, ви разом з усіма експортами, що мають ім’я, експортуєте також і замовчування, які можуть вплинути на те, що ви можете експортувати з вашого модуля. Наприклад, якщо "example.js" має експорт за замовчуванням, ви будете не в змозі визначити новий експорту за замовчуванням при використанні цього синтаксису.

Імпортування без зв’язування

Деякі модулі можуть нічого не експортувати, а, замість цього, тільки вносити зміни в об'єкти в глобальній області видимості. Навіть якщо змінні верхнього рівня, функції і класи всередині модулів автоматично не потрапляють в глобальну область видимості, це не означає, що модулі не можуть отримати доступ до неї. Загальні визначення вбудованих об'єктів, таких як Array іObject, доступні всередині модуля і зміни до цих об'єктів будуть віддзеркалені в інших модулях.

Наприклад, припустимо, що ви хочете додати метод до всіх масивів з ім’ям pushAll(), ви можете визначити модуль наступним чином:

// Код модуля без експортів чи імпортів
Array.prototype.pushAll = function(items) {

   // items має бути масивом
   if (!Array.isArray(items)) {
       throw new TypeError("Argument must be an array.");
   }

   // використовуємо вбудовані push() та spread оператор
   return this.push(...items);
};

Це дійсний модуль, навіть якщо немає експорту або імпорту. Цей код може бути використаний як в якості модуля, так і сценарію. Оскільки він не експортує нічого, ви можете використовувати спрощений імпорт для виконання коду модуля без імпорту будь-яких зв’язувань:

import "./example.js";

let colors = ["red", "green", "blue"];
let items = [];

items.pushAll(colors);

Цей код імпортує та виконує модуль, що містить метод pushAll(), таким чином pushAll() додається до прототипу масиву. Це означає, що pushAll() тепер доступний для використання у всіх масивах всередині цього модуля.

I> Імпорти без зв’язування, швидше за все, будуть використовуватися для створення поліфілів (polyfills) та шимів (shims).

Завантаження Модулів

В той час як ECMAScript 6 визначає синтаксис для модулів, він не визначає як їх завантажувати. Це частина складності специфікації, яка повинна буди агностичною до реалізації в різних оточеннях. Замість того щоб намагатися створити єдину специфікацію, яка буде працювати для всіх середовищ JavaScript, ECMAScript 6 визначає тільки синтаксис і тези з механізму завантаження який залишаэться невизначеним до внутрішньої операції під назвою HostResolveImportedModule. Веб-браузерам та Node.js надається можливість вирішити, як реалізувати HostResolveImportedModule шляхом, що буде мати сенс для їх середовищ.

Використання модулів в веб-браузерах

Ще до ECMAScript 6, веб-браузери мали кілька способів щоб включати JavaScript у веб-додатки. Цими опціями завантаження скриптів є:

  1. Завантаження файлів коду JavaScript за допомогою елемента <script> з атрибутом src, що вказує місце розташування, з якого завантажується код.
  2. Вбудовування коду JavaScript безпосередньо в потік елементів HTML документа за допомогою елемента <script> без атрибута src.
  3. Завантаження коду файлів JavaScript для виконання в якості "воркера" (наприклад, "веб-воркера" або "сервіс-воркера").

Для того, щоб повною мірою підтримувати модулі, веб-браузери повинні були оновити кожен з цих механізмів. Ці деталі визначені в HTML специфікації, і я буду підсумовувати їх в цьому розділі.

Використання модулів з <script>

Поведінкою за замовчуванням для елемента <script> є завантаження файлів JavaScript, як скриптів (не модулів). Це відбувається, коли атрибут type відсутній або коли атрибут type містить контент типу JavaScript (наприклад, "text/javascript"). Елемент <script> може потім виконати вбудований код або завантажити файл, вказаний в src. Для підтримки модулів, значення "module" було додано в якості опції type. Встановлення type в значення "module" повідомляє браузеру, що завантажувати будь-який вбудований код або код, що міститься у файлі, заданим в src треба як модуль, а не як скрипт. Ось простий приклад:

<!-- завантажити модуль з JavaScript файлу -->
<script type="module" src="module.js"></script>

<!-- вбудоване завантаження модуля -->
<script type="module">

import { sum } from "./example.js";

let result = sum(1, 2);

</script>

Перший елемент <script> в цьому прикладі завантажує зовнішній файл модуля, використовуючи атрибут src. Єдиною відмінністю від завантаження сценарію є те, що "module" зазначений в type. Другий елемент <script> містить модуль, який вбудований безпосередньо в веб-сторінку. Змінна result не доступна глобально, тому що вона існує тільки всередині модуля (як це визначено в елементі <script>) і, отже, не додається до window в якості властивості.

Як ви можете бачити, підключення модулів на веб-сторінках досить просте і схоже з підключенням скриптів. Проте, є деякі відмінності в тому, як завантажуються модулі.

I> Можливо, ви помітили, що "module" не є типом вмісту, як тип "text/javascript". Файли модуля JavaScript будуть обслуговуватись з тим же типом вмісту у вигляді файлів скриптів JavaScript, так що не можливо диференціювати модулі виключно на основі типу вмісту. Крім того, браузери ігнорують елементи <script>, коли type незрозумілий, тому браузери, які не підтримують модулі будуть автоматично ігнорувати рядок <script type="module">, забезпечуючи хорошу зворотну сумісність.

Послідовність завантаження модулів у веб-браузері

Модулі є унікальними в тому, що, на відміну від скриптів, вони можуть використовувати import, щоб вказати, що інші файли повинні бути завантажені, щоб правильно виконати скрипт. Для підтримки цієї функціональності, <script type="module"> завжди діє так, ніби було застосовано атрибут defer.

Атрибут defer не є обов'язковим для завантаження скриптів, але завжди застосовується для завантаження файлів модуля. Файл модуля почне завантажуватися, як тільки HTML парсер зустріне <script type="module"> з атрибутом src, але не буде виконуватися до тих пір, поки документ не буде повністю проаналізовано. Модулі також виконуються в тому порядку, в якому вони з'являються в HTML-файлі. Це означає, що перший <script type="module"> завжди гарантовано буде виконаний перед другим, навіть якщо один модуль містить вбудований код замість вказаного src. Наприклад:

<!-- цей буде виконуватися першим -->
<script type="module" src="module1.js"></script>

<!-- цей буде виконано другим -->
<script type="module">
import { sum } from "./example.js";

let result = sum(1, 2);
</script>

<!-- цей буде виконуватися третім -->
<script type="module" src="module2.js"></script>

Ці три елементи <script> виконуються в порядку, в якому вони вказані, тому module1.js буде гарантовано виконаний до вбудованого модуля, а вбудований модуль гарантовано буде виконаний перед module2.js.

Кожен модуль може мати import з одного або декількох інших модулів, що ускладнює справу. Тому модулі спочатку повністю аналізуються, щоб ідентифікувати всі оператори import. Кожен оператор import потім запускає вибірку (з мережі або з кешу), і жоден модуль не виконується до тих пір, поки не будуть завантажені і проаналізовані всі ресурси import.

Всі модулі, ті що явно включені за допомогою <script type="module"> і ті, що неявно включені за допомогою import, завантажуються і виконуються в заданому порядку. У попередньому прикладі, повна послідовність завантаження:

  1. Завантажити і проаналізувати module1.js.
  2. Рекурсивно завантажити і проаналізувати import ресурси в module1.js.
  3. Проаналізувати вбудований модуль.
  4. Рекурсивно завантажити і проаналізувати import ресурси у вбудованому модулі.
  5. Завантажити і проаналізувати module2.js.
  6. Рекурсивно завантажити і проаналізувати import ресурси в module2.js

Після завершення завантаження, нічого не виконується до тих пір, доки документ не буде повністю розібраний. Після завершення синтаксичного аналізу документа, відбувається наступне:

  1. Рекурсивно виконати import ресурсів для module1.js.
  2. Виконання module1.js.
  3. Рекурсивно виконати import ресурсів для вбудованого модуля.
  4. Виконання вбудованого модуля.
  5. Рекурсивно виконати import ресурсів для module2.js.
  6. Виконання module2.js.

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

I> Атрибут defer ігнорується для <script type="module">, тому що він вже веде себе так наче defer вже застосовується.

Асинхронне завантаження модулів в веб-браузерах

Можливо, ви вже знайомі з async атрибутом для елемента <script>. При використанні з скриптами, async гарантує, що файл сценарію буде виконано, як тільки він буде повністю завантажений і розібраний. Порядок async скриптів в документі не впливає на порядок, в якому виконуються сценарії. Сценарії завжди виконуються, як тільки закінчилось їх завантаження, не чекаючи повного розбору документа, що їх містить.

Атрибут async може бути застосований також і до модулів. Використання async з <script type="module"> змушує модуль поводитись аналогічно скрипту. Єдина відмінність полягає в тому, що всі ресурси з import у модулі завантажуються до того, як буде виконано сам модуль. Це гарантує, що всі ресурси модуля які потрібні йому щоб функціонувати будуть завантажені до того, як сам модуль буде виконано; ви просто не можете гарантувати коли модуль буде виконуватися. Розглянемо наступний код:

<!-- немає ніякої гарантії, який з них буде виконаний першим -->
<script type="module" async src="module1.js"></script>
<script type="module" async src="module2.js"></script>

У цьому прикладі є два файли модулів, завантажені в асинхронному режимі. Неможливо сказати, який модуль буде виконуватися першим, просто глянувши на цей код. Якщо module1.js закінчить завантаження першим (включаючи всі його ресурси import), то він буде виконуватись в першу чергу. Якщо module2.js закінчить завантаження першим, то цей модуль буде виконуватися в першу чергу.

Завантаження модулів у якості “воркерів”

Воркери, як веб-воркери або сервіс-воркери, виконують код JavaScript поза контекстом веб-сторінки. Створення нового воркера включає в себе створення нового екземпляру Worker (або інший клас) і передачу його в файл JavaScript. Механізм завантаження за замовчуванням для завантаження файлів у вигляді скриптів, виглядає так:

// завантажити script.js як скрипт
let worker = new Worker("script.js");

Для підтримки завантаження модулів, розробники стандарту HTML додали другий аргумент для цих конструкторів. Другим аргументом є об'єкт з властивістю type зі значенням за замовчуванням "script". Ви можете встановити type в "module" для того, щоб завантажити файли модулів:

// завантажити module.js як модуль
let worker = new Worker("module.js", { type: "module" });

Цей приклад завантажує module.js як модуль замість скрипту, передавши другий аргумент "module" як значення властивості type. (Властивість type призначена для імітації того, як атрибут type у <script> диференціює модулі і скрипти.) Другий аргумент підтримується для всіх типів воркерів у браузері.

Модулі-воркери, як правило, такі самі, як і скрипти-воркери, але є кілька винятків. По-перше, скрипти-воркери обмежені завантаженням з того місця, що і веб-сторінка, на яку вони посилаються, але модулі-воркери не настільки обмежені. Хоча модулі-воркери мають однакові обмеження за замовчуванням, вони можуть також завантажувати файли, які мають відповідні Cross-Origin Resource Sharing (CORS) заголовки для розширення прав доступу. По-друге, в той час як скрипт-воркер може використовувати метод self.importScripts() для завантаження додаткових скриптів у воркер, self.importScripts() завжди зазнає невдачі у модулі-воркері, тому що ви повинні використовувати import.

Специфіка підключення браузерних модулів

Всі приклади в розділі до цього часу використовували відносний шлях специфікатору модуля, такий як "./example.js". Браузери вимагають щоб шляхи специфікатору модуля були в одному з наступних форматів:

  • Починаючи з /, щоб підключити модуль з кореневого каталогу
  • Починаючи з ./, щоб підключити модуль з поточного каталогу
  • Починаючи з ../, щоб підключити модуль з батьківського каталогу
  • Формат URL

Наприклад, припустимо, що у вас є файл модуля, розташований в https://www.example.com/modules/module.js який містить наступний код:

// імпорт з https://www.example.com/modules/example1.js
import { first } from "./example1.js";

// імпорт з https://www.example.com/example2.js
import { second } from "../example2.js";

// імпорт з https://www.example.com/example3.js
import { third } from "/example3.js";

// імпорт з https://www2.example.com/example4.js
import { fourth } from "https://www2.example.com/example4.js";

Кожен з специфікаторів модулів в даному прикладі є валідними для використання в браузері, в тому числі повний URL в останньому рядку (ви повинні бути впевнені, що ww2.example.com має належним чином налаштований свій Cross-Origin Resource Sharing (CORS) заголовок, щоб дозволити міждоменні завантаження). Це єдиний специфікатор форматів модулів, що браузери можуть виконати за замовчуванням (хоча поки ще не повна специфікація завантаження модулів забезпечить інщі шляхи завантаження також інших форматів). Це означає, що деякі на перший погляд нормальні специфікатори модулів насправді є недійсними в браузерах і призведуть до помилки, наприклад:

// недійсний - не починається з /, ./, чи ../
import { first } from "example.js";

// недійсний - не починається з  /, ./, чи ../
import { second } from "example/index.js";

Кожен з цих специфікаторів модулів не може бути завантажений браузером. Два специфікатори модуля мають невірний формат (немає правильних перших символів) незважаючи навіть на те, що обидва будуть працювати, коли будуть використані в якості значення для src в елементі <script>. Це суттєва відмінність в поведінці між <script> та import.

Підсумок

ECMAScript 6 додає модулі до мови як спосіб пакування і інкапсуляції функціональності. Модулі поводяться інакше, ніж скрипти, оскільки вони не змінюють глобальну область видимості їхніми змінними верхнього рівня, функціями і класами, і this є undefined. Для того, щоб працювати не так, як скрипти, модулі повинні бути завантажені в інший спосіб.

Ви повинні експортувати будь-які функціональні можливості, які б ви хотіли зробити доступними для використання у модулях. Змінні, функції і класи можуть бути експортовані, також для кожного модуля допускається один експорт за замовчуванням. Після експорту, інший модуль може імпортувати всі або деякі з експортованих імен. Ці імена діють так, ніби їх було визначено через let, і тому поводяться як блокові зв’язування, які не можуть бути повторно оголошені в тому ж модулі.

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

Оскільки модулі повинні працювати в іншому режимі, браузери ввели <script type="module"> щоб сигналізувати, що початковий файл або вбудований код має бути виконаний у вигляді модуля. Файли модулів завантажені за допомогою <script type="module"> завантажуються так, ніби до них був застосований атрибут defer. Модулі також виконуються в тому порядку, в якому вони з'являються в документі, як тільки документ буде повністю розібраний.

results matching ""

    No results matching ""