Модули
Информация для этой страницы была позаимствована с сайта learn.javascript.ru

Модуль – это просто файл. Один скрипт – это один модуль.

Модули могут загружать друг друга и использовать директивы 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!

Директива import загружает модуль по пути ./sayHi.js относительно текущего файла и записывает экспортированную функцию sayHi в соответствующую переменную.

Так как модули поддерживают ряд специальных ключевых слов, и у них есть ряд особенностей, то необходимо явно сказать браузеру, что скрипт является модулем, при помощи атрибута <script type="module">.

//  index.html  
<script type="module">
  import {sayHi} from './say.js';
  document.body.innerHTML = sayHi('John');
</script>

// say.js
export function sayHi(user) {
  return `Hello, ${user}!`;
}

Браузер автоматически загрузит и запустит импортированный модуль (и те, которые он импортирует, если надо), а затем запустит скрипт.

Модули не работают локально. Только через HTTP(s)

Если вы попытаетесь открыть веб-страницу локально, через протокол file://, вы обнаружите, что директивы import/export не работают. Для тестирования модулей используйте локальный веб-сервер, например, static-server или используйте возможности «живого сервера» вашего редактора, например, расширение Live Server для VS Code.

Основные возможности модулей

Чем отличаются модули от «обычных» скриптов?

Есть основные возможности и особенности, работающие как в браузере, так и в серверном JavaScript.

Всегда «use strict»

В модулях всегда используется режим use strict. Например, присваивание к необъявленной переменной вызовет ошибку.

<script type="module">
  a = 5; // ошибка
</script>

Своя область видимости переменных

Каждый модуль имеет свою собственную область видимости. Другими словами, переменные и функции, объявленные в модуле, не видны в других скриптах.

В следующем примере импортированы 2 скрипта, и hello.js пытается использовать переменную user, объявленную в user.js. В итоге ошибка:

// index.html
<!doctype html>
<script type="module" src="user.js"></script>
<script type="module" src="hello.js"></script>

// user.js
let user = "John";

// hello.js
alert(user); // в этом модуле нет такой переменной (каждый модуль имеет независимые переменные)

Модули должны экспортировать функциональность, предназначенную для использования извне. А другие модули могут её импортировать.

Так что нам надо импортировать user.js в hello.js и взять из него нужную функциональность, вместо того чтобы полагаться на глобальные переменные.

Правильный вариант:

// index.html
<!doctype html>
<script type="module" src="user.js"></script>
<script type="module" src="hello.js"></script>

// user.js
export let user = "John";

// hello.js
import {user} from './user.js';
document.body.innerHTML = user; // John

В браузере также существует независимая область видимости для каждого скрипта <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

Объект import.meta содержит информацию о текущем модуле.

Содержимое зависит от окружения. В браузере он содержит ссылку на скрипт или ссылку на текущую веб-страницу, если модуль встроен в HTML:

<!doctype html>
<body>
<script type="module">
  alert(import.meta.url); // ссылка на html страницу для встроенного скрипта
</script>
</body>

В модуле «this» не определён

Это незначительная особенность, но для полноты картины нам нужно упомянуть об этом.

В модуле на верхнем уровне this не определён (undefined).

Сравним с не-модульными скриптами, там this – глобальный объект:

<!doctype html>
<body>
<script>
  alert(this); // window
</script>

<script type="module">
  alert(this); // undefined
</script>
</body>

Особенности в браузерах

Есть и несколько других, именно браузерных особенностей скриптов с type="module" по сравнению с обычными скриптами.

Модули являются отложенными (deferred)

Модули всегда выполняются в отложенном (deferred) режиме

Как побочный эффект, модули всегда видят полностью загруженную HTML-страницу, включая элементы под ними.

Например:

<!doctype html>
<body>
<script type="module">
  alert(typeof button); // object: скрипт может 'видеть' кнопку под ним
  // так как модули являются отложенными, то скрипт начнёт выполнятся только после полной загрузки страницы
</script>

Сравните с обычным скриптом ниже:

<script>
  alert(typeof button); // Ошибка: кнопка не определена, скрипт не видит элементы под ним
  // обычные скрипты запускаются сразу, не дожидаясь полной загрузки страницы
</script>

<button id="button">Кнопка</button>
</body>

Пожалуйста, обратите внимание: второй скрипт выполнится раньше, чем первый! Поэтому мы увидим сначала undefined, а потом object.

Это потому, что модули начинают выполняться после полной загрузки страницы. Обычные скрипты запускаются сразу же, поэтому сообщение из обычного скрипта мы видим первым.

При использовании модулей нам стоит иметь в виду, что HTML-страница будет показана браузером до того, как выполнятся модули и JavaScript-приложение будет готово к работе. Некоторые функции могут ещё не работать. Нам следует разместить «индикатор загрузки» или что-то ещё, чтобы не смутить этим посетителя.

Атрибут async работает во встроенных скриптах

Для не-модульных скриптов атрибут async работает только на внешних скриптах. Скрипты с ним запускаются сразу по готовности, они не ждут другие скрипты или HTML-документ.

Для модулей атрибут async работает на любых скриптах.

Например, в скрипте ниже есть async, поэтому он выполнится сразу после загрузки, не ожидая других скриптов.

Скрипт выполнит импорт (загрузит ./analytics.js) и сразу запустится, когда будет готов, даже если HTML документ ещё не будет загружен, или если другие скрипты ещё загружаются.

Это очень полезно, когда модуль ни с чем не связан, например для счётчиков, рекламы, обработчиков событий.

<!-- загружаются зависимости (analytics.js) и скрипт запускается -->
<!-- модуль не ожидает загрузки документа или других тэгов <script> -->
<script async type="module">
  import {counter} from './analytics.js';

  counter.count();
</script>

Внешние скрипты

Внешние скрипты с атрибутом type="module" имеют два отличия:

  1. Внешние скрипты с одинаковым атрибутом src запускаются только один раз:

    <!-- скрипт my.js загрузится и будет выполнен только один раз -->
    <script type="module" src="my.js"></script>
    <script type="module" src="my.js"></script>
    
  2. Внешний скрипт, который загружается с другого домена, требует указания заголовков CORS. Другими словами, если модульный скрипт загружается с другого домена, то удалённый сервер должен установить заголовок Access-Control-Allow-Origin означающий, что загрузка скрипта разрешена.

    <!-- another-site.com должен указать заголовок Access-Control-Allow-Origin -->
    <!-- иначе, скрипт н</script>е выполнится -->
    <script type="module" src="http://another-site.com/their.js"></script>
    

    Это обеспечивает лучшую безопасность по умолчанию.

Не допускаются «голые» модули

В браузере import должен содержать относительный или абсолютный путь к модулю. Модули без пути называются «голыми» (bare). Они не разрешены в import.

Например, этот import неправильный:

import {sayHi} from 'sayHi'; // Ошибка, "голый" модуль
// путь должен быть, например './sayHi.js' или абсолютный

Другие окружения, например Node.js, допускают использование «голых» модулей, без путей, так как в них есть свои правила, как работать с такими модулями и где их искать. Но браузеры пока не поддерживают «голые» модули.

Совместимость, «nomodule»

Старые браузеры не понимают атрибут type="module". Скрипты с неизвестным атрибутом type просто игнорируются. Мы можем сделать для них «резервный» скрипт при помощи атрибута nomodule:

<!doctype html>
<body>
<script type="module">
  alert("Работает в современных браузерах");
</script>

<script nomodule>
  alert("Современные браузеры понимают оба атрибута - и type=module, и nomodule, поэтому пропускают этот тег script")
  alert("Старые браузеры игнорируют скрипты с неизвестным атрибутом type=module, но выполняют этот.");
</script>
</body>

Инструменты сборки

В реальной жизни модули в браузерах редко используются в «сыром» виде. Обычно, мы объединяем модули вместе, используя специальный инструмент, например Webpack и после выкладываем код на рабочий сервер.

Одно из преимуществ использования сборщика – он предоставляет больший контроль над тем, как модули ищутся, позволяет использовать «голые» модули и многое другое «своё», например CSS/HTML-модули.

Сборщик делает следующее:

  1. Берёт «основной» модуль, который мы собираемся поместить в <script type="module"> в HTML.
  2. Анализирует зависимости (импорты, импорты импортов и так далее)
  3. Собирает один файл со всеми модулями (или несколько файлов, это можно настроить), перезаписывает встроенный import функцией импорта от сборщика, чтобы всё работало. «Специальные» типы модулей, такие как HTML/CSS тоже поддерживаются.
  4. В процессе могут происходить и другие трансформации и оптимизации кода:
    • Недостижимый код удаляется.
    • Неиспользуемые экспорты удаляются («tree-shaking»).
    • Специфические операторы для разработки, такие как console и debugger, удаляются.
    • Современный синтаксис JavaScript также может быть трансформирован в предыдущий стандарт, с похожей функциональностью, например, с помощью Babel.
    • Полученный файл можно минимизировать (удалить пробелы, заменить названия переменных на более короткие и т.д.).

Если мы используем инструменты сборки, то они объединяют модули вместе в один или несколько файлов, и заменяют import/export на свои вызовы. Поэтому итоговую сборку можно подключать и без атрибута type="module", как обычный скрипт:

<!-- Предположим, что мы собрали bundle.js, используя например утилиту Webpack -->
<script src="bundle.js"></script>

Хотя и «как есть» модули тоже можно использовать, а сборщик настроить позже при необходимости.

Экспорт и импорт

Директивы экспорт и импорт имеют несколько вариантов вызова.

В предыдущей главе мы видели простое использование, давайте теперь посмотрим больше примеров.

Экспорт до объявления

Мы можем пометить любое объявление как экспортируемое, разместив 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');

На первый взгляд «импортировать всё» выглядит очень удобно, не надо писать лишнего, зачем нам вообще может понадобиться явно перечислять список того, что нужно импортировать?

Для этого есть несколько причин.

  1. Современные инструменты сборки (webpack и другие) собирают модули вместе и оптимизируют их, ускоряя загрузку и удаляя неиспользуемый код.

    Предположим, мы добавили в наш проект стороннюю библиотеку say.js с множеством функций:

    // say.js
    export function sayHi() { ... }
    export function sayBye() { ... }
    export function becomeSilent() { ... }
    

    Теперь, если из этой библиотеки в проекте мы используем только одну функцию:

    // main.js
    import {sayHi} from './say.js';
    

    …Тогда оптимизатор увидит, что другие функции не используются, и удалит остальные из собранного кода, тем самым делая код меньше. Это называется «tree-shaking».

  2. Явно перечисляя то, что хотим импортировать, мы получаем более короткие имена функций: sayHi() вместо say.sayHi().

  3. Явное перечисление импортов делает код более понятным, позволяет увидеть, что именно и где используется. Это упрощает поддержку и рефакторинг кода.

Импорт «как»

Мы также можем использовать 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!

Экспорт по умолчанию

На практике модули встречаются в основном одного из двух типов:

  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');

Импорты без фигурных скобок выглядят красивее. Обычная ошибка начинающих: забывать про фигурные скобки. Запомним: фигурные скобки необходимы в случае именованных экспортов, для 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»

В некоторых ситуациях для обозначения экспорта по умолчанию в качестве имени используется 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';
...

Реэкспорт экспорта по умолчанию

При реэкспорте экспорт по умолчанию нужно обрабатывать особым образом.

Например, у нас есть user.js, из которого мы хотим реэкспортировать класс User:

// user.js
export default class User {
  // ...
}

Такое особое поведение реэкспорта с экспортом по умолчанию – одна из причин того, почему некоторые разработчики их не любят.

Динамические импорты

Инструкции экспорта и импорта, которые мы рассматривали в предыдущей главе, называются «статическими». Синтаксис у них весьма простой и строгий.

Во-первых, мы не можем динамически задавать никакие из параметров import.

Путь к модулю должен быть строковым примитивом и не может быть вызовом функции. Вот так работать не будет:

import ... from getModuleName(); // Ошибка, должна быть строка

Во-вторых, мы не можем делать импорт в зависимости от условий или в процессе выполнения.

if(...) {
  import ...; // Ошибка, запрещено
}

{
  import ...; // Ошибка, мы не можем ставить импорт в блок
}

Всё это следствие того, что цель директив import/export – задать костяк структуры кода. Благодаря им она может быть проанализирована, модули могут быть собраны в один файл специальными инструментами, а неиспользуемые экспорты удалены. Это возможно только благодаря тому, что всё статично.

Но как мы можем импортировать модуль динамически, по запросу?

Выражение import()

Выражение import(module) загружает модуль и возвращает промис, результатом которого становится объект модуля, содержащий все его экспорты.

Использовать его мы можем динамически в любом месте кода, например, так:

let modulePath = prompt("Какой модуль загружать?");

import(modulePath)
  .then(obj => <объект модуля>)
  .catch(err => <ошибка загрузки, например если нет такого модуля>)

Или если внутри асинхронной функции, то можно let module = await import(modulePath).

Например, если у нас есть такой модуль say.js:

//  say.js
export function hi() {
  alert(`Привет`);
}

export function bye() {
  alert(`Пока`);
}

…То динамический импорт может выглядеть так:

let {hi, bye} = await import('./say.js');

hi();
bye();

А если в say.js указан экспорт по умолчанию:

// say.js
export default function() {
  alert("Module loaded (export default)!");
}

…То для доступа к нему нам следует взять свойство default объекта модуля:

let obj = await import('./say.js');
let say = obj.default;
// или, одной строкой: let {default: say} = await import('./say.js');

say();

Вот полный пример:

<!doctype html>
<html><body>
<script>
  async function load() {
    let say = await import('./say1.js');
    say.hi(); // Привет!
    say.bye(); // Пока!
    say.default(); // Модуль загружен (экспорт по умолчанию)!
  }
</script>
<button onclick="load()">Нажми меня</button>
</body></html>
export function hi() {
  alert(`Привет`);
}

export function bye() {
  alert(`Пока`);
}

export default function() {
  alert("Модуль загружен (экспорт по умолчанию)!");
}

Динамический импорт работает в обычных скриптах, он не требует указания script type="module".

Хотя import() и выглядит похоже на вызов функции, на самом деле это специальный синтаксис, так же, как, например, super().

Так что мы не можем скопировать import в другую переменную или вызвать при помощи .call/apply. Это не функция.

Итого

  1. Модуль – это файл. Чтобы работал import/export, нужно для браузеров указывать атрибут <script type="module">. У модулей есть ряд особенностей:
    • Отложенное (deferred) выполнение по умолчанию.
    • Атрибут async работает во встроенных скриптах.
    • Для загрузки внешних модулей с другого источника, он должен ставить заголовки CORS.
    • Дублирующиеся внешние скрипты игнорируются.
  2. У модулей есть своя область видимости, обмениваться функциональностью можно через import/export.
  3. В модулях всегда включена директива use strict.
  4. Код в модулях выполняется только один раз. Экспортируемая функциональность создаётся один раз и передаётся всем импортёрам.
  5. Когда мы используем модули, каждый модуль реализует свою функциональность и экспортирует её. Затем мы используем import, чтобы напрямую импортировать её туда, куда необходимо. Браузер загружает и анализирует скрипты автоматически.
  6. В реальной жизни часто используется сборщик Webpack, чтобы объединить модули: для производительности и других «плюшек»
  7. .
  8. Вот все варианты export:
    • Перед объявлением класса/функции/…:
      • export [default] class/function/variable ...
    • Отдельный экспорт:
      • export {x [as y], ...}.
    • Реэкспорт:
      • export {x [as y], ...} from "module"
      • export * from "module" (не реэкспортирует export default).
      • export {default [as y]} from "module" (реэкспортирует только export default).
  9. Импорт:
    • Именованные экспорты из модуля:
      • import {x [as y], ...} from "module"
    • Импорт по умолчанию:
      • import x from "module"
      • import {default as x} from "module"
    • Всё сразу:
      • import * as obj from "module"
    • Только подключить модуль (его код запустится), но не присваивать его переменной:
      • import "module"
  10. Мы можем поставить import/export в начало или в конец скрипта, это не имеет значения.

    То есть, технически, такая запись вполне корректна:

    sayHi();
    
    // ...
    
    import {sayHi} from './say.js'; // импорт в конце файла
    

    На практике импорты, чаще всего, располагаются в начале файла. Но это только для большего удобства.

    Обратите внимание, что инструкции import/export не работают внутри {...}.

    Условный импорт, такой как ниже, работать не будет:

    if (something) {
      import {sayHi} from "./say.js"; // Ошибка: импорт должен быть на верхнем уровне
    }
    

    …Но что, если нам в самом деле нужно импортировать что-либо в зависимости от условий? Или в определённое время? Например, загрузить модуль, только когда он станет нужен?

Справочник JavaScript
×
Справочник JavaScript