devServer.static: раздача статики

Параметр devServer.static в Webpack Dev Server отвечает за раздачу статических файлов без участия механизма сборки Webpack. Через него обслуживаются директории с HTML-файлами, изображениями, шрифтами, favicon, mock-данными и любыми другими ресурсами, которые должны быть доступны браузеру напрямую.

Механизм особенно важен при локальной разработке, когда часть файлов не импортируется через JavaScript и не проходит через loaders/plugins.

Простейшая конфигурация:

module.exports = {
    devServer: {
        static: './public'
    }
};

В этом случае Webpack Dev Server начинает обслуживать содержимое директории public.

Например:

public/
├── index.html
├── favicon.ico
├── images/
│   └── logo.png

Файлы становятся доступны по адресам:

http://localhost:8080/index.html
http://localhost:8080/favicon.ico
http://localhost:8080/images/logo.png

Отличие devServer.static от output.path

Частая ошибка — путать раздачу статики с каталогом сборки.

output.path

output: {
    path: path.resolve(__dirname, 'dist')
}

Определяет директорию, куда Webpack складывает результаты сборки.


devServer.static

devServer: {
    static: './public'
}

Раздаёт файлы напрямую с диска, без обработки Webpack.


Почему статика не должна проходить через Webpack

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

  • JS
  • CSS
  • SCSS
  • TypeScript
  • изображения через import
  • шрифты через asset modules

Но многие файлы:

  • не импортируются;
  • не участвуют в dependency graph;
  • должны существовать как есть.

Типичные примеры:

robots.txt
sitemap.xml
manifest.json
favicon.ico
static JSON
mock API files

Для них используется devServer.static.


Строковое сокращение

Вместо объекта можно передать строку:

devServer: {
    static: './public'
}

Webpack автоматически интерпретирует это как:

devServer: {
    static: {
        directory: path.join(__dirname, 'public')
    }
}

Полная объектная форма

На практике чаще используется объект:

const path = require('path');

module.exports = {
    devServer: {
        static: {
            directory: path.join(__dirname, 'public')
        }
    }
};

Свойство directory

Главный параметр.

static: {
    directory: path.join(__dirname, 'public')
}

Определяет физическую директорию на диске.


Абсолютные пути

Рекомендуется всегда использовать path.join() или path.resolve().

Правильно:

directory: path.resolve(__dirname, 'public')

Нежелательно:

directory: './public'

Причина — зависимость от текущей рабочей директории процесса.


Раздача нескольких директорий

Webpack Dev Server поддерживает массив.

module.exports = {
    devServer: {
        static: [
            {
                directory: path.join(__dirname, 'public')
            },
            {
                directory: path.join(__dirname, 'assets')
            }
        ]
    }
};

Теперь сервер обслуживает обе директории.


Порядок поиска файлов

При совпадении имён используется первый найденный файл.

Пример:

public/logo.png
assets/logo.png

Если public указана первой, браузер получит именно этот файл.


Свойство publicPath

Позволяет изменить URL-префикс.

module.exports = {
    devServer: {
        static: {
            directory: path.join(__dirname, 'public'),
            publicPath: '/static/'
        }
    }
};

Теперь:

public/logo.png

будет доступен по адресу:

http://localhost:8080/static/logo.png

Разделение URL и файловой структуры

Это позволяет:

  • скрывать реальную структуру каталогов;
  • организовывать CDN-подобные пути;
  • разделять API и статические ресурсы;
  • избегать конфликтов маршрутов.

Несколько publicPath

devServer: {
    static: [
        {
            directory: path.join(__dirname, 'images'),
            publicPath: '/img/'
        },
        {
            directory: path.join(__dirname, 'fonts'),
            publicPath: '/fonts/'
        }
    ]
}

Результат:

/images/logo.png  → /img/logo.png
/fonts/main.woff → /fonts/main.woff

Свойство watch

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

devServer: {
    static: {
        directory: path.join(__dirname, 'public'),
        watch: true
    }
}

При изменении файлов браузер автоматически обновляется.


Отключение слежения

Иногда наблюдение за файлами создаёт лишнюю нагрузку.

watch: false

Особенно актуально:

  • в Docker;
  • на сетевых файловых системах;
  • в больших монорепозиториях;
  • при работе через WSL.

Настройка watch через объект

Можно передать дополнительные параметры chokidar.

devServer: {
    static: {
        directory: path.join(__dirname, 'public'),
        watch: {
            ignored: '*.psd',
            usePolling: false
        }
    }
}

Полезные параметры watch

ignored

Игнорирование файлов.

watch: {
    ignored: /node_modules/
}

usePolling

Использование polling вместо native watchers.

watch: {
    usePolling: true
}

Полезно:

  • в Docker;
  • внутри виртуальных машин;
  • при проблемах с inotify.

interval

Интервал polling.

watch: {
    usePolling: true,
    interval: 1000
}

Свойство serveIndex

Позволяет отображать список файлов директории.

devServer: {
    static: {
        directory: path.join(__dirname, 'public'),
        serveIndex: true
    }
}

При переходе в каталог браузер показывает список файлов.


Пример

public/
├── docs/
│   ├── a.txt
│   └── b.txt

URL:

http://localhost:8080/docs/

Покажет файловый индекс.


Отключение

serveIndex: false

Чаще используется именно этот вариант.


Свойство staticOptions

Передаёт параметры библиотеке serve-static.

devServer: {
    static: {
        directory: path.join(__dirname, 'public'),
        staticOptions: {
            extensions: ['html']
        }
    }
}

Автоматическое расширение

Теперь URL:

/about

может автоматически открыть:

about.html

Настройка HTTP-заголовков

Через staticOptions.setHeaders.

devServer: {
    static: {
        directory: path.join(__dirname, 'public'),
        staticOptions: {
            setHeaders(res) {
                res.setHeader('X-Test', 'webpack');
            }
        }
    }
}

Управление кэшированием

Статические ресурсы можно кэшировать.

staticOptions: {
    maxAge: '1d'
}

Полный пример

const path = require('path');

module.exports = {
    devServer: {
        static: {
            directory: path.resolve(__dirname, 'public'),
            publicPath: '/assets/',
            watch: {
                ignored: /\.psd$/
            },
            serveIndex: false,
            staticOptions: {
                maxAge: '1d'
            }
        }
    }
};

Использование вместе с HtmlWebpackPlugin

Частая схема:

src/
public/
dist/

Где:

  • src — исходники;
  • dist — сборка;
  • public — статические файлы.

Конфигурация

module.exports = {
    plugins: [
        new HtmlWebpackPlugin({
            template: './public/index.html'
        })
    ],

    devServer: {
        static: {
            directory: path.join(__dirname, 'public')
        }
    }
};

Отличие от CopyWebpackPlugin

devServer.static

Раздаёт файлы напрямую.

Файлы физически не копируются.

Работает только в dev server.


CopyWebpackPlugin

Копирует файлы в output directory.

new CopyWebpackPlugin({
    patterns: [
        {
            from: 'public',
            to: 'dist'
        }
    ]
})

Работает во время сборки.


Совместное использование

Частая конфигурация:

const isDev = process.env.NODE_ENV === 'development';

module.exports = {
    plugins: [
        !isDev &&
            new CopyWebpackPlugin({
                patterns: [
                    {
                        from: 'public',
                        to: '.'
                    }
                ]
            })
    ].filter(Boolean),

    devServer: {
        static: {
            directory: path.join(__dirname, 'public')
        }
    }
};

Работа с SPA

Для Single Page Application часто используется:

historyApiFallback: true

Полная конфигурация:

devServer: {
    historyApiFallback: true,

    static: {
        directory: path.join(__dirname, 'public')
    }
}

Как взаимодействуют historyApiFallback и static

Порядок обработки:

  1. Проверка статического файла
  2. Проверка webpack assets
  3. Fallback на index.html

Пример

URL:

/profile/settings

Если файла нет:

public/profile/settings

сервер отдаст:

index.html

Раздача favicon

Одна из самых популярных задач.

public/favicon.ico

Конфигурация:

devServer: {
    static: {
        directory: path.join(__dirname, 'public')
    }
}

Файл автоматически доступен:

/favicon.ico

Раздача mock JSON

public/api/users.json

Доступ:

http://localhost:8080/api/users.json

Это удобно:

  • при frontend-only разработке;
  • для mock API;
  • во время интеграции;
  • при тестировании UI.

Использование с изображениями

Статика напрямую

public/images/banner.jpg

HTML:

<img src="/images/banner.jpg">

Через Webpack import

import banner from './banner.jpg';

В этом случае файл проходит через asset modules.


Когда выбирать static, а когда import

devServer.static

Подходит для:

  • favicon;
  • robots.txt;
  • mock data;
  • больших медиафайлов;
  • файлов вне dependency graph.

import

Подходит для:

  • оптимизации;
  • хэширования;
  • tree shaking;
  • asset pipeline;
  • автоматической генерации путей.

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

devServer.static почти не нагружает Webpack, потому что:

  • файлы не парсятся;
  • loaders не запускаются;
  • plugins не участвуют;
  • dependency graph не строится.

Это особенно важно для:

  • видео;
  • больших изображений;
  • архивов;
  • бинарных файлов.

Типичные ошибки

Неправильный путь

Ошибка:

directory: 'public'

При запуске из другой директории файлы не находятся.


Конфликт publicPath

output: {
    publicPath: '/'
}

devServer: {
    static: {
        publicPath: '/'
    }
}

Иногда это вызывает путаницу между webpack assets и обычной статикой.


Ожидание обработки Webpack

Файлы из static:

  • не минифицируются;
  • не хэшируются;
  • не оптимизируются;
  • не проходят loaders.

Использование static вместо asset modules

Некоторые разработчики складывают все изображения в public, теряя:

  • contenthash;
  • оптимизацию;
  • автоматическое управление путями;
  • lazy loading pipeline.

Миграция со старого contentBase

До Webpack Dev Server v4 использовался параметр:

devServer: {
    contentBase: path.join(__dirname, 'public')
}

В новых версиях:

devServer: {
    static: {
        directory: path.join(__dirname, 'public')
    }
}

Полная production-like конфигурация

const path = require('path');

module.exports = {
    mode: 'development',

    output: {
        publicPath: '/'
    },

    devServer: {
        port: 3000,

        hot: true,

        compress: true,

        historyApiFallback: true,

        static: [
            {
                directory: path.resolve(__dirname, 'public'),

                publicPath: '/',

                watch: {
                    ignored: /\.tmp$/
                },

                serveIndex: false,

                staticOptions: {
                    maxAge: '1h'
                }
            },

            {
                directory: path.resolve(__dirname, 'mock'),

                publicPath: '/api/',

                watch: true
            }
        ]
    }
};