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