Webpack. Начало работы, часть 2 из 2

09.07.2022

Теги: FrontendJavaScriptNode.jsWeb-разработкаКонфигурацияНастройкаУстановкаШаблонСайта

Модули и загрузчики

6. Загрузка шрифтов

Давайте скачаем шрифт Roboto, распакуем архив и положим ttf-файлы в директорию src/font/roboto. Потом создадим файл src/css/roboto.css и импортируем его в файле src/css/style.css. И запустим сборку, чтобы посмотреть, что получилось.

@font-face { /* regular */
    font-family: Roboto;
    src: url('../font/roboto/Roboto-Regular.ttf') format('truetype');
    font-weight: 400;
    font-style: normal;
    font-stretch: normal;
    font-variant: normal;
}
@font-face { /* italic */
    font-family: Roboto;
    src: url('../font/roboto/Roboto-Italic.ttf') format('truetype');
    font-weight: 400;
    font-style: italic;
    font-stretch: normal;
    font-variant: normal;
}
@font-face { /* bold */
    font-family: Roboto;
    src: url('../font/roboto/Roboto-Bold.ttf') format('truetype');
    font-weight: 700;
    font-style: normal;
    font-stretch: normal;
    font-variant: normal;
}
@font-face { /* bold italic */
    font-family: Roboto;
    src: url('../font/roboto/Roboto-BoldItalic.ttf') format('truetype');
    font-weight: 700;
    font-style: italic;
    font-stretch: normal;
    font-variant: normal;
}
@import url('roboto.css');

body {
    margin: 0;
    padding: 0;
    background-color: #ccc;
    box-sizing: border-box;
    font-family: 'Roboto', sans-serif;
}
$ npm run build

Все отработало правильно, шрифт подключился, но ttf-файлы оказались в корне директории dist, создавая беспорядок. Давайте добавим еще одну настройку, чтобы при сборке все ttf-файлы оказались в директории dist/font. И еще одну настройку, чтобы файлы ресурсов, для которых не указана директория, сохранялись в dist/asset.

const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const CopyWebpackPlugin = require('copy-webpack-plugin')

module.exports = {
    mode: 'development',
    entry: {
        main: path.resolve(__dirname, 'src/js/index.js'),
    },
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: '[name].bundle.js',
        // NEW здесь будут файлы ресурсов, для которых не задан путь в настройках загрузчика
        assetModuleFilename: 'asset/[hash][ext][query]',
        clean: true,
    },
    plugins: [
        new HtmlWebpackPlugin({
            title: 'Webpack config example',
            template: path.resolve(__dirname, 'src/index.html'), // файл шаблона
            filename: 'index.html', // выходной файл
        }),
        new CopyWebpackPlugin({
            patterns: [
                {
                    from: path.resolve(__dirname, 'src/favicon.png'),
                    to: path.resolve(__dirname, 'dist')
                },
            ],
        }),
    ],
    module: {
        rules: [
            {
                test: /\.txt$/i, // простой текст
                type: 'asset/source'
            },
            {
                test: /\.(png|jpe?g|gif|webp)$/i, // изображение
                type: 'asset',
                parser: {
                    dataUrlCondition: {
                        maxSize: 4096 // ограничение 4kb
                    }
                },
                generator: {
                    filename: 'img/[hash][ext][query]' // все изображения в dist/img
                }
            },
            {
                test: /\.css$/, // файл css-стилей
                use: [
                    'style-loader',
                    'css-loader',
                    {
                        loader: 'postcss-loader',
                        options: {
                            postcssOptions: {
                                plugins: {
                                    'postcss-preset-env': {
                                        browsers: 'last 3 versions',
                                    },
                                },
                            },
                        },
                    },
                ],
            },
            {
                test: /\.(woff2?|eot|ttf|otf)$/i, // файлы шрифтов
                type: 'asset/resource',
                generator: {
                    filename: 'font/[hash][ext][query]' // все шрифты в dist/font
                }
            }
        ],
    }
}
$ npm run build

В принципе, никто не запрещает загрузить шрифты с использованием asset/inline — в этом случае ttf-файлы будут представлены в файле dist/main.bundle.js в виде base64 строки.

module.exports = {
    /* .......... */
    module: {
        rules: [
            /* .......... */
            {
                test: /\.(woff2?|eot|ttf|otf)$/i, // файлы шрифтов
                type: 'asset/inline'
            }
        ],
    }
}

7. Загрузчик sass-loader

Загружает sass/scss файл и компилирует его в css. Давайте создадим директорию src/scss и внутри нее — файлы base.scss, _vars.scss, _font.scss.

// файл src/scss/base.scss
@import 'vars';
@import 'font';

body {
    margin: 0;
    padding: 0;
    background-color: $background-color;
    box-sizing: border-box;
    font-family: $font-family;
}
// файл src/scss/_vars.scss
$background-color: #ccc;
$font-family: 'Roboto', sans-serif;
// файл src/scss/_font.scss
@font-face {
    font-family: Roboto;
    src: url('../font/roboto/Roboto-Regular.ttf') format('truetype');
    font-weight: 400;
    font-style: normal;
    font-stretch: normal;
    font-variant: normal;
}

Устанавим sass-loader, пропишем в файле конфигурации, импортируем base.scss в файле src/js/index.js.

$ npm install sass-loader sass --save-dev
module.exports = {
    /* .......... */
    module: {
        rules: [
            /* .......... */
            {
                test: /\.s?css$/, // файл css-стилей
                use: [
                    'style-loader',
                    'css-loader',
                    {
                        loader: 'postcss-loader',
                        options: {
                            /* .......... */
                        },
                    },
                    'sass-loader',
                ],
            },
            /* .......... */
        ],
    }
}
import data from '../data/webpack.txt'
import logo from '../img/logo.png'
import icon from '../img/icon.png'
// import '../css/style.css'
import '../scss/base.scss'

document.body.insertAdjacentHTML('beforeend', `<p>${data}</p>`)
document.body.insertAdjacentHTML('afterbegin', `<img src="${logo}" alt="">`)
document.body.insertAdjacentHTML('beforeend', `<img src="${icon}" alt="">`)
$ npm run build

8. Загрузчик babel-loader

Позволяет транспилировать js-код ES6+ в js-код ES5, чтобы этот код понимали старые браузеры. Создадим файл User.js в директории src/js/class и импортируем класс в файле src/js/index.js.

class User {
    name = 'Аноним'

    hello() {
        console.log(`Привет, ${this.name}!`)
    }
}

export default User
import data from '../data/webpack.txt'
import logo from '../img/logo.png'
import icon from '../img/icon.png'
// import '../css/style.css'
import '../scss/base.scss'
import User from './class/User.js'

document.body.insertAdjacentHTML('beforeend', `<p>${data}</p>`)
document.body.insertAdjacentHTML('afterbegin', `<img src="${logo}" alt="">`)
document.body.insertAdjacentHTML('beforeend', `<img src="${icon}" alt="">`)

const user = new User()
user.hello()

Установим загрузчик и все необходимое для транспиляции js-кода, пропишем в файле конфигурации обработку js-файлов.

$ npm install babel-loader @babel/core @babel/preset-env --save-dev
module.exports = {
    /* .......... */
    module: {
        rules: [
            /* .......... */
            {
                test: /\.js$/, // js-файлы, транспиляция
                exclude: /node_modules/,
                use: {
                    loader: 'babel-loader',
                    options: {
                        presets: ['@babel/preset-env']
                    }
                }
            }
        ],
    }
}
$ npm run build

9. Загрузчик ts-loader

Позволяет транспилировать ts-код в js-код ES5, чтобы этот код понимали старые браузеры. Создадим файл src/ts/index.ts, который будет новой точкой входа. Изменим файл конфигурации, чтобы обрабатывать ts-файлы, добавим файл конфигурации tsconfig.json и запустим сборку.

$ npm install ts-loader typescript --save-dev
import User from './class/User'

const user: User = new User()
user.hello()
class User {
    name = 'Аноним'

    hello(): void {
        console.log(`Привет, ${this.name}!`)
    }
}

export default User
module.exports = {
    mode: 'development',
    entry: {
        main: path.resolve(__dirname, 'src/ts/index.ts'), // NEW новая точка входа
    },
    output: {
        /* .......... */
    },
    resolve: {
        extensions: ['.ts', '...'], // NEW добавляем поддержку ts-файлов
    },
    plugins: [
        /* .......... */
    ],
    module: {
        rules: [
            /* .......... */
            {
                test: /\.ts$/, // NEW транспиляция ts-файлов
                exclude: /node_modules/,
                use: {
                    loader: 'ts-loader',
                }
            }
        ],
    }
}
{
    "compilerOptions": {
        "target": "es5"
    }
}
$ npm run build

Еще один вариант обработки ts-файлов — вместо ts-loader использовать @babel/preset-typescript.

$ npm install @babel/preset-typescript --save-dev

module.exports = {
    mode: 'development',
    entry: {
        main: path.resolve(__dirname, 'src/ts/index.ts'), // NEW новая точка входа
    },
    output: {
        /* .......... */
    },
    resolve: {
        extensions: ['.ts', '...'], // NEW добавляем поддержку ts-файлов
    },
    plugins: [
        /* .......... */
    ],
    module: {
        rules: [
            /* .......... */
            {
                test: /\.(ts|js)$/, // транспиляция js- и ts-файлов
                exclude: /node_modules/,
                use: {
                    loader: 'babel-loader',
                    options: {
                        // NEW используем @babel/preset-typescript
                        presets: ['@babel/preset-env', '@babel/preset-typescript']
                    }
                }
            },
        ],
    }
}
$ npm run build

10. Файл css-стилей

Сейчас стили добавляются загрузчиком style-loader, который динамически вставляет тег <style> внутрь <head>. Но можно сохранить стили как css-файл и подключить его с помощью <link> внутри <head>.

$ npm install mini-css-extract-plugin --save-dev
/* .......... */
const MiniCssExtractPlugin = require('mini-css-extract-plugin')

module.exports = {
    /* .......... */
    plugins: [
        /* .......... */
        new MiniCssExtractPlugin() // NEW плагин
    ],
    module: {
        rules: [
            /* .......... */
            {
                test: /\.s?css$/, // файл css-стилей
                use: [
                    MiniCssExtractPlugin.loader, // NEW загрузчик
                    'css-loader',
                    {
                        loader: 'postcss-loader',
                        options: {
                            postcssOptions: {
                                plugins: {
                                    'postcss-preset-env': {
                                        browsers: 'last 3 versions',
                                    },
                                },
                            },
                        },
                    },
                    'sass-loader',
                ],
            },
            /* .......... */
        ],
    },
    /* .......... */
}

Сервер разработки

Установим пакет webpack-dev-server, зададим для него настройки в webpack.config.js и добавим команду start в файл package.json.

$ npm install webpack-dev-server --save-dev
module.exports = {
    /* .......... */
    devServer: {
        port: 9000,
        open: true, // открыть браузер
    }
    watchOptions: {
        ignored: /node_modules/, // не отслеживать node_modules
        poll: 1000, // проверять изменения каждую секунду
    },
}
Чаще всего в опции poll нет небходимости, но она позволяет решить проблемы с NFS, VirtualBox и Docker.
{
    ..........
    "scripts": {
        "start": "webpack serve", // запустить сервер, выполянять сборку при изменении файлов
        "build": "webpack build", // выполнить сборку проекта
        "watch": "webpack watch"  // выполянять сборку при изменении файлов
    },
    ..........
}

Если есть несколько точек входа, нужно добавить еще одну настройку, чтобы избежать проблем (см.документацию):

module.exports = {
    /* .......... */
    optimization: {
        runtimeChunk: 'single',
    },
}
$ npm run start

При запуске сервера настройка watch всегда имеет значение true. Вебпак отслеживает файлы в директории проекта (за исключением node_modules) и при изменении — выполняет пересборку. Но сохраняет файлы сборки не в директорию dist, а в виртуальную директорию в оперативной памяти. То есть, в отличие от команд build и watch — директория dist даже не создается. Посмотреть файлы сборки можно, если в браузере перейти по адресу

http://localhost:9000/webpack-dev-server

Впрочем, такое поведение можно изменить и сохранять файлы сборки в директории dist — тогда у нас будет актуальная копия виртуальной директории.

module.exports = {
    /* .......... */
    devServer: {
        port: 9000,
        open: true, // открыть браузер
        devMiddleware: {
            writeToDisk: true, // записывать файлы сборки на диск
        },
    },
    watchOptions: {
        ignored: /node_modules/, // не отслеживать node_modules
        poll: 1000, // проверять изменения каждую секунду
    },
}

Но здесь есть неприятный сюрприз — старые файлы сборки не удаляются, опция output.clean не работает, так что лучше использовать плагин CleanWebpackPlugin. Разработчики вебпак знают об этом баге, но не спешат его исправлять.

Режим сборки

Может быть development или production, до сих пор мы использовали только development. В режиме production вебпак удаляет комментарии и минифицирует код. Давайте будем передавать командам в файле package.json переменную окружения NODE_DEV, а в файле webpack.config.js проверять значение этой переменной. В Windows и Linux переменные окружения устанавливаются по разному, так что установим пакет cross-env.

$ npm install cross-env --save-dev
{
    ..........
    "scripts": {
        "start": "webpack serve",
        "build": "webpack build",
        "watch": "webpack watch",
        "build:prod": "cross-env NODE_ENV=production webpack"
    },
    ..........
}
Передать значение переменной окружения NODE_ENV можно и с помощью опции командной строки --node-env.
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const CopyWebpackPlugin = require('copy-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')

const buildMode = process.env.NODE_ENV === 'production' ? 'production' : 'development'
const outputDir = process.env.NODE_ENV === 'production' ? 'build' : 'dist'
const sourceMap = process.env.NODE_ENV === 'production' ? 'nosources-source-map' : 'eval-source-map'

const loaderCSS = process.env.NODE_ENV === 'production' ? MiniCssExtractPlugin.loader : 'style-loader'

const config = {
    mode: buildMode, // NEW режим сборки
    devtool: sourceMap, // NEW карта кода
    entry: {
        main: path.resolve(__dirname, 'src/ts/index.ts'),
    },
    output: {
        path: path.resolve(__dirname, outputDir), // NEW точка выхода
        filename: '[name].[contenthash].js',
        // здесь будут файлы ресурсов, для которых не задан путь в настройках загрузчика
        assetModuleFilename: 'asset/[hash][ext][query]',
        clean: true
    },
    resolve: {
        extensions: ['.ts', '...'],
    },
    plugins: [
        new HtmlWebpackPlugin({
            title: 'Webpack config example',
            template: path.resolve(__dirname, 'src/index.html'), // файл шаблона
            filename: 'index.html', // выходной файл
        }),
        new CopyWebpackPlugin({
            patterns: [
                {
                    from: path.resolve(__dirname, 'src/favicon.png'),
                    to: path.resolve(__dirname, outputDir) // NEW куда копировать
                },
            ],
        }),
        // NEW еще один плагин для production
    ],
    module: {
        rules: [
            {
                test: /\.txt$/i, // простой текст
                type: 'asset/source'
            },
            {
                test: /\.(png|jpe?g|gif|webp)$/i, // изображение
                type: 'asset',
                parser: {
                    dataUrlCondition: {
                        maxSize: 4096 // ограничение 4kb
                    }
                },
                generator: {
                    filename: 'img/[hash][ext][query]' // все изображения в dist/img или build/img
                }
            },
            {
                test: /\.s?css$/, // файл css-стилей
                use: [
                    loaderCSS, // NEW style или link в head
                    'css-loader',
                    {
                        loader: 'postcss-loader',
                        options: {
                            postcssOptions: {
                                plugins: {
                                    'postcss-preset-env': {
                                        browsers: 'last 3 versions',
                                    },
                                },
                            },
                        },
                    },
                    'sass-loader',
                ],
            },
            {
                test: /\.(woff2?|eot|ttf|otf)$/i, // файлы шрифтов
                type: 'asset/resource',
                generator: {
                    filename: 'font/[hash][ext][query]' // все шрифты в dist/font или build/font
                }
            },
            {
                test: /\.(ts|js)$/, // js и ts файлы, транспиляция
                exclude: /node_modules/,
                use: {
                    loader: 'babel-loader',
                    options: {
                        presets: ['@babel/preset-env', '@babel/preset-typescript']
                    }
                }
            },
        ],
    },
    devServer: {
        port: 9000,
        open: true, // открыть браузер
    },
    watchOptions: {
        ignored: /node_modules/, // не отслеживать node_modules
        poll: 1000, // проверять изменения каждую секунду
    },
}

if (process.env.NODE_ENV === 'production') {
    config.plugins.push(
        new MiniCssExtractPlugin({filename: '[name].[contenthash].css'})
    )
}

module.exports = config

Сборка для production у нас теперь будет в директории build, можно запустить Live Server (см. здесь), чтобы проверить работу сборки (нужно изменить настройки, потому что по умолчанию Live Server обслуживает dist). Но не следует запускать dev-сервер вебпак для обслуживания этой директории — нужно помнить, что он обслуживает виртуальную директорию.

Сейчас отличий при сборке в режиме development и production немного, но это учебный проект. В реальной жизни отличий может быть очень много, и код в webpack.config.js может быть весьма запутанным. Но можно создать файлы webpack.development.js и webpack.production.js и передавать имя файла конфигурации в командной строке.

{
    ..........
    "scripts": {
        "build": "webpack build --config webpack.development.js",
        "build:prod": "webpack build --config webpack.production.js"
    },
    ..........
}

Но тут тоже есть подводные камни — многие опции не отличаются для development и production. Так что при изменении такой опции — изменять нужно сразу в двух местах. Но есть решение и на этот случай — вынести такие опции в отдельный файл webpack.common.js и объединять файлы конфигурации.

$ npm install webpack-merge --save-dev
// файл webpack.config.js
const { merge } = require('webpack-merge')
const commonConfig = require('./webpack.common.js')

module.exports = (env, argv) => {
    const buildMode = process.env.NODE_ENV === 'production' ? 'production' : 'development'
    const modeConfig = require(`./webpack.${buildMode}.js`)
    return merge(commonConfig, modeConfig)
}
// файл webpack.common.js
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')

module.exports = {
    entry: {
        main: path.resolve(__dirname, 'src/ts/index.ts'),
    },
    resolve: {
        extensions: ['.ts', '...'],
    },
    plugins: [
        new HtmlWebpackPlugin({
            title: 'Webpack config example',
            template: path.resolve(__dirname, 'src/index.html'), // файл шаблона
            filename: 'index.html', // выходной файл
        }),
    ],
    module: {
        rules: [
            {
                test: /\.txt$/i, // простой текст
                type: 'asset/source'
            },
            {
                test: /\.(png|jpe?g|gif|webp)$/i, // изображение
                type: 'asset',
                parser: {
                    dataUrlCondition: {
                        maxSize: 4096 // ограничение 4kb
                    }
                },
                generator: {
                    filename: 'img/[hash][ext][query]' // все изображения в dist/img или build/img
                }
            },
            {
                test: /\.(woff2?|eot|ttf|otf)$/i, // файлы шрифтов
                type: 'asset/resource',
                generator: {
                    filename: 'font/[hash][ext][query]' // все шрифты в dist/font или build/font
                }
            },
            {
                test: /\.(ts|js)$/, // js и ts файлы, транспиляция
                exclude: /node_modules/,
                use: {
                    loader: 'babel-loader',
                    options: {
                        presets: ['@babel/preset-env', '@babel/preset-typescript']
                    }
                }
            },
        ],
    },
}
// файл webpack.development.js
const path = require('path')
const CopyWebpackPlugin = require('copy-webpack-plugin')

module.exports = {
    mode: 'development',
    devtool: 'eval-source-map',
    output: {
        path: path.resolve(__dirname, 'dist'), // директория dist
        filename: '[name].[contenthash].js',
        assetModuleFilename: 'asset/[hash][ext][query]',
        clean: true
    },
    plugins: [
        new CopyWebpackPlugin({
            patterns: [
                {
                    from: path.resolve(__dirname, 'src/favicon.png'),
                    to: path.resolve(__dirname, 'dist') // директория dist
                },
            ],
        }),
    ],
    module: {
        rules: [
            {
                test: /\.s?css$/, // файл css-стилей
                use: [
                    'style-loader', // <style> внутри <head>
                    'css-loader',
                    {
                        loader: 'postcss-loader',
                        options: {
                            postcssOptions: {
                                plugins: {
                                    'postcss-preset-env': {
                                        browsers: 'last 3 versions',
                                    },
                                },
                            },
                        },
                    },
                    'sass-loader',
                ],
            },
        ]
    },
    devServer: {
        port: 9000,
        open: true, // открыть браузер
    },
    watchOptions: {
        ignored: /node_modules/, // не отслеживать node_modules
        poll: 1000, // проверять изменения каждую секунду
    },
}
// файл webpack.production.js
const path = require('path')
const CopyWebpackPlugin = require('copy-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')

module.exports = {
    mode: 'production',
    devtool: 'nosources-source-map',
    output: {
        path: path.resolve(__dirname, 'build'), // директория build
        filename: '[name].[contenthash].js',
        assetModuleFilename: 'asset/[hash][ext][query]',
        clean: true
    },
    plugins: [
        new CopyWebpackPlugin({
            patterns: [
                {
                    from: path.resolve(__dirname, 'src/favicon.png'),
                    to: path.resolve(__dirname, 'build') // директория build
                },
            ],
        }),
        new MiniCssExtractPlugin({ // <link> внутри <head>
            filename: '[name].[contenthash].css'
        })
    ],
    module: {
        rules: [
            {
                test: /\.s?css$/, // файл css-стилей
                use: [
                    MiniCssExtractPlugin.loader, // <link> внутри <head>
                    'css-loader',
                    {
                        loader: 'postcss-loader',
                        options: {
                            postcssOptions: {
                                plugins: {
                                    'postcss-preset-env': {
                                        browsers: 'last 3 versions',
                                    },
                                },
                            },
                        },
                    },
                    'sass-loader',
                ],
            },
        ]
    },
}

Вебпак будет по-прежнему искать файл конфигурации по умолчанию webpack.config.js, но поскольку мы устанавливаем значение NODE_ENV — будет объединение конфигурации из webpack.common.js с конфигурацией из webpack.development.js или webpack.production.js.

Исходные коды здесь.

Поиск: Frontend • JavaScript • Node.js • Web-разработка • Конфигурация • Настройка • Установка • Шаблон сайта • Webpack • Сборка

Каталог оборудования
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
Производители
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
Функциональные группы
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.