ES-модули в браузере и обзор Temporal API
Два мира модулей
ES-модули — это официальный стандарт модульности в языке ECMAScript, введенный в спецификации ECMAScript 2015, который представляет собой нативную систему модулей, поддерживаемую всеми современными браузерами и средами выполнения JavaScript, такими как Node.js, и который использует ключевые слова import и export для декларативного определения зависимостей и интерфейсов между файлами. В отличие от более старых систем модульности, таких как CommonJS, ES-модули работают асинхронно и имеют статическую структуру, что означает, что все импорты и экспорты анализируются на этапе парсинга кода до его выполнения, что позволяет браузерам и сборщикам выполнять оптимизации, такие как tree-shaking для удаления неиспользуемого кода и более эффективную загрузку зависимостей. Каждый файл в системе ES-модулей автоматически становится модулем со своей собственной областью видимости, и переменные, объявленные внутри модуля, не доступны глобально, если они явно не экспортированы, что обеспечивает инкапсуляцию и предотвращает загрязнение глобального пространства имен. ES-модули поддерживают два основных способа экспорта: экспорт по умолчанию, когда модуль экспортирует одно основное значение, и именованный экспорт, когда модуль экспортирует несколько значений с определенными именами, причем оба типа могут использоваться одновременно в одном модуле. Импорт в ES-модулях осуществляется с помощью оператора import, который может импортировать экспорт по умолчанию, именованные экспорты или их комбинацию, при этом синтаксис импорта позволяет переименовывать импортируемые значения, импортировать все экспорты модуля в одно пространство имен или импортировать модуль только для его побочных эффектов, без захвата каких-либо экспортов. Важной особенностью ES-модулей является их поддержка динамического импорта с помощью функции import, которая возвращает промис и позволяет загружать модули асинхронно и условно, что открывает возможности для lazy-loading и разделения кода в больших приложениях, существенно улучшая время начальной загрузки. ES-модули являются фундаментом современной веб-разработки, поддерживаются нативно в браузерах через тег script type module и в Node.js через расширение файлов mjs или настройку type module в package.json, и их использование стало стандартом для всех новых проектов, постепенно вытесняя CommonJS и другие альтернативные системы модульности.
В структуре и подключении кода разобраны теги <script>, bundler и CommonJS в Node. Переиспользуемые теги без сборки — Web Components. В браузере без сборки стандарт — ES-модули — файлы с import / export, отдельная область видимости, строгий режим по умолчанию.
| Классический скрипт | ES-модуль | |
|---|---|---|
| Подключение | <script src="app.js"> | <script type="module" src="app.js"> |
| Область видимости | глобальная (засоряет window) | модульная |
import / export | синтаксическая ошибка | да |
| Отложенная загрузка | defer вручную | как defer по умолчанию |
| CORS | обычный скрипт | модули — только same-origin или CORS |
Структура проекта на ES-модулях
Растущий монолит в одном app.js трудно сопровождать. ES-модули делят код на файлы с явными import/export (сеть, UI, утилиты). Синтаксис одинаков в браузере (type="module") и в сборках Vite/Webpack — навык переносится между "голым" JS и фреймворком.
Подключение в HTML
<script type="module" src="./main.js"></script>
<script type="module">
import { init } from './widgets/carousel.js';
init();
</script>
Разбор:
type="module"переводит скрипт в режим ES-модуля: доступныimport/export, и код выполняется в изолированной модульной области видимости.- Встроенный модульный
<script>используетimport { init } ...и импортирует именованный экспортinitиз файлаcarousel.js. - Вызов
init()запускает инициализацию виджета сразу после загрузки модуля, обычно это подписки на события и подготовка DOM. - Такой шаблон отделяет точку входа (
main) от реализации виджета (widgets/carousel.js) и делает код проще для поддержки.
Второй блок — inline-модуль; пути import считаются от URL страницы, не от файла (для предсказуемости выносите логику в .js).
Именованный и экспорт по умолчанию
Экспорт по умолчанию — это механизм в системе ES-модулей, который позволяет модулю экспортировать одно основное значение, функцию, класс или объект, которое считается главным или наиболее важным для данного модуля, и которое может быть импортировано без использования фигурных скобок с любым произвольным именем по выбору импортирующего разработчика. Экспорт по умолчанию объявляется с помощью ключевого слова export default, за которым следует выражение, значение которого будет экспортироваться, при этом модуль может иметь только один экспорт по умолчанию, хотя может одновременно содержать и именованные экспорты, что создает гибкую систему, где основной функционал модуля доступен через экспорт по умолчанию, а дополнительные утилиты — через именованные экспорты. При импорте экспорта по умолчанию в другом модуле используется синтаксис import имя from путь, где имя может быть выбрано произвольно, и оно будет ссылаться на значение экспорта по умолчанию из импортируемого модуля, что делает этот тип экспорта особенно удобным для модулей, которые экспортируют единственный класс, компонент или функцию, такие как библиотеки, реакт-компоненты или основные сервисы приложения. Экспорт по умолчанию может быть использован не только с объявлением функции или класса, но и с любым выражением, включая литералы объектов, строки, числа или даже результат вычислений, хотя на практике он чаще всего используется с функциями, классами и объектами, представляющими основной API модуля. Одним из преимуществ экспорта по умолчанию является гибкость при импорте, поскольку импортирующий модуль не обязан использовать то же имя, что и в модуле-экспортере, что может быть полезно для переименования или адаптации к контексту, но при этом может привести к неоднозначности и сложностям с автодополнением в редакторах кода, поэтому некоторые руководства по стилю рекомендуют использовать именованные экспорты как более явный и предсказуемый подход. В современных проектах экспорт по умолчанию широко используется в сочетании с экспортом именованных функций и констант, предоставляя разработчикам возможность выбора наиболее подходящего способа импорта в зависимости от того, нужен им весь модуль или только его часть, причем инструменты сборки и линтеры могут помочь обеспечить согласованность использования этого механизма в проекте.
Именованный экспорт — это механизм в системе ES-модулей, который позволяет модулю экспортировать несколько значений, функций, классов, констант или переменных с уникальными именами, которые становятся доступными для импорта в других модулях с использованием тех же имен в фигурных скобках, обеспечивая явное и точное указание того, какие именно части модуля необходимы в конкретном месте кода. Именованный экспорт объявляется с помощью ключевого слова export перед объявлением переменной, функции или класса, например, export const PI = 3.14159, или экспортом уже объявленных сущностей в отдельной строке в конце файла, например, export { функция1, функция2 }, что позволяет собирать экспорты в одном месте для лучшей читаемости и контроля. При импорте именованных экспортов используется синтаксис import { имя1, имя2 } from путь, где имена должны точно соответствовать тем, что экспортируются в исходном модуле, однако можно переименовывать импорты с помощью ключевого слова as, например, import { имя as псевдоним } from путь, что полезно для избегания конфликтов имен при импорте из нескольких модулей. Именованные экспорты являются более явными и предпочтительными для многих разработчиков и проектов, поскольку они делают зависимости более прозрачными, упрощают автоматический рефакторинг и автодополнение в инструментах разработки, а также облегчают обнаружение неиспользуемого кода, поскольку инструменты сборки могут легко определить, какие именно экспорты не импортируются нигде в проекте, и исключить их из финального бандла через tree-shaking. Модуль может содержать любое количество именованных экспортов, причем они могут быть как отдельно объявленными сущностями, так и сгруппированными в один объект с последующим экспортом его свойств, что позволяет организовать код наиболее удобным способом в зависимости от архитектуры и требований проекта. При работе с библиотеками и фреймворками именованные экспорты часто используются для экспорта множества вспомогательных функций, утилит, констант, компонентов и хуков, позволяя разработчику импортировать только то, что действительно необходимо в каждом конкретном файле, что способствует уменьшению размера финального бандла и улучшению времени загрузки приложения. В сочетании с экспортом по умолчанию именованные экспорты предоставляют разработчику полную гибкость в проектировании публичного API модуля, позволяя выделить основную функциональность через экспорт по умолчанию и дополнительные возможности через именованные экспорты, создавая модули, которые одновременно просты для начала использования и достаточно мощны для продвинутых сценариев.
utils.js:
export const PI = 3.14159;
export function sum(a, b) {
return a + b;
}
export default function createId() {
return crypto.randomUUID();
}
Разбор:
export const PIпубликует именованную константу: её можно импортировать по имени в любом другом модуле.export function sum(a, b)объявляет именованную функцию сложения и сразу делает её частью публичного API модуля.export default function createId()задаёт экспорт по умолчанию: при импорте ему можно дать любое локальное имя.crypto.randomUUID()генерирует уникальный идентификатор в формате UUID v4 на стороне браузера.- В одном модуле сочетаются именованные и default-экспорты, что удобно для утилитного файла.
main.js:
import createId, { sum, PI } from './utils.js';
console.log(sum(2, PI));
console.log(createId());
Разбор:
import createId, { sum, PI }показывает комбинированный импорт: default-экспорт (createId) и именованные (sum,PI) из одного файла.- Путь
./utils.jsуказан относительно текущего файла, и расширение.jsважно для нативных модулей в браузере. sum(2, PI)использует импортированные символы как обычные локальные переменные текущего модуля.console.log(createId())демонстрирует вызов default-функции и вывод результата в консоль для отладки.
Расширение .js в пути обязательно в нативных модулях браузера (в отличие от некоторых bundler-конфигов).
Динамический import
Динамический import — это функция в системе ES-модулей, которая была введена в спецификации ECMAScript 2020 и представляет собой асинхронный механизм для условной и отложенной загрузки модулей во время выполнения, в отличие от статического импорта, который загружается на этапе парсинга кода и выполняется немедленно при инициализации. Динамический импорт реализуется с помощью вызова import, который является функцией, принимающей строку с путем к модулю и возвращающей промис, который разрешается в объект модуля, содержащий все его экспорты, что позволяет загружать модули только тогда, когда они действительно необходимы, например, при определенных действиях пользователя, переходе на определенный маршрут или в зависимости от условий среды. Ключевым преимуществом динамического импорта является возможность реализации техники разделения кода, или code splitting, когда приложение разбивается на небольшие фрагменты, которые загружаются по мере необходимости, значительно уменьшая размер начального загрузочного бандла и улучшая время до первого отображения страницы, что особенно важно для больших приложений с множеством маршрутов и компонентов. Динамический импорт также позволяет загружать модули на основе данных, полученных во время выполнения, например, выбор локали для интернационализации, загрузка полифиллов для старых браузеров или динамическая подгрузка модулей в зависимости от роли или прав пользователя, что делает приложения более гибкими и адаптивными к различным сценариям использования. При использовании динамического импорта синтаксис для доступа к экспортам отличается от статического импорта: поскольку промис разрешается в объект модуля, доступ к экспорту по умолчанию осуществляется через свойство default объекта, а к именованным экспортам через их имена, например, import .then module => console.logmodule.default, или с использованием деструктуризации после await в асинхронной функции. Важно отметить, что динамический импорт может использоваться в любом месте кода, а не только на верхнем уровне, и не требует декларации типа module, что делает его доступным даже в скриптах без type module, хотя поддержка в браузерах и средах выполнения стала стандартной и повсеместной, что делает этот механизм универсальным инструментом для всех разработчиков. Динамический импорт стал фундаментальной частью современных фреймворков и инструментов сборки, таких как Webpack, Vite и другие, которые используют его под капотом для автоматического разделения кода и управления асинхронной загрузкой, и его понимание необходимо для создания производительных и масштабируемых веб-приложений.
Загрузка по условию (ленивая вкладка, маршрут):
button.addEventListener('click', async () => {
const { renderChart } = await import('./chart.js');
renderChart('#host');
});
Разбор:
button.addEventListener('click', async () => { ... })регистрирует асинхронный обработчик клика по кнопке.await import('./chart.js')выполняет динамический импорт: модуль подгружается только в момент, когда он реально нужен.- Деструктуризация
{ renderChart }сразу извлекает именованный экспорт функции из загруженного модуля. renderChart('#host')запускает рендер графика в целевом контейнере, что уменьшает стартовый вес приложения.
Возвращает промис модуля — удобно с async/await.
import.meta
Метаданные текущего модуля:
console.log(import.meta.url); // адрес файла модуля
Разбор:
import.meta.urlвозвращает полный URL текущего модуля, включая протокол и путь.- Эта информация полезна для вычисления относительных путей к ресурсам рядом с модулем.
console.log(...)здесь используется как диагностический вывод, чтобы увидеть реальное местоположение файла в рантайме.
В dev-средах (Vite) через import.meta.env пробрасывают переменные окружения — это расширение инструмента, не часть чистого стандарта HTML.
Отличие от сборки (Vite, Webpack)
Браузер загружает каждый import отдельным HTTP-запросом. В продакшене почти всегда сборщик склеивает дерево в один или несколько chunk-файлов, но синтаксис import/export тот же.
Правила проекта (алиасы @/components, импорт без .js) настраиваются в bundler; в учебных примерах "голого" браузера пишите относительные пути с .js.
Модули и Node
В Node ESM включается через "type": "module" в package.json или расширение .mjs — см. Node.js. Один и тот же файл .js с export может работать и в браузере (через type="module"), и в Node при правильной конфигурации.
Temporal API — зачем смотреть вперёд
Temporal API — это новый встроенный объект и набор классов в стандарте ECMAScript, который был разработан для замены устаревшего объекта Date и предоставления современного, мощного и удобного API для работы с датами, временем, часовыми поясами, длительностями и интервалами, устраняя многолетние проблемы и недостатки старого API, включая его изменчивость, сложность, неясности с часовыми поясами и отсутствие встроенной поддержки для многих распространенных операций. Temporal API вводит несколько основных классов, включая Temporal.PlainDate для представления даты без времени, Temporal.PlainTime для представления времени без даты, Temporal.PlainDateTime для представления даты и времени без часового пояса, Temporal.ZonedDateTime для представления даты и времени с конкретным часовым поясом, Temporal.Duration для представления длительности или периода времени, и Temporal.Instant для представления момента времени в UTC с высокой точностью до наносекунд. Одним из ключевых преимуществ Temporal API является его неизменяемость, где все операции возвращают новые объекты, а не изменяют существующие, что делает код более предсказуемым и безопасным, особенно в многопоточных средах или при работе с сложными состояниями, а также его строгая типизация и явное разделение между разными типами данных о времени, что предотвращает путаницу и ошибки, связанные с неоднозначностью интерпретации дат и времен. Temporal API обеспечивает нативную, из коробки поддержку международных часовых поясов и календарей, используя базу данных IANA для точного учета перехода на летнее время и других особенностей, что делает его идеальным для глобальных приложений, работающих с пользователями из разных регионов мира, без необходимости использования сторонних библиотек, таких как Moment.js или date-fns. Дополнительно Temporal API предоставляет множество удобных методов для вычислений, включая добавление и вычитание длительностей, сравнение дат, нахождение различий между датами, округление до определенных единиц, получение начала и конца месяца или года, проверку на пересечение интервалов и множество других операций, которые ранее требовали сложных ручных вычислений или использования внешних библиотек. Temporal API на данный момент находится на стадии активного обсуждения и разработки, но уже реализован в некоторых браузерах и средах выполнения, и обещает стать стандартным инструментом для работы с датами и временем в будущем, при этом он доступен как полифилл для текущих проектов, позволяя разработчикам начать использовать его уже сегодня и быть готовыми к его широкому внедрению в ближайшие годы.
Объект Date в JavaScript исторически неудобен — месяцы с нуля, мутации через setMonth, путаница локали и UTC, слабая арифметика интервалов.
Temporal (стадия стандарта ECMAScript) разделяет понятия:
| Тип | Назначение |
|---|---|
Temporal.PlainDate | календарная дата без часового пояса |
Temporal.PlainTime | время суток |
Temporal.PlainDateTime | дата + время без пояса |
Temporal.Instant | момент на шкале UTC |
Temporal.ZonedDateTime | момент в конкретном часовом поясе |
Temporal.Duration | длительность |
Temporal.Now | "сейчас" в нужном представлении |
Пример стиля API (когда движок поддерживает глобальный Temporal):
const birth = Temporal.PlainDate.from('1990-06-15');
const today = Temporal.Now.plainDateISO();
const years = today.since(birth, { largestUnit: 'year' }).years;
Разбор:
Temporal.PlainDate.from('1990-06-15')создаёт календарную дату без часового пояса и времени суток.Temporal.Now.plainDateISO()получает текущую дату в ISO-календаре, тоже без привязки к часовому поясу пользователя в виде времени.today.since(birth, { largestUnit: 'year' })считает разницу между датами и просит вернуть её с крупнейшей единицейyear..yearsизвлекает готовое количество полных лет, что делает расчёт возраста читаемым и менее подверженным ошибкам, чем ручная арифметика сDate.
Пока поддержка в браузерах неполная, в продакшене используют Date, Intl.DateTimeFormat (см. материалы про Intl в объектах) или библиотеки вроде Luxon / date-fns / Day.js.
Практическая рекомендация
- Хранить и передавать на сервер ISO 8601 (
2024-05-25T14:00:00.000Z). - Показывать пользователю —
Intl.DateTimeFormatс локалью. - Считать интервалы — миллисекунды или библиотека; следить за обновлением Temporal в истории языка.
Практический каркас проекта без сборщика
Минимальная структура для учебного или внутреннего инструмента:
index.htmlподключает толькоmain.jsчерезtype="module".main.jsимпортирует функции изapi.js,ui.js,router.js.- Взаимодействие с сетью вынесено в отдельный модуль и использует AbortController.
- Все даты от API приходят в ISO-формате и отображаются через
Intl.DateTimeFormat.
Такой подход даёт прозрачную архитектуру без магии bundler на старте. При росте проекта те же модули переносятся в Vite/Webpack почти без переписывания.
Типичные ошибки при переходе на ES-модули
| Ошибка | Симптом | Решение |
|---|---|---|
| Путь без расширения | Failed to resolve module specifier | указывать ./utils.js полностью |
| Открытие файла напрямую с диска | CORS/модуль не загружается | запускать через локальный HTTP-сервер |
| Смешивание CommonJS и ESM в браузере | require is not defined | использовать только import/export |
Слепая работа с Date в разных часовых поясах | неверные даты у пользователей | передавать UTC/ISO и форматировать на клиенте |
Краткий итог
type="module" даёт изолированные файлы с import/export прямо в браузере; import() — ленивая загрузка. Temporal — будущая замена хрупкому Date; до широкой поддержки опирайтесь на ISO-строки, Intl и проверенные библиотеки.
Базовый разбор HTTP и HTTPS находится в отдельной статье — HTTP как основа веб-интеграций.