{ "index": 322, "slug": "editorial-2019-01-field-jquery-webpack", "title": "jQuery и Webpack: как вернуть legacy-плагин после production-сборки", "excerpt": "Старый jQuery-плагин работает в development, но пропадает после production-сборки. Разбираем порядок исполнения, глобальный window.jQuery и второй экземпляр зависимости, затем проверяем исправление в браузере и stats.json.", "contentHtml": "

В development поле с маской номера работает. После production-сборки вызов $('.js-phone').legacyMask() падает: метода нет в $.fn. В реальном проекте вместо учебного legacyMask здесь может быть метод старого inputmask или другого плагина. Цена ошибки — не только красная строка в Console: пользователь не вводит номер, форма отклоняет корректное значение, а релиз приходится откатывать или срочно пересобирать.

\n

Главный вопрос — не «сломал ли production jQuery», а какой контракт нарушен на границе модулей. Старый файл может читать window.jQuery, выполниться до создания этого глобала или расширить другой экземпляр jQuery. Поэтому проверяем три факта: какой объект получил плагин, когда он выполнился и тем ли объектом пользуется приложение. Ни production-бандл конкретного проекта, ни браузерную трассу этой статьи нельзя выдавать за выполненные результаты: ниже учебная схема и воспроизводимый маршрут проверки.

\n

Сначала фиксируем, что именно исчезло

\n

jQuery-плагин обычно добавляет функцию в $.fn. Значит, полезна проверка typeof $.fn.legacyMask, а не общий вопрос «загрузился ли файл». Если метод отсутствует, это ещё не говорит, почему он пропал: файл мог не выполниться, мог получить другой объект или мог загрузиться после вызова приложения.

\n

Модульный импорт и глобальная переменная — разные механизмы. Строка import $ from 'jquery' даёт модулю ссылку на экспорт пакета, но сама по себе не обязана записывать её в window.jQuery. ProvidePlugin решает более узкую задачу: Webpack подставляет импорт вместо свободного идентификатора в обработанных модулях. Это не универсальная команда присвоить значение свойству window.

\n

Для 2019 года это типичная граница между знакомым jQuery и модульной сборкой. В development глобал мог появляться из отдельного тега script или другого entry. В production зависимости попадают в граф модулей и chunks, поэтому случайный порядок перестаёт маскировать скрытое ожидание старого файла.

\n
Дерево гипотез для одного симптома
СимптомПричинаПроверкаДействие
window.jQuery пуст до запуска плагинаИмпорт существует только внутри модуляОстановиться перед legacy-файлом и вывести window.jQueryСоздать bridge и загрузить плагин после записи глобала
Метод есть у глобала, но отсутствует у импортированного $Плагин и приложение держат разные экземплярыСравнить window.jQuery === $ и тип метода у обоих объектовУбрать второй путь резолвации или выровнять зависимость
Метода нет ни у глобала, ни у импортаФайл не выполнился, получил ошибку или не попал в chunkПроверить Console, Network и modules в stats.jsonИсправить entry или порядок и повторить production-проверку
Сбой появляется только со вторым entryОбщий модуль продублирован или версии jQuery различаютсяНайти resolved-пути и chunks для jqueryПроверить общий chunk, версии и все точки входа
\n

Таблица задаёт гипотезы, а не готовый диагноз. Тот же внешний симптом дают 404 чанка, CSP, старый кеш CDN и несовместимая версия плагина. Поэтому сначала подтверждаем объект и порядок, а уже затем меняем минификацию или оптимизацию.

\n

Учебный сценарий: старый файл читает window.jQuery

\n

Ниже минимальный legacy-файл. Он намеренно не импортирует jQuery: автор плагина рассчитывает, что глобальная переменная уже существует. Такой контракт неудобен для модулей, но встречается в старых виджетах. Пример воспроизводим без сервера: если в момент выполнения root.jQuery отсутствует, файл бросает понятное исключение; если объект есть, метод появляется на его fn.

\n
(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));
\n

Это учебный код, а не результат конкретного production-проекта. Он показывает границу: к моменту выполнения файла в window.jQuery должен лежать объект с прототипом fn. Если объект появится позже, уже выполненный плагин не узнает о нём.

\n

Почему порядок импортов обманывает

\n

Следующий код выглядит как последовательность сверху вниз:

\n
import $ from 'jquery';\nimport './vendor/legacy-mask';\n\nwindow.jQuery = window.$ = $;\n\n$('.js-phone').legacyMask();
\n

Но статический import — это объявление зависимости, а не вызов в данной строке. Импортированные модули связываются и дают свои побочные эффекты до выполнения тела текущего модуля. Поэтому legacy-mask.js может прочитать window.jQuery до присваивания ниже. Webpack здесь не «переставляет строки»: он следует семантике модулей, которую исходный код ошибочно принял за обычный императивный сценарий.

\n

Разделим старый side effect и новый код маленьким bridge. Он импортирует один объект jQuery, публикует его в глобальной области и только потом загружает legacy-файл:

\n
// 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();
\n

Вызов require() здесь локальный и учебный: он делает момент выполнения старого файла видимым. Он не является советом смешивать CommonJS и ES-модули во всём новом коде. Если конкретный плагин экспортирует функцию, лучше импортировать её явно и передать тот же объект; bridge нужен именно для файла, который действительно читает глобал.

\n

Проверяем идентичность экземпляра

\n

Наличие одинаковой строки версии не доказывает, что объекты совпадают. Два экземпляра могут иметь одну версию и разные прототипы. Плагин расширит один $.fn, а экран вызовет другой. Поэтому проверяем ссылки и метод после выполнения bridge:

\n
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);
\n

Для учебной схемы ожидаем true в hasGlobal и sameInstance, а также function в обоих полях плагина. Это проверяемое условие, а не обещание, что пользовательский сценарий уже исправлен: на конкретной странице ещё нужно проверить реальное поле и загруженные chunks.

\n

Почему ProvidePlugin не заменяет bridge

\n

У ProvidePlugin другая граница. Webpack 4 описывает его как способ сделать пакет доступным по имени в каждом скомпилированном модуле, где встречается этот идентификатор. Если legacy-модуль обращается к свободной переменной jQuery, настройка может помочь после проверки того, как этот файл обрабатывается сборщиком. Но обращение к window.jQuery — это чтение свойства глобального объекта, и один только ProvidePlugin не обязан его заполнить.

\n

Решение следует из исходного файла. Для свободного идентификатора проверяем настройку ProvidePlugin и результат сборки. Для явного window.jQuery создаём bridge или применяем loader только с документированным контрактом. Не подменяем оба случая одной конфигурацией: она может скрыть ошибку в development и оставить её в production.

\n
\"Схема
Проверка идёт от порядка исполнения к идентичности экземпляра: сначала bridge, затем плагин, затем сравнение объектов.
\n

Как искать второй экземпляр в production-графе

\n

Команда npm ls jquery показывает дерево пакетов, но не доказывает, что браузер создал два объекта. Совместимые зависимости сборщик может объединить. Обратная ситуация тоже возможна: один и тот же пакет попадёт в разные chunks по разным resolved-путям. Размер итогового JavaScript — слабое доказательство, потому что на него влияют общие chunks, минификация, сжатие и кеш.

\n

Production-статистика нужна для ответа на другой вопрос: какие assets, chunks и modules вошли в конкретную сборку. Команду ниже выполняем в реальном репозитории на том же lock-файле; это не вывод из статьи. Source map включаем только на время диагностики и только если политика проекта разрешает хранить исходники в таком артефакте.

\n
# Учебный маршрут диагностики для реального проекта.\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.
\n

Несколько веток в npm ls jquery — повод изучить резолвинг, но не окончательный диагноз. Несколько путей в stats.json — также только гипотеза о дубликате. Её закрывает связка из трёх наблюдений: window.jQuery === $, метод есть на нужном $.fn, а Network показывает фактически загруженные актуальные chunks без 404.

\n

Порядок проверки

\n
  1. Зафиксируйте страницу, один селектор, имя метода, текст ошибки, версию ассетов и commit сборки. Не меняйте конфигурацию до записи исходного симптома.
  2. Перед вызовом плагина выведите window.jQuery, импортированный $, равенство ссылок и тип метода в $.fn.
  3. Откройте Network. Проверьте entry и lazy-chunk, статус ответа, hash файлов и отсутствие старого HTML или кешированного bundle.
  4. Соберите production-статистику на той же версии lock-файла. Найдите resolved-пути jquery, их chunks и причины подключения.
  5. Если плагин читает глобал, создайте один изолированный bridge до его side effect. Если плагин принимает импорт, уберите глобальную зависимость.
  6. Повторите проверку на чистой странице, в каждом entry и в сценарии ленивой загрузки. Успешно исчезнувшая ошибка без совпадения объектов не считается исправлением.
\n

Ограничения решения

\n

Bridge применим в браузерном коде, где старый файл действительно читает window.jQuery. В SSR, worker и тестовом окружении глобальный объект может отсутствовать. Адаптер должен выполняться только в браузере или получать объект окружения явно.

\n

Глобальная jQuery остаётся техническим слоем совместимости. Она увеличивает связанность и усложняет порядок загрузки. Для нового кода лучше использовать явный импорт и не менять глобальную область. Если найден 404 чанка, CSP, несовместимая версия плагина или старый кеш CDN, bridge не устраняет эту причину — её нужно проверить отдельно.

\n

Source map помогает сопоставить production-код с исходником, но может раскрыть пути и код. Не публикуйте его без проверки политики проекта. Также не объявляйте учебную проверку результатом production-мониторинга: без запуска на конкретной сборке нельзя утверждать, что её chunks загружены или что пользовательский сценарий исправлен.

\n

Критерий готовности

\n

Исправление готово, когда для каждого затронутого entry на production-сборке одновременно выполняются четыре условия: window.jQuery существует до запуска legacy-файла; window.jQuery === $; typeof $.fn.legacyMask === 'function'; Network показывает актуальные chunks без 404. Дополнительно проверяется само поле, а не только Console.

\n

Если любое условие не выполнено, проблема не закрыта. Если все условия выполнены, граница стала явной: один объект создаётся, bridge публикует его, плагин расширяет его прототип, а приложение вызывает тот же объект. Это локальное исправление для legacy-контракта, а не универсальное правило для любой jQuery-сборки.

\n

Проверяемые источники

" }