Простой статический сайт на Webpack 5
Давно уже не верстал статичные сайты, но тут появилась халтурка от хороших людей. Отказать не смог, вручную верстать уже не тот вайб, а старые сборки которые у меня были, они уже совсем устарели, хотя и на них можно сделать, как в старые добрые времена. Решил задаться вопросом, попробовать чтото более новое, может чтото уже появилось? Пошло поехало, начал собирать простой статический сайт из нескольких HTML-страниц. Хотелось использовать современные инструменты, но без лишних сложностей вроде React или Vue, т.к. они не подходили под задачу, верстку под битрикс. В итоге остановился на Webpack 5 — он отлично справляется с такой задачей.
В этой статье хотел бы рассказать, как сделать сборку статического сайта с помощью Webpack 5. Проект собирает статичные HTML страницы, компилирует SASS в CSS, объединяет JavaScript-файлы и автоматически создаёт SVG-спрайты для иконок.
Что мы получим в итоге
В результате получится проект, который:
собирает несколько HTML страниц из шаблонов с общими header и footer
компилирует SASS/SCSS в один CSS-файл
объединяет JavaScript-код и библиотеки в один bundle
создаёт inline SVG-спрайт для иконок
минифицирует код для production
Сборка работает на последней версии webpack 5, который на данный момент является стандартом для сборки фронтенд-проектов под статичную верстку.
Структура проекта
Структуру папок выглядит так:
. ├── dist - папка с собранным сайтом ├── src - исходники │ ├── favicon - иконки сайта │ ├── fonts - шрифты │ ├── html - HTML-шаблоны │ │ ├── includes - общие части (header, footer) │ │ └── views - страницы сайта │ ├── icons - SVG-иконки для спрайта │ ├── img - изображения │ ├── js - JavaScript-файлы │ ├── scss - стили SASS/SCSS │ └── uploads - дополнительные файлы ├── package.json └── webpack.config.js
Папка icons нужна специально для SVG-иконок, которые будут автоматически собираться в спрайт. Обычные изображения идут в img.
1 шаг. Начальная настройка
Создаём новый проект и инициализируем его:
npm init
Терминал может предлагать варианты настройки, я выбрал все по умолчанию. После этого устанавливаем базовые пакеты Webpack:
npm install webpack webpack-cli webpack-dev-server --save-dev
Теперь у нас в package.json появились зависимости. Теперь у нас появились нужные зависимости, приступим к настройке сборки.
Сборка JavaScript
Начнём с JavaScript, так как это основа Webpack. В проекте я буду использовать Bootstrap 5, jQuery и Popper.js, т.к. это является основополагающим для верстки(можно обойтись только Jquery) поэтому установим их:
npm install jquery --save npm install bootstrap @popperjs/core --save
Обращу внимание, что эти пакеты нужны для самого сайта, поэтому ставим их с флагом --save, а не --save-dev(только для разработки).
Теперь создадим файл webpack.config.js с базовой конфигурацией:
const path = require("path"); module.exports = { entry: ["./src/js/index.js", "./src/scss/style.scss"], output: { path: path.resolve(__dirname, "dist"), filename: "js/bundle.js", clean: true, }, devtool: "source-map", };
В entry мы указываем пути по которым подключаются - главный JS-файл и главный SCSS-файл. Указываем параметр clean: true чтобы автоматически очистить папку dist перед каждой сборкой.
В src/js/index.js подключаем библиотеки или статичные файлы JS:
import "bootstrap"; import "./static-js";
Здесь подключаем Bootstrap и наш собственный JavaScript-код. Если нужен jQuery глобально, можно добавить:
import $ from "jquery"; window.$ = window.jQuery = $;
Сборка стилей CSS из SASS
Для работы с SASS нужны несколько пакетов:
npm install sass sass-loader css-loader mini-css-extract-plugin --save-dev
mini-css-extract-plugin — это современная замена старому extract-text-webpack-plugin. Он извлекает CSS в отдельный файл, что удобно для многостраничных сайтов.
Добавим в конфиг вебпака webpack.config.js:
const path = require("path"); const MiniCssExtractPlugin = require("mini-css-extract-plugin"); module.exports = { entry: ["./src/js/index.js", "./src/scss/style.scss"], output: { path: path.resolve(__dirname, "dist"), filename: "js/bundle.js", clean: true, }, devtool: "source-map", module: { rules: [ { test: /\.(scss|sass)$/, include: path.resolve(__dirname, "src/scss"), use: [ MiniCssExtractPlugin.loader, { loader: "css-loader", options: { sourceMap: true, url: false } }, { loader: "sass-loader", options: { sourceMap: true } }, ], }, ], }, plugins: [ new MiniCssExtractPlugin({ filename: "css/style.bundle.css" }), ], };
Устанавливаем параметр url: false в css-loader чтобы не обрабатывать пути к файлам в CSS (шрифты, изображения). Это удобно, когда файлы копируются отдельно через CopyPlugin.
Файл src/scss/style.scss является главным, где подключаются все стили разбитые на группы, чтобы проще было ориентироваться:
// =================== Modules ======================== @import 'modules/reset'; @import 'utilities/variables'; @import 'utilities/mixins'; @import 'utilities/utils'; @import 'modules/mixin_font-face'; @import 'modules/fontstylesheet'; // ==================== Plugins ======================= // Bootstrap @import '../../node_modules/bootstrap/scss/mixins/breakpoints'; @import '../../node_modules/bootstrap/scss/bootstrap-grid'; // ==================== Default ======================= @import "elements/ui"; @import "elements/typography"; @import "elements/buttons"; @import "elements/inputs"; @import "elements/forms"; @import "elements/icons"; @import "layout/general"; // ==================== components ====================== @import 'components/header'; @import 'components/footer'; @import 'components/modals'; @import 'components/main-info-boxes'; // ==================== Pages ====================== @import "pages/pages"; // ===================== Media ======================== @import "modules/print";
SVG-спрайты для иконок
Одна из крутых фишек этого проекта — автоматическое создание SVG-спрайта. Вместо того чтобы вручную собирать иконки в один файл, просто кладём SVG-файлы в папку src/icons, и они автоматически попадут в inline-спрайт, удобно и практично изменять цвета иконок при каких то событиях
Установим нужный лоадер:
npm install svg-sprite-loader --save-dev
Добавляем правило в webpack.config.js для svg sprite:
{ test: /\.svg$/, include: path.resolve(__dirname, "src/icons"), use: [ { loader: "svg-sprite-loader", options: { symbolId: "icon-[name]", }, }, ], },
префикс "icon-" можно убрать чтобы обращаться сразу по имени файла svg.
Важно: это правило применяется только к файлам из папки src/icons. Остальные SVG обрабатываются как обычные изображения.
В src/js/index.js добавляем автоматический импорт всех иконок:
const importAll = (r) => r.keys().forEach(r); importAll(require.context("../icons", false, /\.svg$/));
Теперь все SVG из папки icons автоматически попадут в спрайт. Использовать их можно так:
<svg class="icon"> <use xlink:href="#icon-logo"></use> </svg>
Имена иконок формируются как icon-<имя-файла>. Например, файл logo.svg станет #icon-logo.
Сборка HTML-страниц
Для сборки HTML используем html-webpack-plugin. Он умеет работать с шаблонами и автоматически подставлять пути к CSS и JS.
npm install html-webpack-plugin raw-loader --save-dev
raw-loader нужен для интеграции частей шаблонов например header или footer.
В проекте используется lodash-шаблонизатор.
Вот пример страницы src/html/views/index.html:
<% var data = { title: "Главная страница", copyright: "2025" }; %> <%= _.template(require('./../includes/header.html').default)(data) %> <div class="container"> <h1>Контент страницы</h1> </div> <%= _.template(require('./../includes/footer.html').default)(data) %>
В includes/header.html содержится html верстка шапки сайта и если нужно внести какието изменения в шапку сайта, то мы вносим их в одном только файле и она меняется на всем сайте.
В header.html можно использовать переменные из data:
<!doctype html> <html lang="ru"> <head> <meta charset="utf-8"> <title><%=title%></title> </head> <body>
Чтобы автоматически генерировать HTML для всех страниц из папки views, добавляем функцию в webpack.config.js:
const fs = require("fs"); const HtmlWebpackPlugin = require("html-webpack-plugin"); function generateHtmlPlugins(templateDir) { const templateFiles = fs.readdirSync(path.resolve(__dirname, templateDir)); return templateFiles.map((item) => { const parsedPath = path.parse(item); const name = parsedPath.name; const extension = parsedPath.ext.substring(1); return new HtmlWebpackPlugin({ filename: `${name}.html`, template: path.resolve(__dirname, `${templateDir}/${name}.${extension}`), inject: true, scriptLoading: "blocking", }); }); } const htmlPlugins = generateHtmlPlugins("src/html/views");
И добавляем правило для обработки includes:
{ test: /\.html$/, include: path.resolve(__dirname, "src/html/includes"), use: ["raw-loader"], },
В plugins добавляем:
plugins: [ // ... другие плагины ].concat(htmlPlugins),
Теперь каждая страница из src/html/views автоматически соберётся в отдельный HTML-файл.
Копирование статических файлов
При сборке production версии, мы должны скопировать изображений, шрифты и другие файлы, для этого используем copy-webpack-plugin:
npm install copy-webpack-plugin --save-dev
В конфиг webpack добавляем:
const CopyPlugin = require("copy-webpack-plugin"); // В plugins: new CopyPlugin({ patterns: [ { from: "src/fonts", to: "fonts", noErrorOnMissing: true }, { from: "src/favicon", to: "favicon", noErrorOnMissing: true }, { from: "src/img", to: "img", noErrorOnMissing: true }, { from: "src/uploads", to: "uploads", noErrorOnMissing: true }, ], }),
noErrorOnMissing: true означает, что если папка отсутствует, ошибки не появится.
Обработка изображений
Для обработки изображений (кроме SVG из папки icons) добавим правило:
{ test: /\.(png|jpe?g|gif|svg|webp|avif)$/i, exclude: path.resolve(__dirname, "src/icons"), type: "asset", parser: { dataUrlCondition: { maxSize: 8 * 1024 } }, generator: { filename: "img/[name][ext]" }, },
Файлы меньше 8 КБ будут встроены в код как base64, остальные скопируются в dist/img.
Шрифты:
{ test: /\.(woff|woff2|eot|ttf|otf)$/i, type: "asset/resource", generator: { filename: "fonts/[name][ext]" }, },
Финальная стадия, оптимизация для production
Для production-сборки добавим минификацию CSS и JS. Установим плагины:
npm install css-minimizer-webpack-plugin terser-webpack-plugin --save-dev
В конфиге нужно добавить для оптимизации production сборки:
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin"); const TerserPlugin = require("terser-webpack-plugin"); module.exports = (env, argv) => { const mode = argv.mode || "development"; const config = { // ... базовая конфигурация }; if (mode === "production") { config.optimization = { minimize: true, minimizer: [ new CssMinimizerPlugin({ minimizerOptions: { preset: ["default", { discardComments: { removeAll: true } }], }, }), new TerserPlugin({ extractComments: true, terserOptions: { compress: { drop_console: true } }, }), ], splitChunks: { chunks: "all", cacheGroups: { vendor: { test: /[\\/]node_modules[\\/]/, name: "vendors", chunks: "all", }, }, }, }; } return config; };
В production-режиме код сжимается, минифицируется, удаляются комментарии и убираются console.log. Разделяем код на chunks для лучшего кеширования.
Настройка dev-сервера для разработки
Для удобной разработки настраиваем dev-сервер:
devServer: { static: { directory: path.join(__dirname, "dist") }, port: 9000, hot: true, open: true, watchFiles: ["src/**/*"], },
Сервер запускается на порту 9000(можете указать свой порт), автоматически открывает браузер и следит за изменениями файлов.
Донастраиваем package.json
Добавляем удобные команды:
{ "scripts": { "dev": "webpack --mode development", "watch": "webpack --mode development --watch", "start": "webpack serve --no-client-overlay-warnings --open", "build": "webpack --mode production && prettier --print-width=120 --parser html --write dist/*.html" } }
npm run dev— однократная сборка в режиме разработкиnpm run watch— сборка с отслеживанием измененийnpm start— запуск dev-сервера с автоматическим открытием сайта в браузереnpm run build— production-сборка с форматированием HTML через Prettier
Итоговая структура webpack.config.js
Вот так выглядит полный конфиг:
const path = require("path"); const fs = require("fs"); const CopyPlugin = require("copy-webpack-plugin"); const HtmlWebpackPlugin = require("html-webpack-plugin"); const MiniCssExtractPlugin = require("mini-css-extract-plugin"); const CssMinimizerPlugin = require("css-minimizer-webpack-plugin"); const TerserPlugin = require("terser-webpack-plugin"); function generateHtmlPlugins(templateDir) { const templateFiles = fs.readdirSync(path.resolve(__dirname, templateDir)); return templateFiles.map((item) => { const parsedPath = path.parse(item); const name = parsedPath.name; const extension = parsedPath.ext.substring(1); return new HtmlWebpackPlugin({ filename: `${name}.html`, template: path.resolve(__dirname, `${templateDir}/${name}.${extension}`), inject: true, scriptLoading: "blocking", }); }); } const htmlPlugins = generateHtmlPlugins("src/html/views"); const baseConfig = { entry: ["./src/js/index.js", "./src/scss/style.scss"], output: { path: path.resolve(__dirname, "dist"), filename: "js/bundle.js", clean: true, assetModuleFilename: "assets/[name][ext]", }, devtool: "source-map", devServer: { static: { directory: path.join(__dirname, "dist") }, port: 9000, hot: true, open: true, watchFiles: ["src/**/*"], }, module: { rules: [ { test: /\.(scss|sass)$/, include: path.resolve(__dirname, "src/scss"), use: [ MiniCssExtractPlugin.loader, { loader: "css-loader", options: { sourceMap: true, url: false } }, { loader: "sass-loader", options: { sourceMap: true } }, ], }, { test: /\.html$/, include: path.resolve(__dirname, "src/html/includes"), use: ["raw-loader"], }, { test: /\.(png|jpe?g|gif|svg|webp|avif)$/i, exclude: path.resolve(__dirname, "src/icons"), type: "asset", parser: { dataUrlCondition: { maxSize: 8 * 1024 } }, generator: { filename: "img/[name][ext]" }, }, { test: /\.svg$/, include: path.resolve(__dirname, "src/icons"), use: [ { loader: "svg-sprite-loader", options: { symbolId: "icon-[name]", }, }, ], }, { test: /\.(woff|woff2|eot|ttf|otf)$/i, type: "asset/resource", generator: { filename: "fonts/[name][ext]" }, }, ], }, plugins: [ new MiniCssExtractPlugin({ filename: "css/style.bundle.css" }), new CopyPlugin({ patterns: [ { from: "src/fonts", to: "fonts", noErrorOnMissing: true }, { from: "src/favicon", to: "favicon", noErrorOnMissing: true }, { from: "src/img", to: "img", noErrorOnMissing: true }, { from: "src/uploads", to: "uploads", noErrorOnMissing: true }, ], }), ].concat(htmlPlugins), }; module.exports = (env, argv) => { const mode = argv.mode || "development"; baseConfig.mode = mode; if (mode === "production") { baseConfig.devtool = "source-map"; baseConfig.output.filename = "js/[name].js"; baseConfig.optimization = { minimize: true, minimizer: [ new CssMinimizerPlugin({ minimizerOptions: { preset: ["default", { discardComments: { removeAll: true } }], }, }), new TerserPlugin({ extractComments: true, terserOptions: { compress: { drop_console: true } }, }), ], splitChunks: { chunks: "all", cacheGroups: { vendor: { test: /[\\/]node_modules[\\/]/, name: "vendors", chunks: "all", }, }, }, runtimeChunk: "single", moduleIds: "named", chunkIds: "named", }; } else { baseConfig.devtool = "eval-source-map"; baseConfig.output.filename = "js/bundle.js"; baseConfig.optimization = { minimize: false, splitChunks: false, runtimeChunk: false, }; } return baseConfig; }; ```</spoiler> ## Что в итоге получилось В итоге мы получили удобный и быстрый инструмент для сборки статических сайтов. Хотел бы отметить его плюсы для скорости разработки: - собирает HTML-страницы из шаблонов и можно подключить отдельно модули, детали сайта(header, footer, модальные окна) которые будут использованы на всех остальных страницах. - компилирует и оптимизирует SASS в CSS - объединять минифицирует JavaScript - автоматически создает SVG-спрайт - копирует статические файлы для финальной сборки - минификация и очистка от ненужных комментариев и console.log код для production Всё это работает на Webpack 5 с современными подходами. Данную сборку можно взять как за основу для верстки. ## Полезные мелочи **SASS-миксины для медиазапросов.** В проекте есть удобные миксины для работы с брейкпоинтами, медиа эндпоинты можно найти в scss/utilities/_variables.sass:
$xss: 360
$xs: 450
$sm: 600
$md: 768
$lg: 1023
$xxl: 1160
$xl: 1200
$hd: 1440
```sass =r($width) @media only screen and (max-width: $width + "px") @content =rmin($width) @media only screen and (min-width: $width + "px") @content
Использование:
.block font-size: 14px +r($md) font-size: 16px +rmin(768) font-size: 16px
Модульная структура SASS. Стили разбиты на логические части: utilities (переменные, миксины), elements (кнопки, формы), components (header, footer), pages (стили страниц). Это удобно для больших проектов.
Готовый шаблон стартового проекта для верстки можно найти в репозитории. Там же есть примеры использования и дополнительная документация.