{"index":321,"slug":"editorial-2019-02-practice-es-modules","title":"ES-модули в браузере: как найти причину 404, MIME и CORS","excerpt":"Страница загрузила HTML, но интерфейс не запустился: браузер не нашёл entry-модуль или его зависимость. Разбираем нативный граф ES-модулей, проверяем URL и ответ сервера, а затем фиксируем критерий готовности без сборщика.","contentHtml":"
HTML открылся, но кнопка не появилась. В Console видна ошибка импорта, а в Network — 404, ответ с HTML или отказ CORS. Цена ошибки — не только один пустой экран. Если заменить модуль готовым bundle наугад, причина останется в URL или сервере и проявится на следующем экране. Команда потратит время на повторные сборки, хотя браузер не получил нужный JavaScript.
\nТезис простой: нативный ES-модуль запускается только после успешной загрузки всего графа. Нужно проверить entry, URL каждого импорта, HTTP-ответ и результат выполнения. Синтаксис import не настраивает Webpack, не ищет файл в node_modules и не исправляет SPA-маршрутизацию. Ниже — самостоятельный учебный пример. Он показывает механизм и порядок проверки, но не утверждает, что так устроен конкретный production-сайт.
В HTML ставят <script type=\"module\" src=\"./assets/app.js\">. Значение module меняет режим скрипта. Браузер получает URL entry, читает его статические импорты, строит граф и загружает зависимости. Затем он вычисляет модули в порядке их связей. Объявления верхнего уровня модуля не становятся случайными свойствами window. Связь между файлами задают экспорт и импорт.
Здесь есть две границы. ECMAScript описывает модуль, его экспорты и зависимости. HTML и Fetch описывают загрузку ресурса, URL, CORS и момент запуска. Сборщик может заранее разрешить имя пакета, объединить файлы и создать chunk. Нативный браузер этого не делает только потому, что встретил слово import.
Статический импорт — это не вызов функции в середине тела модуля. Зависимость должна быть доступна до вычисления импортёра. Поэтому ошибка в message.js не позволяет считать app.js готовым. Один console.log в entry не закрывает проверку: лог может не появиться из-за неверного URL, а появившийся лог не доказывает правильный ответ всех зависимостей.
| Симптом | Причина | Проверка | Действие |
|---|---|---|---|
| HTML есть, интерфейс пуст | Entry не загрузился или не вычислился | Network: запрос app.js; Console: текст ошибки | Проверить type=\"module\", URL, статус и ответ entry |
| Импорт отвечает 404 | Относительный путь указывает не в ту папку | Скопировать фактический Request URL из Network | Исправить спецификатор в файле-импортёре |
| Статус 200, но модуль не запускается | Сервер вернул HTML fallback или неверный MIME | Открыть Response и проверить Content-Type | Отделить маршрут assets от маршрута приложения |
| Другой origin блокирует импорт | Ответ не проходит CORS-проверку модуля | Проверить origin и CORS-заголовки ответа | Разрешить нужный origin или отдать файл с того же origin |
| Работает после сборки, не работает напрямую | Сборщик добавил resolution, alias или расширение | Сравнить исходный import с URL native-запроса | Либо дать браузеру URL, либо запускать проверенный bundle |
404 и MIME-ошибку нельзя лечить одной перестановкой импортов. Сначала сохраняют Request URL, статус, redirect, Response и Content-Type. Если ответ содержит разметку страницы, сервер вернул не модуль. Статус 200 не превращает HTML в JavaScript.
Пример рассчитан на локальный HTTP-сервер. Открытие через file:// не является проверкой веб-доставки: у файлового URL другая модель origin, а поведение доступа к зависимостям отличается от HTTP. В каталоге должны лежать index.html, assets/app.js и assets/message.js.
<!-- index.html -->\n<!doctype html>\n<meta charset=\"utf-8\">\n<main>\n <h1>Статус загрузки</h1>\n <output id=\"status\">ожидание</output>\n</main>\n<script type=\"module\" src=\"./assets/app.js\"></script>\n\n// assets/message.js\nexport function message(name) {\n return \"модуль \" + name + \" получен\";\n}\n\n// assets/app.js\nimport { message } from \"./message.js\";\n\nconst status = document.querySelector(\"#status\");\nstatus.textContent = message(\"app.js\");\nconsole.log(\"Учебный entry:\", import.meta.url);\nЭто учебная схема, а не трасса реального проекта. После запуска браузер должен запросить assets/app.js, затем assets/message.js. В #status появляется строка из экспорта. В Console виден URL entry. Для готовности нужны все три наблюдения: два корректных сетевых ответа, DOM-результат и отсутствие ошибки разрешения.
Спецификатор ./message.js считается от URL файла, который его содержит. Если entry находится по адресу /demo/assets/app.js, браузер запрашивает /demo/assets/message.js. Он не считает путь от index.html и не обязан добавлять расширение. Поэтому ./message — это другой URL, а не сокращённая запись ./message.js.
// /demo/pages/index.html\n<script type=\"module\" src=\"../assets/app.js\"></script>\n\n// /demo/assets/app.js\nimport { message } from \"./message.js\";\n\n// Браузер ищет: /demo/assets/message.js\n// Он не ищет: /demo/message.js\n// и не добавляет .js к import \"./message\"\nПрактическая проверка не требует догадки. Откройте failed request, определите URL импортёра и сравните его каталог с каталогом ожидаемого файла. Если файл лежит в другом месте, исправьте один спецификатор или структуру каталогов. Если файл существует, но приходит HTML, ищите правило раздачи статических файлов и SPA fallback.
\nBare-имя вроде date-fns не является обычным URL для этого учебного native-сценария. Webpack может найти пакет по node_modules, alias и полю resolve. Браузер без import map или другого явно настроенного механизма не получает эти правила. Не переносите конфигурацию сборщика в Console браузера.
Браузер получает модуль как ресурс по URL. Для того же origin всё равно важны статус, итоговый URL после redirect и тип содержимого. Для другого origin добавляется CORS-проверка. Ошибка MIME или CORS находится на границе доставки. Её не исправит добавление ещё одного import и не объяснит отсутствие файла в JavaScript-коде.
Особенно часто ломается SPA fallback. Маршрутизатор знает, что неизвестный путь страницы надо заменить на index.html. Но запрос /assets/message.js должен получить JavaScript, а не тот же HTML. Если Network показывает 200, откройте Response. Содержимое важнее одного status code.
При cross-origin загрузке проверяйте точный origin страницы и заголовки ответа. Не отключайте защиту браузера как способ доказать исправность production. Такая настройка скрывает проблему, а не проверяет серверный контракт. Надёжнее временно отдать учебные файлы с того же origin и затем отдельно проверить разрешённый cross-origin путь.
\nModule script без async ведёт себя как отложенный скрипт относительно разбора документа: браузер может загружать граф параллельно, а вычисляет его после разбора HTML. Поэтому в примере элемент #status уже существует. Это не повод считать любой верхнеуровневый код безопасным. Если модулю нужен элемент, состояние или другой bootstrap, условие должно быть видно в коде.
Атрибут async меняет момент вычисления. Скрипт может выполниться сразу после готовности графа, ещё до конца разбора документа. Для виджета это создаёт отрицательный путь: querySelector вернёт null, хотя URL и MIME исправны. Не добавляйте async как универсальное ускорение. Сначала проверьте, что код не зависит от ещё не разобранной разметки.
Для старых браузеров можно держать classic-версию с nomodule. Это отдельный артефакт и отдельный сценарий. Нельзя считать fallback проверенным только потому, что современный браузер успешно выполнил module script.
file:// как эквивалент веб-среды.type=\"module\" и точным src. Не подключать bundle «для надёжности».Content-Type для entry и каждой зависимости.#status и лог только как дополнительную отметку, а не как единственное доказательство.Нативные модули не заменяют сборку во всех проектах. Старый браузер может не поддерживать module scripts. Приложению могут требоваться transpile, polyfill, code splitting, tree shaking или обработка пакетов. Эти задачи принадлежат сборщику и не решаются добавлением type=\"module\".
Даже успешный минимальный пример не доказывает производительность страницы. Он не измеряет размер ответа, кэш, CDN, время CPU и реальный порядок виджетов. Он доказывает только маршрут загрузки простого графа. Учебные логи и import.meta.url нельзя переносить в production без отдельного решения о наблюдаемости.
Если entry загрузился, но DOM не изменился, не объявляйте виноватым URL. Проверьте исключение внутри модуля, наличие элемента, порядок запуска и данные функции. Если два запроса имеют 200, но один ответ — HTML, причина всё ещё на серверной границе. Если native-сценарий не поддерживается целевой средой, отрицательный результат честно ведёт к сборке и fallback, а не к бесконечным правкам пути.
\nПроверка готова, когда другой инженер повторяет её с чистой перезагрузки и получает тот же результат. В Network видны entry и все статические зависимости с ожидаемыми URL, допустимыми статусами и JavaScript-ответами. Console не содержит ошибок разрешения, MIME и CORS. DOM содержит ожидаемую строку. Отдельно зафиксирован отрицательный путь: неверный URL даёт обнаруживаемую ошибку, а HTML fallback не принимается за рабочий модуль.
\nДля production-страницы критерий дополняют целевой браузер, политика cross-origin, нужный fallback и способ доставки. Если эти условия не заданы, готовым считается только учебный маршрут, а не вся система. Такой результат проверяем: он связывает симптом с конкретным запросом, запрос с причиной, а действие — с наблюдаемым изменением.
\nasync, nomodule и URL ресурса.