This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# Производство редакционных партий
|
||||
|
||||
На 31 июля 2026 года строгий аудит проходит 36 из 358 созданных материалов. Остальные 322 не считаются «почти готовыми»: их нужно заменить, а не косметически удлинить.
|
||||
На 31 июля 2026 года строгий аудит проходит 37 из 358 созданных материалов. Остальные 321 не считаются «почти готовыми»: их нужно заменить, а не косметически удлинить.
|
||||
|
||||
## Одна партия
|
||||
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
# О-01 · 2019-01 · jQuery + Webpack: legacy-плагин в production
|
||||
|
||||
Статус: **принят в публикационный слой 31 июля 2026 года**. Ревизия
|
||||
накладывается registry только по стабильному slug; дата и автор остаются в
|
||||
базовом архиве.
|
||||
|
||||
## Граница материала
|
||||
|
||||
- Единственный slug: `editorial-2019-01-field-jquery-webpack`.
|
||||
- Голос: М2, 2019 год. У автора уже есть опыт PHP/Bitrix и первые заметки о сборке; здесь он расширяет его до границы модулей, global-объекта и production-артефакта.
|
||||
- Главный вопрос: как отличить отсутствие `window.jQuery`, неправильный порядок исполнения legacy-плагина и второй экземпляр `jquery`.
|
||||
- Артефакт: учебный ESM/webpack-сценарий плюс отдельная команда, которую следует запускать только в реальном проекте для получения `stats.json` и проверки сетевых чанков.
|
||||
- Не заявляются: результат настоящей production-сборки, browser trace, состав зависимостей или поведение конкретного legacy-плагина вне учебного файла.
|
||||
|
||||
## Pass 1 — факты и техника
|
||||
|
||||
- Проверена причинная модель: `jQuery.fn.extend()` расширяет прототип конкретного объекта jQuery; поэтому сравнение `window.jQuery === $` отличает один объект от двух.
|
||||
- Сверены первичные источники Webpack 4: shimming/ProvidePlugin, code splitting, stats data и `devtool`; сверена первичная документация jQuery по `jQuery.fn.extend()`.
|
||||
- В тексте не приписывается `ProvidePlugin` автоматическая запись в `window`. Для плагина, который явно читает `window.jQuery`, указан отдельный bridge.
|
||||
- Уточнён порядок: статический `import './vendor/legacy-mask'` не является строковым вызовом после присваивания ниже; в учебном bridge `require()` расположен после `window.jQuery = $` именно ради наблюдаемого порядка.
|
||||
- В команде production-диагностики явно сказано, что она не запускалась для этого черновика. Source map ограничен политикой публикации исходников.
|
||||
- Вердикт: соответствует. Локальный аудит подтвердил единственную ревизию и ссылку на asset.
|
||||
|
||||
## Pass 2 — редактура и голос
|
||||
|
||||
- Симптом и цена ошибки стоят в первых двух предложениях: отсутствует метод в `$.fn`, риск — неработающая форма в релизе и импульсивная правка сборки.
|
||||
- Текст держит один маршрут «симптом → причина → проверка → действие»: фиксируем идентичность объектов, строим bridge, затем ищем resolved-пути в production-статистике.
|
||||
- Удалены общие оценки и ложная уверенность. Короткие фразы привязаны к объекту, команде, условию или наблюдаемому результату.
|
||||
- В словаре М2 используются `entry`, `chunk`, `source map`, `resolved-путь`, `runtime`; каждый термин появляется рядом с артефактом проверки, а не как декорация.
|
||||
- Один учебный ESM-пример отделён от реального репозитория. В конце есть проверяемый критерий: плагин находится на том же `$.fn`, который использует экран.
|
||||
- Вердикт: соответствует голосу 2019 года; нет зрелых рамок 2025–2027 и выдуманных метрик.
|
||||
|
||||
## Pass 3 — визуал и выпуск
|
||||
|
||||
- SVG `jquery-webpack-production-bridge-2019.svg` показывает оба порядка: ошибочный статический импорт и bridge; отдельный нижний блок показывает проверку второго экземпляра.
|
||||
- Alt описывает не оформление, а причинную модель. Подпись возвращает читателя к проверке «порядок → идентичность».
|
||||
- Таблица имеет `thead` и `scope="col"`; она пригодна для узкого экрана через существующий контейнер `table-scroll`.
|
||||
- В статье есть пять воспроизводимых блоков кода, нумерованная последовательность проверки, ограничения и пять первичных источников.
|
||||
- Перед выпуском выполнены: синтаксис Node, JSON-only CLI, import-safe проверка через draft gate, XML-проверка SVG и ручной просмотр SVG. Production-сборка сайта не входит в этот автономный черновик и не заявляется.
|
||||
- Рендер сначала выявил обрезание длинного заголовка в горизонтальной версии схемы. SVG перестроен в вертикальную композицию; повторный рендер на ширине 343 px показывает весь заголовок и все три диагностических блока без обрезания.
|
||||
- Вердикт: выпускной проход завершён для автономного черновика. Интеграционное ревью понадобится только перед изменением архива.
|
||||
|
||||
## Фактические проверки
|
||||
|
||||
| Проверка | Команда или метод | Результат |
|
||||
| --- | --- | --- |
|
||||
| Node syntax | `node --check scripts/upgrade-2019-01-field.mjs` из `web/` | успешно |
|
||||
| JSON-only CLI | `node scripts/upgrade-2019-01-field.mjs --print-revisions` | успешно внутри draft gate: stdout парсится как JSON, stderr пустой, import возвращает те же данные |
|
||||
| Import-safe и content gate | `npm run audit:draft -- scripts/upgrade-2019-01-field.mjs --expected-count=1` из `web/` | `PASS editorial-2019-01-field-jquery-webpack: 10910 body chars` |
|
||||
| XML | `xmllint --noout public/assets/editorial/2019/jquery-webpack-production-bridge-2019.svg` из `web/` | успешно |
|
||||
| Визуал | SVG отрендерен через Sharp в 720×1460 и 343×696; выполнен ручной просмотр | успешно после перевода схемы в вертикальный формат |
|
||||
|
||||
Основной прозаический текст без кода, таблицы, рисунка и источников — **8 488 знаков**. Метрика draft gate, которая считает основной HTML-контент без источников, — **10 910 знаков**.
|
||||
|
||||
## Выпусковой вердикт
|
||||
|
||||
После интеграции основной редактор повторно прогнал строгий аудит: ревизия
|
||||
прошла объём 10 910 знаков, figure, таблицу, пять примеров кода, маршрут
|
||||
действий и источники. `npm run build` завершился с кодом 0 и сгенерировал
|
||||
374 статические страницы.
|
||||
|
||||
Ревизия принята к публикации. `articles.json` не менялся: registry подменяет
|
||||
только редакционные поля по стабильному slug.
|
||||
@@ -7,6 +7,7 @@ import { revisions as september2018Revisions } from '../scripts/upgrade-2018-09.
|
||||
import { revisions as october2018January2019Revisions } from '../scripts/upgrade-2018-10-2019-01.mjs';
|
||||
import { revisions as november2018Revisions } from '../scripts/upgrade-2018-11.mjs';
|
||||
import { revisions as december2018Revisions } from '../scripts/upgrade-2018-12.mjs';
|
||||
import { revisions as january2019FieldRevisions } from '../scripts/upgrade-2019-01-field.mjs';
|
||||
|
||||
// This layer replaces archived source entries without losing their stable slug and date.
|
||||
export const editorialRevisions = [
|
||||
@@ -19,4 +20,5 @@ export const editorialRevisions = [
|
||||
...october2018January2019Revisions,
|
||||
...november2018Revisions,
|
||||
...december2018Revisions,
|
||||
...january2019FieldRevisions,
|
||||
];
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 720 1460" role="img" aria-labelledby="title description">
|
||||
<title id="title">Диагностика legacy jQuery-плагина в production-сборке Webpack</title>
|
||||
<desc id="description">Вертикальная схема показывает ошибочный статический импорт, мост с правильным порядком и проверку того, что глобальный и импортированный jQuery являются одним объектом.</desc>
|
||||
<defs>
|
||||
<linearGradient id="background" x1="0" y1="0" x2="1" y2="1">
|
||||
<stop offset="0" stop-color="#111b2e"/>
|
||||
<stop offset="1" stop-color="#182d43"/>
|
||||
</linearGradient>
|
||||
<filter id="shadow" x="-20%" y="-12%" width="140%" height="124%">
|
||||
<feDropShadow dx="0" dy="8" stdDeviation="12" flood-color="#060b14" flood-opacity="0.36"/>
|
||||
</filter>
|
||||
<marker id="arrowBlue" markerWidth="10" markerHeight="10" refX="8" refY="3" orient="auto">
|
||||
<path d="M0,0 L0,6 L9,3 z" fill="#62d7ff"/>
|
||||
</marker>
|
||||
<marker id="arrowRed" markerWidth="10" markerHeight="10" refX="8" refY="3" orient="auto">
|
||||
<path d="M0,0 L0,6 L9,3 z" fill="#ff8e8e"/>
|
||||
</marker>
|
||||
</defs>
|
||||
<rect width="720" height="1460" fill="url(#background)" rx="28"/>
|
||||
<text x="52" y="68" fill="#f4f8ff" font-family="system-ui, sans-serif" font-size="34" font-weight="700">jQuery + Webpack:</text>
|
||||
<text x="52" y="108" fill="#f4f8ff" font-family="system-ui, sans-serif" font-size="34" font-weight="700">порядок и один экземпляр</text>
|
||||
<text x="52" y="145" fill="#b7c9df" font-family="system-ui, sans-serif" font-size="21">Начинаем с момента, когда legacy-плагин читает window.jQuery.</text>
|
||||
|
||||
<rect x="52" y="184" width="616" height="506" rx="22" fill="#202d42" stroke="#80565e" stroke-width="2" filter="url(#shadow)"/>
|
||||
<text x="86" y="235" fill="#ffb6b6" font-family="system-ui, sans-serif" font-size="29" font-weight="700">Сбой: статический импорт</text>
|
||||
<text x="86" y="270" fill="#d5e0ef" font-family="system-ui, sans-serif" font-size="21">bootstrap.bad.js</text>
|
||||
|
||||
<rect x="84" y="304" width="552" height="90" rx="14" fill="#293b55"/>
|
||||
<text x="110" y="340" fill="#d9f0ff" font-family="ui-monospace, SFMono-Regular, monospace" font-size="21">import './legacy-mask'</text>
|
||||
<text x="110" y="370" fill="#b6c7dd" font-family="system-ui, sans-serif" font-size="19">зависимость выполняется первой</text>
|
||||
<path d="M360 394 L360 432" stroke="#ff8e8e" stroke-width="4" marker-end="url(#arrowRed)"/>
|
||||
|
||||
<rect x="84" y="451" width="552" height="104" rx="14" fill="#4b3039"/>
|
||||
<text x="110" y="489" fill="#ffe5e5" font-family="ui-monospace, SFMono-Regular, monospace" font-size="20">legacy-mask читает</text>
|
||||
<text x="110" y="519" fill="#ffe5e5" font-family="ui-monospace, SFMono-Regular, monospace" font-size="20">window.jQuery = отсутствует</text>
|
||||
<path d="M360 555 L360 593" stroke="#ff8e8e" stroke-width="4" marker-end="url(#arrowRed)"/>
|
||||
|
||||
<rect x="84" y="596" width="552" height="78" rx="14" fill="#293b55"/>
|
||||
<text x="110" y="630" fill="#d9f0ff" font-family="ui-monospace, SFMono-Regular, monospace" font-size="20">window.jQuery = $</text>
|
||||
<text x="110" y="656" fill="#b6c7dd" font-family="system-ui, sans-serif" font-size="18">присваивание произошло поздно</text>
|
||||
|
||||
<rect x="52" y="742" width="616" height="506" rx="22" fill="#203a44" stroke="#4c9c9c" stroke-width="2" filter="url(#shadow)"/>
|
||||
<text x="86" y="793" fill="#b8f1e8" font-family="system-ui, sans-serif" font-size="29" font-weight="700">Мост: явный порядок</text>
|
||||
<text x="86" y="828" fill="#d5e0ef" font-family="system-ui, sans-serif" font-size="21">legacy-jquery-bridge.js</text>
|
||||
|
||||
<rect x="84" y="862" width="552" height="78" rx="14" fill="#294a5a"/>
|
||||
<text x="110" y="896" fill="#d9f6ff" font-family="ui-monospace, SFMono-Regular, monospace" font-size="20">import $ from 'jquery'</text>
|
||||
<text x="110" y="922" fill="#b6d9e4" font-family="system-ui, sans-serif" font-size="18">один объект в модуле</text>
|
||||
<path d="M360 940 L360 978" stroke="#62d7ff" stroke-width="4" marker-end="url(#arrowBlue)"/>
|
||||
|
||||
<rect x="84" y="997" width="552" height="78" rx="14" fill="#294a5a"/>
|
||||
<text x="110" y="1031" fill="#d9f6ff" font-family="ui-monospace, SFMono-Regular, monospace" font-size="20">window.jQuery = $</text>
|
||||
<text x="110" y="1057" fill="#b6d9e4" font-family="system-ui, sans-serif" font-size="18">legacy-контракт выполнен</text>
|
||||
<path d="M360 1075 L360 1113" stroke="#62d7ff" stroke-width="4" marker-end="url(#arrowBlue)"/>
|
||||
|
||||
<rect x="84" y="1132" width="552" height="78" rx="14" fill="#294a5a"/>
|
||||
<text x="110" y="1166" fill="#d9f6ff" font-family="ui-monospace, SFMono-Regular, monospace" font-size="20">require('./legacy-mask')</text>
|
||||
<text x="110" y="1192" fill="#b6d9e4" font-family="system-ui, sans-serif" font-size="18">плагин расширяет тот же $.fn</text>
|
||||
|
||||
<rect x="52" y="1300" width="616" height="112" rx="22" fill="#15243a" stroke="#365c7f" stroke-width="2"/>
|
||||
<text x="84" y="1340" fill="#f4f8ff" font-family="system-ui, sans-serif" font-size="24" font-weight="700">Проверка второго экземпляра</text>
|
||||
<text x="84" y="1376" fill="#c9dced" font-family="ui-monospace, SFMono-Regular, monospace" font-size="21">window.jQuery === imported$ ?</text>
|
||||
<text x="84" y="1403" fill="#b7c9df" font-family="system-ui, sans-serif" font-size="17">false: ищем другой resolved-путь или entry.</text>
|
||||
<circle cx="619" cy="1358" r="27" fill="#183e48" stroke="#62d7ff" stroke-width="3"/>
|
||||
<path d="M606 1358 L616 1368 L633 1348" fill="none" stroke="#74efd3" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.8 KiB |
@@ -0,0 +1,261 @@
|
||||
import { resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
function escapeHtml(value) {
|
||||
return String(value)
|
||||
.replaceAll('&', '&')
|
||||
.replaceAll('<', '<')
|
||||
.replaceAll('>', '>')
|
||||
.replaceAll('"', '"')
|
||||
.replaceAll("'", ''');
|
||||
}
|
||||
|
||||
function paragraph(text) {
|
||||
return '<p>' + text + '</p>';
|
||||
}
|
||||
|
||||
function heading(text) {
|
||||
return '<h2>' + text + '</h2>';
|
||||
}
|
||||
|
||||
function codeBlock(code) {
|
||||
return '<pre><code>' + escapeHtml(String(code).trim()) + '</code></pre>';
|
||||
}
|
||||
|
||||
function figure(src, alt, caption) {
|
||||
return '<figure><img src="' + src + '" alt="' + alt + '" loading="lazy" /><figcaption>' + caption + '</figcaption></figure>';
|
||||
}
|
||||
|
||||
function orderedList(items) {
|
||||
return '<ol>' + items.map((item) => '<li>' + item + '</li>').join('') + '</ol>';
|
||||
}
|
||||
|
||||
function bulletList(items) {
|
||||
return '<ul>' + items.map((item) => '<li>' + item + '</li>').join('') + '</ul>';
|
||||
}
|
||||
|
||||
function dataTable(headers, rows) {
|
||||
const head = '<thead><tr>' + headers.map((header) => '<th scope="col">' + header + '</th>').join('') + '</tr></thead>';
|
||||
const body = '<tbody>' + rows.map((row) => '<tr>' + row.map((cell) => '<td>' + cell + '</td>').join('') + '</tr>').join('') + '</tbody>';
|
||||
return '<div class="table-scroll"><table>' + head + body + '</table></div>';
|
||||
}
|
||||
|
||||
function sourceList(items) {
|
||||
return '<ul>' + items.map((item) => '<li><a href="' + item.url + '" target="_blank" rel="noopener noreferrer">' + item.title + '</a> — ' + item.note + '</li>').join('') + '</ul>';
|
||||
}
|
||||
|
||||
function visibleText(html) {
|
||||
return html
|
||||
.replace(/<[^>]*>/g, ' ')
|
||||
.replaceAll(' ', ' ')
|
||||
.replaceAll('"', '"')
|
||||
.replaceAll(''', "'")
|
||||
.replaceAll('<', '<')
|
||||
.replaceAll('>', '>')
|
||||
.replaceAll('&', '&')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim();
|
||||
}
|
||||
|
||||
function proseText(html) {
|
||||
return visibleText(
|
||||
html
|
||||
.replace(/<pre><code>[\s\S]*?<\/code><\/pre>/g, '')
|
||||
.replace(/<figure>[\s\S]*?<\/figure>/g, '')
|
||||
.replace(/<div class="table-scroll">[\s\S]*?<\/div>/g, ''),
|
||||
);
|
||||
}
|
||||
|
||||
function createRevision(meta, bodyParts, sources) {
|
||||
const bodyHtml = bodyParts.join('\n');
|
||||
const proseLength = proseText(bodyHtml).length;
|
||||
|
||||
if (proseLength < 5000 || proseLength > 15000) {
|
||||
throw new Error(meta.slug + ': prose length must be 5000–15000, got ' + proseLength);
|
||||
}
|
||||
|
||||
if (sources.length < 2) {
|
||||
throw new Error(meta.slug + ': at least two primary sources are required');
|
||||
}
|
||||
|
||||
const contentHtml = [
|
||||
bodyHtml,
|
||||
heading('Проверяемые источники'),
|
||||
sourceList(sources),
|
||||
].join('\n');
|
||||
|
||||
return {
|
||||
...meta,
|
||||
contentHtml,
|
||||
proseLength,
|
||||
};
|
||||
}
|
||||
|
||||
const webpackShimming = {
|
||||
title: 'Webpack 4: Shimming',
|
||||
url: 'https://v4.webpack.js.org/guides/shimming/',
|
||||
note: 'назначение ProvidePlugin, legacy-модули с глобальными зависимостями и границы shim-подхода',
|
||||
};
|
||||
|
||||
const webpackCodeSplitting = {
|
||||
title: 'Webpack 4: Code Splitting',
|
||||
url: 'https://v4.webpack.js.org/guides/code-splitting/',
|
||||
note: 'entry, динамические импорты и дублирование модуля между несколькими entry',
|
||||
};
|
||||
|
||||
const webpackStats = {
|
||||
title: 'Webpack 4: Stats Data',
|
||||
url: 'https://v4.webpack.js.org/api/stats/',
|
||||
note: 'структура JSON-статистики сборки: assets, chunks, modules, errors и warnings',
|
||||
};
|
||||
|
||||
const webpackDevtool = {
|
||||
title: 'Webpack 4: devtool',
|
||||
url: 'https://v4.webpack.js.org/configuration/devtool/',
|
||||
note: 'варианты source map и компромиссы между удобством диагностики, скоростью и раскрытием исходного кода',
|
||||
};
|
||||
|
||||
const jqueryExtend = {
|
||||
title: 'jQuery API: jQuery.fn.extend()',
|
||||
url: 'https://api.jquery.com/jQuery.fn.extend/',
|
||||
note: 'плагин добавляет метод в прототип конкретного объекта jQuery, то есть в конкретный $.fn',
|
||||
};
|
||||
|
||||
const fieldArticle = createRevision(
|
||||
{
|
||||
slug: 'editorial-2019-01-field-jquery-webpack',
|
||||
title: 'jQuery + Webpack: почему legacy-плагин исчезает только в production',
|
||||
categories: ['JavaScript', 'Webpack', 'jQuery', 'Разбор'],
|
||||
cover: '/assets/editorial/2019/jquery-webpack-production-bridge-2019.svg',
|
||||
excerpt: 'Плагин виден в development, но после production-сборки у $.fn нет метода. Разделяем три причины: глобал не создан, плагин выполнился рано или приложение держит второй jQuery.',
|
||||
readingMinutes: 11,
|
||||
},
|
||||
[
|
||||
paragraph('В development старый плагин маски ввода работает, а в production вызов <code>$(input).legacyMask()</code> падает: метода нет в <code>$.fn</code>. Цена ошибки выше сломанного поля — сборка может уйти в релиз с нерабочим оформлением или проверкой номера, а срочная правка начнёт менять Webpack вслепую.'),
|
||||
paragraph('Разберём один вопрос: как доказать, что плагин потерялся именно на границе <code>window.jQuery</code>, порядка исполнения или второго экземпляра <code>jquery</code>, а не из-за минификации. Это учебный сценарий для Webpack 4 и jQuery; он показывает форму проверки. Ни production-бандл конкретного проекта, ни браузерную трассу я здесь не выдаю за выполненные.'),
|
||||
heading('Сначала фиксируем, что именно исчезло'),
|
||||
paragraph('Плагин jQuery обычно добавляет функцию в <code>$.fn</code>. Значит, выражение <code>typeof $.fn.legacyMask</code> отвечает не на вопрос «плагин подключён вообще», а на более полезный: установил ли он метод на тот экземпляр jQuery, которым пользуется код страницы. Официальный API описывает <code>jQuery.fn.extend()</code> как расширение прототипа jQuery. Если в окне живут два разных объекта jQuery, метод может оказаться только на одном из них.'),
|
||||
paragraph('Development часто скрывает границу. На локальной странице jQuery мог приехать отдельным тегом <code>script</code> раньше vendor-файла, а dev-сервер мог не собрать второй entry в тот же путь. Production собирает зависимости в модули и чанки. Это не делает Webpack виновником: меняется способ доставки, и скрытая зависимость плагина от глобального объекта становится видна.'),
|
||||
dataTable(
|
||||
['Наблюдение', 'Вероятная причина', 'Короткая проверка', 'Действие после проверки'],
|
||||
[
|
||||
['<code>window.jQuery</code> пустой до старого плагина', 'Импорт есть только в области модуля', 'Остановиться перед загрузкой плагина и вывести <code>window.jQuery</code>', 'Создать явный legacy-мост до выполнения плагина'],
|
||||
['Метод есть у <code>window.jQuery.fn</code>, но нет у <code>$ .fn</code> приложения', 'Плагин и приложение держат разные экземпляры', 'Сравнить <code>window.jQuery === $</code>', 'Убрать второй путь резолвации и оставить одну зависимость'],
|
||||
['Метода нет у обоих объектов', 'Файл плагина не выполнился или выполнился до моста', 'Проверить порядок модулей, ошибку в Console и включение файла в stats', 'Подключить адаптер после моста, затем проверить production-артефакт'],
|
||||
['Сбой только при втором entry', 'jQuery попал в несколько entry или загрузка чанков не согласована', 'Посмотреть modules/chunks в <code>stats.json</code>', 'Настроить общую зависимость и проверить все точки входа'],
|
||||
],
|
||||
),
|
||||
paragraph('В таблице есть важное ограничение: это дерево гипотез, не диагноз вашего сайта. Один и тот же текст ошибки может появиться при 404 vendor-файла, при CSP или при версии плагина, которая не совместима с закреплённой jQuery. Поэтому сначала собираем идентичности объектов и факт выполнения кода, а уже затем трогаем оптимизацию.'),
|
||||
heading('Учебный пример: плагин читает window.jQuery'),
|
||||
paragraph('Ниже минимальный legacy-файл. Он намеренно не импортирует jQuery: автор плагина рассчитывает, что глобальная переменная уже существует. Такой код неудобен для модулей, но он встречается в старых виджетах. Ошибка в примере воспроизводима без сервера: если в момент выполнения <code>root.jQuery</code> отсутствует, модуль бросает понятное исключение; если объект есть, метод появляется именно на его <code>fn</code>.'),
|
||||
codeBlock(String.raw`
|
||||
// src/vendor/legacy-mask.js
|
||||
(function installLegacyMask(root) {
|
||||
var $ = root.jQuery;
|
||||
|
||||
if (!$ || !$.fn) {
|
||||
throw new Error('legacy-mask ожидает window.jQuery до своего запуска');
|
||||
}
|
||||
|
||||
$.fn.legacyMask = function legacyMask() {
|
||||
return this.addClass('has-legacy-mask');
|
||||
};
|
||||
}(window));
|
||||
`),
|
||||
paragraph('Соблазнительный вариант ниже выглядит как правильный порядок сверху вниз: сначала импорт, затем присваивание, затем импорт плагина. Но статический <code>import</code> описывает зависимость модуля, а не вызов, который исполняется на этой строке после предыдущего оператора. Зависимый <code>legacy-mask.js</code> должен быть подготовлен и выполнен до тела модуля <code>bootstrap.bad.js</code>; присваивание в <code>window</code> для него опаздывает.'),
|
||||
codeBlock(String.raw`
|
||||
// src/bootstrap.bad.js
|
||||
import $ from 'jquery';
|
||||
import './vendor/legacy-mask';
|
||||
|
||||
window.jQuery = window.$ = $; // для legacy-mask это уже поздно
|
||||
|
||||
$('.js-phone').legacyMask();
|
||||
`),
|
||||
paragraph('Для одного старого плагина не обязательно переводить весь код назад на CommonJS. Достаточно сделать маленький адаптер с явной границей. В нём <code>require()</code> вызывается после записи в <code>window</code>; поэтому legacy-файл получает именно тот объект, который импортировал bootstrap. Этот приём выбран здесь ради порядка выполнения, а не как общий стиль нового кода.'),
|
||||
codeBlock(String.raw`
|
||||
// src/legacy-jquery-bridge.js
|
||||
import $ from 'jquery';
|
||||
|
||||
window.jQuery = window.$ = $;
|
||||
require('./vendor/legacy-mask');
|
||||
|
||||
export default $;
|
||||
|
||||
// src/bootstrap.js
|
||||
import $ from './legacy-jquery-bridge';
|
||||
|
||||
$('.js-phone').legacyMask();
|
||||
`),
|
||||
paragraph('После такого моста минимальная проверка не должна ограничиваться отсутствием исключения. Нужно удостовериться, что глобальный объект и импорт имеют одну ссылку, а метод лежит на том же <code>$.fn</code>, с которым работает приложение. Так мы отделяем проблему порядка от ситуации, когда плагин успешно установился, но на чужой копии jQuery.'),
|
||||
codeBlock(String.raw`
|
||||
// В Console страницы или во временном диагностическом модуле.
|
||||
import $ from './legacy-jquery-bridge';
|
||||
|
||||
var report = {
|
||||
hasGlobal: Boolean(window.jQuery),
|
||||
sameInstance: window.jQuery === $,
|
||||
pluginOnImport: typeof $.fn.legacyMask,
|
||||
pluginOnGlobal: window.jQuery && typeof window.jQuery.fn.legacyMask,
|
||||
};
|
||||
|
||||
console.table(report);
|
||||
console.assert(report.sameInstance, 'jQuery резолвится в разные экземпляры');
|
||||
console.assert(report.pluginOnImport === 'function', 'плагин не установлен на импорт');
|
||||
`),
|
||||
heading('Почему ProvidePlugin не заменяет мост'),
|
||||
paragraph('У <code>ProvidePlugin</code> другая задача. Документация Webpack 4 описывает его как способ сделать пакет переменной в модулях, которые компилирует Webpack, когда встречается нужный идентификатор. Это полезно для legacy-модуля, который обращается к свободной переменной <code>jQuery</code>, но не является автоматическим присваиванием свойства <code>window.jQuery</code>.'),
|
||||
paragraph('Поэтому решение должно следовать из самого плагина. Если файл принимает <code>jQuery</code> как параметр или использует свободную переменную, можно проверить настройку <code>ProvidePlugin</code>. Если он явно читает <code>window.jQuery</code>, нужен мост в <code>window</code> либо отдельная настройка loader, которая документированно передаёт зависимость. Подменять оба варианта одной конфигурацией опасно: в development симптом может исчезнуть из-за случайного порядка тегов, а в production вернуться.'),
|
||||
figure('/assets/editorial/2019/jquery-webpack-production-bridge-2019.svg', 'Схема диагностики legacy jQuery-плагина: неправильный статический импорт запускает плагин до назначения window.jQuery, а проверка равенства выявляет второй экземпляр jQuery', 'Проверка идёт от порядка исполнения к идентичности экземпляра: сначала мост, затем плагин, затем сравнение объектов.'),
|
||||
heading('Как искать второй экземпляр без догадки по размеру bundle'),
|
||||
paragraph('Два пакета не обязаны выглядеть как два одинаковых больших файла. Один экземпляр мог войти в entry страницы, другой — в зависимость виджета по другому пути или версии. Поэтому размер итогового JavaScript — слабое доказательство. Надёжнее сравнить объекты в рантайме и посмотреть список модулей в статистике именно той production-сборки, которая будет доставлена пользователю.'),
|
||||
paragraph('Webpack 4 документирует JSON-статистику как представление assets, chunks и modules. В реальном репозитории её можно запросить отдельной командой. Команду ниже не запускали для этой статьи: она является маршрутом диагностики, который нужно выполнять в ветке проекта и сохранять рядом с версией lock-файла. Source map включается только на время разбора и только если правила публикации исходников это позволяют.'),
|
||||
codeBlock(String.raw`
|
||||
# Команды для реального проекта, а не результат этого учебного примера.
|
||||
npx webpack --mode production --devtool source-map --profile --json > dist/stats.json
|
||||
npm ls jquery
|
||||
|
||||
# В stats.json ищем пути и чанки, где встречается jquery.
|
||||
# Затем в браузере сравниваем window.jQuery с импортом из legacy-моста.
|
||||
`),
|
||||
paragraph('Если <code>npm ls jquery</code> показывает несколько веток, это ещё не приговор: сборщик может дедуплицировать совместимые зависимости. Если в <code>stats.json</code> видны несколько модулей jQuery или разные resolved-пути, это также не доказывает, что два объекта дошли до страницы. Диагноз закрывается только вместе с равенством <code>window.jQuery === $</code>, фактом появления метода и сетевой записью тех чанков, которые фактически загрузил браузер.'),
|
||||
heading('Порядок проверки production-сценария'),
|
||||
orderedList([
|
||||
'Зафиксировать точный симптом: имя метода, страницу, один input и версию собранных ассетов. Не менять конфигурацию до записи исходной ошибки.',
|
||||
'В точке перед вызовом плагина вывести <code>window.jQuery</code>, импорт <code>$</code>, их равенство и тип <code>$.fn.legacyMask</code>. Это отделяет отсутствие глобала от второго экземпляра.',
|
||||
'Открыть Network и убедиться, что нужные entry и lazy-chunk действительно получили ответ, а не старую страницу из кеша. Записать имена файлов и hash, не делать вывод по одному названию bundle.',
|
||||
'Собрать production-статистику на той же версии lock-файла. Найти resolved-пути <code>jquery</code>, их chunks и причины подключения.',
|
||||
'Если плагин читает глобал, создать один изолированный bridge до его загрузки; если он принимает импорт, удалить глобальную зависимость вместо добавления нового shim.',
|
||||
'Повторить проверку на чистой странице и в каждом entry, где живёт виджет. Готовность — одинаковый объект jQuery и функция плагина на его <code>fn</code>, а не только исчезнувшая ошибка в Console.',
|
||||
]),
|
||||
heading('Что считать исправлением'),
|
||||
paragraph('Исправлением является не строка <code>window.$ = $</code> сама по себе. Нужен маленький контракт: bridge импортирует одну зависимость, публикует её в global до legacy-файла, подключает нужный side effect и экспортирует тот же объект приложению. Так следующий разработчик видит, где заканчивается старый API и почему этот файл нельзя переставить среди обычных импортов.'),
|
||||
paragraph('Если обнаружен второй экземпляр, сначала устраняем причину его попадания в граф: разный resolved-путь, несовместимая версия или отдельный entry без общей оптимизации. Webpack 4 отдельно предупреждает, что несколько entry могут дублировать общий импорт; для этого есть SplitChunksPlugin. Но включить splitChunks «для лечения плагина» недостаточно: после изменения всё равно проверяем идентичность объектов и метод в runtime.'),
|
||||
heading('Ограничения и границы примера'),
|
||||
bulletList([
|
||||
'Пример предполагает браузерный <code>window</code> и Webpack 4. В SSR, worker или тестовой среде global-объект другой; мост надо либо не исполнять на сервере, либо изолировать адаптером окружения.',
|
||||
'Вызов <code>require()</code> оставлен только внутри legacy-моста ради наблюдаемого порядка. Он не является рекомендацией смешивать системы модулей во всём новом коде.',
|
||||
'Source map может раскрывать пути и исходный код. Его нельзя включать в публичную production-доставку без проверки политики проекта; для расследования подойдёт защищённый артефакт.',
|
||||
'CSP, 404 чанка, несовместимая версия плагина и кеш CDN могут дать похожий внешний симптом. Этот текст не заменяет Network, Console и точную версию зависимостей.',
|
||||
'Глобальная jQuery — совместимость с конкретным legacy-файлом, а не архитектурная цель. Новый виджет лучше получает зависимость импортом и не меняет <code>window</code>.',
|
||||
]),
|
||||
heading('Итог'),
|
||||
paragraph('Когда <code>$.fn.legacyMask</code> пропадает только после production-сборки, не стоит первым делом отключать минификацию. Сначала проверяем порядок: есть ли <code>window.jQuery</code> до плагина. Затем проверяем идентичность: тот ли это объект, который импортирует приложение. После этого production-статистика и Network показывают, какой модуль и какой чанк нужно исправлять. Такой маршрут оставляет одно проверяемое условие готовности: плагин установлен на том же <code>$.fn</code>, которым пользуется экран.'),
|
||||
],
|
||||
[webpackShimming, webpackCodeSplitting, webpackStats, webpackDevtool, jqueryExtend],
|
||||
);
|
||||
|
||||
export const revisions = [fieldArticle]
|
||||
.map(({ proseLength, ...revision }) => revision);
|
||||
|
||||
const isDirectRun = process.argv[1]
|
||||
&& resolve(process.argv[1]) === fileURLToPath(import.meta.url);
|
||||
|
||||
if (isDirectRun) {
|
||||
if (process.argv.includes('--print-revisions')) {
|
||||
process.stdout.write(JSON.stringify(revisions, null, 2) + '\n');
|
||||
} else {
|
||||
process.stderr.write('Usage: node web/scripts/upgrade-2019-01-field.mjs --print-revisions\n');
|
||||
process.exitCode = 1;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user