8 lines
15 KiB
JSON
8 lines
15 KiB
JSON
{
|
||
"index": 345,
|
||
"slug": "editorial-2018-06-practice-webpack-entry",
|
||
"title": "Webpack 4: две страницы, общие chunks и проверяемая сборка",
|
||
"excerpt": "Две HTML-страницы могут тянуть один и тот же код дважды. Разбираем границу entry, настройку splitChunks и проверку итоговых ассетов в Webpack 4.",
|
||
"contentHtml": "<p>После добавления страницы заказа production-сборка стала тяжелее. В каталоге появились <code>catalog.js</code> и <code>checkout.js</code>, а jQuery и модуль форматирования цены повторяются в обоих файлах. Пользователь каталога загружает код заказа, хотя не открывает заказ. Ошибка увеличивает первый запрос, усложняет кеширование и маскирует реальную границу страниц.</p>\n<p>Причина проста: два entry запускают два графа зависимостей. Webpack видит общий импорт из каждого графа, но не обязан сам превратить его в отдельный файл. Нужны два решения: entry должны описывать реальные точки запуска, а <code>optimization.splitChunks</code> — выделять код, который достигается из нескольких chunks. <strong>Общий модуль не становится общим только из-за имени папки <code>shared</code>.</strong></p>\n<h2>Что именно считает Webpack</h2>\n<p>Entry — это начало выполнения конкретного сценария. В многостраничном сайте у документа каталога есть свой entry, у документа заказа — свой. Каждый entry строит граф импортов. Если оба графа доходят до <code>src/shared/money.js</code>, модуль присутствует в обоих графах. Дальше оптимизатор сравнивает размер, число использований и правила cache group.</p>\n<p>Эти уровни нельзя смешивать. Entry отвечает на вопрос «какой сценарий стартует». <code>splitChunks</code> отвечает на вопрос «какие модули вынести в отдельный chunk». <code>runtimeChunk</code> отвечает за служебный runtime Webpack, который связывает модули и chunks. Третий entry с именем <code>common</code> не заменяет оптимизацию: он сам становится ещё одной точкой запуска.</p>\n<figure><img src=\"/assets/editorial/2018/webpack-entry-shared-chunks-2018.svg\" alt=\"Две страницы Webpack 4 используют общий runtime, vendors и common, но запускают разные entry\" /><figcaption>Схема показывает роли файлов. <code>catalog</code> и <code>checkout</code> запускают свои сценарии, а общие chunks подключаются к обеим страницам.</figcaption></figure>\n<h2>Минимальный граф зависимостей</h2>\n<p>Ниже учебный пример. Он показывает механизм, но не обещает конкретные размеры файлов и не заменяет сборку проекта. Оба сценария используют jQuery и одну функцию. Код страницы остаётся раздельным.</p>\n<pre><code>// src/catalog.js\nimport $ from 'jquery';\nimport { formatPrice } from './shared/money';\n\n$('[data-price]').each(function () {\n this.textContent = formatPrice(this.dataset.price);\n});\n\n// src/checkout.js\nimport $ from 'jquery';\nimport { formatPrice } from './shared/money';\n\n$('[data-total]').text(formatPrice(window.checkoutTotal));\n\n// src/shared/money.js\nexport function formatPrice(value) {\n return Number(value).toFixed(2) + ' ₽';\n}</code></pre>\n<p>Если собрать такой граф только с двумя entry, сборщик создаст стартовые chunks для каталога и заказа. Общий код может остаться внутри них. Это допустимый результат с точки зрения корректности: оба сценария получают нужный модуль. Но он может быть невыгоден для первой загрузки. Поэтому решение нужно принимать по списку ассетов и сетевому запросу, а не по названию исходной директории.</p>\n<h2>Конфигурация Webpack 4</h2>\n<p>В Webpack 4 оставьте в <code>entry</code> только реальные точки запуска. Для общих библиотек используйте <code>splitChunks</code>. В учебном примере <code>minSize: 0</code> помогает увидеть даже маленький модуль. В рабочей сборке это условие может создать отдельный запрос ради нескольких строк. Порог нужно проверить на размере и времени загрузки конкретного проекта.</p>\n<pre><code>// webpack.config.js\nconst path = require('path');\n\nmodule.exports = {\n mode: 'production',\n entry: {\n catalog: './src/catalog.js',\n checkout: './src/checkout.js',\n },\n output: {\n path: path.resolve(__dirname, 'dist'),\n filename: '[name].[contenthash].js',\n },\n optimization: {\n runtimeChunk: 'single',\n splitChunks: {\n chunks: 'all',\n cacheGroups: {\n vendors: {\n test: /[\\/]node_modules[\\/]/,\n name: 'vendors',\n chunks: 'all',\n priority: -10,\n },\n common: {\n name: 'common',\n minChunks: 2,\n minSize: 0,\n chunks: 'all',\n priority: -20,\n reuseExistingChunk: true,\n },\n },\n },\n },\n};</code></pre>\n<p>Группа <code>vendors</code> отбирает модули из <code>node_modules</code>. Группа <code>common</code> ищет код, который достигается как минимум из двух chunks. Приоритеты разрешают пересечение правил. <code>reuseExistingChunk: true</code> позволяет повторно использовать уже созданный chunk, когда это возможно.</p>\n<p><code>runtimeChunk: 'single'</code> выносит runtime в отдельный общий файл. Он не выносит jQuery и не делает общий chunk из <code>money.js</code>. За это отвечает <code>splitChunks</code>. Если убрать runtime из проверки, можно ошибочно решить, что два похожих служебных фрагмента — это дублирование прикладного кода.</p>\n<h2>Как страница подключает результат</h2>\n<p>Имена с <code>[contenthash]</code> меняются после содержательных изменений. HTML не должен навсегда содержать строку из примера. Шаблонизатор, плагин или серверная сборка должны получить актуальную карту ассетов. Важно также сохранить порядок: runtime, общие chunks, затем код конкретной страницы.</p>\n<pre><code><!-- catalog.html: имена условные, порядок показан явно -->\n<script src=\"/assets/runtime.8ab1.js\"></script>\n<script src=\"/assets/vendors.34cd.js\"></script>\n<script src=\"/assets/common.91ef.js\"></script>\n<script src=\"/assets/catalog.a2b3.js\"></script></code></pre>\n<p>Страница каталога не должна подключать <code>checkout</code>. Страница заказа не должна подключать <code>catalog</code>. Если общий chunk не появился, это не всегда ошибка: модуль мог быть слишком мал, использоваться только одним entry или не пройти условия cache group. Проверяйте фактическую карту сборки.</p>\n<h2>Симптом → причина → проверка → действие</h2>\n<div class=\"table-scroll\"><table><thead><tr><th scope=\"col\">Симптом</th><th scope=\"col\">Причина</th><th scope=\"col\">Проверка</th><th scope=\"col\">Действие</th></tr></thead><tbody><tr><td>Оба entry заметно повторяют jQuery</td><td>Общий модуль не попал в подходящую cache group</td><td>Посмотреть stats и список модулей в chunks</td><td>Проверить <code>chunks</code>, <code>minChunks</code>, <code>minSize</code> и путь модуля</td></tr><tr><td>На странице заказа запускается каталог</td><td>HTML подключает соседний entry</td><td>Открыть вкладку Network и список script-тегов</td><td>Оставить в шаблоне только runtime, нужные общие chunks и <code>checkout</code></td></tr><tr><td>После сборки не найден общий файл</td><td>Код мал или нужен только одному entry</td><td>Проверить число достижений модуля в stats</td><td>Не создавать chunk ради имени; сравнить стоимость запроса и дублирования</td></tr><tr><td>В консоли ошибка при старте</td><td>Неверный порядок или устаревший hash в HTML</td><td>Очистить кеш, проверить ответы Network и порядок загрузки</td><td>Формировать HTML из актуальной карты ассетов</td></tr><tr><td>Общий chunk стал слишком большим</td><td>В него попал код, который нужен только редкому сценарию</td><td>Сопоставить состав chunk с первым экраном</td><td>Ослабить cache group или перенести поздний код в динамический <code>import()</code></td></tr></tbody></table></div>\n<h2>Порядок действий</h2>\n<ol><li>Перечислите HTML-документы и подтвердите, что каждый документ запускает отдельный сценарий.</li><li>Создайте по одному entry на документ. Не добавляйте в <code>entry</code> библиотеки только ради их общего имени.</li><li>Найдите повторяющийся импорт и соберите минимальный учебный граф с одним общим модулем.</li><li>Настройте <code>splitChunks</code> и отдельно решите, нужен ли единый <code>runtimeChunk</code>.</li><li>Сделайте production-сборку и сохраните stats или карту ассетов до изменения и после него.</li><li>Проверьте состав каждого HTML-документа. Сначала runtime и общие chunks, затем собственный entry.</li><li>Откройте каждую страницу с очищенным кешем. Проверьте консоль, Network и отсутствие кода соседнего сценария.</li><li>Повторите сборку после изменения одного общего модуля и убедитесь, что изменились только ожидаемые hash и chunks.</li></ol>\n<h2>Отрицательный путь и ограничения</h2>\n<p>Не всякий общий импорт нужно выносить. Маленький модуль может дешевле повторить, чем загружать новый файл. Большая библиотека может использоваться двумя entry, но быть нужна только после открытия модального окна. В таком случае общий стартовый chunk ухудшит первую загрузку. Для позднего кода рассмотрите динамический <code>import()</code> и проверяйте его отдельным сценарием.</p>\n<p>Эта схема относится к Webpack 4. В ней нет современного API <code>dependOn</code>, и нельзя переносить настройки из другой версии без сверки документации. Сборка также не исправит ошибку, если один HTML намеренно подключает два entry: нужно отдельно проверить количество runtime и порядок запуска.</p>\n<p>Учебный пример не доказывает выигрыш в production. На него влияют размер модулей, HTTP-протокол, кеш, HTML, который формирует сервер, и порядок загрузки. Сравнивайте измеренные ассеты и запросы своего проекта.</p>\n<h2>Проверяемый критерий готовности</h2>\n<p>Считайте работу готовой, когда для каждой страницы можно показать четыре доказательства: entry, который запускает сценарий; список общих chunks; отсутствие entry соседней страницы; успешный запуск с очищенным кешем. Эти сведения должны совпадать в конфигурации, сгенерированном HTML, stats и вкладке Network. Размер одного стартового файла сам по себе не является критерием.</p>\n<h2>Проверяемые источники</h2><ul><li><a href=\"https://v4.webpack.js.org/concepts/entry-points/\" target=\"_blank\" rel=\"noopener noreferrer\">Webpack 4: Entry Points</a></li><li><a href=\"https://v4.webpack.js.org/guides/code-splitting/\" target=\"_blank\" rel=\"noopener noreferrer\">Webpack 4: Code Splitting</a></li><li><a href=\"https://v4.webpack.js.org/configuration/optimization/\" target=\"_blank\" rel=\"noopener noreferrer\">Webpack 4: optimization</a></li></ul>"
|
||
}
|