8 lines
18 KiB
JSON
8 lines
18 KiB
JSON
{
|
||
"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 > 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>"
|
||
}
|