{ "index": 323, "slug": "editorial-2019-01-mechanism-jquery-webpack", "title": "jQuery в Webpack: почему legacy-плагин теряет глобальный объект", "excerpt": "В development форма работает, а production-сборка получает undefined вместо window.jQuery. Разбираем разницу между ProvidePlugin и глобальным объектом, порядок запуска legacy-плагина и проверку фактических production-ассетов.", "contentHtml": "
В development поле телефона принимает ввод, а после production-сборки браузер сообщает, что window.jQuery не определён. Иногда ошибка выглядит иначе: jQuery существует, но у поля нет метода старого плагина. Локальная форма работает, релизная — нет. Цена ошибки — сломанная форма на реальном маршруте и повторная сборка с догадками о порядке скриптов.
Тезис простой: доступный в модуле идентификатор $ и свойство window.jQuery — разные контракты. Webpack может подставить модуль в код, который обращается к свободному имени. Legacy-плагин может читать глобальный объект сразу при загрузке. Тогда важны не только пакет и конфигурация, но и момент выполнения каждого файла.
Проверьте один и тот же сценарий в development и production: открыть страницу, найти поле, дождаться загрузки, ввести значение и посмотреть консоль. Запишите точное место отказа. Ошибка window.jQuery is undefined указывает на глобальный контракт. Ошибка $(...).inputmask is not a function может означать ранний запуск плагина, вторую копию jQuery или отсутствие регистрации метода.
Учебный пример ниже использует старый jQuery-плагин, который выполняется во время загрузки файла. Это не утверждение о поведении каждой версии inputmask. Конкретный пакет нужно проверить в своей версии: прочитать entry файла, поставить остановку перед инициализацией и посмотреть, какое имя он читает.
\nМодуль получает свои импорты через граф зависимостей. Глобальный объект живёт в окружении страницы. Когда код пишет import $ from 'jquery', он получает локальную переменную. Эта строка сама по себе не обязана создать window.$ или window.jQuery. Присваивание в window делает отдельный мост.
ProvidePlugin работает на этапе сборки. Webpack подставляет модуль, когда встречает свободный идентификатор в анализируемом модуле. Поэтому конфигурация с $ и jQuery помогает исходному коду, который вызывает их без явного импорта. Если библиотека ищет именно window.jQuery, задайте этот контракт явно или создайте его в bootstrap-модуле.
Порядок тоже имеет значение. Статический импорт объявляет зависимость до тела текущего модуля. Если импортированный плагин выполняет проверку сразу, он может прочитать глобал до строки, которая должна его создать. Вызов CommonJS require() в учебном примере расположен после присваивания, чтобы граница была видна в runtime. Это приём для legacy-шва, а не рекомендация строить новый код на глобальных переменных.
import $ from 'jquery';\n\nfunction exposeJQuery(jq) {\n if (window.jQuery && window.jQuery !== jq) {\n throw new Error('Two jQuery instances reached the page');\n }\n\n window.$ = jq;\n window.jQuery = jq;\n}\n\nexposeJQuery($);\nrequire('inputmask/dist/jquery.inputmask');\nrequire('./legacy-form');\nПример учебный. Он предполагает, что плагин имеет CommonJS-совместимый вход и читает глобал во время загрузки. Если пакет экспортирует фабрику, требует вызова инициализации или использует другой путь, адаптируйте только точку подключения. Не переносите этот код в проект без проверки entry и версии зависимости.
\nconst webpack = require('webpack');\n\nmodule.exports = {\n mode: 'production',\n entry: {\n site: './src/bootstrap-legacy.js',\n },\n plugins: [\n new webpack.ProvidePlugin({\n $: 'jquery',\n jQuery: 'jquery',\n }),\n ],\n};\nВ таком варианте Webpack обслуживает свободные имена внутри модулей, которые он анализирует. Конфигурация не доказывает, что в момент загрузки внешнего legacy-файла уже существует window.jQuery. Она также не доказывает, что HTML подключил одну копию jQuery. Поэтому после настройки проверяйте три факта отдельно: какое имя читает плагин, какой объект назначен глобалу и сколько экземпляров попало в production-граф.
Если зависимость действительно требует глобальное свойство, ProvidePlugin можно настроить на window.jQuery. На старых сборках всё равно полезно оставить явный bootstrap: он показывает владельца глобала, позволяет проверить конфликт экземпляров и задаёт точку перед запуском plugin-кода. Оба подхода требуют проверки конкретного пакета и собранного результата.
| Симптом | Причина | Проверка | Действие |
|---|---|---|---|
В development работает, в production — undefined | В dev глобал случайно создаёт layout или внешний script | Сравнить production HTML, Network и initiator | Убрать случайный источник или назначить глобал в явном bootstrap |
В модуле есть $, плагин не видит window.jQuery | ProvidePlugin подставил локальный идентификатор, но не выполнен нужный глобальный контракт | Поставить остановку перед загрузкой plugin и проверить window.jQuery | Создать глобал до runtime-загрузки плагина или настроить точное сопоставление |
| После splitChunks метод пропал | HTML, runtime и chunks выпущены не одним набором или изменился порядок исполнения | Сверить имена фактических ассетов и stats-файл | Публиковать HTML и assets как один выпуск; не угадывать имя vendor-файла |
| На странице две копии jQuery | Одна пришла из layout или CDN, вторая — из bundle | Проверить window.jQuery === $ и модули с именем jquery | Оставить одного владельца и объяснить каждую копию в графе |
| Глобал есть, но метода нет | Плагин не загрузился, выполнился до моста или подключён не тот entry | Проверить Network, экспорт plugin и момент регистрации метода | Исправить точку подключения; не маскировать отказ повторным вызовом |
window.jQuery, затем запускается legacy-плагин. Красная ветка обозначает ранний запуск до создания глобала.Исходный файл показывает намерение, но не фактический порядок production-страницы. После сборки откройте HTML и перечислите стартовые script-теги. Затем в DevTools проверьте запросы, initiator и ошибки выполнения. Если используется runtime Webpack, убедитесь, что он подгружает нужные chunks до вызова bootstrap. Не подставляйте вручную вчерашнее имя vendors~site.js: hashed-имя и набор chunks зависят от конфигурации.
Для учебной проверки можно получить stats-файл и найти в нём модули jQuery:
\nwebpack --mode production --profile --json > dist/stats.json\nКоманда не выдаёт готовый диагноз. В stats-файле ищите все вхождения jQuery, связь с entry и причины появления chunks. Повторное вхождение требует объяснения, но само число строк не доказывает наличие двух runtime-экземпляров. Сопоставьте граф с проверкой объектов в браузере.
\nwindow.jQuery, свободное имя или экспорт функции.window.jQuery, window.$ и равенство глобала импортированному объекту.require() после присваивания.Проверяйте не только успешную форму. Если legacy-плагин не загрузился, приложение не должно тихо показать видимость исправной маски. Добавьте явную ошибку в development, fallback для поля и сообщение, которое не блокирует ввод без необходимости. Если загрузка optional-части падает, основная форма должна сохранить понятное состояние.
\nГлобальный jQuery остаётся техническим долгом. Новые модули лучше писать с явными импортами и локальными зависимостями. Не отключайте splitChunks только потому, что после миграции проявился сбой. Сначала докажите, что нарушен порядок, дублируется библиотека или HTML ссылается на несовместимый набор ассетов.
Версии Webpack, формат пакета, loader и способ генерации HTML меняют детали. Поэтому статья не обещает фиксированное имя chunk и не утверждает конкретный production-результат. Учебная проверка применима только после сверки с версией проекта, исходником плагина и фактическим dist.
Исправление готово, когда один и тот же production-сценарий проходит после очистки кэша, window.jQuery равен ожидаемому экземпляру, legacy-плагин регистрирует нужный метод, а форма работает без внешнего случайного script. В Network нет 404, HTML и chunks принадлежат одному выпуску, а stats-файл объясняет каждую копию jQuery. Отказ optional-плагина не скрывает состояние формы.
Если хотя бы один факт не подтверждён, результатом остаётся гипотеза. Не называйте её исправлением. Сначала вернитесь к моменту чтения глобала и отделите проблему видимости от проблемы порядка, графа или DOM.
\n