watching: запуск наблюдателя программно

Webpack предоставляет механизм наблюдения за изменениями файлов (watch mode), который позволяет автоматически пересобирать бандл при изменении исходного кода. Помимо CLI-режима, этот функционал доступен через программный API, что особенно важно при интеграции Webpack в собственные инструменты, dev-серверы, сборочные пайплайны и системы автоматизации.

Режим наблюдения основан на системе отслеживания файловой системы и повторном запуске компиляции при изменениях. В Webpack этот механизм реализован через метод watch, доступный у экземпляра компилятора.

Ключевая идея:

  • создаётся компилятор через webpack(config)
  • вызывается compiler.watch(...)
  • Webpack начинает отслеживать зависимости графа модулей
  • при изменениях запускается новая компиляция
  • результаты передаются через callback

Создание компилятора

Перед запуском наблюдения необходимо создать экземпляр компилятора:

const webpack = require('webpack');
const config = require('./webpack.config');

const compiler = webpack(config);

Функция webpack(config) возвращает объект компилятора, который является центральной точкой управления сборкой.

Запуск watch через программный API

Метод compiler.watch принимает два основных параметра:

  • watchOptions — настройки наблюдения
  • handler — callback, вызываемый при каждой компиляции

Сигнатура:

compiler.watch(watchOptions, (err, stats) => {});

Простейший пример

const webpack = require('webpack');
const config = require('./webpack.config');

const compiler = webpack(config);

compiler.watch({}, (err, stats) => {
  if (err) {
    console.error(err);
    return;
  }

  console.log(stats.toString({
    colors: true,
    chunks: false,
    modules: false,
  }));
});

В этом режиме Webpack будет непрерывно отслеживать изменения и пересобирать проект без необходимости перезапуска процесса.

Параметры watchOptions

watchOptions управляют поведением наблюдателя.

aggregateTimeout

Определяет задержку перед запуском сборки после изменений.

compiler.watch({
  aggregateTimeout: 300
}, handler);

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

poll

Включает режим периодического опроса файловой системы.

compiler.watch({
  poll: 1000
}, handler);

Значение задаётся в миллисекундах. Этот режим используется, когда файловая система не поддерживает событийное отслеживание (например, в некоторых Docker или сетевых средах).

ignored

Позволяет исключить файлы или директории из наблюдения:

compiler.watch({
  ignored: /node_modules/
}, handler);

Исключение больших директорий значительно снижает нагрузку на систему наблюдения.

Объект stats

Callback watch получает объект stats, содержащий информацию о сборке:

  • список модулей
  • чанки
  • ошибки и предупреждения
  • время сборки
  • хеш бандла

Пример форматирования:

stats.toString({
  all: false,
  errors: true,
  warnings: true,
  colors: true
});

Проверка ошибок

compiler.watch({}, (err, stats) => {
  if (err) {
    console.error('Fatal error:', err);
    return;
  }

  if (stats.hasErrors()) {
    console.log('Compilation errors occurred');
  }

  if (stats.hasWarnings()) {
    console.log('Compilation warnings occurred');
  }
});

Управление остановкой наблюдения

Метод watch возвращает объект Watching, который позволяет управлять процессом:

const watching = compiler.watch({}, handler);

Остановка наблюдения

watching.close(() => {
  console.log('Watching stopped');
});

После вызова close Webpack прекращает отслеживание изменений и освобождает ресурсы.

Инвалидация сборки

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

watching.invalidate();

Это приводит к отмене текущей компиляции и запуску новой.

Типичный кейс — интеграция с dev-сервером, где требуется обновление состояния сборки при внешних событиях.

Событийная модель watch

Хотя основной интерфейс — callback, Webpack также предоставляет события через compiler hooks.

Пример использования hooks

compiler.hooks.watchRun.tap('MyPlugin', (comp) => {
  console.log('Watch run started');
});

compiler.hooks.watchClose.tap('MyPlugin', () => {
  console.log('Watch closed');
});

Это позволяет расширять поведение watch-механизма без модификации основного кода.

Отличие watch от run

Обычный запуск:

compiler.run((err, stats) => {});

Watch-режим:

compiler.watch({}, (err, stats) => {});

Ключевые различия:

  • run выполняется один раз
  • watch остаётся активным процессом
  • watch отслеживает зависимости графа модулей
  • run не имеет механизма повторного запуска

Особенности работы с файловой системой

Webpack строит граф зависимостей и отслеживает только те файлы, которые участвуют в сборке. Это означает:

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

При использовании кастомных loaders или plugins важно корректно регистрировать зависимости через:

this.addDependency(filePath);

иначе изменения файлов могут не триггерить rebuild.

Оптимизация производительности watch

При больших проектах режим наблюдения может создавать нагрузку на CPU и файловую систему.

Основные стратегии оптимизации:

Ограничение наблюдаемых директорий

ignored: /dist|node_modules/

Использование кеширования

Webpack cache уменьшает время повторной сборки:

cache: {
  type: 'filesystem'
}

Уменьшение детализации stats

stats: 'errors-only'

или

stats: {
  modules: false,
  chunks: false
}

Watch в связке с DevServer

Хотя Webpack DevServer использует собственный механизм наблюдения, он основан на том же API компилятора. При программной интеграции часто требуется аналогичная логика:

  • запуск watch через compiler
  • обработка stats
  • управление lifecycle через Watching

Поведение при ошибках компиляции

При ошибке сборки watch-режим не останавливается. Он продолжает отслеживать изменения и повторяет попытки компиляции.

compiler.watch({}, (err, stats) => {
  if (stats.hasErrors()) {
    return;
  }

  // успешная сборка
});

Такое поведение важно для dev-режима, где исправление ошибки должно автоматически приводить к пересборке без перезапуска процесса.

Гибридные сценарии использования

Программный watch часто используется в следующих архитектурах:

  • SSR-серверы с hot rebuild
  • Electron приложения с live reload main/renderer процессов
  • кастомные сборочные CLI
  • монорепозитории с несколькими компиляторами

В сложных системах может использоваться несколько компиляторов:

const clientCompiler = webpack(clientConfig);
const serverCompiler = webpack(serverConfig);

clientCompiler.watch({}, handler);
serverCompiler.watch({}, handler);

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

Управление параллельными сборками

При нескольких watch-инстансах важно учитывать:

  • разделение output path
  • независимые кеши
  • синхронизацию событий завершения сборки

Без этого возможны гонки записи файлов и нестабильные состояния бандла.

Внутренние аспекты механизма watch

Внутри Webpack watch строится на следующих принципах:

  • построение dependency graph
  • использование file system watcher (chokidar-like механизмы)
  • инкрементальная пересборка
  • сохранение state компиляции между итерациями

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

Завершение жизненного цикла watch

Корректное завершение процесса важно для освобождения ресурсов:

process.on('SIGINT', () => {
  watching.close(() => {
    process.exit();
  });
});

Без явного закрытия watcher может продолжать удерживать файловые дескрипторы и процессы наблюдения.