JavaScript / 38 минут
Модули и организация проекта
Освойте import/export, CommonJS, разрешение путей и публичные границы модулей.
- Освойте import/export, CommonJS, разрешение путей и публичные границы модулей.
- Применить тему в задаче «Модули и организация проекта» и объяснить граничные случаи.
Около 8 минут на чтение и разбор примеров. Практика — отдельно, в своём темпе.
Зачем нужны границы файлов
Когда программа растёт, одной функции и одного файла становится недостаточно. Модуль выделяет связную часть поведения и описывает, какие имена она предоставляет другим частям приложения. Внутренние переменные не должны случайно превращаться в общий глобальный словарь. Пользователь модуля знает его публичный контракт, а детали реализации можно менять без переписывания всего проекта.
Разделение по файлам полезно, если соответствует обязанностям. Например, чистый расчёт стоимости, доступ к HTTP и обновление DOM имеют разные причины для изменения. Не создавайте десятки обёрток только ради количества файлов: маленький проект может начинаться с нескольких понятных модулей. Главное — направление зависимостей и отсутствие неявного общего состояния.
Именованные экспорты и импорты
export открывает выбранное объявление. Именованный import использует публичное имя модуля; локально его можно переименовать через as. Остальные объявления остаются внутренними. Импорты ESM являются связями с экспортами, а не произвольными копиями всех значений на момент загрузки. Не пытайтесь переназначать импортированное имя со стороны потребителя.
// math.js
export function add(left, right) { return left + right; }
export const currency = 'RUB';
// checkout.js
import { add, currency as unit } from './math.js';
console.log(add(20, 30), unit); // 50 RUBdefault export обозначает один экспорт по умолчанию, который импортируют без фигурных скобок. Он не отменяет возможности иметь именованные экспорты в том же файле. Выберите соглашение команды так, чтобы поиск использования и автоматические изменения были предсказуемы. Ошибку «имя не экспортировано» исправляют согласованием публичного API, а не случайным удалением скобок у всех импортов.
Разрешение модулей зависит от среды
В браузере specifier обычно разрешается как URL. Относительный путь начинается с ./ или ../, а расширение часто необходимо указывать явно. Голое имя npm-пакета не начнёт работать в браузере только потому, что пакет установлен на диске. Для этого нужен подходящий сборщик, import map или другая настройка загрузки.
В Node.js существуют ESM и CommonJS. Поле type в package.json и расширения .mjs/.cjs помогают определить формат файла. В CommonJS используются require и module.exports; в ESM — import и export. Совместимость между форматами имеет правила и ограничения, поэтому не копируйте фрагмент из старого примера, не выяснив формат вашего проекта.
// tools.cjs: CommonJS
const path = require('node:path');
module.exports = { filename: value => path.posix.basename(value) };Префикс node: явно указывает встроенный модуль Node.js. Это помогает отличить API среды от внешнего пакета. Он не делает этот модуль доступным в обычном браузере. Пакетный менеджер устанавливает зависимости, а среда выполнения загружает код по своим правилам — это разные этапы.
Динамический импорт
import(specifier) возвращает Promise пространства имён модуля. Он полезен, когда загрузку нужно выполнить по условию или отложить до востребованности функциональности. Это асинхронное выражение, в отличие от обычной статической декларации import в верхнем уровне модуля. Ошибку загрузки необходимо обрабатывать так же осознанно, как другие асинхронные отказы.
async function filename(pathname) {
const path = await import('node:path');
return path.posix.basename(pathname);
}Используйте posix, если контракт задаёт POSIX-пути независимо от операционной системы запуска. Иначе один и тот же набор строк может обрабатываться разными правилами разделителей на разных машинах. Это пример важного общего принципа: платформенные особенности нужно либо принять как часть контракта, либо явно нейтрализовать.
Побочные эффекты, кэш и циклы
Модуль может выполнять код при загрузке, создавать глобальные подписки или держать состояние между вызовами. Обычно модуль загружается с кэшированием, поэтому экспортированный изменяемый объект способен стать общим состоянием нескольких потребителей. Это бывает намеренно, но должно иметь понятного владельца. Не храните результаты одного пользовательского запроса в случайной общей переменной.
Циклические зависимости появляются, когда модули прямо или косвенно импортируют друг друга. Иногда они допустимы, но усложняют порядок инициализации и могут привести к чтению ещё не готового значения. Часто помогает выделить общий контракт в отдельный независимый модуль или передать зависимость параметром. Не лечите архитектурный цикл бесконечными отложенными импортами без понимания причины.
Практика модулей и воспроизводимость
В задаче вы динамически импортируете node:path и возвращаете последний компонент POSIX-пути. Это проверяет настоящую загрузку встроенного модуля, а не ручной split, случайно совпавший с парой примеров. async solve должна дождаться импорта и вернуть результат вычисления. Обратите внимание на путь с завершающим слешем и на пустую строку: у библиотеки уже есть определённое поведение для них.
Для самостоятельного проекта вынесите форматирование цены в один модуль, а вызов из интерфейса — в другой. Проверьте, что расчёт можно протестировать без document и сетевого подключения. Затем опишите публичные экспорты каждого файла одной фразой. Если описание требует перечислить несвязанные обязанности, границы модулей, вероятно, стоит пересмотреть.