{ "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: пользователь не вводит номер, форма отклоняет корректное значение, а релиз приходится откатывать или срочно пересобирать.
Главный вопрос — не «сломал ли production jQuery», а какой контракт нарушен на границе модулей. Старый файл может читать window.jQuery, выполниться до создания этого глобала или расширить другой экземпляр jQuery. Поэтому проверяем три факта: какой объект получил плагин, когда он выполнился и тем ли объектом пользуется приложение. Ни production-бандл конкретного проекта, ни браузерную трассу этой статьи нельзя выдавать за выполненные результаты: ниже учебная схема и воспроизводимый маршрут проверки.
jQuery-плагин обычно добавляет функцию в $.fn. Значит, полезна проверка typeof $.fn.legacyMask, а не общий вопрос «загрузился ли файл». Если метод отсутствует, это ещё не говорит, почему он пропал: файл мог не выполниться, мог получить другой объект или мог загрузиться после вызова приложения.
Модульный импорт и глобальная переменная — разные механизмы. Строка import $ from 'jquery' даёт модулю ссылку на экспорт пакета, но сама по себе не обязана записывать её в window.jQuery. ProvidePlugin решает более узкую задачу: Webpack подставляет импорт вместо свободного идентификатора в обработанных модулях. Это не универсальная команда присвоить значение свойству window.
Для 2019 года это типичная граница между знакомым jQuery и модульной сборкой. В development глобал мог появляться из отдельного тега script или другого entry. В production зависимости попадают в граф модулей и chunks, поэтому случайный порядок перестаёт маскировать скрытое ожидание старого файла.
| Симптом | Причина | Проверка | Действие |
|---|---|---|---|
window.jQuery пуст до запуска плагина | Импорт существует только внутри модуля | Остановиться перед legacy-файлом и вывести window.jQuery | Создать bridge и загрузить плагин после записи глобала |
Метод есть у глобала, но отсутствует у импортированного $ | Плагин и приложение держат разные экземпляры | Сравнить window.jQuery === $ и тип метода у обоих объектов | Убрать второй путь резолвации или выровнять зависимость |
| Метода нет ни у глобала, ни у импорта | Файл не выполнился, получил ошибку или не попал в chunk | Проверить Console, Network и modules в stats.json | Исправить entry или порядок и повторить production-проверку |
| Сбой появляется только со вторым entry | Общий модуль продублирован или версии jQuery различаются | Найти resolved-пути и chunks для jquery | Проверить общий chunk, версии и все точки входа |
Таблица задаёт гипотезы, а не готовый диагноз. Тот же внешний симптом дают 404 чанка, CSP, старый кеш CDN и несовместимая версия плагина. Поэтому сначала подтверждаем объект и порядок, а уже затем меняем минификацию или оптимизацию.
\nНиже минимальный legacy-файл. Он намеренно не импортирует jQuery: автор плагина рассчитывает, что глобальная переменная уже существует. Такой контракт неудобен для модулей, но встречается в старых виджетах. Пример воспроизводим без сервера: если в момент выполнения root.jQuery отсутствует, файл бросает понятное исключение; если объект есть, метод появляется на его fn.
(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. Если объект появится позже, уже выполненный плагин не узнает о нём.
Следующий код выглядит как последовательность сверху вниз:
\nimport $ from 'jquery';\nimport './vendor/legacy-mask';\n\nwindow.jQuery = window.$ = $;\n\n$('.js-phone').legacyMask();\nНо статический import — это объявление зависимости, а не вызов в данной строке. Импортированные модули связываются и дают свои побочные эффекты до выполнения тела текущего модуля. Поэтому legacy-mask.js может прочитать window.jQuery до присваивания ниже. Webpack здесь не «переставляет строки»: он следует семантике модулей, которую исходный код ошибочно принял за обычный императивный сценарий.
Разделим старый 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 нужен именно для файла, который действительно читает глобал.
Наличие одинаковой строки версии не доказывает, что объекты совпадают. Два экземпляра могут иметь одну версию и разные прототипы. Плагин расширит один $.fn, а экран вызовет другой. Поэтому проверяем ссылки и метод после выполнения bridge:
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.
У ProvidePlugin другая граница. Webpack 4 описывает его как способ сделать пакет доступным по имени в каждом скомпилированном модуле, где встречается этот идентификатор. Если legacy-модуль обращается к свободной переменной jQuery, настройка может помочь после проверки того, как этот файл обрабатывается сборщиком. Но обращение к window.jQuery — это чтение свойства глобального объекта, и один только ProvidePlugin не обязан его заполнить.
Решение следует из исходного файла. Для свободного идентификатора проверяем настройку ProvidePlugin и результат сборки. Для явного window.jQuery создаём bridge или применяем loader только с документированным контрактом. Не подменяем оба случая одной конфигурацией: она может скрыть ошибку в development и оставить её в production.
Команда npm ls jquery показывает дерево пакетов, но не доказывает, что браузер создал два объекта. Совместимые зависимости сборщик может объединить. Обратная ситуация тоже возможна: один и тот же пакет попадёт в разные chunks по разным resolved-путям. Размер итогового JavaScript — слабое доказательство, потому что на него влияют общие chunks, минификация, сжатие и кеш.
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.
window.jQuery, импортированный $, равенство ссылок и тип метода в $.fn.jquery, их chunks и причины подключения.Bridge применим в браузерном коде, где старый файл действительно читает window.jQuery. В SSR, worker и тестовом окружении глобальный объект может отсутствовать. Адаптер должен выполняться только в браузере или получать объект окружения явно.
Глобальная jQuery остаётся техническим слоем совместимости. Она увеличивает связанность и усложняет порядок загрузки. Для нового кода лучше использовать явный импорт и не менять глобальную область. Если найден 404 чанка, CSP, несовместимая версия плагина или старый кеш CDN, bridge не устраняет эту причину — её нужно проверить отдельно.
\nSource map помогает сопоставить production-код с исходником, но может раскрыть пути и код. Не публикуйте его без проверки политики проекта. Также не объявляйте учебную проверку результатом production-мониторинга: без запуска на конкретной сборке нельзя утверждать, что её chunks загружены или что пользовательский сценарий исправлен.
\nИсправление готово, когда для каждого затронутого entry на production-сборке одновременно выполняются четыре условия: window.jQuery существует до запуска legacy-файла; window.jQuery === $; typeof $.fn.legacyMask === 'function'; Network показывает актуальные chunks без 404. Дополнительно проверяется само поле, а не только Console.
Если любое условие не выполнено, проблема не закрыта. Если все условия выполнены, граница стала явной: один объект создаётся, bridge публикует его, плагин расширяет его прототип, а приложение вызывает тот же объект. Это локальное исправление для legacy-контракта, а не универсальное правило для любой jQuery-сборки.
\n