Архитектура плагинов: хуки компилятора и компиляции

Система плагинов Webpack построена вокруг событийной модели, основанной на библиотеке Tapable. В основе лежит идея: весь процесс сборки разбит на стадии, на каждой из которых публикуются хуки (hooks), позволяющие подключаться к внутреннему жизненному циклу компиляции и изменять его поведение.

Ключевыми сущностями выступают два центральных объекта: Compiler и Compilation. Они формируют ядро архитектуры, вокруг которого строятся все плагины, оптимизации и расширения функциональности.


Compiler как точка входа всего процесса сборки

Объект Compiler создаётся один раз при инициализации Webpack. Он представляет собой глобальный контроллер процесса сборки.

Его основные задачи:

  • запуск и координация процесса сборки
  • управление файловой системой и контекстом проекта
  • создание экземпляров Compilation
  • хранение конфигурации и плагинов
  • управление watch-режимом

Compiler существует на протяжении всего жизненного цикла работы Webpack-инстанса. Именно он инициирует сборку при первом запуске и при каждом изменении файлов в режиме наблюдения.

Основные хуки Compiler

Compiler предоставляет множество хуков, каждый из которых соответствует определённому этапу жизненного цикла:

  • initialize — инициализация компилятора
  • run — запуск полной сборки
  • watchRun — запуск сборки в watch-режиме
  • compile — начало компиляции
  • compilation — создание нового объекта Compilation
  • make — построение графа модулей
  • emit — момент перед записью файлов на диск
  • done — завершение сборки

Каждый хук позволяет вмешиваться в процесс, добавляя, изменяя или полностью заменяя поведение сборщика.


Compilation как контекст конкретной сборки

Если Compiler — это глобальный контроллер, то Compilation — это объект, описывающий конкретный цикл сборки.

Каждый запуск сборки (включая watch-режим) создаёт новый экземпляр Compilation. Он содержит:

  • граф зависимостей модулей
  • результаты трансформации кода
  • список чанков (chunks)
  • информацию об ассетах
  • промежуточные данные плагинов

Compilation является «рабочей областью», в которой происходит основная магия Webpack: анализ импортов, транспиляция, оптимизация и генерация выходных файлов.

Основные хуки Compilation

Compilation предоставляет ещё более детализированные точки расширения:

  • buildModule — сборка конкретного модуля
  • seal — фиксация графа модулей перед генерацией
  • optimize — общий этап оптимизации
  • optimizeChunks — оптимизация чанков
  • processAssets — обработка финальных ассетов
  • afterSeal — завершение фиксации графа
  • finishModules — завершение обработки модулей

Именно хуки Compilation чаще всего используются для реализации сложной логики плагинов, так как они дают доступ к внутренним структурам сборки.


Tapable как основа системы хуков

Вся система плагинов Webpack построена на библиотеке Tapable, которая реализует различные типы хуков:

  • SyncHook — синхронные события без возвращаемого значения
  • SyncBailHook — останавливает выполнение при возврате значения
  • SyncWaterfallHook — передаёт результат между обработчиками
  • AsyncParallelHook — параллельное выполнение асинхронных задач
  • AsyncSeriesHook — последовательное асинхронное выполнение

Эти хуки обеспечивают гибкость управления процессом сборки и позволяют точно контролировать порядок выполнения логики.


Жизненный цикл компиляции

Процесс сборки Webpack можно представить как последовательность стадий, каждая из которых связана с определёнными хуками Compiler и Compilation.

Инициализация

На этапе инициализации происходит:

  • создание Compiler
  • регистрация плагинов
  • применение конфигурации

Здесь активируются базовые хуки, и система подготавливается к запуску.


Запуск сборки

При вызове сборки активируется цепочка:

  • run или watchRun
  • compile
  • создание нового Compilation

На этом этапе формируется контекст конкретной сборки.


Построение графа модулей

Самый важный этап:

  • анализ entry points
  • рекурсивное разрешение импортов
  • построение dependency graph

Здесь активно используется хук make, который запускает загрузку и обработку модулей.

Каждый модуль проходит стадии:

  • загрузка исходного кода
  • применение loader’ов
  • парсинг AST
  • извлечение зависимостей

Стадия seal

После завершения построения графа вызывается seal.

На этом этапе:

  • фиксируется структура модулей
  • формируются чанки
  • применяются оптимизации
  • подготавливаются ассеты

Этот этап критичен, так как после него структура сборки становится неизменяемой.


Оптимизация

Webpack выполняет множество оптимизаций:

  • удаление неиспользуемого кода (tree shaking)
  • разделение кода на чанки (code splitting)
  • минификация
  • объединение модулей

Хуки оптимизации позволяют плагинам вмешиваться в этот процесс и изменять структуру выходного бандла.


Генерация ассетов

На этапе processAssets формируются финальные файлы:

  • JavaScript-бандлы
  • CSS-файлы (при использовании соответствующих плагинов)
  • source maps
  • вспомогательные ресурсы

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


Завершение

Финальный этап:

  • запись файлов (emit)
  • завершение сборки (done)
  • освобождение ресурсов Compilation

После этого Compiler остаётся активным и может запускать следующий цикл сборки.


Механика подключения плагинов

Плагин в Webpack — это функция или класс с методом apply, принимающим Compiler:

class ExamplePlugin {
  apply(compiler) {
    compiler.hooks.emit.tap('ExamplePlugin', (compilation) => {
      // изменение ассетов перед записью
    });
  }
}

Подключение плагина фактически означает регистрацию обработчиков на хуки Compiler или Compilation.


Взаимодействие Compiler и Compilation

Связь между двумя уровнями выглядит следующим образом:

  • Compiler управляет жизненным циклом сборки
  • Compilation содержит данные конкретного билда
  • Compiler создаёт Compilation на каждом запуске
  • Compilation передаётся в хуки Compiler (например, make, emit)
  • плагины используют оба уровня в зависимости от задачи

Такое разделение позволяет отделить глобальную логику от контекста конкретной сборки.


Расширение поведения через хуки

Плагины могут:

  • добавлять новые модули в граф
  • изменять исходный код модулей
  • вмешиваться в резолвинг импортов
  • изменять структуру чанков
  • модифицировать итоговые файлы

Пример вмешательства в Compilation:

compiler.hooks.compilation.tap('MyPlugin', (compilation) => {
  compilation.hooks.optimize.tap('MyPlugin', () => {
    // изменение структуры модулей перед оптимизацией
  });
});

Система приоритетов и порядок выполнения

Tapable позволяет управлять порядком выполнения обработчиков:

  • tap — стандартная регистрация
  • tapAsync — асинхронный обработчик
  • tapPromise — обработчик через Promise

Кроме того, существуют механизмы управления порядком:

  • stage — приоритет выполнения
  • name — идентификация плагина
  • последовательность регистрации

Это критично для сложных плагинов, которые зависят от других расширений.


Внутренние структуры Compilation

Compilation содержит множество внутренних структур:

  • modules — все обработанные модули
  • chunks — группы модулей
  • assets — итоговые файлы
  • fileDependencies — зависимости файловой системы
  • contextDependencies — контекстные зависимости

Эти структуры постоянно изменяются в процессе сборки до момента seal.


Значение архитектуры хуков

Архитектура Webpack построена так, чтобы:

  • обеспечить полную расширяемость без изменения ядра
  • дать доступ к любому этапу сборки
  • разделить ответственность между уровнями Compiler и Compilation
  • обеспечить детерминированный жизненный цикл сборки

Благодаря этому плагины могут работать на уровне ядра сборщика, а не поверх него, что делает систему гибкой, но одновременно сложной для понимания без знания внутреннего устройства.