Простой статический сайт на 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 (стили страниц). Это удобно для больших проектов.
Готовый шаблон стартового проекта для верстки можно найти в репозитории. Там же есть примеры использования и дополнительная документация.