8 lines
18 KiB
JSON
8 lines
18 KiB
JSON
{
|
||
"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> проходит разные правила. Модульный скрипт без сборщика передаёт спецификатор браузеру: браузер строит URL, загружает граф модулей и проверяет HTTP-ответы. Webpack разбирает исходник раньше, применяет свой resolver и выпускает bundle или chunk. Поэтому первый вопрос в диагностике такой: этот <code>import</code> сейчас читает браузер или сборщик?</p>\n<h2>Две границы одного import</h2>\n<p>В браузерном сценарии HTML подключает entry-файл как модуль. Пусть браузер получил <code>/demo/assets/main.js</code>, а файл содержит <code>import { apiRoot } from './config.js'</code>. Относительный спецификатор разрешается от URL импортёра. Запрос уйдёт на <code>/demo/assets/config.js</code>; адрес страницы <code>/demo/index.html</code> базой не становится.</p>\n<p>Браузер не ищет <code>date-kit</code> в <code>node_modules</code> и не применяет <code>resolve.extensions</code> Webpack. Запись <code>./config.js</code> — относительный спецификатор с явным расширением. Запись <code>./config</code> не обязана превратиться в <code>./config.js</code>. Bare-имя вроде <code>date-kit</code> требует отображения в URL, например через import map, либо должно попасть в результат сборки.</p>\n<p>Webpack сначала разрешает модуль по правилам <code>resolve</code>: учитывает alias, поля <code>exports</code> пакета и список расширений. После этого правила module/loaders обрабатывают найденный файл. В опубликованном HTML браузер может увидеть только <code>/assets/app.8f3c.js</code> и не знает, как resolver нашёл <code>date-kit</code>. Ошибка resolver относится к сборке; HTML вместо JavaScript или отказ CORS — к доставке.</p>\n<div class=\"table-scroll\"><table><thead><tr><th scope=\"col\">Запись import</th><th scope=\"col\">Браузер без сборки</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 или chunk</td><td>Сверить Request URL с путём entry</td></tr><tr><td><code>/assets/format.js</code></td><td>URL от origin страницы</td><td>Обрабатывает запись по правилам своего resolver</td><td>Сопоставить public path и URL ответа</td></tr><tr><td><code>date-kit</code></td><td>Не разрешается без import map или другого отображения</td><td>Ищет пакет, alias и поля package.json</td><td>Искать причину на этапе сборки</td></tr><tr><td><code>./format</code></td><td>Не обязан добавлять <code>.js</code></td><td>Может подобрать расширение из <code>resolve.extensions</code></td><td>Проверить точный URI отдельно</td></tr></tbody></table></div>\n<p>Таблица разделяет два этапа, а не задаёт универсальную конфигурацию. Сборщик меняет то, что дойдёт до браузера. Его правила не становятся правилами браузерного модуля только потому, что исходная строка выглядит одинаково.</p>\n<h2>Сценарий с неверным относительным путём</h2>\n<p>Минимальный каталог ниже не зависит от Webpack и использует явные расширения. Путь в комментариях нужен, чтобы расчёт можно было повторить.</p>\n<pre><code><!-- /demo/pages/index.html -->\n<script type=\"module\" src=\"../assets/app/main.js\"></script>\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>import './config'</code>, браузер запросит URL без суффикса. Сервер может ответить 404. SPA-fallback может вернуть 200 и тело <code>index.html</code>. В обоих случаях сначала проверяют URL и раздачу статики, а не tree shaking.</p>\n<p>Webpack может собрать тот же исходник без расширения, если оно есть в <code>resolve.extensions</code>. Это не противоречие: Webpack применил свой resolver до появления браузерного запроса. Сравните исходную строку, сообщение сборщика и фактический Request URL. Успешная сборка не доказывает, что тот же исходник можно подключить напрямую.</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 и динамические chunks, но это уже его asset-граф. Эти детали не исправляют неверный HTTP-ответ native-модуля.</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>Браузер не получил отображение имени в URL</td><td>Открыть исходный HTML и проверить режим запуска</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-проверку module script</td><td>Проверить origin, заголовки и итоговый URL</td><td>Исправить политику сервера или origin asset</td></tr><tr><td>Пакет не найден при сборке</td><td>Ошибка resolver, alias, версии или <code>exports</code></td><td>Сохранить сообщение Webpack и stats той же сборки</td><td>Исправить конфигурацию или зависимость до публикации</td></tr><tr><td>Инициализация повторилась</td><td>Два entry, разные URL или второй bootstrap</td><td>Сравнить <code>import.meta.url</code>, теги, iframe и Network</td><td>Оставить одного владельца или разделить entry явно</td></tr></tbody></table></div>\n<p>Один текст ошибки не доказывает одну причину. Статус 200 не означает, что модуль загрузился: тело может быть <code>index.html</code>. Два тега с одним URL также не доказывают двойное вычисление: в одном <code>Window</code> браузер может переиспользовать тот же модуль. Сначала зафиксируйте URL и тело ответа, затем делайте вывод о графе.</p>\n<h2>Проверка URL и module identity</h2>\n<p>Карта модулей браузера связывает загруженный URL с модулем в текущем контексте. Для временной диагностики учебной страницы можно записать <code>import.meta.url</code> и число запусков. Это наблюдение, а не production-метрика.</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>, это разные URL. Query-параметр может быть осознанным cache busting и сам по себе не является ошибкой. Риск появляется, когда второй URL незаметно запускает тот же побочный эффект.</p>\n<p>Если один точный URL отмечен дважды, проверьте повторную загрузку документа, 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 проходит два этапа. Браузер идёт от URL импортёра к сетевому графу. Webpack сначала разрешает зависимости, затем отдаёт браузеру опубликованный asset.</figcaption></figure>\n<h2>Порядок проверки</h2>\n<ol><li>Зафиксируйте симптом, адрес страницы и режим запуска: прямой <code>type=\"module\"</code> или production-bundle.</li><li>Для браузерного сценария выпишите полный 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>Module scripts требуют поддержки модульных скриптов в браузере. Для старого браузера можно выпустить отдельный classic-артефакт с <code>nomodule</code>, но это второй путь доставки, который проверяют отдельно. Fallback не исправляет неверный 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 и строгая MIME-проверка.</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><li><a href=\"https://webpack.js.org/configuration/resolve/#resolveextensions\" target=\"_blank\" rel=\"noopener noreferrer\">Webpack: resolve.extensions</a> — подбор расширений resolver до публикации bundle.</li></ul>"
|
||
}
|