Перейти к основному содержимому

ES-модули в браузере и обзор Temporal API

Разработчику Архитектору

Два мира модулей

В структуре и подключении кода разобраны теги <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).


Именованный и экспорт по умолчанию

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

Загрузка по условию (ленивая вкладка, маршрут):

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 — зачем смотреть вперёд

Объект 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 как основа веб-интеграций.