Files

8 lines
22 KiB
JSON
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{
"index": 321,
"slug": "editorial-2019-02-practice-es-modules",
"title": "ES-модули в браузере: как найти причину 404, MIME и CORS",
"excerpt": "Страница загрузила HTML, но интерфейс не запустился: браузер не нашёл entry-модуль или его зависимость. Разбираем нативный граф ES-модулей, проверяем URL и ответ сервера, а затем фиксируем критерий готовности без сборщика.",
"contentHtml": "<p>HTML открылся, но кнопка не появилась. В Console видна ошибка импорта, а в Network — 404, ответ с HTML или отказ CORS. Цена ошибки — не только один пустой экран. Если заменить модуль готовым bundle наугад, причина останется в URL или сервере и проявится на следующем экране. Команда потратит время на повторные сборки, хотя браузер не получил нужный JavaScript.</p>\n<p>Правило для проверки такое: нативный ES-модуль запускается только после успешной загрузки всего графа. Нужно проверить entry, URL каждого импорта, HTTP-ответ и результат выполнения. Синтаксис <code>import</code> не настраивает Webpack, не ищет файл в <code>node_modules</code> и не исправляет SPA-маршрутизацию. Ниже — самостоятельный учебный пример. Он показывает механизм и порядок проверки, но не утверждает, что так устроен конкретный production-сайт.</p>\n<h2>Сценарий: пустой экран после подключения модуля</h2>\n<p>Представим учебную страницу, где разработчик подключил <code>app.js</code> к <code>index.html</code>. После обновления HTML отображается, но кнопка не появляется. Сначала он меняет порядок <code>import</code> и повторяет загрузку; симптом не меняется. Затем открывает Network и видит, что <code>app.js</code> получил 200, а <code>message.js</code> — 200 с <code>Content-Type: text/html</code>. Проверка Response показывает SPA fallback: сервер вернул страницу вместо зависимости. После исправления маршрута assets браузер получает JavaScript, <code>#status</code> меняется, а в Console остаётся только ожидаемая учебная отметка. Если тот же запрос даёт 404, действие будет другим — исправить URL или расположение файла.</p>\n<h2>Что именно загружает браузер</h2>\n<p>В HTML ставят <code>&lt;script type=\"module\" src=\"./assets/app.js\"&gt;</code>. Значение <code>module</code> меняет режим скрипта. Браузер получает URL entry, читает его статические импорты, строит граф и загружает зависимости. Затем он вычисляет модули в порядке их связей. Объявления верхнего уровня модуля не становятся случайными свойствами <code>window</code>. Связь между файлами задают экспорт и импорт.</p>\n<p>Здесь есть две границы. ECMAScript описывает модуль, его экспорты и зависимости. HTML и Fetch описывают загрузку ресурса, URL, CORS и момент запуска. Сборщик может заранее разрешить имя пакета, объединить файлы и создать chunk. Нативный браузер этого не делает только потому, что встретил слово <code>import</code>.</p>\n<p>Статический импорт — это не вызов функции в середине тела модуля. Зависимость должна быть доступна до вычисления импортёра. Поэтому ошибка в <code>message.js</code> не позволяет считать <code>app.js</code> готовым. Один <code>console.log</code> в entry не закрывает проверку: лог может не появиться из-за неверного URL, а появившийся лог не доказывает правильный ответ всех зависимостей.</p>\n<h2>Симптом → причина → проверка → действие</h2>\n<div class=\"table-scroll\"><table><caption>Матрица диагностики нативного графа</caption><thead><tr><th scope=\"col\">Симптом</th><th scope=\"col\">Причина</th><th scope=\"col\">Проверка</th><th scope=\"col\">Действие</th></tr></thead><tbody><tr><td>HTML есть, интерфейс пуст</td><td>Entry не загрузился или не вычислился</td><td>Network: запрос <code>app.js</code>; Console: текст ошибки</td><td>Проверить <code>type=\"module\"</code>, URL, статус и ответ entry</td></tr><tr><td>Импорт отвечает 404</td><td>Относительный путь указывает не в ту папку</td><td>Скопировать фактический Request URL из Network</td><td>Исправить спецификатор в файле-импортёре</td></tr><tr><td>Статус 200, но модуль не запускается</td><td>Сервер вернул HTML fallback или неверный MIME</td><td>Открыть Response и проверить <code>Content-Type</code></td><td>Отделить маршрут assets от маршрута приложения</td></tr><tr><td>Другой origin блокирует импорт</td><td>Ответ не проходит CORS-проверку модуля</td><td>Проверить origin и CORS-заголовки ответа</td><td>Разрешить нужный origin или отдать файл с того же origin</td></tr><tr><td>Работает после сборки, не работает напрямую</td><td>Сборщик добавил resolution, alias или расширение</td><td>Сравнить исходный import с URL native-запроса</td><td>Либо дать браузеру URL, либо запускать проверенный bundle</td></tr></tbody></table></div>\n<p>404 и MIME-ошибку нельзя лечить одной перестановкой импортов. Сначала сохраняют Request URL, статус, redirect, Response и <code>Content-Type</code>. Если ответ содержит разметку страницы, сервер вернул не модуль. Статус 200 не превращает HTML в JavaScript.</p>\n<h2>Минимальный пример с тремя файлами</h2>\n<p>Пример рассчитан на локальный HTTP-сервер. Открытие через <code>file://</code> не является проверкой веб-доставки: у файлового URL другая модель origin, а поведение доступа к зависимостям отличается от HTTP. В каталоге должны лежать <code>index.html</code>, <code>assets/app.js</code> и <code>assets/message.js</code>.</p>\n<pre><code>&lt;!-- index.html --&gt;\n&lt;!doctype html&gt;\n&lt;meta charset=\"utf-8\"&gt;\n&lt;main&gt;\n &lt;h1&gt;Статус загрузки&lt;/h1&gt;\n &lt;output id=\"status\"&gt;ожидание&lt;/output&gt;\n&lt;/main&gt;\n&lt;script type=\"module\" src=\"./assets/app.js\"&gt;&lt;/script&gt;\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);</code></pre>\n<p>Это учебная схема, а не трасса реального проекта. После запуска браузер должен запросить <code>assets/app.js</code>, затем <code>assets/message.js</code>. В <code>#status</code> появляется строка из экспорта. В Console виден URL entry. Для готовности нужны все три наблюдения: два корректных сетевых ответа, DOM-результат и отсутствие ошибки разрешения.</p>\n<h2>Как вычисляется относительный путь</h2>\n<p>Спецификатор <code>./message.js</code> считается от URL файла, который его содержит. Если entry находится по адресу <code>/demo/assets/app.js</code>, браузер запрашивает <code>/demo/assets/message.js</code>. Он не считает путь от <code>index.html</code> и не обязан добавлять расширение. Поэтому <code>./message</code> — это другой URL, а не сокращённая запись <code>./message.js</code>.</p>\n<pre><code>// /demo/pages/index.html\n&lt;script type=\"module\" src=\"../assets/app.js\"&gt;&lt;/script&gt;\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\"</code></pre>\n<p>Практическая проверка не требует догадки. Откройте failed request, определите URL импортёра и сравните его каталог с каталогом ожидаемого файла. Если файл лежит в другом месте, исправьте один спецификатор или структуру каталогов. Если файл существует, но приходит HTML, ищите правило раздачи статических файлов и SPA fallback.</p>\n<p>Bare-имя вроде <code>date-fns</code> не является обычным URL для этого учебного native-сценария. Webpack может найти пакет по <code>node_modules</code>, alias и полю <code>resolve</code>. Браузер без import map или другого явно настроенного механизма не получает эти правила. Не переносите конфигурацию сборщика в Console браузера.</p>\n<h2>Сервер входит в контракт модуля</h2>\n<p>Браузер получает модуль как ресурс по URL. Для того же origin всё равно важны статус, итоговый URL после redirect и тип содержимого. Для другого origin добавляется CORS-проверка. Ошибка MIME или CORS находится на границе доставки. Её не исправит добавление ещё одного <code>import</code> и не объяснит отсутствие файла в JavaScript-коде.</p>\n<p>Особенно часто ломается SPA fallback. Маршрутизатор знает, что неизвестный путь страницы надо заменить на <code>index.html</code>. Но запрос <code>/assets/message.js</code> должен получить JavaScript, а не тот же HTML. Если Network показывает 200, откройте Response. Содержимое важнее одного status code.</p>\n<p>При cross-origin загрузке проверяйте точный origin страницы и заголовки ответа. Не отключайте защиту браузера как способ доказать исправность production. Такая настройка скрывает проблему, а не проверяет серверный контракт. Надёжнее временно отдать учебные файлы с того же origin и затем отдельно проверить разрешённый cross-origin путь.</p>\n<figure><img src=\"/assets/editorial/2019/es-modules-native-graph-2019.svg\" alt=\"Учебный граф нативных ES-модулей: HTML загружает app.js, тот запрашивает message.js, после готовности графа app.js меняет элемент status\" loading=\"lazy\" /><figcaption>Граф проверяют по цепочке: entry, зависимость, ответ сервера и изменение DOM. Иллюстрация показывает учебный маршрут, а не измерение production.</figcaption></figure>\n<h2>Момент запуска и async</h2>\n<p>Module script без <code>async</code> ведёт себя как отложенный скрипт относительно разбора документа: браузер может загружать граф параллельно, а вычисляет его после разбора HTML. Поэтому в примере элемент <code>#status</code> уже существует. Это не повод считать любой верхнеуровневый код безопасным. Если модулю нужен элемент, состояние или другой bootstrap, условие должно быть видно в коде.</p>\n<p>Атрибут <code>async</code> меняет момент вычисления. Скрипт может выполниться сразу после готовности графа, ещё до конца разбора документа. Для виджета это создаёт отрицательный путь: <code>querySelector</code> вернёт <code>null</code>, хотя URL и MIME исправны. Не добавляйте <code>async</code> как универсальное ускорение. Сначала проверьте, что код не зависит от ещё не разобранной разметки.</p>\n<p>Для старых браузеров можно держать classic-версию с <code>nomodule</code>. Это отдельный артефакт и отдельный сценарий. Нельзя считать fallback проверенным только потому, что современный браузер успешно выполнил module script.</p>\n<h2>Порядок проверки</h2>\n<ol><li>Зафиксировать симптом: пустой элемент, ошибка Console, 404, MIME или CORS. Сохранить адрес страницы и время перезагрузки.</li><li>Отдать пример через HTTP-сервер и открыть URL страницы. Не использовать <code>file://</code> как эквивалент веб-среды.</li><li>Проверить HTML: оставить один entry с <code>type=\"module\"</code> и точным <code>src</code>. Не подключать bundle «для надёжности».</li><li>В Network включить сохранение запросов и перезагрузить страницу. Записать Request URL, статус, redirect, Response и <code>Content-Type</code> для entry и каждой зависимости.</li><li>Для каждого относительного импорта считать путь от файла-импортёра. Проверить расширение и фактическое расположение файла.</li><li>Сверить Console и DOM. Успехом считать отсутствие ошибок загрузки, строку в <code>#status</code> и лог только как дополнительную отметку, а не как единственное доказательство.</li><li>Если ответ пришёл с другого origin, проверить CORS-заголовки. Если ответ содержит HTML, исправить раздачу assets, а не менять JavaScript наугад.</li><li>Повторить тот же сценарий после одной правки. Затем удалить учебные отметки и отдельно проверить fallback, если он нужен продукту.</li></ol>\n<h2>Ограничения и отрицательный путь</h2>\n<p>Нативные модули не заменяют сборку во всех проектах. Старый браузер может не поддерживать module scripts. Приложению могут требоваться transpile, polyfill, code splitting, tree shaking или обработка пакетов. Эти задачи принадлежат сборщику и не решаются добавлением <code>type=\"module\"</code>.</p>\n<p>Даже успешный минимальный пример не доказывает производительность страницы. Он не измеряет размер ответа, кэш, CDN, время CPU и реальный порядок виджетов. Он доказывает только маршрут загрузки простого графа. Учебные логи и <code>import.meta.url</code> нельзя переносить в production без отдельного решения о наблюдаемости.</p>\n<p>Если entry загрузился, но DOM не изменился, не объявляйте виноватым URL. Проверьте исключение внутри модуля, наличие элемента, порядок запуска и данные функции. Если два запроса имеют 200, но один ответ — HTML, причина всё ещё на серверной границе. Если native-сценарий не поддерживается целевой средой, отрицательный результат честно ведёт к сборке и fallback, а не к бесконечным правкам пути.</p>\n<h2>Проверяемый критерий готовности</h2>\n<p>Проверка готова, когда другой инженер повторяет её с чистой перезагрузки и получает тот же результат. В Network видны entry и все статические зависимости с ожидаемыми URL, допустимыми статусами и JavaScript-ответами. Console не содержит ошибок разрешения, MIME и CORS. DOM содержит ожидаемую строку. Отдельно зафиксирован отрицательный путь: неверный URL даёт обнаруживаемую ошибку, а HTML fallback не принимается за рабочий модуль.</p>\n<p>Для production-страницы критерий дополняют целевой браузер, политика cross-origin, нужный fallback и способ доставки. Если эти условия не заданы, готовым считается только учебный маршрут, а не вся система. Такой результат проверяем: он связывает симптом с конкретным запросом, запрос с причиной, а действие — с наблюдаемым изменением.</p>\n<h2>Проверяемые источники</h2>\n<ul><li><a href=\"https://html.spec.whatwg.org/multipage/scripting.html#the-script-element\" target=\"_blank\" rel=\"noopener noreferrer\">HTML Living Standard: the script element</a> — официальный алгоритм и атрибуты module script, включая <code>async</code>, <code>nomodule</code> и URL ресурса.</li><li><a href=\"https://html.spec.whatwg.org/multipage/webappapis.html#javascript-module-scripts\" target=\"_blank\" rel=\"noopener noreferrer\">HTML Living Standard: JavaScript module scripts</a> — официальная модель загрузки и выполнения графа модулей в веб-среде.</li><li><a href=\"https://tc39.es/ecma262/multipage/ecmascript-language-scripts-and-modules.html\" target=\"_blank\" rel=\"noopener noreferrer\">ECMAScript Language Specification: Scripts and Modules</a> — нормативная модель модулей, импортов, экспортов и их вычисления.</li>\n<li><a href=\"https://fetch.spec.whatwg.org/#cors-protocol\" target=\"_blank\" rel=\"noopener noreferrer\">Fetch Standard: CORS protocol</a> — официальные правила для cross-origin ответов, заголовков и проверки доступа.</li></ul>"
}