8 lines
19 KiB
JSON
8 lines
19 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> одинаково выглядит в исходнике, но его разрешают разные системы. 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><!-- /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>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>"
|
||
}
|