Files
progcode/editorial/agent-rewrites/320.json
T

8 lines
18 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> проходит разные правила. Модульный скрипт без сборщика передаёт спецификатор браузеру: браузер строит 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>&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>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>"
}