Files
progcode/editorial/agent-rewrites/320.json
T
huncode 2d914b543f
Build and deploy / deploy (push) Failing after 15s
Publish rewritten technical article archive
2026-08-02 22:19:34 +03:00

8 lines
19 KiB
JSON
Raw 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": 320,
"slug": "editorial-2019-02-mechanism-es-modules",
"title": "ES-модули в браузере и Webpack: где разрешается import",
"excerpt": "Одинаковый синтаксис import проходит разные границы. Разбираем, как браузер строит URL графа модулей, что добавляет Webpack и как быстро найти причину 404, MIME-ошибки или «модуль не найден».",
"contentHtml": "<p>Сборка проходит, но страница падает в браузере: <code>Failed to resolve module specifier</code>, 404 на зависимость или ответ с <code>index.html</code> вместо JavaScript. Иногда приложение запускается дважды после добавления второго тега <code>script</code>. Цена ошибки — не только сломанный экран. Команда тратит время на настройку Webpack, хотя браузер не получил корректный URL, либо чинит путь в HTML, хотя проблема возникла ещё на этапе сборки.</p>\n<p>Главный тезис прост: слово <code>import</code> одинаково выглядит в исходнике, но его разрешают разные системы. Native-модуль передаёт спецификатор браузеру. Браузер превращает его в URL, загружает граф и проверяет ответы сервера. Webpack читает этот же исходник раньше, находит пакет по своим правилам и выпускает готовый asset. Поэтому диагноз начинается с вопроса: какой именно файл сейчас читает <code>import</code> — браузер или сборщик?</p>\n<h2>Две границы одного import</h2>\n<p>В native-сценарии HTML подключает entry-файл как модуль. Например, браузер получает <code>/demo/assets/main.js</code>. Внутри файл содержит <code>import { apiRoot } from './config.js'</code>. Спецификатор разрешается относительно URL импортёра, то есть относительно <code>/demo/assets/main.js</code>. Запрос уйдёт на <code>/demo/assets/config.js</code>. Адрес страницы <code>/demo/index.html</code> здесь не является базой.</p>\n<p>Браузер не ищет файл в <code>node_modules</code> и не применяет <code>resolve.extensions</code> из Webpack. Для учебного native-сценария <code>./config.js</code> — URL-подобный адрес, а <code>config</code> не обязан автоматически превратиться в <code>config.js</code>. Bare-спецификатор вроде <code>date-kit</code> тоже не становится URL сам по себе. Для него нужен отдельный механизм отображения, например import map, либо сборка.</p>\n<p>Webpack работает до браузера. Он читает <code>import { format } from 'date-kit'</code>, проверяет alias, package exports, расширения и loaders, а затем включает код в bundle или отдельный chunk. В HTML браузер может увидеть только <code>/assets/app.8f3c.js</code>. Он не знает, как Webpack нашёл <code>date-kit</code>. Если пакет не разрешился, ошибка относится к сборке. Если asset загрузился, но браузер получил HTML или заблокировал cross-origin-запрос, ошибка относится к доставке.</p>\n<div class=\"table-scroll\"><table><thead><tr><th scope=\"col\">Запись import</th><th scope=\"col\">Native-браузер</th><th scope=\"col\">Webpack</th><th scope=\"col\">Проверка</th></tr></thead><tbody><tr><td><code>./format.js</code></td><td>URL от файла-импортёра</td><td>Может оставить путь или включить файл в bundle</td><td>Сверить Request URL и путь entry</td></tr><tr><td><code>/assets/format.js</code></td><td>URL от origin страницы</td><td>Может обработать как путь проекта</td><td>Сопоставить public path и URL ответа</td></tr><tr><td><code>date-kit</code></td><td>Не готовый URL без дополнительного отображения</td><td>Ищет пакет, alias или поле package.json</td><td>Искать причину на этапе сборки</td></tr><tr><td><code>./format</code></td><td>Не обязан добавлять <code>.js</code></td><td>Может подобрать расширение по <code>resolve</code></td><td>Проверить точный URI отдельно</td></tr></tbody></table></div>\n<p>Эта таблица разделяет наблюдения, а не предлагает универсальную конфигурацию. Сборщик может изменить любой результат до отправки к браузеру. Но его правила не становятся правилами native-модулей только потому, что исходная строка выглядит одинаково.</p>\n<h2>Почему путь считают от импортёра</h2>\n<p>Рассмотрим минимальный каталог. Он намеренно использует явные расширения и не зависит от Webpack.</p>\n<pre><code>&lt;!-- /demo/pages/index.html --&gt;\n&lt;script type=\"module\" src=\"../assets/app/main.js\"&gt;&lt;/script&gt;\n\n// /demo/assets/app/main.js\nimport { apiRoot } from './config.js';\nconsole.log('API:', apiRoot);\n\n// /demo/assets/app/config.js\nexport const apiRoot = '/api/v1';</code></pre>\n<p>После открытия страницы Network должен показать запрос к <code>/demo/assets/app/main.js</code>, затем к <code>/demo/assets/app/config.js</code>. Если написать в <code>main.js</code> <code>import './config'</code>, браузер отправит запрос к адресу без суффикса. Сервер может ответить 404. SPA-правило может вернуть статус 200 и тело <code>index.html</code>. В обоих случаях ошибка находится в URL или в раздаче ассетов, а не в tree shaking.</p>\n<p>Та же папка в Webpack может собраться без расширения. Это не противоречие. Webpack применил свой <code>resolve.extensions</code> до появления браузерного запроса. Чтобы увидеть границу, сравните исходную строку, сообщение сборщика и фактический Request URL. Нельзя использовать успешное разрешение в bundle как доказательство, что тот же файл можно подключить напрямую.</p>\n<h2>Граф модулей и порядок вычисления</h2>\n<p>Статический <code>import</code> не является вызовом, который выполняется в середине тела файла. Среда сначала строит связи графа. Зависимость должна быть найдена и подготовлена до вычисления модуля, который её импортирует. Поэтому строка после <code>import</code> не может заранее создать глобальную переменную для импортируемого файла.</p>\n<pre><code>// config.js\nconsole.log('1. вычисляется config.js');\nexport const apiRoot = '/api/v1';\n\n// main.js\nimport { apiRoot } from './config.js';\nconsole.log('2. main.js получил ' + apiRoot);</code></pre>\n<p>В учебной странице сначала появится сообщение из <code>config.js</code>, затем сообщение из <code>main.js</code>. Это не делает побочные эффекты верхнего уровня хорошим способом инициализации. Если два entry меняют один <code>window</code>-объект, результат становится хрупким. Надёжнее оставить одного владельца запуска и передать ему явную функцию.</p>\n<p>У module script без <code>async</code> браузер учитывает готовность графа при запуске. Атрибут <code>async</code> меняет момент выполнения относительно документа и других скриптов. Webpack может добавить runtime, динамические чанки и собственный порядок загрузки. Эти детали относятся к его asset-графу. Они не меняют смысл ошибки native import и не исправляют неверный HTTP-ответ.</p>\n<h2>Симптомы, причины и действия</h2>\n<div class=\"table-scroll\"><table><thead><tr><th scope=\"col\">Симптом</th><th scope=\"col\">Причина</th><th scope=\"col\">Проверка</th><th scope=\"col\">Действие</th></tr></thead><tbody><tr><td><code>Failed to resolve module specifier</code> на bare-имени</td><td>Native-браузер не получил отображение имени пакета в URL</td><td>Открыть исходный HTML и Network; проверить тип запуска</td><td>Использовать URL, import map или bundle</td></tr><tr><td>404 на <code>./config.js</code></td><td>Путь считают от HTML, а не от импортёра, или файл не раздаётся</td><td>Сравнить URL <code>main.js</code>, Request URL и дерево assets</td><td>Исправить спецификатор или маршрут статики</td></tr><tr><td>200, но MIME-ошибка</td><td>Сервер вернул HTML, неверный MIME или SPA fallback</td><td>Посмотреть Response, <code>Content-Type</code> и redirect</td><td>Настроить раздачу JavaScript; не менять alias</td></tr><tr><td>Cross-origin import заблокирован</td><td>Ответ не прошёл CORS-проверку модуля</td><td>Проверить origin, заголовки и фактический URL ответа</td><td>Исправить политику сервера или разместить asset в нужном origin</td></tr><tr><td>Пакет не найден при сборке</td><td>Ошибка resolver, alias, версии или package exports</td><td>Сохранить сообщение Webpack и stats той же сборки</td><td>Исправить конфигурацию или зависимость до публикации bundle</td></tr><tr><td>Инициализация повторилась</td><td>Два entry, разные URL модуля или второй документ</td><td>Сравнить <code>import.meta.url</code>, теги, iframe и Network</td><td>Оставить одного владельца запуска или явно разделить entry</td></tr></tbody></table></div>\n<p>Один текст ошибки может иметь несколько причин. Например, статус 200 не доказывает, что модуль загрузился: сервер мог вернуть HTML. Два тега с одинаковым URL тоже не доказывают двойное вычисление. Сначала зафиксируйте URL и тело ответа, затем делайте вывод о графе.</p>\n<h2>Учебная проверка URL и identity</h2>\n<p>Следующий фрагмент нужен только для локальной учебной страницы. Он показывает URL, который среда передала модулю, и число отметок для этого URL. Это не production-метрика и не замена Network.</p>\n<pre><code>// assets/init.js — временная учебная диагностика\nconst url = import.meta.url;\nconst runs = window.__moduleRuns || (window.__moduleRuns = {});\nruns[url] = (runs[url] || 0) + 1;\nconsole.log('[module-check]', { url, count: runs[url] });\n\nexport function startWidget(root) {\n root.textContent = 'widget started';\n}\n\n// assets/main.js\nimport { startWidget } from './init.js';\nstartWidget(document.querySelector('#widget'));</code></pre>\n<p>При одном entry ожидается одна запись с URL вроде <code>.../assets/init.js</code>. Если появились <code>/assets/init.js</code> и <code>/assets/init.js?variant=second</code>, это два разных адреса, а значит, их нужно рассматривать как две module identity. Query может быть осознанным cache busting и не является ошибкой сам по себе. Ошибка возникает, когда второй адрес незаметно запускает тот же побочный эффект.</p>\n<p>Если один URL отмечен дважды, проверяйте не только module map. Посмотрите iframe, повторную загрузку документа, classic-скрипт с тем же действием и второй bootstrap. Лог помогает построить гипотезу. Окончательный вывод требует фактических URL, initiator и ответа сервера.</p>\n<figure><img src=\"/assets/editorial/2019/es-modules-resolution-boundary-2019.svg\" alt=\"Граница разрешения ES-модуля: браузер строит URL графа, а Webpack заранее разрешает пакет и выпускает asset\" loading=\"lazy\" /><figcaption>Одна строка import проходит разные этапы. Native-браузер идёт от URL импортёра к сетевому графу. Webpack сначала разрешает зависимости, затем отдаёт браузеру готовый asset.</figcaption></figure>\n<h2>Порядок проверки</h2>\n<ol><li>Зафиксируйте симптом, адрес страницы и способ запуска: прямой <code>type=\"module\"</code> или production-bundle.</li><li>Для native-сценария выпишите полный URL entry и каждого failed import. Считайте относительный путь от файла-импортёра.</li><li>В Network проверьте статус, итоговый URL после redirect, <code>Content-Type</code>, Response и Initiator для entry и зависимости.</li><li>Если спецификатор — bare-имя или не содержит нужного расширения, решите, должен ли его обработать import map, серверный маршрут или Webpack.</li><li>Для сборки сохраните сообщение resolver и stats той же версии lock-файла. Проверьте resolved-путь, alias, chunk и asset, не перенося эти правила в Console браузера.</li><li>Если важен порядок запуска, добавьте временную учебную отметку с <code>import.meta.url</code>, сравните URL и уберите отметку после диагноза.</li><li>Повторите проверку после одной правки. Готовность подтверждается чистой загрузкой страницы без ошибки разрешения, корректным ответом каждого модуля и одним понятным владельцем инициализации.</li></ol>\n<h2>Ограничения</h2>\n<p>Native-модули требуют браузерной поддержки module scripts. Для старого браузера можно выпускать отдельный classic-артефакт с <code>nomodule</code>, но это второй путь доставки, который проверяют отдельно. Наличие fallback не исправляет неверный native URL.</p>\n<p>CORS, redirect, CSP, service worker и серверный rewrite могут изменить наблюдаемую загрузку. Проверяйте итоговый ответ, а не только исходную строку в файле. Отключение защиты браузера не подтверждает рабочую политику сервера.</p>\n<p>Webpack здесь служит конкретным примером сборщика. Другие инструменты иначе называют chunks и настраивают resolver, но граница сохраняется: до браузера инструмент строит свой граф, после браузер загружает опубликованные URL. Учебные фрагменты показывают механизм и форму наблюдения. Они не сообщают production-результаты и не заменяют трассу конкретного приложения.</p>\n<h2>Проверяемые источники</h2>\n<ul><li><a href=\"https://tc39.es/ecma262/#sec-modules\" target=\"_blank\" rel=\"noopener noreferrer\">ECMAScript Language Specification: Modules</a> — нормативная модель статических import/export и графа модулей.</li><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 scripts, async и nomodule.</li><li><a href=\"https://fetch.spec.whatwg.org/#cors-protocol-and-credentials\" target=\"_blank\" rel=\"noopener noreferrer\">Fetch Standard: CORS protocol and credentials</a> — правила CORS для сетевой загрузки.</li></ul>"
}