8 lines
21 KiB
JSON
8 lines
21 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>$.fn</code>. В реальном проекте вместо учебного <code>legacyMask</code> здесь может быть метод старого inputmask или другого плагина. Цена ошибки — не только красная строка в Console: пользователь не вводит номер, форма отклоняет корректное значение, а релиз приходится откатывать или срочно пересобирать.</p>\n<p>Главный вопрос — не «сломал ли production jQuery», а какой контракт нарушен на границе модулей. Старый файл может читать <code>window.jQuery</code>, выполниться до создания этого глобала или расширить другой экземпляр jQuery. Поэтому проверяем три факта: какой объект получил плагин, когда он выполнился и тем ли объектом пользуется приложение. Ни production-бандл конкретного проекта, ни браузерную трассу этой статьи нельзя выдавать за выполненные результаты: ниже учебная схема и воспроизводимый маршрут проверки.</p>\n<h2>Сначала фиксируем, что именно исчезло</h2>\n<p>jQuery-плагин обычно добавляет функцию в <code>$.fn</code>. Значит, полезна проверка <code>typeof $.fn.legacyMask</code>, а не общий вопрос «загрузился ли файл». Если метод отсутствует, это ещё не говорит, почему он пропал: файл мог не выполниться, мог получить другой объект или мог загрузиться после вызова приложения.</p>\n<p>Модульный импорт и глобальная переменная — разные механизмы. Строка <code>import $ from 'jquery'</code> даёт модулю ссылку на экспорт пакета, но сама по себе не обязана записывать её в <code>window.jQuery</code>. <code>ProvidePlugin</code> решает более узкую задачу: Webpack подставляет импорт вместо свободного идентификатора в обработанных модулях. Это не универсальная команда присвоить значение свойству <code>window</code>.</p>\n<p>Для 2019 года это типичная граница между знакомым jQuery и модульной сборкой. В development глобал мог появляться из отдельного тега <code>script</code> или другого entry. В production зависимости попадают в граф модулей и chunks, поэтому случайный порядок перестаёт маскировать скрытое ожидание старого файла.</p>\n<div class=\"table-scroll\"><table><caption>Дерево гипотез для одного симптома</caption><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>Плагин и приложение держат разные экземпляры</td><td>Сравнить <code>window.jQuery === $</code> и тип метода у обоих объектов</td><td>Убрать второй путь резолвации или выровнять зависимость</td></tr><tr><td>Метода нет ни у глобала, ни у импорта</td><td>Файл не выполнился, получил ошибку или не попал в chunk</td><td>Проверить Console, Network и modules в <code>stats.json</code></td><td>Исправить entry или порядок и повторить production-проверку</td></tr><tr><td>Сбой появляется только со вторым entry</td><td>Общий модуль продублирован или версии jQuery различаются</td><td>Найти resolved-пути и chunks для <code>jquery</code></td><td>Проверить общий chunk, версии и все точки входа</td></tr></tbody></table></div>\n<p>Таблица задаёт гипотезы, а не готовый диагноз. Тот же внешний симптом дают 404 чанка, CSP, старый кеш CDN и несовместимая версия плагина. Поэтому сначала подтверждаем объект и порядок, а уже затем меняем минификацию или оптимизацию.</p>\n<h2>Учебный сценарий: старый файл читает window.jQuery</h2>\n<p>Ниже минимальный legacy-файл. Он намеренно не импортирует jQuery: автор плагина рассчитывает, что глобальная переменная уже существует. Такой контракт неудобен для модулей, но встречается в старых виджетах. Пример воспроизводим без сервера: если в момент выполнения <code>root.jQuery</code> отсутствует, файл бросает понятное исключение; если объект есть, метод появляется на его <code>fn</code>.</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>Но статический <code>import</code> — это объявление зависимости, а не вызов в данной строке. Импортированные модули связываются и дают свои побочные эффекты до выполнения тела текущего модуля. Поэтому <code>legacy-mask.js</code> может прочитать <code>window.jQuery</code> до присваивания ниже. Webpack здесь не «переставляет строки»: он следует семантике модулей, которую исходный код ошибочно принял за обычный императивный сценарий.</p>\n<p>Разделим старый side effect и новый код маленьким bridge. Он импортирует один объект jQuery, публикует его в глобальной области и только потом загружает legacy-файл:</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> здесь локальный и учебный: он делает момент выполнения старого файла видимым. Он не является советом смешивать CommonJS и ES-модули во всём новом коде. Если конкретный плагин экспортирует функцию, лучше импортировать её явно и передать тот же объект; bridge нужен именно для файла, который действительно читает глобал.</p>\n<h2>Проверяем идентичность экземпляра</h2>\n<p>Наличие одинаковой строки версии не доказывает, что объекты совпадают. Два экземпляра могут иметь одну версию и разные прототипы. Плагин расширит один <code>$.fn</code>, а экран вызовет другой. Поэтому проверяем ссылки и метод после выполнения bridge:</p>\n<pre><code>import $ from './legacy-jquery-bridge';\n\nvar 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>true</code> в <code>hasGlobal</code> и <code>sameInstance</code>, а также <code>function</code> в обоих полях плагина. Это проверяемое условие, а не обещание, что пользовательский сценарий уже исправлен: на конкретной странице ещё нужно проверить реальное поле и загруженные chunks.</p>\n<h2>Почему ProvidePlugin не заменяет bridge</h2>\n<p>У <code>ProvidePlugin</code> другая граница. Webpack 4 описывает его как способ сделать пакет доступным по имени в каждом скомпилированном модуле, где встречается этот идентификатор. Если legacy-модуль обращается к свободной переменной <code>jQuery</code>, настройка может помочь после проверки того, как этот файл обрабатывается сборщиком. Но обращение к <code>window.jQuery</code> — это чтение свойства глобального объекта, и один только <code>ProvidePlugin</code> не обязан его заполнить.</p>\n<p>Решение следует из исходного файла. Для свободного идентификатора проверяем настройку ProvidePlugin и результат сборки. Для явного <code>window.jQuery</code> создаём bridge или применяем loader только с документированным контрактом. Не подменяем оба случая одной конфигурацией: она может скрыть ошибку в development и оставить её в production.</p>\n<figure><img src=\"/assets/editorial/2019/jquery-webpack-production-bridge-2019.svg\" alt=\"Схема диагностики legacy jQuery-плагина: неправильный статический импорт запускает плагин до назначения window.jQuery, а проверка равенства выявляет второй экземпляр jQuery\" loading=\"lazy\" /><figcaption>Проверка идёт от порядка исполнения к идентичности экземпляра: сначала bridge, затем плагин, затем сравнение объектов.</figcaption></figure>\n<h2>Как искать второй экземпляр в production-графе</h2>\n<p>Команда <code>npm ls jquery</code> показывает дерево пакетов, но не доказывает, что браузер создал два объекта. Совместимые зависимости сборщик может объединить. Обратная ситуация тоже возможна: один и тот же пакет попадёт в разные chunks по разным resolved-путям. Размер итогового JavaScript — слабое доказательство, потому что на него влияют общие chunks, минификация, сжатие и кеш.</p>\n<p>Production-статистика нужна для ответа на другой вопрос: какие assets, chunks и modules вошли в конкретную сборку. Команду ниже выполняем в реальном репозитории на том же lock-файле; это не вывод из статьи. Source map включаем только на время диагностики и только если политика проекта разрешает хранить исходники в таком артефакте.</p>\n<pre><code># Учебный маршрут диагностики для реального проекта.\nnpx webpack --mode production --devtool source-map --profile --json > dist/stats.json\nnpm ls jquery\n\n# В stats.json ищем пути и chunks, где встречается jquery.\n# Затем в браузере сравниваем window.jQuery с импортом из bridge.</code></pre>\n<p>Несколько веток в <code>npm ls jquery</code> — повод изучить резолвинг, но не окончательный диагноз. Несколько путей в <code>stats.json</code> — также только гипотеза о дубликате. Её закрывает связка из трёх наблюдений: <code>window.jQuery === $</code>, метод есть на нужном <code>$.fn</code>, а Network показывает фактически загруженные актуальные chunks без 404.</p>\n<h2>Порядок проверки</h2>\n<ol><li>Зафиксируйте страницу, один селектор, имя метода, текст ошибки, версию ассетов и commit сборки. Не меняйте конфигурацию до записи исходного симптома.</li><li>Перед вызовом плагина выведите <code>window.jQuery</code>, импортированный <code>$</code>, равенство ссылок и тип метода в <code>$.fn</code>.</li><li>Откройте Network. Проверьте entry и lazy-chunk, статус ответа, hash файлов и отсутствие старого HTML или кешированного bundle.</li><li>Соберите production-статистику на той же версии lock-файла. Найдите resolved-пути <code>jquery</code>, их chunks и причины подключения.</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 остаётся техническим слоем совместимости. Она увеличивает связанность и усложняет порядок загрузки. Для нового кода лучше использовать явный импорт и не менять глобальную область. Если найден 404 чанка, CSP, несовместимая версия плагина или старый кеш CDN, bridge не устраняет эту причину — её нужно проверить отдельно.</p>\n<p>Source map помогает сопоставить production-код с исходником, но может раскрыть пути и код. Не публикуйте его без проверки политики проекта. Также не объявляйте учебную проверку результатом production-мониторинга: без запуска на конкретной сборке нельзя утверждать, что её chunks загружены или что пользовательский сценарий исправлен.</p>\n<h2>Критерий готовности</h2>\n<p>Исправление готово, когда для каждого затронутого entry на production-сборке одновременно выполняются четыре условия: <code>window.jQuery</code> существует до запуска legacy-файла; <code>window.jQuery === $</code>; <code>typeof $.fn.legacyMask === 'function'</code>; Network показывает актуальные chunks без 404. Дополнительно проверяется само поле, а не только Console.</p>\n<p>Если любое условие не выполнено, проблема не закрыта. Если все условия выполнены, граница стала явной: один объект создаётся, bridge публикует его, плагин расширяет его прототип, а приложение вызывает тот же объект. Это локальное исправление для legacy-контракта, а не универсальное правило для любой jQuery-сборки.</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-модули с глобальными зависимостями и границы shim-подхода.</li><li><a href=\"https://v4.webpack.js.org/guides/code-splitting/\" target=\"_blank\" rel=\"noopener noreferrer\">Webpack 4: Code Splitting</a> — entry, динамические импорты и дублирование модулей между entry.</li><li><a href=\"https://v4.webpack.js.org/api/stats/\" target=\"_blank\" rel=\"noopener noreferrer\">Webpack 4: Stats Data</a> — структура JSON-статистики сборки: assets, chunks, modules, errors и warnings.</li><li><a href=\"https://v4.webpack.js.org/configuration/devtool/\" target=\"_blank\" rel=\"noopener noreferrer\">Webpack 4: devtool</a> — варианты source map и компромиссы диагностики и раскрытия исходного кода.</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>"
|
||
}
|