Обработка @import и url() внутри CSS

В CSS существуют две важные конструкции, связанные с подключением внешних ресурсов:

  • @import — импортирует другие CSS-файлы;

  • url() — подключает файлы внутри CSS:

    • изображения;
    • шрифты;
    • SVG;
    • видео;
    • аудио;
    • другие ресурсы.

Webpack не умеет анализировать CSS самостоятельно. Для понимания таких конструкций используются специальные загрузчики, главным образом css-loader.


Как Webpack обрабатывает CSS

При подключении CSS-файла:

import './styles/main.css';

Webpack передаёт файл цепочке loaders.

Типичная схема:

{
    test: /\.css$/,
    use: [
        'style-loader',
        'css-loader'
    ]
}

Процесс выглядит так:

  1. css-loader

    • анализирует CSS;
    • ищет @import;
    • ищет url();
    • превращает зависимости в JavaScript-модули.
  2. style-loader

    • вставляет готовый CSS в <style> внутри страницы.

Обработка @import

Базовый пример

Файл:

/* main.css */

@import './reset.css';

body {
    margin: 0;
}

Webpack воспринимает импорт как зависимость.

Файл reset.css будет:

  • найден;
  • прочитан;
  • включён в сборку;
  • объединён с основным CSS.

Что делает css-loader

css-loader преобразует:

@import './reset.css';

примерно в:

import './reset.css';

То есть CSS начинает работать как модульная система.


Последовательность обработки импортов

Если существует цепочка:

/* main.css */
@import './base.css';

/* base.css */
@import './theme.css';

Webpack рекурсивно проходит все зависимости.

В результате формируется единый граф модулей.


Импорт CSS из node_modules

Можно импортировать стили библиотек:

@import 'normalize.css';

Webpack ищет пакет в:

node_modules/

Это работает аналогично JavaScript-import.


Использование тильды ~

В старых версиях Webpack и css-loader применялся синтаксис:

@import '~bootstrap/dist/css/bootstrap.css';

Символ ~ означал:

искать внутри node_modules

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

  • ~ больше не обязателен;
  • рекомендуется обычный импорт:
@import 'bootstrap/dist/css/bootstrap.css';

Порядок выполнения @import

CSS сохраняет последовательность импортов.

Например:

@import './reset.css';
@import './theme.css';

body {
    background: white;
}

Webpack объединяет стили в указанном порядке.

Это критически важно для:

  • каскадности;
  • переопределения правил;
  • CSS-переменных;
  • темизации.

Ограничения @import

Импорты должны быть в начале файла

Корректно:

@import './reset.css';

body {
    margin: 0;
}

Некорректно:

body {
    margin: 0;
}

@import './reset.css';

CSS-спецификация требует размещать @import до обычных правил.


Импорт медиа-стилей

CSS поддерживает media queries внутри import:

@import './mobile.css' screen and (max-width: 768px);

Webpack сохраняет media condition.


Обработка url()

Основной принцип

Webpack анализирует:

background: url('./image.png');

и превращает файл в зависимость проекта.


Пример структуры проекта

src/
├── css/
│   └── style.css
├── images/
│   └── logo.png

CSS:

.logo {
    background-image: url('../images/logo.png');
}

Webpack:

  1. находит файл;
  2. копирует его в output;
  3. генерирует новый путь;
  4. заменяет исходный URL.

Как выглядит результат

Исходный CSS:

background-image: url('../images/logo.png');

После сборки:

background-image: url(/assets/logo.a1b2c3.png);

Asset Modules

В Webpack 5 обработка файлов встроена в ядро.

Пример настройки:

module.exports = {
    module: {
        rules: [
            {
                test: /\.(png|jpg|gif|svg)$/i,
                type: 'asset/resource'
            }
        ]
    }
};

Типы asset modules

asset/resource

Копирует файл отдельно.

type: 'asset/resource'

Результат:

dist/
└── images/
    └── logo.8d91fa.png

asset/inline

Встраивает файл в Base64.

type: 'asset/inline'

Результат:

background-image: url(data:image/png;base64,...);

asset/source

Импортирует содержимое файла как строку.


asset

Автоматически выбирает:

  • inline;
  • resource.

Обычно решение принимается по размеру файла.


Настройка генерации путей

Пример:

module.exports = {
    module: {
        rules: [
            {
                test: /\.(png|jpg)$/i,
                type: 'asset/resource',
                generator: {
                    filename: 'images/[name].[contenthash][ext]'
                }
            }
        ]
    }
};

contenthash

contenthash создаёт уникальное имя файла:

logo.a4f92d.png

Хеш меняется только при изменении содержимого.

Это важно для:

  • кеширования;
  • CDN;
  • production-сборок.

Обработка шрифтов через url()

Подключение шрифта

@font-face {
    font-family: 'Roboto';

    src: url('../fonts/Roboto-Regular.woff2') format('woff2');
}

Webpack:

  • обрабатывает файл;
  • копирует его;
  • заменяет путь.

Поддержка SVG

SVG может подключаться через:

background-image: url('./icon.svg');

или:

mask-image: url('./mask.svg');

SVG также проходит через asset modules.


Относительные пути

Как считается путь

Путь внутри CSS вычисляется относительно самого CSS-файла.

Структура:

css/style.css
images/logo.png

CSS:

background: url('../images/logo.png');

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

Webpack может работать с абсолютными путями:

background: url('/images/logo.png');

Но здесь есть важный нюанс.

Такой URL:

  • не считается модульной зависимостью;
  • не проходит через сборку;
  • не копируется Webpack.

Файл должен существовать физически на сервере.


publicPath

Webpack умеет автоматически добавлять базовый URL.

Пример:

output: {
    publicPath: '/static/'
}

Результат:

background-image: url(/static/images/logo.png);

css-loader и параметр url

Полное отключение обработки url()

{
    loader: 'css-loader',
    options: {
        url: false
    }
}

Теперь Webpack не анализирует:

url(...)

Пути останутся как есть.


Когда отключают url

Это используется:

  • при работе с CDN;
  • при внешнем хранилище файлов;
  • в legacy-проектах;
  • при серверной генерации путей.

Фильтрация url()

Можно обрабатывать не все URL.

Пример:

{
    loader: 'css-loader',
    options: {
        url: {
            filter: (url) => {
                return !url.includes('external');
            }
        }
    }
}

css-loader и параметр import

Отключение обработки @import

{
    loader: 'css-loader',
    options: {
        import: false
    }
}

Теперь:

@import './style.css';

не будет анализироваться Webpack.


importLoaders

Очень важный параметр.

Пример:

{
    loader: 'css-loader',
    options: {
        importLoaders: 1
    }
}

Зачем нужен importLoaders

Предположим:

use: [
    'style-loader',
    'css-loader',
    'postcss-loader'
]

И существует:

@import './theme.css';

Без importLoaders:

  • theme.css не пройдёт через postcss-loader.

С:

importLoaders: 1

импортированные файлы тоже обрабатываются PostCSS.


importLoaders и цепочка loaders

Значение 1

use: [
    'style-loader',
    'css-loader',
    'postcss-loader'
]
importLoaders: 1

Будет применён:

postcss-loader

Значение 2

use: [
    'style-loader',
    'css-loader',
    'postcss-loader',
    'sass-loader'
]
importLoaders: 2

Импортированные файлы пройдут через:

  • postcss-loader;
  • sass-loader.

Обработка Sass-import

SCSS:

@import './variables';

Webpack передаёт импорт в sass-loader.

После компиляции:

  • результат попадает в css-loader;
  • затем анализируются url() и оставшиеся @import.

URL внутри Sass

.hero {
    background: url('../img/bg.jpg');
}

После компиляции Sass:

  • CSS передаётся в css-loader;
  • Webpack обрабатывает URL.

Source Maps

При работе с CSS полезны source maps.

Пример:

{
    loader: 'css-loader',
    options: {
        sourceMap: true
    }
}

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

  • видеть исходные файлы;
  • определять источник url();
  • отслеживать импортированные стили.

MiniCssExtractPlugin и URL

В production часто используется:

MiniCssExtractPlugin.loader

вместо:

style-loader

Пример:

use: [
    MiniCssExtractPlugin.loader,
    'css-loader'
]

Особенности путей при extraction

Когда CSS выносится в отдельный файл:

dist/css/main.css

пути к ресурсам пересчитываются относительно нового расположения.

Иногда требуется:

publicPath

или:

MiniCssExtractPlugin.loader

с дополнительными настройками.


Пример production-конфигурации

const MiniCssExtractPlugin = require('mini-css-extract-plugin');

module.exports = {
    module: {
        rules: [
            {
                test: /\.css$/,
                use: [
                    MiniCssExtractPlugin.loader,
                    {
                        loader: 'css-loader',
                        options: {
                            importLoaders: 1
                        }
                    },
                    'postcss-loader'
                ]
            },

            {
                test: /\.(png|jpg|svg|woff2?)$/i,
                type: 'asset/resource',
                generator: {
                    filename: 'assets/[name].[contenthash][ext]'
                }
            }
        ]
    },

    plugins: [
        new MiniCssExtractPlugin({
            filename: 'css/[name].[contenthash].css'
        })
    ]
};

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

Module not found

Ошибка:

Module not found: Error: Can't resolve './image.png'

Причины:

  • неверный путь;
  • файл отсутствует;
  • ошибка регистра;
  • неправильная структура каталогов.

Ошибки относительных путей

Частая проблема:

url('/img/logo.png')

вместо:

url('../img/logo.png')

Абсолютный URL не проходит через Webpack.


Дублирование файлов

Иногда один файл импортируется:

  • из JavaScript;
  • из CSS.

Webpack может создать несколько копий при неправильной конфигурации.


Большие inline-файлы

asset/inline не подходит для крупных изображений.

Минусы:

  • увеличивается размер CSS;
  • ухудшается кеширование;
  • растёт время загрузки.

Использование alias

Webpack поддерживает aliases.

Настройка:

resolve: {
    alias: {
        '@images': path.resolve(__dirname, 'src/images')
    }
}

CSS:

background-image: url('~@images/logo.png');

URL с query-параметрами

Webpack корректно обрабатывает:

background-image: url('./icon.svg?v=1');

и:

background-image: url('./icon.svg#hash');

Data URL

css-loader поддерживает:

background-image: url('data:image/png;base64,...');

Такие URL не обрабатываются Webpack как файлы.


Lazy loading CSS

При динамическом импорте:

import('./theme.css');

Webpack создаёт отдельный chunk.

Все ресурсы внутри CSS:

  • изображения;
  • шрифты;
  • SVG;

также становятся частью соответствующего чанка.


Связь CSS и dependency graph

Webpack рассматривает CSS как полноценный модуль.

Это означает:

  • построение dependency graph;
  • оптимизацию зависимостей;
  • tree shaking некоторых CSS-конструкций;
  • code splitting;
  • hashing;
  • lazy loading.

Внутреннее преобразование url()

Условно:

background: url('./logo.png');

превращается в:

import logo from './logo.png';

element.style.background = `url(${logo})`;

Именно поэтому Webpack способен:

  • отслеживать зависимости;
  • менять имена файлов;
  • оптимизировать assets.