Модули и инструменты

Что это такое

Модули делят JavaScript на файлы с явными импортами и экспортами, так что каждый файл сам заявляет, что ему нужно и что он предлагает. Инструменты сборки превращают эти файлы вместе с зависимостями и не-JavaScript-ресурсами во что-то, что браузер загрузит эффективно. Изучают их обычно вместе, потому что осмысленными инструменты делает именно наличие модулей.

Почему это важно

Один файл скрипта не масштабируется дальше нескольких сотен строк: каждое имя глобально, порядок загрузки решает, будет ли код корректен, а переиспользовать что-либо без копирования не получится. Модули дают коду границу и граф зависимостей — без них тестирование, tree-shaking и отложенная загрузка попросту невозможны. Инструменты нужны потому, что браузеры всего этого сами не умеют — по крайней мере, с теми же гарантиями, — и потому, что альтернатива, то есть доставка каждого файла и каждой зависимости каждому посетителю, чаще всего и делает первую загрузку медленной.

Ключевые идеи

Именованные экспорты, явные импорты

ES-модуль объявляет, что он предлагает и что берёт. Именованные экспорты предпочтительнее экспорта по умолчанию: при импорте их переименовывают осознанно, опечатка в имени сразу заметна, а сам символ становится понятнее. Импорты поднимаются и являются живыми привязками: модуль, который их импортировал, увидит более поздние изменения экспортированного let, — в отличие от скопированного значения, и это иногда удивляет.

// format.js
export function formatMoney(cents) { /* … */ }
export const CURRENCY = "GBP";

// invoice.js
import { formatMoney } from "./format.js";

Модуль выполняется один раз

Выполнение модуля происходит один раз на URL, сколько бы мест его ни импортировало, а порядок задаёт граф, а не теги <script>. Поэтому операторы верхнего уровня выполняются при первом импорте; если такой код делает работу, момент её запуска никто не выбирал.

Динамический импорт разбивает граф

import() возвращает промис и загружает модуль по требованию — так маршрут или тяжёлый редактор остаются вне начального бандла. Однако статический import так не умеет: сборщику придётся включить цель в бандл точки входа.

async function openEditor() {
  const { Editor } = await import("./editor.js");
  return new Editor();
}

Сборщик существует, чтобы разрешать, преобразовывать и удалять

Сборщик разрешает спецификаторы модулей, преобразует синтаксис, которого не понимают целевые браузеры, встраивает ресурсы и удаляет код, до которого дело не доходит. Vite — распространённый современный выбор: в разработке он отдаёт нативные ES-модули, а для продакшна собирает бандл, поэтому цикл «правка — обновление» остаётся быстрым даже на больших проектах.

Доставляйте меньше, измеряя

Инструменты без измерений растут. Смотрите, что в итоге попадает в собранный вывод, держите самые большие зависимости осознанными и предпочитайте возможности платформы библиотеке, когда платформа уже делает работу.

Практика

Разбейте скрипт на модули

Возьмите один файл с несколькими несвязанными функциями и разбейте его на три модуля с именованными экспортами: один из них импортирует два других.

Готово, когда: ни у одного модуля нет больше одной причины меняться, а файл точки входа читается как короткий список импортов и один вызов.

Лениво загрузите одну тяжёлую функцию

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

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

Частые ошибки

  • Смешивание синтаксиса CommonJS и ES-модулей — require в модуле падает, а import нельзя использовать условно; исправить: выбирайте ES-модули для нового кода и переводите границы осознанно.
  • Импорт ради побочного эффекта без пояснения — зависимость невидима, и её могут удалить как «неиспользуемую»; исправить: импортируйте используемую привязку или поясните комментарием, зачем нужен голый импорт.
  • Работа на верхнем уровне модуля — она выполняется в непредсказуемый момент, и её нельзя ни отменить, ни повторить; исправить: экспортируйте функцию и вызывайте её из точки входа.
  • Добавление зависимости ради нескольких строк — бандл растёт у всех, а цепочка поставок ширится; исправить: используйте API платформы или добавьте небольшую функцию к себе.
  • Отказ от осмотра продакшн-бандла — фреймворк или файл локали удваивает передачу, и никто не замечает; исправить: проверяйте собранный вывод после добавления зависимости.

Что читать дальше

  • Асинхронный JavaScript (02-frontend/06-javascript/03-asynchronous-javascript) — динамические импорты возвращают промисы, и обработка ошибок здесь та же.
  • Vite (02-frontend/09-vite) — инструмент сборки, на который этот урок лишь намекает, во всех подробностях.
  • Управление пакетами (npm) (02-frontend/04-package-management-npm) — как зависимости, которые импортируют ваши модули, вообще попадают на диск.