This commit is contained in:
@@ -0,0 +1,52 @@
|
||||
# Июнь 2018 — тройное ревью Webpack 4
|
||||
|
||||
Партия подготовлена для отдельной интеграции:
|
||||
|
||||
- `editorial-2018-06-practice-webpack-entry`
|
||||
- `editorial-2018-06-mechanism-webpack-entry`
|
||||
- `editorial-2018-06-field-webpack-entry`
|
||||
|
||||
Дата повторной проверки: 31 июля 2026 года. Пакет принят в публикационный слой:
|
||||
скрипт печатает ревизии, а `web/data/editorial-revisions.mjs` сопоставляет их только
|
||||
по стабильным slug, не меняя даты, автора и базовый архив.
|
||||
|
||||
## 1. Факты и техника — пройдено
|
||||
|
||||
### Практика: две страницы без двойной перевозки общего кода
|
||||
|
||||
- Основой стали официальные страницы Webpack 4: [Entry Points](https://v4.webpack.js.org/concepts/entry-points/), [Code Splitting](https://v4.webpack.js.org/guides/code-splitting/), [Optimization](https://v4.webpack.js.org/configuration/optimization/) и [Output](https://v4.webpack.js.org/concepts/output/).
|
||||
- Конфигурация использует синтаксис Webpack 4: object entry, `optimization.splitChunks` и `runtimeChunk: 'single'`. Отдельный `vendor` entry не выдан за рекомендуемый путь: документация v4 прямо направляет разделение vendor/app-кода в `splitChunks`.
|
||||
- `minSize: 0` помечен учебным условием для маленького примера, а не универсальной production-настройкой. Текст называет риск лишнего запроса и требует проверить фактические script-теги.
|
||||
|
||||
### Механизм: один import в двух entry bundle
|
||||
|
||||
- Сверены [object/array-синтаксис entry](https://v4.webpack.js.org/concepts/entry-points/) и [ограничения entry-point splitting](https://v4.webpack.js.org/guides/code-splitting/).
|
||||
- Объяснение разделяет исходный файл, граф достижимости и emitted chunk. В тексте нет ложного вывода, что общий путь на диске автоматически создаёт общий chunk.
|
||||
- Массив файлов описан как один multi-main entry, а два самостоятельных HTML-документа — как два object entry. Новые entry-API не подставляются в пример Webpack 4.
|
||||
|
||||
### Диагностика: рост bundle после нового entry
|
||||
|
||||
- Поля `assets`, `chunks`, `modules` и entrypoint-данные сверены с [Stats Data](https://v4.webpack.js.org/api/stats/) Webpack 4; команда и формат CLI — с [документацией CLI](https://v4.webpack.js.org/api/cli/).
|
||||
- Диагностический фрагмент не трактует присутствие модуля в нескольких chunks как готовое доказательство скачивания. Статья требует проверить HTML и Network отдельно.
|
||||
- Размер ассета в stats не выдан за сетевой transfer size: явно названы влияние сжатия, кеша, режима сборки и source map. В текст не добавлены вымышленные замеры, пользователи или результаты.
|
||||
- Третий редакторский проход после независимого quality-gate: первое предложение теперь начинается с явного маркера «Симптом» и называет наблюдаемые `site.…js` и `admin.…js` в Network; цена ошибки сохранена — пользователь скачивает код панели, которой не откроет.
|
||||
|
||||
## 2. Редактура и голос — пройдено
|
||||
|
||||
- Каждый текст отвечает на отдельный вопрос: настройка двух страниц, причина дублирования в графе, расследование роста ассетов.
|
||||
- Проблема и цена ошибки стоят в первом абзаце; далее идут конкретные куски конфигурации, проверка и ограничение.
|
||||
- Проза без кода, источников и разметки: 5 502, 5 177 и 6 024 знака соответственно. Все значения входят в интервал 5 000–15 000.
|
||||
- Тон оставлен на уровне 2018 года: короткая техническая речь, локальный пример, ручная проверка. Нет позднего инфраструктурного жаргона, неподкреплённых метрик и шаблонов массового архива.
|
||||
- Общие слова заменены условиями: что является entry, когда общий модуль считается кандидатом на chunk, что нужно увидеть в stats и Network.
|
||||
- Во всех трёх ревизиях список источников теперь начинается точным отдельным заголовком `<h2>Проверяемые источники</h2>` непосредственно перед `<ul>`.
|
||||
|
||||
## 3. Визуал и передача — пройдено с оговоркой
|
||||
|
||||
- Для каждого текста создана отдельная SVG-схема: путь общих chunks, граф двух entry и дерево диагностики. В каждой статье есть осмысленные `alt` и подпись.
|
||||
- SVG не менялись в этой коррекции и повторно прошли `xmllint --noout`; первая визуальная проверка через локальный Quick Look остаётся актуальной. Схемы уже переделаны в вертикальные viewBox `720px`, чтобы текст оставался читаемым на узкой ширине, а не только на desktop.
|
||||
- У каждой ревизии есть 1 рисунок, 1 таблица, 3 блока кода, нумерованная последовательность действий и 3–4 первичных источника.
|
||||
- `node --check web/scripts/upgrade-2018-06.mjs` прошёл. `node web/scripts/upgrade-2018-06.mjs --print-revisions` вернул только валидный JSON из трёх ревизий, без stderr.
|
||||
- Безопасный import проверен отдельно: импорт возвращает `revisions`, не печатает Usage и не меняет `process.exitCode`. Вывод Usage доступен только при прямом запуске скрипта без флага.
|
||||
- После интеграции основной агент повторно прогнал strict audit всех трёх slug и production build. Сборка прошла и сгенерировала 374 статические страницы.
|
||||
|
||||
Статус: принят к публикации. В этой зоне не изменялись базовый архив статей, общий стандарт, аудит-скрипты и история Git.
|
||||
@@ -1,8 +1,10 @@
|
||||
import { revisions as march2018Revisions } from '../scripts/upgrade-2018-03.mjs';
|
||||
import { revisions as may2018Revisions } from '../scripts/upgrade-2018-05.mjs';
|
||||
import { revisions as june2018Revisions } from '../scripts/upgrade-2018-06.mjs';
|
||||
|
||||
// This layer replaces archived source entries without losing their stable slug and date.
|
||||
export const editorialRevisions = [
|
||||
...march2018Revisions,
|
||||
...may2018Revisions,
|
||||
...june2018Revisions,
|
||||
];
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 720 1160" role="img" aria-labelledby="title desc">
|
||||
<title id="title">Webpack 4: расследование роста bundle после нового entry</title>
|
||||
<desc id="desc">Вертикальная схема диагностики: одинаковая production-сборка, stats JSON, связь modules и chunks, проверка HTML и Network, затем решение о splitChunks.</desc>
|
||||
<defs>
|
||||
<linearGradient id="bg" x1="0" y1="0" x2="1" y2="1">
|
||||
<stop offset="0%" stop-color="#f8fbff"/>
|
||||
<stop offset="100%" stop-color="#fff8f0"/>
|
||||
</linearGradient>
|
||||
<marker id="arrow" markerWidth="10" markerHeight="10" refX="8" refY="5" orient="auto">
|
||||
<path d="M0,0 L10,5 L0,10 z" fill="#5e6d82"/>
|
||||
</marker>
|
||||
<filter id="shadow" x="-10%" y="-10%" width="120%" height="120%">
|
||||
<feDropShadow dx="0" dy="6" stdDeviation="8" flood-color="#253858" flood-opacity="0.12"/>
|
||||
</filter>
|
||||
</defs>
|
||||
<rect width="720" height="1160" rx="28" fill="url(#bg)"/>
|
||||
<text x="40" y="62" fill="#14213d" font-family="Arial, sans-serif" font-size="31" font-weight="700">Bundle вырос после entry</text>
|
||||
<text x="40" y="98" fill="#516079" font-family="Arial, sans-serif" font-size="20">Сначала доказательство,</text>
|
||||
<text x="40" y="124" fill="#516079" font-family="Arial, sans-serif" font-size="20">потом настройка splitChunks.</text>
|
||||
|
||||
<g filter="url(#shadow)">
|
||||
<rect x="40" y="170" width="640" height="120" rx="18" fill="#ffffff" stroke="#c9d6e8"/>
|
||||
<circle cx="84" cy="214" r="22" fill="#255a9b"/>
|
||||
<text x="77" y="222" fill="#ffffff" font-family="Arial, sans-serif" font-size="18" font-weight="700">1</text>
|
||||
<text x="126" y="219" fill="#14213d" font-family="Arial, sans-serif" font-size="25" font-weight="700">Одинаковый production build</text>
|
||||
<text x="126" y="254" fill="#516079" font-family="Arial, sans-serif" font-size="18">До и после: одна команда и тот же webpack-cli.</text>
|
||||
</g>
|
||||
<path d="M360 290 V328" fill="none" stroke="#5e6d82" stroke-width="4" marker-end="url(#arrow)"/>
|
||||
|
||||
<g filter="url(#shadow)">
|
||||
<rect x="40" y="328" width="640" height="120" rx="18" fill="#ffffff" stroke="#c9d6e8"/>
|
||||
<circle cx="84" cy="372" r="22" fill="#226b46"/>
|
||||
<text x="77" y="380" fill="#ffffff" font-family="Arial, sans-serif" font-size="18" font-weight="700">2</text>
|
||||
<text x="126" y="377" fill="#14213d" font-family="Arial, sans-serif" font-size="25" font-weight="700">stats.json</text>
|
||||
<text x="126" y="412" fill="#516079" font-family="Arial, sans-serif" font-size="18">Сначала assets, затем modules и chunks.</text>
|
||||
</g>
|
||||
<path d="M360 448 V486" fill="none" stroke="#5e6d82" stroke-width="4" marker-end="url(#arrow)"/>
|
||||
|
||||
<g filter="url(#shadow)">
|
||||
<rect x="40" y="486" width="640" height="120" rx="18" fill="#ffffff" stroke="#c9d6e8"/>
|
||||
<circle cx="84" cy="530" r="22" fill="#8a5a00"/>
|
||||
<text x="77" y="538" fill="#ffffff" font-family="Arial, sans-serif" font-size="18" font-weight="700">3</text>
|
||||
<text x="126" y="535" fill="#14213d" font-family="Arial, sans-serif" font-size="25" font-weight="700">HTML и Network</text>
|
||||
<text x="126" y="570" fill="#516079" font-family="Arial, sans-serif" font-size="18">Что запрашивает конкретная страница?</text>
|
||||
</g>
|
||||
<path d="M360 606 V644" fill="none" stroke="#5e6d82" stroke-width="4" marker-end="url(#arrow)"/>
|
||||
|
||||
<g filter="url(#shadow)">
|
||||
<rect x="40" y="644" width="640" height="142" rx="20" fill="#ffffff" stroke="#b8dcc8" stroke-width="2"/>
|
||||
<text x="68" y="698" fill="#226b46" font-family="Arial, sans-serif" font-size="26" font-weight="700">Есть реальное дублирование?</text>
|
||||
<text x="68" y="738" fill="#516079" font-family="Arial, sans-serif" font-size="18">Один модуль нужен двум initial-путям</text>
|
||||
<text x="68" y="766" fill="#516079" font-family="Arial, sans-serif" font-size="18">и обе страницы действительно его запрашивают.</text>
|
||||
</g>
|
||||
|
||||
<path d="M202 786 V846" fill="none" stroke="#a14a4a" stroke-width="4" marker-end="url(#arrow)"/>
|
||||
<path d="M518 786 V846" fill="none" stroke="#226b46" stroke-width="4" marker-end="url(#arrow)"/>
|
||||
<text x="174" y="826" fill="#a14a4a" font-family="Arial, sans-serif" font-size="20" font-weight="700">нет</text>
|
||||
<text x="497" y="826" fill="#226b46" font-family="Arial, sans-serif" font-size="20" font-weight="700">да</text>
|
||||
|
||||
<g filter="url(#shadow)">
|
||||
<rect x="40" y="846" width="300" height="176" rx="18" fill="#fff0f0" stroke="#efb8b8"/>
|
||||
<text x="66" y="898" fill="#8f3636" font-family="Arial, sans-serif" font-size="23" font-weight="700">Не менять</text>
|
||||
<text x="66" y="929" fill="#8f3636" font-family="Arial, sans-serif" font-size="23" font-weight="700">splitChunks</text>
|
||||
<text x="66" y="968" fill="#6c5555" font-family="Arial, sans-serif" font-size="17">Исправить шаблон</text>
|
||||
<text x="66" y="993" fill="#6c5555" font-family="Arial, sans-serif" font-size="17">или сравнение build.</text>
|
||||
</g>
|
||||
<g filter="url(#shadow)">
|
||||
<rect x="380" y="846" width="300" height="176" rx="18" fill="#e7f7ed" stroke="#b8dcc8"/>
|
||||
<text x="406" y="898" fill="#226b46" font-family="Arial, sans-serif" font-size="23" font-weight="700">Точечная</text>
|
||||
<text x="406" y="929" fill="#226b46" font-family="Arial, sans-serif" font-size="23" font-weight="700">cache group</text>
|
||||
<text x="406" y="968" fill="#3f6d53" font-family="Arial, sans-serif" font-size="17">Повторить stats</text>
|
||||
<text x="406" y="993" fill="#3f6d53" font-family="Arial, sans-serif" font-size="17">и Network.</text>
|
||||
</g>
|
||||
|
||||
<rect x="40" y="1062" width="640" height="60" rx="14" fill="#14213d"/>
|
||||
<text x="66" y="1100" fill="#ffffff" font-family="Arial, sans-serif" font-size="18">Рост dist не равен росту загрузки одной страницы.</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.0 KiB |
@@ -0,0 +1,75 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 720 1120" role="img" aria-labelledby="title desc">
|
||||
<title id="title">Webpack 4: от двух entry к общему модулю</title>
|
||||
<desc id="desc">Вертикальный граф показывает, как entry site и admin достигают собственных модулей и затем общих зависимостей date-format и jquery.</desc>
|
||||
<defs>
|
||||
<linearGradient id="bg" x1="0" y1="0" x2="1" y2="1">
|
||||
<stop offset="0%" stop-color="#fffaf3"/>
|
||||
<stop offset="100%" stop-color="#f3f7ff"/>
|
||||
</linearGradient>
|
||||
<marker id="arrow" markerWidth="10" markerHeight="10" refX="8" refY="5" orient="auto">
|
||||
<path d="M0,0 L10,5 L0,10 z" fill="#6c7280"/>
|
||||
</marker>
|
||||
<filter id="shadow" x="-10%" y="-10%" width="120%" height="120%">
|
||||
<feDropShadow dx="0" dy="6" stdDeviation="8" flood-color="#253858" flood-opacity="0.12"/>
|
||||
</filter>
|
||||
</defs>
|
||||
<rect width="720" height="1120" rx="28" fill="url(#bg)"/>
|
||||
<text x="40" y="62" fill="#14213d" font-family="Arial, sans-serif" font-size="31" font-weight="700">От entry к графу</text>
|
||||
<text x="40" y="98" fill="#516079" font-family="Arial, sans-serif" font-size="20">Общий исходник достижим</text>
|
||||
<text x="40" y="124" fill="#516079" font-family="Arial, sans-serif" font-size="20">из двух независимых стартов.</text>
|
||||
|
||||
<g filter="url(#shadow)">
|
||||
<rect x="40" y="174" width="300" height="132" rx="18" fill="#ffffff" stroke="#bcd3e7"/>
|
||||
<rect x="40" y="174" width="300" height="35" rx="18" fill="#d9eaff"/>
|
||||
<text x="62" y="198" fill="#255a9b" font-family="Arial, sans-serif" font-size="16" font-weight="700">ENTRY 1</text>
|
||||
<text x="62" y="254" fill="#14213d" font-family="Arial, sans-serif" font-size="29" font-weight="700">site.js</text>
|
||||
<text x="62" y="283" fill="#516079" font-family="Arial, sans-serif" font-size="17">сценарий обычной страницы</text>
|
||||
</g>
|
||||
<g filter="url(#shadow)">
|
||||
<rect x="380" y="174" width="300" height="132" rx="18" fill="#ffffff" stroke="#bcd3e7"/>
|
||||
<rect x="380" y="174" width="300" height="35" rx="18" fill="#e9dcff"/>
|
||||
<text x="402" y="198" fill="#6542a1" font-family="Arial, sans-serif" font-size="16" font-weight="700">ENTRY 2</text>
|
||||
<text x="402" y="254" fill="#14213d" font-family="Arial, sans-serif" font-size="29" font-weight="700">admin.js</text>
|
||||
<text x="402" y="283" fill="#516079" font-family="Arial, sans-serif" font-size="17">сценарий панели</text>
|
||||
</g>
|
||||
|
||||
<path d="M190 306 V354" fill="none" stroke="#255a9b" stroke-width="4" marker-end="url(#arrow)"/>
|
||||
<path d="M530 306 V354" fill="none" stroke="#6542a1" stroke-width="4" marker-end="url(#arrow)"/>
|
||||
|
||||
<g filter="url(#shadow)">
|
||||
<rect x="40" y="354" width="300" height="130" rx="18" fill="#ffffff" stroke="#c9d6e8"/>
|
||||
<text x="62" y="410" fill="#14213d" font-family="Arial, sans-serif" font-size="24" font-weight="700">site/search.js</text>
|
||||
<text x="62" y="444" fill="#516079" font-family="Arial, sans-serif" font-size="17">нужен только site</text>
|
||||
</g>
|
||||
<g filter="url(#shadow)">
|
||||
<rect x="380" y="354" width="300" height="130" rx="18" fill="#ffffff" stroke="#c9d6e8"/>
|
||||
<text x="402" y="410" fill="#14213d" font-family="Arial, sans-serif" font-size="24" font-weight="700">admin/report.js</text>
|
||||
<text x="402" y="444" fill="#516079" font-family="Arial, sans-serif" font-size="17">нужен только admin</text>
|
||||
</g>
|
||||
|
||||
<path d="M190 484 V538 H360 V570" fill="none" stroke="#255a9b" stroke-width="4" marker-end="url(#arrow)"/>
|
||||
<path d="M530 484 V538 H360 V570" fill="none" stroke="#6542a1" stroke-width="4" marker-end="url(#arrow)"/>
|
||||
|
||||
<g filter="url(#shadow)">
|
||||
<rect x="40" y="570" width="640" height="150" rx="18" fill="#ffffff" stroke="#b8dcc8" stroke-width="2"/>
|
||||
<rect x="40" y="570" width="640" height="35" rx="18" fill="#d8f1e2"/>
|
||||
<text x="66" y="594" fill="#226b46" font-family="Arial, sans-serif" font-size="16" font-weight="700">ОБЩИЙ ИСХОДНИК</text>
|
||||
<text x="66" y="653" fill="#14213d" font-family="Arial, sans-serif" font-size="29" font-weight="700">shared/date-format.js</text>
|
||||
<text x="66" y="688" fill="#516079" font-family="Arial, sans-serif" font-size="18">кандидат для common, если отдельный запрос оправдан</text>
|
||||
</g>
|
||||
|
||||
<path d="M360 720 V766" fill="none" stroke="#6c7280" stroke-width="4" marker-end="url(#arrow)"/>
|
||||
|
||||
<g filter="url(#shadow)">
|
||||
<rect x="40" y="766" width="640" height="150" rx="18" fill="#ffffff" stroke="#e6d093"/>
|
||||
<rect x="40" y="766" width="640" height="35" rx="18" fill="#f6e8c3"/>
|
||||
<text x="66" y="790" fill="#8a5a00" font-family="Arial, sans-serif" font-size="16" font-weight="700">ВНЕШНИЙ ПАКЕТ</text>
|
||||
<text x="66" y="849" fill="#14213d" font-family="Arial, sans-serif" font-size="29" font-weight="700">jquery</text>
|
||||
<text x="66" y="884" fill="#516079" font-family="Arial, sans-serif" font-size="18">кандидат для vendors при двух entry</text>
|
||||
</g>
|
||||
|
||||
<rect x="40" y="980" width="640" height="96" rx="16" fill="#14213d"/>
|
||||
<text x="66" y="1018" fill="#ffffff" font-family="Arial, sans-serif" font-size="20" font-weight="700">Важная граница</text>
|
||||
<text x="66" y="1051" fill="#ffffff" font-family="Arial, sans-serif" font-size="18">Общий файл на диске не гарантирует общий chunk в сборке.</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.5 KiB |
@@ -0,0 +1,76 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 720 1120" role="img" aria-labelledby="title desc">
|
||||
<title id="title">Webpack 4: два entry и общие chunks</title>
|
||||
<desc id="desc">Вертикальная схема: общий runtime ведёт к chunks vendors и common, которые нужны двум отдельным entry catalog и checkout.</desc>
|
||||
<defs>
|
||||
<linearGradient id="bg" x1="0" y1="0" x2="1" y2="1">
|
||||
<stop offset="0%" stop-color="#f7fafc"/>
|
||||
<stop offset="100%" stop-color="#eef4ff"/>
|
||||
</linearGradient>
|
||||
<marker id="arrow" markerWidth="10" markerHeight="10" refX="8" refY="5" orient="auto">
|
||||
<path d="M0,0 L10,5 L0,10 z" fill="#60708a"/>
|
||||
</marker>
|
||||
<filter id="shadow" x="-10%" y="-10%" width="120%" height="120%">
|
||||
<feDropShadow dx="0" dy="6" stdDeviation="8" flood-color="#253858" flood-opacity="0.12"/>
|
||||
</filter>
|
||||
</defs>
|
||||
<rect width="720" height="1120" rx="28" fill="url(#bg)"/>
|
||||
<text x="40" y="62" fill="#14213d" font-family="Arial, sans-serif" font-size="31" font-weight="700">Webpack 4: два entry</text>
|
||||
<text x="40" y="98" fill="#516079" font-family="Arial, sans-serif" font-size="20">Общий код выделяется отдельно,</text>
|
||||
<text x="40" y="124" fill="#516079" font-family="Arial, sans-serif" font-size="20">а каждая страница запускает свой сценарий.</text>
|
||||
|
||||
<g filter="url(#shadow)">
|
||||
<rect x="40" y="170" width="640" height="112" rx="18" fill="#ffffff" stroke="#c9d6e8"/>
|
||||
<rect x="40" y="170" width="640" height="35" rx="18" fill="#dceeff"/>
|
||||
<text x="66" y="194" fill="#255a9b" font-family="Arial, sans-serif" font-size="17" font-weight="700">ОБЩИЙ RUNTIME</text>
|
||||
<text x="66" y="243" fill="#14213d" font-family="Arial, sans-serif" font-size="27" font-weight="700">runtime</text>
|
||||
<text x="205" y="243" fill="#516079" font-family="Arial, sans-serif" font-size="18">связывает modules и chunks</text>
|
||||
</g>
|
||||
|
||||
<path d="M260 282 V328 H190 V352" fill="none" stroke="#60708a" stroke-width="4" marker-end="url(#arrow)"/>
|
||||
<path d="M460 282 V328 H530 V352" fill="none" stroke="#60708a" stroke-width="4" marker-end="url(#arrow)"/>
|
||||
|
||||
<g filter="url(#shadow)">
|
||||
<rect x="40" y="352" width="300" height="160" rx="18" fill="#ffffff" stroke="#c9d6e8"/>
|
||||
<rect x="40" y="352" width="300" height="35" rx="18" fill="#f6e8c3"/>
|
||||
<text x="62" y="376" fill="#8a5a00" font-family="Arial, sans-serif" font-size="16" font-weight="700">ВНЕШНИЕ ПАКЕТЫ</text>
|
||||
<text x="62" y="429" fill="#14213d" font-family="Arial, sans-serif" font-size="27" font-weight="700">vendors</text>
|
||||
<text x="62" y="460" fill="#516079" font-family="Arial, sans-serif" font-size="18">например, jquery</text>
|
||||
<text x="62" y="488" fill="#516079" font-family="Arial, sans-serif" font-size="16">из node_modules</text>
|
||||
</g>
|
||||
|
||||
<g filter="url(#shadow)">
|
||||
<rect x="380" y="352" width="300" height="160" rx="18" fill="#ffffff" stroke="#c9d6e8"/>
|
||||
<rect x="380" y="352" width="300" height="35" rx="18" fill="#d8f1e2"/>
|
||||
<text x="402" y="376" fill="#226b46" font-family="Arial, sans-serif" font-size="16" font-weight="700">ОБЩИЙ КОД</text>
|
||||
<text x="402" y="429" fill="#14213d" font-family="Arial, sans-serif" font-size="27" font-weight="700">common</text>
|
||||
<text x="402" y="460" fill="#516079" font-family="Arial, sans-serif" font-size="18">src/shared/money.js</text>
|
||||
<text x="402" y="488" fill="#516079" font-family="Arial, sans-serif" font-size="16">достигнут из двух entry</text>
|
||||
</g>
|
||||
|
||||
<path d="M190 512 V564 H360" fill="none" stroke="#60708a" stroke-width="4" marker-end="url(#arrow)"/>
|
||||
<path d="M530 512 V564 H360" fill="none" stroke="#60708a" stroke-width="4" marker-end="url(#arrow)"/>
|
||||
|
||||
<g filter="url(#shadow)">
|
||||
<rect x="40" y="564" width="640" height="142" rx="18" fill="#ffffff" stroke="#bcd3e7"/>
|
||||
<rect x="40" y="564" width="640" height="36" rx="18" fill="#d9eaff"/>
|
||||
<text x="66" y="589" fill="#255a9b" font-family="Arial, sans-serif" font-size="17" font-weight="700">СТРАНИЦА /CATALOG/</text>
|
||||
<text x="66" y="650" fill="#14213d" font-family="Arial, sans-serif" font-size="30" font-weight="700">catalog entry</text>
|
||||
<text x="66" y="681" fill="#516079" font-family="Arial, sans-serif" font-size="18">Только код каталога; checkout сюда не подключается.</text>
|
||||
</g>
|
||||
|
||||
<path d="M190 512 V738 H360 V762" fill="none" stroke="#60708a" stroke-width="4" marker-end="url(#arrow)"/>
|
||||
<path d="M530 512 V738 H360 V762" fill="none" stroke="#60708a" stroke-width="4" marker-end="url(#arrow)"/>
|
||||
|
||||
<g filter="url(#shadow)">
|
||||
<rect x="40" y="762" width="640" height="142" rx="18" fill="#ffffff" stroke="#bcd3e7"/>
|
||||
<rect x="40" y="762" width="640" height="36" rx="18" fill="#e7dcff"/>
|
||||
<text x="66" y="787" fill="#6542a1" font-family="Arial, sans-serif" font-size="17" font-weight="700">СТРАНИЦА /CHECKOUT/</text>
|
||||
<text x="66" y="848" fill="#14213d" font-family="Arial, sans-serif" font-size="30" font-weight="700">checkout entry</text>
|
||||
<text x="66" y="879" fill="#516079" font-family="Arial, sans-serif" font-size="18">Только код заказа; catalog сюда не подключается.</text>
|
||||
</g>
|
||||
|
||||
<rect x="40" y="968" width="640" height="98" rx="16" fill="#14213d"/>
|
||||
<text x="66" y="1007" fill="#ffffff" font-family="Arial, sans-serif" font-size="20" font-weight="700">Проверка в HTML</text>
|
||||
<text x="66" y="1040" fill="#ffffff" font-family="Arial, sans-serif" font-size="18">У каждой страницы свой entry и нужные общие chunks.</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.6 KiB |
@@ -0,0 +1,401 @@
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const escapeHtml = (value) => String(value)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
|
||||
const paragraph = (content) => '<p>' + content + '</p>';
|
||||
const heading = (content) => '<h2>' + content + '</h2>';
|
||||
const codeBlock = (source) => '<pre><code>' + escapeHtml(source.trim()) + '</code></pre>';
|
||||
const figure = (src, alt, caption) => [
|
||||
'<figure>',
|
||||
'<img src="' + src + '" alt="' + alt + '" />',
|
||||
'<figcaption>' + caption + '</figcaption>',
|
||||
'</figure>',
|
||||
].join('');
|
||||
|
||||
function dataTable(headers, rows) {
|
||||
const head = headers.map((header) => '<th scope="col">' + header + '</th>').join('');
|
||||
const body = rows.map((row) => (
|
||||
'<tr>' + row.map((cell) => '<td>' + cell + '</td>').join('') + '</tr>'
|
||||
)).join('');
|
||||
|
||||
return '<div class="table-scroll"><table><thead><tr>' + head
|
||||
+ '</tr></thead><tbody>' + body + '</tbody></table></div>';
|
||||
}
|
||||
|
||||
function orderedList(items) {
|
||||
return '<ol>' + items.map((item) => '<li>' + item + '</li>').join('') + '</ol>';
|
||||
}
|
||||
|
||||
function sourceList(items) {
|
||||
return heading('Проверяемые источники') + '<ul>' + items.map(({ label, url }) => (
|
||||
'<li><a href="' + url + '" target="_blank" rel="noopener noreferrer">' + label + '</a></li>'
|
||||
)).join('') + '</ul>';
|
||||
}
|
||||
|
||||
const sources = {
|
||||
entry: {
|
||||
label: 'Webpack 4 — Entry Points',
|
||||
url: 'https://v4.webpack.js.org/concepts/entry-points/',
|
||||
},
|
||||
splitChunks: {
|
||||
label: 'Webpack 4 — Code Splitting',
|
||||
url: 'https://v4.webpack.js.org/guides/code-splitting/',
|
||||
},
|
||||
optimization: {
|
||||
label: 'Webpack 4 — optimization.splitChunks и runtimeChunk',
|
||||
url: 'https://v4.webpack.js.org/configuration/optimization/',
|
||||
},
|
||||
output: {
|
||||
label: 'Webpack 4 — Output',
|
||||
url: 'https://v4.webpack.js.org/concepts/output/',
|
||||
},
|
||||
stats: {
|
||||
label: 'Webpack 4 — Stats Data',
|
||||
url: 'https://v4.webpack.js.org/api/stats/',
|
||||
},
|
||||
cli: {
|
||||
label: 'Webpack 4 — CLI',
|
||||
url: 'https://v4.webpack.js.org/api/cli/',
|
||||
},
|
||||
};
|
||||
|
||||
const practiceArticle = {
|
||||
slug: 'editorial-2018-06-practice-webpack-entry',
|
||||
title: 'Webpack 4. Как добавить две страницы и не возить общий код дважды',
|
||||
categories: ['JavaScript', 'Webpack'],
|
||||
cover: '/assets/editorial/2018/webpack-entry-shared-chunks-2018.svg',
|
||||
excerpt: 'Собираем две независимые страницы в Webpack 4: отделяем точки запуска от общего кода, подключаем нужные файлы в HTML и проверяем результат без догадок.',
|
||||
readingMinutes: 10,
|
||||
contentHtml: [
|
||||
paragraph('В многостраничном сайте после добавления страницы оформления заказа появляются два entry-файла: <code>catalog.js</code> и <code>checkout.js</code>. Если обе страницы используют jQuery и один модуль с форматированием цены, production-build может дать два похожих файла. Это не ошибка Webpack: у него появились два старта выполнения, и каждый дошёл до общих импортов. Ошибка возникает, когда от сборщика ждут, что общий код исчезнет сам по себе.'),
|
||||
paragraph('Главный вопрос этой заметки: <strong>как в Webpack 4 собрать две HTML-страницы так, чтобы общий модуль и зависимости не попадали в каждый стартовый bundle?</strong> Ниже — небольшая конфигурация для многостраничного сайта. Она намеренно не использует современный <code>dependOn</code>: это не API Webpack 4.'),
|
||||
heading('Сначала отделяю две страницы от одной страницы с двумя файлами'),
|
||||
paragraph('Entry — это не список библиотек и не место, куда складывают всё «общее». Это файл, с которого браузер начинает конкретный сценарий. Если сервер отдаёт отдельные документы <code>/catalog/</code> и <code>/checkout/</code>, у них могут быть два entry. Если же один документ подключает два entry только потому, что так проще в конфигурации, сначала стоит исправить это: пользователь будет загружать лишний сценарий ещё до оптимизации.'),
|
||||
figure(
|
||||
'/assets/editorial/2018/webpack-entry-shared-chunks-2018.svg',
|
||||
'Две страницы Webpack 4: entry catalog и checkout используют runtime, vendors и common, затем каждая запускает только собственный код.',
|
||||
'Entry остаётся точкой запуска страницы. Общие части создаёт оптимизация, а не третий фиктивный entry.',
|
||||
),
|
||||
dataTable(
|
||||
['Часть сборки', 'Зачем она нужна', 'Что подключает страница каталога'],
|
||||
[
|
||||
['<code>catalog</code>', 'Запускает обработчики и код каталога', 'Да'],
|
||||
['<code>checkout</code>', 'Запускает только сценарий заказа', 'Нет'],
|
||||
['<code>vendors</code>', 'Внешние пакеты из <code>node_modules</code>', 'Да, если попали в группу'],
|
||||
['<code>common</code>', 'Наши модули, достигнутые из двух entry', 'Да, если группа их выделила'],
|
||||
['<code>runtime</code>', 'Код Webpack, который связывает модули и chunks', 'Да'],
|
||||
],
|
||||
),
|
||||
heading('Минимальный пример с двумя сценариями'),
|
||||
paragraph('В примере оба entry импортируют один модуль из <code>src/shared</code> и jQuery. Содержимое функции не важно; важен путь импорта. Пока сборщик видит два стартовых графа, он имеет право положить достижимые модули в оба начальных файла.'),
|
||||
codeBlock(String.raw`
|
||||
// src/catalog.js
|
||||
import $ from 'jquery';
|
||||
import { formatPrice } from './shared/money';
|
||||
|
||||
$('[data-price]').each(function () {
|
||||
this.textContent = formatPrice(this.dataset.price);
|
||||
});
|
||||
|
||||
// src/checkout.js
|
||||
import $ from 'jquery';
|
||||
import { formatPrice } from './shared/money';
|
||||
|
||||
$('[data-total]').text(formatPrice(window.checkoutTotal));
|
||||
|
||||
// src/shared/money.js
|
||||
export function formatPrice(value) {
|
||||
return Number(value).toFixed(2) + ' ₽';
|
||||
}
|
||||
`),
|
||||
heading('Конфигурация для Webpack 4'),
|
||||
paragraph('В Webpack 4 отдельный entry для <code>vendor.js</code> уже не является хорошей отправной точкой. Официальная документация советует оставлять entry только у начала выполнения, а разделение внешних и общих модулей поручить <code>optimization.splitChunks</code>. В конфигурации ниже <code>minSize: 0</code> нужен для учебного примера: без него крошечный <code>money.js</code> может остаться в entry. В реальном проекте этот ноль обычно слишком агрессивен — он может создать лишний запрос ради пары строк.'),
|
||||
codeBlock(String.raw`
|
||||
// webpack.config.js
|
||||
const path = require('path');
|
||||
|
||||
module.exports = {
|
||||
mode: 'production',
|
||||
entry: {
|
||||
catalog: './src/catalog.js',
|
||||
checkout: './src/checkout.js',
|
||||
},
|
||||
output: {
|
||||
path: path.resolve(__dirname, 'dist'),
|
||||
filename: '[name].[contenthash].js',
|
||||
},
|
||||
optimization: {
|
||||
runtimeChunk: 'single',
|
||||
splitChunks: {
|
||||
chunks: 'all',
|
||||
cacheGroups: {
|
||||
vendors: {
|
||||
test: /[\\/]node_modules[\\/]/,
|
||||
name: 'vendors',
|
||||
chunks: 'all',
|
||||
priority: -10,
|
||||
},
|
||||
common: {
|
||||
name: 'common',
|
||||
minChunks: 2,
|
||||
minSize: 0,
|
||||
chunks: 'all',
|
||||
priority: -20,
|
||||
reuseExistingChunk: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
`),
|
||||
paragraph('У <code>runtimeChunk: "single"</code> здесь своя работа: Webpack 4 выносит runtime в один общий файл вместо того, чтобы встраивать его в каждый entry. Это не замена <code>splitChunks</code>. Первый вариант управляет runtime, второй — группами модулей. На этой границе легко запутаться, поэтому я проверяю оба результата в каталоге <code>dist</code>.'),
|
||||
paragraph('У правил выделения тоже есть граница. <code>vendors</code> смотрит только на путь внутри <code>node_modules</code>; пакет, который нужен одному <code>checkout</code>, не обязан переезжать в общий файл. <code>common</code> смотрит на повторное достижение модуля из двух chunks. Поэтому я не называю папку <code>shared</code> гарантией оптимизации: имя папки помогает человеку, а решение принимает правило сборки по графу и его условиям.'),
|
||||
paragraph('Перед тем как менять пороги, полезно сохранить список ассетов первого build. После изменения я сравниваю не общую сумму каталога, а роли файлов: появился ли <code>vendors</code>, попал ли <code>money.js</code> в <code>common</code>, остался ли код каталога в <code>catalog</code>. Так видно, какое именно правило сработало, и не приходится угадывать по одному числу в терминале.'),
|
||||
heading('Проверка не заканчивается на появлении файлов'),
|
||||
paragraph('После сборки должны появиться файлы с именами, зависящими от хеша: <code>runtime.…js</code>, <code>vendors.…js</code>, при нашем маленьком примере <code>common.…js</code>, а также <code>catalog.…js</code> и <code>checkout.…js</code>. Хеши нельзя вшивать в шаблон HTML вручную. Шаблонизатор, плагин или серверный код должен получить актуальный список ассетов из сборки.'),
|
||||
codeBlock(String.raw`
|
||||
<!-- catalog.html: порядок — часть договора страницы -->
|
||||
<script src="/assets/runtime.8ab1.js"></script>
|
||||
<script src="/assets/vendors.34cd.js"></script>
|
||||
<script src="/assets/common.91ef.js"></script>
|
||||
<script src="/assets/catalog.a2b3.js"></script>
|
||||
`),
|
||||
paragraph('Имена в примере условные. Важен набор: страница каталога не должна подключать <code>checkout</code>, а страница заказа — <code>catalog</code>. Если общий chunk выделен, он нужен обеим. После этого открываю обе страницы с пустым кешем, смотрю Network и проверяю, что на каждой нет ошибки <code>undefined is not a function</code> от неправильного порядка скриптов.'),
|
||||
heading('Короткий порядок работы'),
|
||||
orderedList([
|
||||
'Назвать HTML-документы, которые действительно существуют, и создать по одному entry на документ.',
|
||||
'Найти импорт, который повторяется в двух entry: сначала достаточно одного модуля из <code>src/shared</code>.',
|
||||
'Включить <code>splitChunks</code> для начальных chunks и временно поставить <code>minSize: 0</code>, чтобы увидеть механизм на маленьком примере.',
|
||||
'Вывести runtime в один файл, собрать production-вариант и передать актуальный список ассетов в HTML.',
|
||||
'Открыть каждую страницу отдельно: проверить набор script-тегов, консоль и факт, что код другой страницы не загружается.',
|
||||
]),
|
||||
heading('Где этот рецепт не подходит'),
|
||||
paragraph('Не всякий общий импорт стоит выносить. Маленький модуль может добавить ещё один запрос и не дать выигрыша; крупная библиотека, которая нужна только модальному окну, не должна попадать в стартовый общий chunk только потому, что так легче настроить. Для кода, который не нужен при первом открытии страницы, в Webpack 4 есть отдельный путь — динамический <code>import()</code>. Ещё одно ограничение: если один HTML-документ намеренно запускает несколько entry, нужно особенно внимательно проверить число runtime-экземпляров и порядок загрузки.'),
|
||||
heading('Что считаю готовым'),
|
||||
paragraph('Я не считаю задачу закрытой по размеру одного файла. Готовый результат отвечает на три простых вопроса: какой entry запускает страницу, какие общие chunks она реально получает и не подключён ли соседний entry. Если эти ответы видны в конфигурации, в HTML и в Network, оптимизацию потом можно менять без лотереи.'),
|
||||
sourceList([sources.entry, sources.splitChunks, sources.optimization, sources.output]),
|
||||
].join('\n'),
|
||||
};
|
||||
|
||||
const mechanismArticle = {
|
||||
slug: 'editorial-2018-06-mechanism-webpack-entry',
|
||||
title: 'Webpack 4. Почему общий import оказывается в двух entry bundle',
|
||||
categories: ['JavaScript', 'Webpack'],
|
||||
cover: '/assets/editorial/2018/webpack-entry-graph-2018.svg',
|
||||
excerpt: 'Разбираем один вопрос: что именно Webpack строит от entry, почему массив файлов — всё ещё один старт, и где появляется дублирование до настройки splitChunks.',
|
||||
readingMinutes: 10,
|
||||
contentHtml: [
|
||||
paragraph('После добавления <code>admin.js</code> в конфигурацию и <code>site.js</code>, и <code>admin.js</code> могут содержать <code>date-format.js</code>. Руки тянутся перенести модуль в отдельную папку или добавить третий entry с названием <code>vendor</code>. Это не объясняет причину. Файл уже общий на диске; проблема возникает позже, когда Webpack строит стартовые графы.'),
|
||||
paragraph('Главный вопрос здесь один: <strong>почему один и тот же import попадает в два entry bundle до настройки общего chunk?</strong> Разобрав этот механизм, можно отличить две настоящие страницы от одного entry с подготовительными файлами и не превратить библиотеку в фальшивую точку запуска.'),
|
||||
heading('Entry не равен bundle, но задаёт его начало'),
|
||||
paragraph('Webpack начинает с entry и рекурсивно проходит <code>import</code> и <code>require</code>. Результатом становится граф зависимостей. При одном entry у графа один старт. При объекте из <code>site</code> и <code>admin</code> — два старта. Если оба пути доходят до одного модуля, сам модуль остаётся одним исходным файлом, но без дополнительного правила может оказаться в обоих начальных chunks.'),
|
||||
figure(
|
||||
'/assets/editorial/2018/webpack-entry-graph-2018.svg',
|
||||
'Граф Webpack 4: entry site и admin проходят к своим модулям и оба достигают shared/date-format и jquery; до splitChunks общие зависимости могут присутствовать в обоих стартовых chunks.',
|
||||
'Две стрелки к одному исходнику не означают две копии файла в репозитории. Они объясняют, почему сборщик должен отдельно решить судьбу общего участка графа.',
|
||||
),
|
||||
heading('Минимальный граф, который показывает проблему'),
|
||||
codeBlock(String.raw`
|
||||
// src/site.js
|
||||
import { formatDate } from './shared/date-format';
|
||||
import { mountSearch } from './site/search';
|
||||
|
||||
mountSearch(formatDate);
|
||||
|
||||
// src/admin.js
|
||||
import { formatDate } from './shared/date-format';
|
||||
import { mountReport } from './admin/report';
|
||||
|
||||
mountReport(formatDate);
|
||||
|
||||
// src/shared/date-format.js
|
||||
export function formatDate(date) {
|
||||
return date.getFullYear() + '-' + String(date.getMonth() + 1).padStart(2, '0');
|
||||
}
|
||||
`),
|
||||
paragraph('В этом примере <code>site/search</code> и <code>admin/report</code> принадлежат разным страницам. <code>shared/date-format</code> достижим из обеих. Это полезная граница: переносить <code>search</code> в общий chunk ради симметрии не нужно; он не нужен админке. А <code>date-format</code> можно рассматривать как кандидата на общий chunk, если цена дополнительного файла оправдана.'),
|
||||
dataTable(
|
||||
['Запись в <code>entry</code>', 'Сколько стартов выполнения', 'Когда использовать'],
|
||||
[
|
||||
['<code>"./src/site.js"</code>', 'Один', 'Одна страница или библиотека с одним началом'],
|
||||
['<code>["./src/polyfills.js", "./src/site.js"]</code>', 'Один', 'Нужно выполнить подготовительный файл перед главным кодом той же страницы'],
|
||||
['<code>{ site: "./src/site.js", admin: "./src/admin.js" }</code>', 'Два', 'Сервер выдаёт два независимых HTML-документа'],
|
||||
['<code>{ vendor: ["jquery"], site: "./src/site.js" }</code>', 'Два, один из них фиктивный', 'Для Webpack 4 это плохая модель; общий код выделяет <code>splitChunks</code>'],
|
||||
],
|
||||
),
|
||||
heading('Почему массив не создаёт вторую страницу'),
|
||||
paragraph('Массив в <code>entry</code> имеет другой смысл: Webpack 4 собирает указанные файлы как один multi-main entry и обходит их зависимости в одном chunk. Это подходит для полифиллов или кода подготовки, который всегда должен выполниться перед приложением. Массив не создаёт отдельную страницу и не заменяет объектную запись для многостраничного сайта.'),
|
||||
codeBlock(String.raw`
|
||||
// Один entry: polyfills и сайт попадают в один стартовый граф.
|
||||
entry: ['./src/polyfills.js', './src/site.js']
|
||||
|
||||
// Два entry: сервер обязан отдать нужный набор файлов каждой странице.
|
||||
entry: {
|
||||
site: './src/site.js',
|
||||
admin: './src/admin.js',
|
||||
}
|
||||
`),
|
||||
heading('Где Webpack 4 разделяет общий участок'),
|
||||
paragraph('До Webpack 4 встречалась привычка писать отдельный entry для библиотек и подключать CommonsChunkPlugin. В документации Webpack 4 этот путь уже помечен как нежелательный: entry должен соответствовать старту выполнения, а внешний и общий код выделяет <code>optimization.splitChunks</code>. Правило не обещает, что любой общий модуль обязательно станет отдельным файлом: на результат влияют условия группы, размер и тип chunk.'),
|
||||
codeBlock(String.raw`
|
||||
optimization: {
|
||||
splitChunks: {
|
||||
chunks: 'all',
|
||||
cacheGroups: {
|
||||
common: {
|
||||
name: 'common',
|
||||
minChunks: 2,
|
||||
minSize: 0,
|
||||
chunks: 'all',
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
`),
|
||||
paragraph('Здесь я снова ставлю <code>minSize: 0</code> только для наглядности. Условия говорят: найти модуль, использованный хотя бы в двух chunks, и вынести его в файл <code>common</code>. В production-конфигурации сначала стоит снять реальные размеры, а потом вернуть порог, который не дробит приложение на множество мелких файлов.'),
|
||||
heading('Runtime — соседняя, но другая деталь'),
|
||||
paragraph('После выделения общего модуля иногда кажется, что дублирование осталось: в каждом entry виден служебный код Webpack. У runtime отдельная роль — он знает, как загружать модули и chunks. В Webpack 4 по умолчанию runtime встроен в entry; <code>runtimeChunk: "single"</code> создаёт один общий runtime-файл. Это решение полезно для нескольких страниц, но не нужно путать его с переносом общего прикладного модуля.'),
|
||||
paragraph('Если одна HTML-страница всё же включает несколько entry, у неё есть дополнительный риск: документация Webpack 4 предупреждает, что импортированные модули инициализируются для каждого runtime отдельно. Поэтому два script-тега не являются нейтральным приёмом. Сначала стоит проверить, нельзя ли оставить один старт и сделать вторую часть модулем внутри него.'),
|
||||
heading('Порядок проверки графа'),
|
||||
orderedList([
|
||||
'Выписать HTML-документы и ответить, нужен ли каждому отдельный старт JavaScript.',
|
||||
'Проверить, не является ли список файлов в массиве одним entry, где второй файл нужен только для подготовки.',
|
||||
'Найти модуль, который достигается от двух независимых стартов, и не выносить в общий код модули, нужные одной странице.',
|
||||
'Настроить <code>splitChunks</code> на маленьком примере, затем вернуть проектный порог размера.',
|
||||
'Отдельно решить, нужен ли единый runtime, и проверить фактические script-теги каждой страницы.',
|
||||
]),
|
||||
heading('Граница объяснения'),
|
||||
paragraph('Эта модель отвечает только на вопрос о начальных chunks. Она не говорит, что нужно вынести каждый импорт, и не заменяет анализ загрузки по действию пользователя. Модуль для редкого окна или отчёта может быть лучше загрузить через динамический <code>import()</code>. Ещё важно помнить о версии: конфигурация и названия опций в тексте относятся к Webpack 4; пример из свежей документации с новыми полями entry нельзя без проверки вставлять в старый проект.'),
|
||||
heading('Итог'),
|
||||
paragraph('Один import попадает в два entry bundle не потому, что файл лежит «не в той папке». Он достижим из двух стартов. Сначала нужно назвать эти старты, затем решить судьбу общего участка графа через <code>splitChunks</code> и только потом смотреть на размер файлов. Такой порядок оставляет в конфигурации причину, а не случайную заплатку.'),
|
||||
sourceList([sources.entry, sources.splitChunks, sources.optimization]),
|
||||
].join('\n'),
|
||||
};
|
||||
|
||||
const fieldArticle = {
|
||||
slug: 'editorial-2018-06-field-webpack-entry',
|
||||
title: 'Webpack 4. Bundle вырос после нового entry: как найти причину по stats.json',
|
||||
categories: ['JavaScript', 'Webpack'],
|
||||
cover: '/assets/editorial/2018/webpack-entry-diagnosis-2018.svg',
|
||||
excerpt: 'Пошаговая диагностика Webpack 4: отделяем новые ассеты от реального дублирования, читаем stats.json и проверяем, что браузер действительно скачивает.',
|
||||
readingMinutes: 10,
|
||||
contentHtml: [
|
||||
paragraph('Симптом: после добавления <code>admin</code>-entry вырос <code>site.…js</code>, а Network обычной страницы показывает запрос к <code>admin.…js</code>. Пользователь получает код панели, которой не откроет; если править только сумму файлов в <code>dist</code>, легко оставить этот лишний запрос или сломать подключение нужного entry.'),
|
||||
paragraph('Главный вопрос статьи: <strong>как по данным Webpack 4 доказать, почему bundle вырос после добавления entry, прежде чем менять конфигурацию?</strong> Для ответа нужны три вещи: список emitted-ассетов, связь модуля с chunks и фактические script-теги в HTML. Одной цифры из файловой системы недостаточно.'),
|
||||
heading('Сначала фиксирую условия сравнения'),
|
||||
paragraph('Сравнивать development-результат с production-результатом бессмысленно: режим, минификация, source map и плагины меняют картину сильнее, чем новый entry. Я делаю два production-build на одном коммите: до изменения и после него. Для каждого сохраняю JSON статистики отдельно, например <code>stats-before.json</code> и <code>stats-after.json</code>.'),
|
||||
figure(
|
||||
'/assets/editorial/2018/webpack-entry-diagnosis-2018.svg',
|
||||
'Диагностика роста Webpack bundle: фиксируем одинаковый build, смотрим assets, связываем модули с chunks, проверяем HTML и только затем меняем splitChunks.',
|
||||
'Статистика сборщика показывает состав компиляции. Network в браузере отвечает на отдельный вопрос: что реально скачала конкретная страница.',
|
||||
),
|
||||
heading('Создаю stats.json из той же команды сборки'),
|
||||
paragraph('Webpack умеет отдать статистику компиляции в JSON. В ней есть ассеты, chunks, модули и их связи. Команду лучше запускать локальным webpack-cli из проекта: тогда версия сборщика совпадает с той, для которой написан <code>webpack.config.js</code>.'),
|
||||
codeBlock(String.raw`
|
||||
# В package.json уже есть webpack и webpack-cli.
|
||||
./node_modules/.bin/webpack --mode production --profile --json > stats-after.json
|
||||
|
||||
# Для второго снимка возвращаем только конфигурацию entry
|
||||
# и повторяем ту же команду:
|
||||
./node_modules/.bin/webpack --mode production --profile --json > stats-before.json
|
||||
`),
|
||||
paragraph('Параметр <code>--profile</code> добавляет сведения о времени по модулям. Для вопроса о размере он не обязателен, но снимок пригодится, если рост размера сопровождается долгой сборкой. Главное — не смешивать JSON со случайными <code>console.log</code> из конфигурации: файл должен остаться валидным JSON.'),
|
||||
heading('Читаю сначала ассеты, а не весь граф'),
|
||||
paragraph('Первый разрез простой: сортирую emitted-ассеты по <code>size</code>. Это показывает, какие выходные файлы появились и какие из них стали больше. Но размер в stats — размер ассета в сборке, а не обязательно число байтов, переданных по сети после gzip или кеширования. Поэтому это место для гипотезы, а не для вывода о скорости страницы.'),
|
||||
codeBlock(String.raw`
|
||||
// tools/print-webpack-stats.js
|
||||
const fs = require('fs');
|
||||
|
||||
const stats = JSON.parse(fs.readFileSync(process.argv[2], 'utf8'));
|
||||
const assets = (stats.assets || [])
|
||||
.map((asset) => ({
|
||||
name: asset.name,
|
||||
size: asset.size,
|
||||
chunks: asset.chunks || [],
|
||||
}))
|
||||
.sort((left, right) => right.size - left.size);
|
||||
|
||||
for (const asset of assets) {
|
||||
console.log(asset.size + '\\t' + asset.name + '\\tchunks=' + asset.chunks.join(','));
|
||||
}
|
||||
|
||||
const repeated = (stats.modules || [])
|
||||
.filter((module) => Array.isArray(module.chunks) && module.chunks.length > 1)
|
||||
.map((module) => ({
|
||||
name: module.name,
|
||||
chunks: module.chunks,
|
||||
size: module.size,
|
||||
}))
|
||||
.sort((left, right) => right.size - left.size);
|
||||
|
||||
console.log('\\nModules present in more than one chunk:');
|
||||
for (const module of repeated.slice(0, 30)) {
|
||||
console.log(module.size + '\\t' + module.name + '\\tchunks=' + module.chunks.join(','));
|
||||
}
|
||||
`),
|
||||
paragraph('Запуск <code>node tools/print-webpack-stats.js stats-after.json</code> не должен автоматически объявлять все строки из второго списка проблемой. Общий модуль может быть правильно связан с несколькими chunks в описании компиляции, а часть chunks может быть асинхронной. Список нужен, чтобы назвать конкретный модуль, после чего его надо сопоставить с entrypoint и HTML.'),
|
||||
dataTable(
|
||||
['Наблюдение', 'Что это может означать', 'Следующее действие'],
|
||||
[
|
||||
['Появился новый <code>admin.…js</code>, старый <code>site.…js</code> почти не изменился', 'В <code>dist</code> лежит ещё одна страница, но старая не стала тяжелее', 'Проверить, что старый HTML не подключает <code>admin</code>'],
|
||||
['Один пакет из <code>node_modules</code> виден у двух initial chunks', 'Внешняя зависимость достигнута из двух entry и не вынесена', 'Проверить <code>splitChunks</code> и условия cache group'],
|
||||
['Оба entry подключены в одном HTML', 'Шаблон страницы получает чужой сценарий', 'Исправить генерацию script-тегов до настройки оптимизации'],
|
||||
['Рост только в development', 'Сравнение сделано в разных режимах или с source map', 'Повторить замер одинаковой production-командой'],
|
||||
['Файл большой в stats, но не запрашивается на странице', 'Ассет существует, но не входит в нужный entrypoint', 'Смотреть Network для конкретного URL, а не сумму каталога'],
|
||||
],
|
||||
),
|
||||
heading('Проверяю entrypoint и сетевой след'),
|
||||
paragraph('В stats есть сведения о chunks и entrypoints. Если новый <code>admin</code> должен жить только на <code>/admin/</code>, я открываю обычную страницу и смотрю список скриптов в HTML и вкладку Network. На ней должны быть только runtime, общие chunks, нужные именно этой странице, и её entry. Если там уже есть <code>admin</code>, проблема находится в шаблоне или плагине, а не в размере модуля.'),
|
||||
paragraph('Затем повторяю проверку для <code>/admin/</code>. Только когда один и тот же большой модуль действительно участвует в двух начальных путях, есть смысл добавлять cache group. В Webpack 4 оптимизация общих chunks по умолчанию ориентирована на динамические imports; для начальных chunks нужно явно выбрать подходящую конфигурацию. Это объясняет, почему «поставил второй entry» и «получил отдельный общий файл» не равны друг другу.'),
|
||||
heading('Небольшая правка после доказательства'),
|
||||
paragraph('Когда stats показал повторяющийся пакет, а обе страницы действительно его загружают, я добавляю минимальную группу, а не копирую чужой длинный конфиг. Сначала отделяю пакеты из <code>node_modules</code>. Общий код приложения стоит выносить отдельным правилом только после того, как видно повтор из двух entry и он достаточно велик для отдельного запроса.'),
|
||||
codeBlock(String.raw`
|
||||
optimization: {
|
||||
splitChunks: {
|
||||
chunks: 'all',
|
||||
cacheGroups: {
|
||||
vendors: {
|
||||
test: /[\\/]node_modules[\\/]/,
|
||||
name: 'vendors',
|
||||
chunks: 'all',
|
||||
},
|
||||
},
|
||||
},
|
||||
runtimeChunk: 'single',
|
||||
}
|
||||
`),
|
||||
paragraph('После изменения я создаю третий <code>stats-fixed.json</code> и повторяю те же три проверки. Ожидаемый результат формулирую не как «стало мало килобайт», а как наблюдаемый контракт: обычная страница не загружает admin-entry; общий пакет появился в предназначенном для него chunk; обе страницы получают все необходимые файлы без ошибки выполнения.'),
|
||||
heading('Последовательность расследования'),
|
||||
orderedList([
|
||||
'Сохранить два stats-снимка из одинаковой production-команды и назвать версии webpack и webpack-cli.',
|
||||
'Сравнить ассеты: какой файл вырос, какой появился, связан ли он с новым entry.',
|
||||
'Найти крупные модули, отмеченные в нескольких chunks, и не путать этот сигнал с доказательством сетевой загрузки.',
|
||||
'Открыть каждый HTML-маршрут с пустым кешем и проверить реальные script-теги и Network.',
|
||||
'Только после подтверждения дублирования настроить одну cache group, пересобрать и повторить тот же снимок.',
|
||||
]),
|
||||
heading('Ограничения метода'),
|
||||
paragraph('Stats JSON отражает конкретную версию Webpack 4 и состав компиляции. Названия полей и формат данных могут меняться после обновления сборщика, поэтому диагностический скрипт не стоит превращать в вечный CI-контракт без фиксации версии. Метод также не измеряет время первой отрисовки и не учитывает серверное сжатие; для этого нужен отдельный сетевой замер. Но он надёжно отделяет «в каталоге стало больше файлов» от конкретного вопроса «какой модуль попал в какой chunk и почему».'),
|
||||
heading('Итог'),
|
||||
paragraph('Новый entry сам по себе увеличивает число ассетов — это ожидаемо. Дублирование начинается не от количества файлов, а от повторно достижимого модуля и от того, какие chunks подключает HTML. <code>stats.json</code> даёт материал для первой части проверки, браузер — для второй. После такой пары доказательств настройка <code>splitChunks</code> становится короткой и объяснимой.'),
|
||||
sourceList([sources.stats, sources.cli, sources.entry, sources.optimization]),
|
||||
].join('\n'),
|
||||
};
|
||||
|
||||
export const revisions = [practiceArticle, mechanismArticle, fieldArticle];
|
||||
|
||||
const isDirectExecution = Boolean(process.argv[1])
|
||||
&& path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
|
||||
|
||||
if (isDirectExecution) {
|
||||
if (process.argv.includes('--print-revisions')) {
|
||||
process.stdout.write(JSON.stringify(revisions, null, 2) + '\n');
|
||||
} else {
|
||||
process.stderr.write('Usage: node web/scripts/upgrade-2018-06.mjs --print-revisions\n');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user