Files
progcode/editorial/agent-rewrites/322.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
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": 322,
"slug": "editorial-2019-01-field-jquery-webpack",
"title": "jQuery и Webpack: как вернуть legacy-плагин после production-сборки",
"excerpt": "Старый jQuery-плагин работает в development, но пропадает после production-сборки. Разбираем порядок исполнения, глобальный window.jQuery и второй экземпляр зависимости, затем проверяем исправление в браузере и stats.json.",
"contentHtml": "<p>В development поле с маской номера работает. После production-сборки вызов <code>$('.js-phone').legacyMask()</code> падает с ошибкой о неизвестном методе. Иногда ошибка выглядит иначе: <code>window.jQuery</code> равен <code>undefined</code>, а иногда маска не падает, но не меняет поле. Цена ошибки — не только красная строка в Console. Пользователь не может ввести номер, форма отбрасывает корректное значение, а релиз приходится откатывать или срочно пересобирать.</p>\n<p>Главный тезис прост: production не «ломает» jQuery сам по себе. Сборка выявляет скрытый контракт старого плагина. Плагин может ожидать глобальный <code>window.jQuery</code>, запуститься до создания этого глобала или расширить другой экземпляр jQuery. Поэтому нужно проверять не только наличие файла в bundle, но и три факта в рантайме: какой объект получил плагин, когда он выполнился и тем ли объектом пользуется приложение.</p>\n<h2>Что именно теряется</h2>\n<p>Большинство старых jQuery-плагинов добавляет метод в <code>$.fn</code>. Для диагностики важен не общий вопрос «загрузился ли inputmask», а конкретная проверка: <code>typeof $.fn.legacyMask</code> равен <code>function</code> или нет. Если метод отсутствует, плагин не установил расширение на тот объект, который вызывает приложение.</p>\n<p>Модульный импорт и глобальная переменная — разные механизмы. Строка <code>import $ from 'jquery'</code> даёт модулю ссылку на экспорт пакета. Она не обязана записывать ту же ссылку в <code>window.jQuery</code>. <code>ProvidePlugin</code> тоже решает более узкую задачу: Webpack подставляет модуль вместо свободного идентификатора в обработанных модулях. Это не универсальная команда присвоить значение свойству <code>window</code>.</p>\n<p>Разница проявляется на границе legacy-кода. Старый файл часто написан как самовызывающаяся функция и читает глобал при выполнении:</p>\n<pre><code>(function installLegacyMask(root) {\n var $ = root.jQuery;\n\n if (!$ || !$.fn) {\n throw new Error('legacyMask expects window.jQuery');\n }\n\n $.fn.legacyMask = function legacyMask() {\n return this.addClass('has-legacy-mask');\n };\n}(window));</code></pre>\n<p>Это учебный пример. Он не сообщает результат конкретного production-проекта. Он показывает контракт: к моменту выполнения файла в <code>window.jQuery</code> должен лежать объект с прототипом <code>fn</code>. Если объект появится позже, плагин уже не узнает о нём.</p>\n<h2>Почему порядок импортов обманывает</h2>\n<p>Такой код выглядит последовательным, но задаёт неправильный порядок:</p>\n<pre><code>import $ from 'jquery';\nimport './vendor/legacy-mask';\n\nwindow.jQuery = window.$ = $;\n\n$('.js-phone').legacyMask();</code></pre>\n<p>Статические импорты образуют граф зависимостей. Тело модуля не является сценой, на которой Webpack выполняет все строки сверху вниз до разбора следующего импорта. Legacy-файл может выполниться до присваивания в <code>window</code>. В development это иногда скрывает внешний тег <code>script</code> или другой entry, который случайно создаёт глобал раньше.</p>\n<p>Безопаснее ограничить старую зависимость маленьким адаптером. Он импортирует один объект jQuery, публикует его в глобальной области и только потом запускает side effect старого файла:</p>\n<pre><code>// src/legacy-jquery-bridge.js\nimport $ from 'jquery';\n\nwindow.jQuery = $;\nwindow.$ = $;\nrequire('./vendor/legacy-mask');\n\nexport default $;\n\n// src/bootstrap.js\nimport $ from './legacy-jquery-bridge';\n\n$('.js-phone').legacyMask();</code></pre>\n<p>Вызов <code>require()</code> здесь учебный и локальный. Он нужен, чтобы явно показать порядок запуска legacy-файла. Это не рекомендация смешивать CommonJS и ES-модули во всём новом коде. Новый компонент должен принимать зависимость импортом и не менять глобальную область.</p>\n<h2>Три причины одного симптома</h2>\n<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>window.jQuery</code> пуст до запуска плагина</td><td>Импорт существует только внутри модуля</td><td>Поставить остановку перед legacy-файлом и вывести <code>window.jQuery</code></td><td>Создать один bridge и загрузить плагин после записи глобала</td></tr><tr><td>Метод есть у глобала, но отсутствует у импортированного <code>$</code></td><td>Плагин расширил другой экземпляр jQuery</td><td>Сравнить <code>window.jQuery === $</code> и оба значения <code>typeof $.fn.legacyMask</code></td><td>Убрать второй путь зависимости или выровнять его разрешение</td></tr><tr><td>Метода нет ни у глобала, ни у импорта</td><td>Файл не попал в чанк, получил ошибку или выполнился до bridge</td><td>Проверить Console, Network и modules в stats-файле</td><td>Исправить entry/порядок, затем повторить production-проверку</td></tr><tr><td>Сбой появляется только на странице со вторым entry</td><td>Общий модуль попал в разные графы или версии jQuery различаются</td><td>Найти resolved-пути и chunks для <code>jquery</code></td><td>Дедуплицировать зависимость, настроить общую часть и проверить рантайм</td></tr></tbody></table>\n<p>Таблица задаёт дерево гипотез, а не готовый диагноз. Тот же симптом дают 404 чанка, CSP, старый кеш CDN и несовместимая версия плагина. Поэтому не стоит начинать с отключения минификации. Сначала нужно подтвердить объект и порядок, затем проверить доставку файлов.</p>\n<figure><img src=\"/assets/editorial/2019/jquery-webpack-production-bridge-2019.svg\" alt=\"Схема диагностики legacy jQuery-плагина: bridge создаёт window.jQuery до запуска плагина, затем проверяется один экземпляр jQuery\" loading=\"lazy\" /><figcaption>Сначала создайте общий объект jQuery, затем выполните legacy-плагин и только после этого проверяйте метод на <code>$.fn</code>.</figcaption></figure>\n<h2>Как доказать, что экземпляр один</h2>\n<p>Проверки в браузерной консоли должны сравнивать ссылки, а не только версии. Два объекта могут иметь одну и ту же строку версии и разные прототипы. Плагин добавит метод одному объекту, а приложение вызовет другой.</p>\n<pre><code>import $ from './legacy-jquery-bridge';\n\nconst report = {\n hasGlobal: Boolean(window.jQuery),\n sameInstance: window.jQuery === $,\n pluginOnImport: typeof $.fn.legacyMask,\n pluginOnGlobal: window.jQuery\n ? typeof window.jQuery.fn.legacyMask\n : 'no-global',\n};\n\nconsole.table(report);\nconsole.assert(report.sameInstance, 'different jQuery instances');\nconsole.assert(\n report.pluginOnImport === 'function',\n 'legacy plugin is not installed on the app instance',\n);</code></pre>\n<p>Ожидаемый результат учебной схемы: <code>hasGlobal</code> и <code>sameInstance</code> равны <code>true</code>, оба поля плагина имеют значение <code>function</code>. Это проверяемое условие, а не обещание производительности. В конкретном проекте нужно также убедиться, что браузер загрузил именно новые entry и lazy-чаны.</p>\n<h2>Как найти дубликат в графе</h2>\n<p>Команда <code>npm ls jquery</code> показывает дерево пакетов, но не доказывает, что браузер создал два объекта. Совместимые зависимости сборщик может объединить. Обратная ситуация тоже возможна: один и тот же пакет войдёт в разные chunks по разным resolved-путям. Для точного ответа нужен production-статс того же lock-файла и ссылка на фактически загруженные чанки.</p>\n<pre><code># Учебный маршрут диагностики. Это не результат запуска в статье.\nnpx webpack --mode production --profile --json &gt; dist/stats.json\nnpm ls jquery\n\n# В stats.json ищите resolved-пути, modules и chunks с jquery.\n# В браузере сравните window.jQuery и импорт из bridge.</code></pre>\n<p>Статистика Webpack содержит assets, chunks и modules. Если она показывает несколько путей к jQuery, это повод изучить граф, но не окончательное доказательство дубликата в рантайме. Закрыть гипотезу можно только вместе с равенством объектов, наличием метода и Network-проверкой загруженных файлов. Если различаются версии, сначала закрепите совместимую версию и проверьте плагин на ней. Если различаются entry, настройте общую зависимость только после проверки всех страниц.</p>\n<h2>Порядок действий</h2>\n<ol><li>Зафиксируйте точный симптом: страницу, селектор, имя метода, текст ошибки, версию ассетов и commit сборки.</li><li>Перед вызовом плагина выведите <code>window.jQuery</code>, импортированный <code>$</code>, результат сравнения ссылок и тип метода в <code>$.fn</code>.</li><li>Откройте Network. Проверьте ответы entry и lazy-чанов, hash файлов и отсутствие старого HTML или кешированного bundle.</li><li>Соберите production-статистику на том же lock-файле. Найдите все resolved-пути, chunks и причины подключения jQuery.</li><li>Если плагин читает глобал, добавьте один bridge до его side effect. Если плагин поддерживает импорт, уберите глобальную зависимость.</li><li>Повторите проверку на чистой странице, в каждом entry и в сценарии, где виджет загружается лениво.</li></ol>\n<h2>Ограничения</h2>\n<p>Bridge применим в браузерном коде, где старый файл действительно читает <code>window.jQuery</code>. В SSR, worker и тестовом окружении глобальный объект может отсутствовать. Адаптер должен выполняться только в браузере или получать объект окружения явно.</p>\n<p>Глобальная jQuery остаётся техническим слоем совместимости. Она увеличивает связанность и усложняет порядок загрузки. Для нового кода лучше использовать явный импорт и передавать зависимость через модульный интерфейс. Не добавляйте <code>ProvidePlugin</code>, если проблема вызвана только отсутствующим <code>window.jQuery</code>: он может скрыть свободный идентификатор, но не исправить внешний контракт.</p>\n<p>Source map помогает расследованию, но может раскрыть пути и исходный код. Храните диагностический stats-файл и карты в защищённом месте, если политика проекта не разрешает их публикацию. Не объявляйте учебную проверку результатом production-мониторинга: без запуска на конкретной сборке нельзя утверждать, что её chunks загружены или что пользовательский сценарий исправлен.</p>\n<h2>Критерий готовности</h2>\n<p>Исправление готово, когда в production-сборке, на чистом браузерном профиле и для каждого затронутого entry одновременно выполняются четыре условия: <code>window.jQuery</code> существует до запуска legacy-файла; <code>window.jQuery === $</code>; <code>typeof $.fn.legacyMask === 'function'</code>; Network показывает актуальные chunks без 404. Дополнительно проверяется реальное поле, а не только Console.</p>\n<p>Если любое условие не выполнено, проблема не закрыта. Ошибка могла исчезнуть из-за случайного порядка или кеша. Если все условия выполнены, граница между модульным кодом и legacy-плагином стала явной: один объект создаётся, bridge публикует его, плагин расширяет его прототип, приложение вызывает тот же объект.</p>\n<h2>Проверяемые источники</h2><ul><li><a href=\"https://v4.webpack.js.org/guides/shimming/\" target=\"_blank\" rel=\"noopener noreferrer\">Webpack 4: Shimming</a> — официальное описание ProvidePlugin и совместимости с legacy-модулями.</li><li><a href=\"https://v4.webpack.js.org/guides/code-splitting/\" target=\"_blank\" rel=\"noopener noreferrer\">Webpack 4: Code Splitting</a> — официальное описание entry, chunks и разделения общих модулей.</li><li><a href=\"https://api.jquery.com/jQuery.fn.extend/\" target=\"_blank\" rel=\"noopener noreferrer\">jQuery API: jQuery.fn.extend()</a> — официальный контракт расширения прототипа jQuery.</li></ul>"
}