Модули: зачем они нужны

модули≈ 7 минут

Здесь будет встроен видео-разбор темы (5–7 минут, YouTube / RuTube / VK Видео) — тем, кто лучше воспринимает на слух, текст ниже не обязателен.

Пока страница состоит из двух скриптов, всё хорошо; когда их двадцать, переменные начинают затирать друг друга и порядок тегов <script> превращается в хрупкую конструкцию. Модули решают именно эту боль: каждый файл получает собственную область видимости и явно говорит, что отдаёт наружу.

Разделы темы:·

Обычные скрипты на странице живут в одной комнате. Всё, что объявлено через var или function на верхнем уровне файла, попадает в общее глобальное пространство — и следующий подключённый файл видит эти имена и может их переписать.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

Ошибки при этом нет: язык честно выполнил то, что попросили. Но такой код невозможно собирать из кусков — стоит двум библиотекам выбрать имя config или init, и одна тихо ломает другую. Плюс к этому порядок тегов становится значимым: файл, которому нужна функция из соседнего файла, обязан быть подключён после него.

Популярный компромисс — занять в глобальном пространстве ровно одно имя и сложить всё внутрь него. Столкновение всё ещё возможно, но вероятность резко падает.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

Раз у функции есть своя область видимости — значит, файл можно целиком обернуть в функцию и тут же её вызвать. Такую конструкцию называют IIFE, немедленно вызываемое функциональное выражение. Наружу возвращают только то, что нужно другим.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

count снаружи не существует — до него нельзя дотянуться и нельзя случайно обнулить. Приём рабочий, и вы до сих пор встретите его в старых библиотеках. Но он не решает главного: зависимости по-прежнему передаются через глобальные переменные, и порядок подключения файлов приходится держать в голове.

Первый по-настоящему модульный формат в JavaScript появился на сервере. В Node.js файл получает объект module.exports, куда складывает всё публичное, а чужой код подключает функцией require("./math"). Никаких глобальных переменных: зависимость названа прямо в том месте, где нужна.

По сути require — обычная функция: она находит файл, выполняет его один раз, запоминает получившийся exports в кэше и возвращает. Если отбросить поиск файла и запуск, останется реестр модулей — вот он на четырёх строках, без магии.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

Браузер такого не умел: require работает синхронно, а тянуть файлы по сети синхронно нельзя. Поэтому появились сборщики — Browserify, затем webpack: они заранее склеивали дерево require в один файл. Отсюда и привычка современного фронтенда собирать проект перед выкладкой.

В 2015 году модули стали частью самого языка: ключевые слова export и import понимают и браузер, и Node.js. Синтаксис разберём в следующей статье, а сейчас важнее три свойства, которыми модуль отличается от обычного скрипта.

Первое — своя область видимости. Всё объявленное в файле принадлежит файлу. Чтобы отдать значение наружу, его нужно пометить export; чтобы получить чужое — написать import. Случайно затереть соседа больше нельзя.

Второе — строгий режим включён всегда. Писать "use strict" в начале файла не нужно, и отключить строгость невозможно. Присваивание необъявленной переменной сразу становится ошибкой, а не молчаливым созданием глобальной.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

Третье — тело модуля выполняется один раз. Сколько бы файлов ни импортировали utils.js, код внутри него отработает единожды, а все импортирующие получат один и тот же готовый результат. Идея та же, что в кэше выше.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

Браузеру нужно сказать, что перед ним не обычный скрипт: <script type="module" src="./main.js"></script>. Достаточно подключить только точку входа — остальные файлы браузер подтянет сам по цепочке import.

Такой скрипт не блокирует разбор страницы: он загружается параллельно и выполняется, когда HTML разобран, — как будто у него стоит defer. Поэтому модуль всегда видит готовую разметку, зато и результат его работы появляется чуть позже.

Обычный скриптМодуль
Область видимостиобщая, глобальнаясвоя, внутри файла
Строгий режимтолько с «use strict»всегда, отключить нельзя
Когда выполняетсясразу, останавливая разбор HTMLпосле разбора HTML, как с defer
Тот же файл подключён дваждывыполняется каждый разтело выполняется один раз
Открытие файла с дискаработаетфайл модуля не загрузится: нужен локальный http-сервер

Открыли index.html двойным кликом — модули из файлов молчат. Им нужен http, даже на своей машине: запустите любой локальный сервер.

Файл модуля браузер загружает строже обычного скрипта: сверяет, откуда пришла страница и откуда файл. У страницы, открытой с диска, адрес начинается с file:///, сайта в нём нет, и файл модуля браузер считает чужим. Так поступают и Chrome 154, и Firefox 157 с настройками по умолчанию, причём молча: на самой странице ошибки не видно. Запустится с диска только модуль, написанный прямо внутри тега, без src и без import. Как устроен адрес страницы с диска, почему путь /style.css уводит там в корень диска и как одной командой запустить локальный сервер, разобрано в прологе — в разделе о ловушке file://.

Кроме статического import есть функция import() — она загружает модуль в момент вызова и возвращает промис. Так подгружают редкие куски приложения: тяжёлый редактор, экран настроек. Работает в том числе в обычном скрипте.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

  • Без модулей все скрипты страницы делят одно глобальное пространство: имена сталкиваются, а порядок тегов становится частью логики.
  • IIFE прятал переменные внутри функции, CommonJS с require и module.exports принёс настоящие модули на сервер, а в браузер их доставляли сборщики.
  • ES-модули встроены в язык: у каждого файла своя область видимости, наружу попадает только помеченное export.
  • В модуле всегда включён строгий режим, и отключить его нельзя.
  • Подключается модуль тегом script с type="module": он не блокирует разбор HTML, а тело каждого файла выполняется ровно один раз. Файлы модулей браузер загружает только по http, поэтому страницу с ними открывают через сервер, хотя бы локальный.

1/3Переменная объявлена на верхнем уровне ES-модуля и не помечена export. Кто её увидит?

2/3Когда выполнится код скрипта с type="module"?

3/3Файл utils.js импортируют три разных модуля. Сколько раз выполнится код в его теле?

Спрячьте счётчик

Ниже count лежит на виду: любой другой скрипт на странице может его обнулить или подменить. Перепишите код так, чтобы снаружи была доступна только функция next, а сама переменная стала недоступной. Вывод должен остаться прежним.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

Оборачиваем всё в функцию и тут же вызываем её. Переменная остаётся в области видимости этой функции, наружу уходит только next.

jsлистинг · не запускается
Листинг только для чтения: этот код не запускается в браузере.

Это и есть ручная замена модуля: приватное внутри, публичное — в возвращаемом значении. В ES-модуле то же самое получилось бы само собой: достаточно было бы написать export перед next и ничего не оборачивать.

Сколько строк в консоли?

Есть три файла. В log.js на верхнем уровне стоит console.log("модуль загружен") и экспортируется функция. Файлы a.js и b.js оба импортируют log.js. На странице подключены два тега: <script type="module" src="./a.js"> и <script type="module" src="./b.js">.

Сколько раз в консоли появится строка «модуль загружен»? И изменится ли ответ, если третьим тегом подключить сам log.js?

Один раз. Браузер выполняет тело модуля единожды и кэширует результат по адресу файла: a.js запустит log.js, а b.js получит уже готовые экспорты, не выполняя тело заново.

Третий тег ничего не изменит — это тот же самый URL, значит та же запись в кэше. Строка по-прежнему будет одна. А вот если бы это были обычные скрипты, каждое подключение выполнялось бы заново.