В development старая маска телефона работает, а после production-сборки браузер сообщает, что window.jQuery не определён. Цена ошибки — релиз с формой, которую нельзя заполнить, хотя локальный сервер и привычный исходный код выглядели исправными.
В январе 2019 года я бы не лечил этот сбой таймаутом. У legacy-плагина есть жёсткое ожидание: в момент его выполнения должна существовать конкретная глобальная переменная. У Webpack другая модель: он собирает модули и может вынести общую зависимость в отдельный chunk. Нужно проверить, кто создаёт window.jQuery, когда это происходит и какой production-asset выполняет плагин.
Почему development даёт ложную уверенность
На локальном сервере плагин нередко случайно получает jQuery из старого тега script, из общего layout или из более простого bundle. Production меняет условия: включается минификация, меняется имя файлов, а Webpack 4 может выделить общие зависимости через optimization.splitChunks. Само выделение не является ошибкой. Ошибка появляется, когда HTML подключает ассеты не тем способом или bootstrap-код запускает плагин до назначения глобала.
Поэтому фиксирую минимальный пример из трёх частей: entry, который создаёт глобал; legacy-плагин, который читает его при загрузке; и production HTML с фактическими тегами script. Пока в диагностике есть только исходники, а собранного dist нет, порядок исполнения остаётся предположением.
| Наблюдение | Вероятная причина | Короткая проверка | Действие |
|---|---|---|---|
В dev всё работает, в production — undefined | layout подключает локальный jQuery раньше bundle | открыть Network и сравнить production script tags | убрать случайный CDN-скрипт или сделать порядок явным |
В модуле доступен $, но плагин читает window.jQuery | ProvidePlugin обслуживает свободный идентификатор, а не скрытую договорённость плагина | поставить остановку перед загрузкой плагина и проверить window.jQuery | назначить глобал до runtime require плагина |
| После splitChunks два разных jQuery | одна копия пришла из layout, другая — из bundle | проверить modules со словом jquery в stats.json | оставить один владелец и проверить граф production-сборки |
| Форма ломается после кеша | HTML ссылается на старый набор hashed-ассетов | сравнить HTML и имена файлов текущего dist | публиковать HTML и ассеты как один артефакт |
Разделяю свободный идентификатор и глобальный объект
Webpack описывает ProvidePlugin как способ автоматически подставить модуль, когда компилятор встречает свободный идентификатор в скомпилированном модуле. Конфигурация с $ и jQuery помогает нашему исходному коду, где они используются без import. Но старый плагин может вообще не иметь свободного идентификатора: он может обратиться именно к window.jQuery во время выполнения своего файла. Это другой контракт, его нельзя проверить поиском по собственным модулям.
Документация Webpack отдельно показывает сопоставление window.jQuery для библиотек с такой зависимостью. На старом проекте я всё равно предпочитаю маленький bootstrap-модуль с явным присваиванием: в нём видно владельца глобала и точку, после которой разрешено запускать plugin. Это не идеальная модульная архитектура, а ограниченный шов для уже существующего кода.
const webpack = require('webpack');
module.exports = {
mode: 'production',
entry: {
site: './src/bootstrap-legacy.js',
},
plugins: [
new webpack.ProvidePlugin({
$: 'jquery',
jQuery: 'jquery',
}),
],
optimization: {
splitChunks: { chunks: 'all' },
},
};
Эта настройка не даёт права подключить plugin в любом месте и ожидать правильный момент. Она лишь делает $ и jQuery доступными там, где Webpack анализирует их как свободные имена. Глобальный объект, порядок загрузки документа и исполнение файлов остаются отдельной частью расследования.
Не ставлю статический import перед созданием глобала
В этом месте легко написать читающийся, но неверный код. Статические import описывают зависимости модуля до того, как выполнится его тело. Если legacy-плагин читает window.jQuery сразу при инициализации, он может сработать раньше строки с присваиванием. В примере ниже сначала настраивается глобал, затем плагин подключается через runtime require().
import $ from 'jquery';
function exposeLegacyJQuery(jq) {
if (window.jQuery && window.jQuery !== jq) {
throw new Error('Two jQuery instances reached the page');
}
window.$ = jq;
window.jQuery = jq;
}
exposeLegacyJQuery($);
require('inputmask/dist/jquery.inputmask');
require('./legacy-form');
Здесь require() выбран не потому, что он современнее import, а потому, что его вызов стоит после exposeLegacyJQuery() в runtime-порядке текущего модуля. Если плагин не читает глобал при загрузке, такой шов может быть не нужен. Но в конкретном production-сбое сначала проверяю это предположение на минимальном плагине и только затем меняю конфигурацию всего проекта.
Собираю production-доказательство, а не только скрин консоли
После исправления я запускаю production-сборку с JSON-статистикой. В stats-файле ищу модули jQuery и связь с entry, а в браузере смотрю Network и порядок инициаторов. Цель не в том, чтобы вручную угадать хеш vendor-файла: порядок должен обеспечивать runtime Webpack или генератор HTML, а не строка с именем вчерашнего chunk.
{
"scripts": {
"build:profile": "webpack --mode production --profile --json > dist/stats.json",
"serve:dist": "npx http-server dist -c-1"
}
}
npm run build:profile
npm run serve:dist
На тестовой странице перед отправкой формы достаточно временно проверить два факта: window.jQuery === $ в bootstrap-коде и наличие метода, который добавляет плагин. Затем эту диагностику убираю или оставляю за отдельным development-флагом. Production-страница должна открываться со свежим HTML, потому что новый bundle с прошлым списком тегов script проверяет не нашу конфигурацию, а несовместимый набор артефактов.
Порядок проверки после сборки
- Собираю чистый production
distи сохраняю имя entry, runtime и общих chunks из фактического вывода. - Открываю страницу без локального CDN-скрипта jQuery и проверяю Network: все стартовые assets пришли без 404 и с одной версией артефакта.
- Ставлю временную проверку в
bootstrap-legacy.js: до runtime requirewindow.jQuery === $должно бытьtrue. - После require проверяю ровно один ожидаемый метод legacy-плагина, затем инициализирую форму через
$(function () { ... }). - Открываю stats.json и ищу modules, содержащие
jquery; повторная копия требует объяснения, а не только сравнения общего размера bundle. - Повторяю сценарий после очистки кеша и после прямого открытия страницы, чтобы не перепутать рабочий старый asset с новым релизом.
Инициализирую форму после DOM, а не до него
Даже правильный глобал не создаёт input в DOM. jQuery .ready() выполняет обработчик, когда документ готов к безопасному изменению, поэтому конечная инициализация формы должна жить после bootstrap-пути. Это отдельная проверка от загрузки plugins: если window.jQuery есть, а селектор не находит поля, причина лежит уже в разметке или в моменте появления формы.
require('./bootstrap-legacy');
jQuery(function ($) {
var $phone = $('#order-phone');
if ($phone.length !== 1 || typeof $phone.inputmask !== 'function') {
throw new Error('Legacy mask is not ready for #order-phone');
}
$phone.inputmask('+7 (999) 999-99-99');
});
Синтаксис с аргументом $ внутри jQuery(function ($) { ... }) снимает зависимость этой конкретной функции от внешнего alias. Но он не отменяет bootstrap: плагин всё ещё должен быть загружен и зарегистрирован до вызова inputmask(). Так граница проблемы остаётся видимой: глобал для старого плагина, локальный alias для кода формы, реальный DOM для финальной инициализации.
Ограничения решения
Глобальный jQuery — технический долг, а не совет для нового модуля. Новые компоненты лучше импортировать явно и не делать window общим API. Также не следует выключать splitChunks только ради одного плагина: сначала нужно показать, что именно нарушено — порядок тегов, два экземпляра библиотеки или статический import.
Число chunks и их хеши зависят от версии Webpack, loaders и графа зависимостей. Поэтому статья не обещает фиксированное имя vendors~site.js. Её критерий другой: production HTML ссылается на выпуск одной сборки, глобал назначен до legacy-плагина, а проверка формы пройдена без случайного script из layout.
Итог
Production-сбой с jQuery в Webpack обычно не лечится ещё одним alias. Нужно назвать, какой код читает window.jQuery, создать его в bootstrap до runtime require и проверить фактический граф assets. После этого legacy-граница остаётся маленькой, а следующая миграция может заменить её модулем без скрытой глобальной зависимости.
Проверяемые источники
- webpack: Shimming — Webpack понимает модули, но старые библиотеки могут ожидать глобальные зависимости; globals следует оставлять только для нужной совместимости
- webpack: ProvidePlugin — ProvidePlugin подставляет модуль для свободного идентификатора в скомпилированном модуле; отдельно показано сопоставление window.jQuery
- webpack: migration to v4 — в webpack 4 CommonsChunkPlugin заменён настройкой optimization.splitChunks
- webpack: SplitChunksPlugin — в webpack 4 общие модули извлекаются правилами splitChunks, что меняет состав начальных ассетов
- jQuery API: .ready() — обработчик запускается, когда DOM готов к безопасному изменению
- jQuery API: .on() — делегированный обработчик работает на потомках существующего контейнера и может быть привязан с namespace