webpack-dev-middleware для интеграции с Express/Koa

webpack-dev-middleware выступает промежуточным слоем между Webpack и HTTP-сервером Node.js, позволяя выполнять компиляцию ассетов «на лету» и отдавать их напрямую из памяти без записи на диск. Это ключевой механизм для интеграции сборщика с серверными фреймворками уровня Express и Koa, где требуется гибкость маршрутизации и возможность комбинировать сборку фронтенда с серверной логикой.

Основная идея заключается в том, что Webpack запускается в режиме watch, а результат компиляции сохраняется не в файловую систему, а в виртуальное хранилище в памяти (memory filesystem). Middleware перехватывает HTTP-запросы к статическим ресурсам и возвращает соответствующие файлы прямо из этого хранилища.

В стандартном режиме Webpack пишет результат в директорию dist/. При использовании middleware поток изменяется:

  • Webpack компилирует исходники
  • Результат помещается в RAM (in-memory filesystem)
  • Express/Koa middleware перехватывает запросы
  • Если запрашиваемый файл существует в памяти — он возвращается клиенту
  • Если файл отсутствует — управление передаётся следующему middleware

Ключевой компонент — memory-fs, который эмулирует API файловой системы Node.js.

Важный момент: middleware не запускает HTTP-сервер, он лишь расширяет уже существующий сервер.

Установка и базовая интеграция с Express

Типичная конфигурация начинается с установки зависимостей:

npm install webpack webpack-cli webpack-dev-middleware express

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

const express = require('express');
const webpack = require('webpack');
const webpackDevMiddleware = require('webpack-dev-middleware');

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

const app = express();

app.use(
  webpackDevMiddleware(compiler, {
    publicPath: config.output.publicPath,
  })
);

app.listen(3000, () => {
  console.log('Server running on http://localhost:3000');
});

Роль publicPath

publicPath является критически важной настройкой. Она определяет базовый URL, по которому доступны собранные ассеты.

Если output.publicPath задан как /assets/, то все запросы вида:

/assets/main.js
/assets/styles.css

будут сопоставляться с файлами внутри in-memory filesystem.

Несоответствие publicPath и маршрутов Express приводит к ситуации, когда middleware не может найти ресурсы, даже если они успешно собраны.

Режим watch и автоматическая перекомпиляция

webpack-dev-middleware автоматически включает watch-режим. Это означает:

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

В отличие от ручного webpack --watch, здесь отсутствует запись на диск, что уменьшает I/O нагрузку и ускоряет цикл разработки.

Опции конфигурации middleware

Основные параметры позволяют управлять поведением промежуточного слоя:

webpackDevMiddleware(compiler, {
  publicPath: config.output.publicPath,
  stats: 'minimal',
  writeToDisk: false,
  headers: {
    'X-Custom-Header': 'webpack-dev',
  },
  index: true,
  serverSideRender: false,
});

stats

Определяет уровень логирования сборки:

  • none — без вывода
  • errors-only — только ошибки
  • minimal — краткий вывод
  • normal — стандартный режим Webpack

Уменьшение verbosity особенно важно при больших проектах, где частые пересборки создают шум в логах.

writeToDisk

По умолчанию отключено. Если включить:

writeToDisk: true

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

headers

Позволяет добавлять кастомные HTTP-заголовки ко всем ответам middleware. Используется для кеш-контроля или отладки.

Интеграция с Express маршрутизацией

Webpack middleware должен располагаться до любых маршрутов, которые обслуживают статические файлы:

app.use(webpackDevMiddleware(compiler, options));

app.get('/api', (req, res) => {
  res.json({ status: 'ok' });
});

Порядок имеет значение: если маршруты Express перекроют запросы ассетов, middleware не будет вызван.

Также часто используется совместно с express.static, но в dev-режиме это обычно избыточно, так как Webpack уже обслуживает ассеты.

Интеграция с Koa

Для Koa используется адаптация middleware, поскольку Koa работает на основе async функций и контекста ctx.

const Koa = require('koa');
const webpack = require('webpack');
const webpackDevMiddleware = require('webpack-dev-middleware');

const app = new Koa();
const compiler = webpack(require('./webpack.config'));

const middleware = webpackDevMiddleware(compiler, {
  publicPath: '/assets/',
});

app.use(async (ctx, next) => {
  await new Promise((resolve, reject) => {
    middleware(ctx.req, {
      end: (content) => {
        ctx.body = content;
        resolve();
      },
      setHeader: ctx.set.bind(ctx),
      write: (data) => {
        ctx.body = data;
      },
    }, next);
  });

  await next();
});

app.listen(3000);

Хотя возможны различные обёртки, принцип остаётся одинаковым: адаптация Node-style middleware к Koa context.

Memory filesystem и производительность

Использование in-memory storage снижает задержки, связанные с дисковыми операциями:

  • отсутствует запись на SSD/HDD
  • чтение ассетов происходит напрямую из RAM
  • уменьшается время отклика при частых rebuild

Однако это увеличивает потребление оперативной памяти, особенно при крупных бандлах и source maps.

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

  • JS bundle
  • CSS chunk
  • sourcemaps
  • asset manifests

Кеширование и invalidation

webpack-dev-middleware автоматически инвалидирует кеш при пересборке. Это означает:

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

При этом HTTP-level кеширование обычно отключается через заголовки:

Cache-Control: no-store

что предотвращает использование устаревших ассетов браузером.

Связка с hot middleware

В связке с HMR используется дополнительный слой:

npm install webpack-hot-middleware
const webpackHotMiddleware = require('webpack-hot-middleware');

app.use(
  webpackHotMiddleware(compiler, {
    log: false,
    heartbeat: 2000,
  })
);

Модель взаимодействия:

  • webpack-dev-middleware отдаёт файлы
  • webpack-hot-middleware обеспечивает WebSocket/long polling канал
  • HMR runtime обновляет модули без перезагрузки страницы

Обработка HTML и SPA

При работе с SPA часто требуется fallback на index.html. Middleware сам по себе не реализует history API fallback, поэтому используется дополнительная логика:

app.use(webpackDevMiddleware(compiler, { publicPath: '/' }));

app.get('*', (req, res) => {
  const fs = compiler.outputFileSystem;
  const file = fs.readFileSync(
    path.join(compiler.outputPath, 'index.html')
  );
  res.send(file.toString());
});

В более сложных конфигурациях применяется отдельный middleware для history API fallback.

Типичные проблемы интеграции

Несовпадение publicPath и маршрутов приводит к 404 на ассеты даже при успешной сборке.

Ошибки memory-fs проявляются при:

  • неправильной версии Webpack и middleware
  • кастомной outputFileSystem
  • конфликте с SSR-логикой

Перегрузка памяти возникает при:

  • больших source maps
  • множественных entry points
  • отсутствующей оптимизации devtool

Работа с SSR

В server-side rendering middleware часто используется для синхронизации клиентской и серверной сборки:

  • сервер читает bundle из memory fs
  • избегается необходимость пересборки на диск
  • ускоряется hot reload SSR шаблонов

Пример доступа к файлу:

const fs = compiler.outputFileSystem;
const html = fs.readFileSync('/index.html', 'utf-8');

Поток обработки запроса

Общий pipeline выглядит следующим образом:

  1. HTTP запрос поступает в Express/Koa
  2. webpack-dev-middleware проверяет путь
  3. выполняется lookup в memory-fs
  4. при наличии файла — возвращается содержимое
  5. при отсутствии — next()
  6. остальные middleware обрабатывают запрос

Такой подход позволяет комбинировать Webpack с любыми серверными маршрутами без жёсткой привязки к dev-server.

Отличие от webpack-dev-server

webpack-dev-server представляет собой полноценный HTTP сервер, тогда как middleware:

  • не создаёт сервер
  • интегрируется в существующую инфраструктуру
  • даёт полный контроль над маршрутизацией
  • позволяет использовать Express/Koa ecosystem

Это делает его предпочтительным в архитектурах, где сервер выполняет не только задачи раздачи фронтенда, но и API, SSR и проксирование.