Крупный гайд по Webpack

webpack — это сборщик модулей JavaScript с открытым исходным кодом. Он создан в первую очередь для JavaScript, но может преобразовывать внешние ресурсы, такие как HTML, CSS и изображения, если включены соответствующие загрузчики. webpack принимает модули с зависимостями и генерирует статические ресурсы, представляющие эти модули.

Крупный гайд по Webpack

В данной статье полностью разберем как работать с данной утилитой для сборки всего на свете в один файл: пройдемся по концептам и закончим реальным использованием с TypeScript и модулями из node_modules, чтобы заставить код работать в браузере😊

Какую проблему решает Webpack

Webpack решает проблему вечного подключения библиотек и фреймворков к HTML. В каком порядке их подключать, как решать конфликты, как сразу же оптимизировать картинки, как включать css в JavaScript, подобно тому, как это делается в React? Всеми этими вопросами занимается Webpack👍

Концепты

Webpack — статический сборщик модулей. Это значит что после компиляции у вас будет один файлик в котором все зависимости будут в правильном порядке, а все ассеты и стили будут зашиты в один файл.У самого WebPack есть несколько основных терминов:

  • Конфигурационный файл (Configuration)
  • Входная точка (Entry)
  • Точка выхода (Output)
  • Загрузчики (Loaders)
  • Плагины (Plugins)
  • Режимы (Modes)

Конфигурационный файл

Сам конфигурационный файл всегда один — webpack.config.js. В нем мы указываем экспортируемый объект, которй и является конфигурацией для Webpack:

module.exports = { // Конфигурация };

Тут нет ничего сложного, в следующих пунктах мы разберем основные свойства для данного объекта, чтобы заставить Webpack работать🔫

Бывают случаи, когда вам требуется несколько конфигурационных файлов (для продакшена и разработки), тогда нам следует создать следующие файлы:

  • webpack.dev.js
  • webpack.prod.js

Названия файлов могут быть любые, это не стандарт, однако, внегласно принято называть их именно так😊

В package.json нужно просто указать команды для сборки, чтобы Webpack знал какую конфигурацию ему таскать:

{ ../ "scripts": { "build:dev": "webpack --config webpack.dev.js", "build:prod": "webpack --config webpack.prod.js" } ../ }

А куда делся webpack.config.js?!

Мы его в данном случае просто не используем Если мы просто запустим Webpack безо всяких флагов, то он будет искать именно webpack.config.js, однако если мы дали точное имя конфигурации, то он не будет ничего искать и просто сжует👄 данную конфигурацию и будет работать по ней👀

Входная точка

Сам Webpack строит граф (дерево, в случае если входной файл - один) зависимостей. Он проходится по всем импортам внутри ваших файлов, строит граф, а затем начинает импортировать все прямо в финальный файл.

Входная точка 🔴 - файл, с которого Webpack начнет построение этого графа. Это файл, который нам нужно включить первым и от которого будут идти все импорты.

Обычно данный файл является ./src/index.js, однако данную входную точку конечно же можно поменять:

module.exports = { entry: './path/to/index.js', };

Таким образом мы поменяем входной файл и Webpack будет работать начиная с файла, который мы указали в пути.

Точка выхода

Не секрет, что Webpack отдает нам один файл (спойлер: можно настроить так, чтобы отдавал несколько, но не суть сейчас). Именно этим "одним файлом" и является точка выхода 📤

По умолчанию этот файл находится в ./dist/main.js, однако вы можете также поменять это с помощью конфигурации:

// Импортируем модуль для работ с путями const path = require('path'); module.exports = { // Указываем входную точку entry: './path/to/index.js', // Указываем точку выхода output: { // Тут мы указываем полный путь к директории, где будет храниться конечный файл path: path.resolve(__dirname, 'dist'), // Указываем имя этого файла filename: 'my-first-webpack.bundle.js', }, };

Хороший вопрос: почему бы нам просто не указать имя точки выхода и на этом все. Зачем этот path, зачем разделять имя файла от пути?

Не менее хороший ответ😁: Webpack может упаковывать все что угодно, даже картинки формата png. Сами картинки он как не странно в код не запихнет, однако он перенесет их. Для переноса ему нужен точный путь к директории, где будет точка выхода, чтобы построить правильный путь. Имя файла и путь мы разделяем именно поэтому.

Лоадеры

Из коробки Webpack понимает только JavaScript и JSON, что будет если мы попытаемся запихнуть внутрь cjs, ejs, html, typescript? Ничего, Webpack отдаст нам ошибку. Для таких вещей придумали лоадеры😊

Лоадеры 📂 — специальные модули, которые созданы для того, чтобы считывать и обрабатывать файлы. Для лоадеров в свою очередь придумали правила.

Правила 📏 — это в свою очередь просто объекты в конфигурации, которые указывают на файлы, которые нужно обработать и указывают лоадер

Все лоадеры указываются в конфигурации следующим образом:

const path = require('path'); module.exports = { entry: './path/to/index.js', output: { path: path.resolve(__dirname, 'dist'), filename: 'my-first-webpack.bundle.js', }, // Указываем тут, что будем использовать спец. модуль для определенных файлов (лоадер) module: { // Указываем правила для данных модулей rules: [ // Указываем правило для каждого лоадера {test: /.txt$/, use: 'raw-loader'}, ], }, };

Как можно увидеть есть свойство module, которое содержит в себе правила, массив rules в свою очередь уже содержит сами правила: в него включаются правила в виде объекта, где test - путь в виде Regex, use - сам лоадер, который должен быть использован.

Стоит уточнить, что перед тем как использовать лоадеры — их надо найти и скачать с помощью NPM😳

npm i -D raw-loader

Существует очень много лоадеров, от CSS и Sass до png и svg, рассмотрим их чуть позже в практическом примере😊

Плагины

Если лоадеры используются для того чтобы просто обработать файл, который мы импортируем в js, то плагины используются для того чтобы управлять не только импортами в JS.

Плагины🔌 — внешние модули для Webpack, которые позволяют управлять и обрабатывать файлы, которые не импортируются в JavaScript

Рассмотрим легенький пример, где мы будем импортировать index.html, это позволит не держать его в директории dist (то есть саму директорию можно будет удалять и ничего не потеряем), а также вставлять внутрь переменные окружения и пути к файлам.

Вот как это будет выглядеть в конфигурации (для упрощения часть кода с входной и выходной точкой - пропущена):

// Для того чтобы достучаться до плагина const HtmlWebpackPlugin = require('html-webpack-plugin'); // У самого Webpack уже есть встроенные плагины, их неплохо тоже импортировать const webpack = require('webpack'); module.exports = { module: { rules: [{ test: /\.txt$/, use: 'raw-loader' }], }, // Указываем новые плагины для обработки файлов plugins: [ // Указываем что будем обрабатывать HTML с помощью плагина new HtmlWebpackPlugin({ template: './src/index.html' }) ], };

Как мы можем увидеть тут мы используем плагин html-webpack-plugin для того чтобы обрабывать HTML. В данном случае он просто перенесет файл из ./src в ./dist 😊

Режим

У Webpack есть три режима (на самом деле два, но один из трёх является отсутсвием любого режима). По умолчанию без конфигурации у Webpack нет режима — none. Если мы напишем конфигурацию, то Webpack настоятельно посоветует (с вот таким лицом: 😠) добавить режим.

Делается это проще простого:

module.exports = { mode: 'production', };

Всего у Webpack (по-настоящему) существуют два режима:

  • Режим для разработки — development. Данный режим как не сложно догадаться рассчитан на разработку, он не сильно сжимает бандл (точку выхода, файл который получится в конце), а также может привязывать карты исходников и кучу других мелких фич
  • Режим продакшена — production. Режим, который будет сжимать ваш бандл, покуда сможет и делать совершенно нечитабельный (такой, что и обфускатор не потребуется), но оптимизированный код.

Продвинутое использование

Замечательно, вот мы и прорвались с основ к более продвинутому использованию🤩 Покрыть одной статьей асболютно все кейсы использования просто напросто невозможно, тут дело ограничивается только вашим воображением и условиями, при которых вам потребуется Webpack, однако пройтись по основным все же стоит😊

Входная точка

И по новой..♻ Снова пройдемся по всем концептам, только уже с более продвинутыми конфигурациями😳 Допустим, что у вас есть несколько файлов, которые сами по себе являются входными точками. Часто для сайтов например указывают следующие файлы:

  • main.js - главный файл, который выполняет функционал сайта
  • vendor.js - файл, который работает непосредственно с модулями

Что будем делать в таком случае? Не придётся же мерджить два файла?!😱

Две и более входные точки

Мы можем разделять файлы на две и более входных точки. Мы можем компилировать их в отдельные точки выхода, а также в один.С помощью данной конфигурации можно указать входные файлы:

module.exports = { entry: { main: './src/main.js', vendor: './src/vendor.js', }, };

Мы также можем передать в entry не объект с названиями файлов, а массив:

module.exports = { entry: ['./src/main.js', './src/vendor.js'], };

Это хорошо, но как нам понять куда будут собираться данные исходники?🤔

В webpack.config.js указываем следующее:

const path = require('path'); module.exports = { output: { path: path.resolve(__dirname, 'dist'), // Тут мы указываем, что будем компилировать каждый файл с таким же именем, но с постфиксом bundle.js filename: '[name].bundle.js', }, };

[name] — в данном случае имя входного файла Данная строка является шаблонной (не в привычном понимании JavaScript), ибо внутри квадратных скобок мы указываем шаблон наименования. Все шаблоны можно посмотреть вот тут

Одним из полезных свойств является [contenthash]. Он используется в основном для продакшена, чтобы каждый раз при компиляции создавался новый хэш (хэш создается по контенту в файле, так что если файл изменился, то хэш будет новый💫). Данное свойство активно используется для обхода кэширования файлов в браузерах. Дело в том, что браузер не будет грузить все ваши файлы с одним и тем же названием каждый раз, как вы переходите на сайт, ибо это очень долго и влияет на время загрузки, даже если файлы поменялись. Проблема решается "в лоб", если так можно сказать😅 — мы просто меняем название файла, если он сам изменился.

А вдруг хочется один выходной файл, что делать тогда?!

Ответ на этот вопрос очень легкий😅 Просто включите файлы, которые вам нужны в одном бандле в main.js. Тот же vendors.js можно включить внутрь main.js и все😳

// File: main.js // Импортируем работу с вендорами import './vendors.js'

Лоадеры

Точки выхода мы пропустим, так как я думаю мы уже обсудили их выше достаточно😊

Лоадеры в свою очередь могут таскать больше чем просто файлики .txt, естественно. Они используются для той же компиляции TypeScript:

module.exports = { module: { rules: [ { test: /\.ts$/, use: 'ts-loader' }, ], }, };

Важно: Помните, что перед тем как использовать лоадер - его нужно установить🚧

npm i -D ts-loader

А что если с файлом определенного типа нужно провести не одну операцию?🤔

Процессинг файлов с помощью лоадеров

Допустим у нас есть задачка: мы хотим процессить CSS-файлы с помощью SASS, а также включать их в JavaScript. Реализация у данной идеи будет следующая:

module.exports = { module: { rules: [ // Правило для CSS { test: /\.css$/, use: [ {loader: 'style-loader'}, {loader: 'css-loader'}, {loader: 'sass-loader'} ] } ] } }

Как мы видим, мы можем указывать несколько лоадеров для одного типа файла, помещая все лоадеры в массив. Все лоадеры будут выполняться последовательно.

Для плагинов последовательности и сам процессинг работает точно также😊

Цели (Target)

То, о чем мы ещё не поговорили - цели😳

JavaScript ныне работает как на клиентской стороне, так и на стороне сервера.

Мы можем создавать множественные цели, чтобы решать такие проблемы:

const path = require('path'); const serverConfig = { target: 'node', output: { path: path.resolve(__dirname, 'dist'), filename: 'main.node.js', }, //… }; const clientConfig = { target: 'web', output: { path: path.resolve(__dirname, 'dist'), filename: 'main.js', }, //… }; module.exports = [serverConfig, clientConfig];

Однако опыт показывает что так делать не всегда удобно, а иногда и вовсе нежелательно🤔 Держать все конфигурации в одном файле - нехорошая идея, лучше создать две директории и два независимых webpack.config.js.

А что же на счёт target? Данное свойство указывает цель для чего мы создаем бандл, это может быть:

  • async-node - бандл для асинхронной ноды, который будет тянуть модули с промисами
  • node - бандл для синхронной ноды
  • electron-renderer - бандл для рендер-процесса в Electron
  • electron-preload - бандл для прелоудера в Electron
  • web - бандл для браузера. Данный таргет является дефолтным.
  • webworker - бандл для воркера.В зависимости от выбора бандла Webpack будет по-разному импортировать модули и обращаться с ними.

Кэширование

Мы можем кэшировать неизменные части нашего приложения, для того чтобы Webpack быстрее собирал наше приложение😌

По умолчанию файлы кэшируются в памяти в mode: development и не кэшируются вовсе, если mode: production🤔

Для того чтобы Webpack кэшировал все не в оперативной памяти, а в постоянной — достаточно просто указать следующее свойство:

module.exports = { cache: { type: 'filesystem', // По умолчанию 'memory' }, };

Webpack будет кэшировать билд в node_modules/.cache/webpack и автоматом пропускать то, что не изменилось!😱

Директорию для кэша можно изменить с помощью cacheDirectory:

const path = require('path'); module.exports = { cache: { type: 'filesystem', // По умолчанию 'memory' // Устанавливаем диреторию для кэша cacheDirectory: path.resolve(__dirname, '.temporary_cache') }, };

Для того чтобы отключить кэш вовсе достаточно просто добавить cache: false, интересным замечанием будет то, что cache: true — то же самое, что и cache: {type: 'memory'} 😉

Watch или как Webpack подсматривает 👁

Webpack может компилировать изменения каждый раз, как только мы перекомпилируем файлы. Это очень полезно при разработке. Для того чтобы Webpack "подсматривал" за нашими файлами достаточно просто указать:

module.exports = { //... watch: true, };

Если после того, как вы изменили файл сборка долго собирается и вообще вся ваша система на время повисла😡 это может быть следствием того, что Webpack начал подсматривать не за теми файлами. Это можно легко исправить, просто исключите огромные директории, которые не изменяются во время разработки:

module.exports = { //... watch: true, // Настройки для watch watchOptions: { // Директории, которые watch будет игнорировать ignored: [/node_modules/] } };

Если случилось так, что вы работаете с pnpm, который берёт модули из ссылок (то есть он не создает все время node_modules, а скачивает модули только один раз), то вам понадобится следующее свойство:

module.exports = { //... watch: true, // Настройки для watch watchOptions: { // Разрешать Webpack следить за символьными ссылками followSymlinks: true } };

DevServer 🛎

Webpack ещё и умеет запускать свой http-сервер, для того чтобы у вас была live-reload (перезагрузка при рекомпиляции)🌈

Для того чтобы заставить🔫 Webpack использовать devServer, достаточно просто указать следующие свойства в конфигурации:

const path = require('path'); module.exports = { // Конфигурация для нашего сервера devServer: { // Здесь указывается вся статика, которая будет на нашем сервере static: { directory: path.join(__dirname, 'public'), }, // Сжимать ли бандл при компиляции📦 compress: true, // Порт на котором будет наш сервер port: 9000, }, };

Мы также можем настроить Webpack так, чтобы он показывал нам ошибки, если вдруг что-то пошло не так, прогресс-бар при компиляции:

module.exports = { //... devServer: { // ... client: { // Показывает ошибки при компиляции в самом браузере overlay: { // Ошибки errors: true, // Предупреждения warnings: false, }, // Показывает прогесс компиляции progress: true }, }, };

У самого сервера есть безграничные возможности: от поддержки Hot Module Replacement до поддержки WS-сервера и HTTPS соединения, все их можно посмотреть здесь😊

Пример на TypeScript

Как и обещал разберем пример😊

Постановка задачи: Написать конфигурацию для TypeScript с подтягиванием файлов [.ts, .tsx, .js], картами для исходников.

Задачу поставили, выполняем😇

// Подтягиваем модуль для удобной работы с путями const path = require('path'); module.exports = { // Точка входа entry: './src/index.ts', mode: 'development' // Говорим, что нам нужна карта исходников🗺️ devtool: 'inline-source-map', module: { rules: [ // Компилируем TypeScript { test: /\.tsx?$/, use: 'ts-loader', exclude: /node_modules/, }, ], }, // Говорим что если не указано расширение файла, то пробуем эти варианты // @see https://webpack.js.org/configuration/resolve/#resolveextensions resolve: { extensions: ['.tsx', '.ts', '.js'], }, // Точка выхода output: { filename: 'bundle.js', path: path.resolve(__dirname, 'dist'), }, };

На этом можем завершать статью🥳 Мы научились пользоваться Webpack'ом, а также рассмотрели некоторые аспекты, которые позволят нам использовать его ещё эффективнее. Если вам понравилась статья, то вы можете перейти в мой канал, там ещё много всего интересного😉 Ещё увидимся!

4141
22 комментария

Очень крутая статья, тоже немного удивлён, что тут не увидел, жаль, конечно, что сейчас не 2018-й, тогда была целая эпопея каждый раз с этой сборкой на каждом проекте, с настройкой сборки всего зоопарка, скопившегося на нем. 💪🏻🙂

3
Ответить

Странно на виси видеть тех детали про разработку, но да - как Артём написал выше, время вебпака уже вышло. Он был крут, но сейчас уже сдаёт позиции. Тот же Vite работает супербыстро, настраивается легче и танцев с бубнами меньше. А есть ещё и другие тулзы, которые менее популярны.

2
Ответить

Странно на виси видеть тех детали про разработку

Хабр стал убежищем для постов около IT, но никак не про IT. Там или темы связанные так или иначе с политикой, или темы очень отдаленные от IT, которое в бизнесе. Поэтому разработка на vc.ru стала моим маленьким убежищем))

Тот же Vite работает супербыстро

Оке-доке, попробумс, может статью ещё по нему напишу с плюсами и минусами. Спасибо и вам, и Артему, что дали наводку😅

2
Ответить

Не согласен насчет прошедшего времени для вебпака. Был кейс, когда нужно было поднять микрофронтенд на module federation, хотели использовать vite и обломались, vite довольно-таки плохо работает с module federation для интерпрайз решений. Вебпак в этом плане гораздо удобнее настраивать и работать с ним

Ответить

Какой webpack и typescript, ну камон, это же не хабр!
Тут надо жаловаться, когда банк вас не устраивает своими махинациями.
Или когда вы что-то купили и оказались обмануты.
Или когда кто-то кем-то недоволен и подал в суд на этого кого-то.
Или про другие проблемы и нытьё.

Ответить

Разве для жалоб сделан тред "Разработка"? В треде разработки пишу о разработке, или я где-то ошибаюсь?🤔

2
Ответить

Так-то сейчас тренд на отказ от Webpack пошёл. В частности, для React, Vue, Svelte можно использовать Vite, основанный на Rollup. Он работает в несколько раз быстрее, чем Webpack, хотя, конечно же, такой тонкой настройки не позволяет. Но многим она и не нужна.

Ответить