Серьёзно и профессионально я начал заниматься вёрсткой в 2019 году, хотя до этого ещё со школы интересовался данной темой как любитель. Поэтому новичком мне себя назвать сложно, но и профессионалом с опытом 5+ лет я тоже не являюсь. Тем не менее, я успел познакомиться со сборщиком Gulp, его плагинами и сделал для себя хорошую, как по мне, сборку для работы. О её возможностях сегодня и расскажу.

ВАЖНО! В этой статье речь пойдёт о самой последней версии сборки. Если вы пользуетесь версиями сборки, вышедшими до публикации этой статьи, информация будет для вас не релевантна, но полезна.

Какие задачи решает эта сборка?

  • вёрстка компонентами (вам не нужно в каждую страницу копировать head, header, footer и другие повторяющиеся элементы, вплоть до кнопок или кастомных чекбоксов);

  • вёрстка с препроцессорами (SASS/SCSS);

  • конвертация шрифтов из ttf в eot, woff, woff2;

  • лёгкое (почти автоматическое) подключение шрифтов;

  • лёгкое (почти автоматическое) создание псевдоэлементов-иконок;

  • обработка изображений "на лету";

  • минификация html/css/js файлов;

  • возможность вёрстки с использованием php;

  • выгрузка файлов на хостинг по FTP;

  • несколько мелких задач с помощью миксинов.

Для тех, кому лень читать и делать всё руками - сразу ссылка на сборку.

Собственно создание сборки

Начнём собирать нашу сборку (простите за тавтологию). Предварительно нам потребуется уже установленная на компьютере LTS-версия Node.js и NPM (входит в пакет Node.js) либо Yarn. Для нашей задачи не имеет значения, какой из этих пакетных менеджеров использовать, однако я буду объяснять на примере NPM, соответственно, для Yarn вам потребуется нагуглить аналоги NPM-команд.

Первое, что нам нужно сделать - это инициализировать проект. Открываем директорию проекта в командной строке (очень надеюсь, вы знаете, как это делается) и вводим команду npm init.

После этого npm задаст нам несколько стандартных вопросов по типу названия проекта, автора, версии и т.д... Отвечаем на них как душе угодно. Для нашей задачи это не имеет никакого значения.

Далее будет намного удобнее работать через Visual Studio Code (поскольку у него есть встроенный терминал) или любой другой удобный вам редактор + терминал.

Прежде всего, нам нужно установить сам Gulp. Делается это двумя командами npm i gulp -global - устанавливаем Gulp глобально на систему и npm i gulp --save-dev - устанавливаем Gulp локально в проект. Ключ --save здесь отвечает за сохранение версии плагина при дальнейшей установке (без него вам может установить более новую, несовместимую с другими плагинами версию), а ключ -dev указывает на то, что этот пакет необходим только во время разработки проекта, а не во время его выполнения. Например, если мы устанавливаем в проект пакет Swiper, который содержит скрипты слайдера и будет отображаться на странице, мы будем устанавливать его без ключа -dev, поскольку он нужен для выполнения, а не для разработки.

После того, как Gulp установился, имеет смысл создать в корне проекта управляющий файл gulpfile.js, в котором мы и будем писать задачи для сборщика.

После этого нам нужно подключить Gulp в нашем файле, для того чтобы он исполнялся. Это делается с помощью require:

const gulp = require('gulp');

Далее, для каждой задачи будем использовать модули в отдельных файлах. Для того, чтобы не подключать каждый модуль отдельно, нужно установить и подключить плагин require-dir. Устанавливается он всё той же командой (как и все последующие плагины, поэтому далее повторяться не буду, просто знайте, что установить - это npm i $PLUGIN-NAME$ --save-dev). После установки подключаем его и прописываем путь к директории, в которую будем складывать модули (у меня это директория tasks):

const gulp = require('gulp');

const requireDir = require('require-dir');
const tasks = requireDir('./tasks');

Первая задача

Давайте проверим, всё ли мы правильно сделали. Создадим в директории tasks файл модуля с именем hello.js. В созданном файле напишем простейшую функцию, которая будет выводить в консоль строку "Hello Gulp!" (можете придумать что-то менее банальное, если хотите).

module.exports = function hello () {
	console.log("Hello Gulp!");
}

Теперь вернёмся в gulpfile.js и зададим там задачу hello:

const gulp = require('gulp');

const requireDir = require('require-dir');
const tasks = requireDir('./tasks');

exports.hello = tasks.hello;

Теперь командой gulp hello в терминале запустим нашу задачу. Если всё сделано правильно - в терминал должно вывестись приблизительно такое сообщение:

[13:17:15] Using gulpfile D:\Web projects\Easy-webdev-startpack-new\gulpfile.js
[13:17:15] Starting 'hello'...
Hello Gulp!
[13:17:15] The following tasks did not complete: hello
[13:17:15] Did you forget to signal async completion?

Так же, можно получить список всех заданных задач командой gulp --tasks.

Файловая структура

Теперь, когда мы создали первую функцию, можно подумать о том, какая структура будет у наших проектов. Я предпочитаю использовать директорию (папку) /src для хранения исходных файлов и директорию /build для готовых файлов проекта.

В директории src/ нам понадобятся следующие поддиректории:
  • components/ - директория для компонентов

  • components/bem-blocks/ - директория для БЭМ-блоков

  • components/page-blocks/ - директория для типовых блоков страницы, таких как хедер, футер и т.п.

  • fonts/ - директория для шрифтов

  • img/ - директория для изображений

  • js/ - директория для файлов JavaScript

  • scss/ - директория для файлов стилей

  • scss/base/ - директория для базовых стилей, которые мы изменять не будем

  • svg/ - директория для файлов SVG

  • svg/css/ - директория для SVG-файлов, которые будут интегрироваться в CSS

Получиться в итоге должно приблизительно следующее:

ВАЖНО: в пустых директориях, таких как img/, fonts/ и т.п. перед тем как пушить в удалённый репозиторий (например на Github) нужно создать пустые файлы с именем .gitkeep. Это нужно для того, чтобы Github не удалил пустые директории из сборки.

Добавление задач и настройка плагинов

Основной целью Gulp является автоматизация рутинных действий в разработке, которые мы можем запрограммировать с помощью задач и плагинов. Первую тестовую задачу мы с вами уже написали. Давайте теперь напишем настоящие задачи, которые будут нам помогать в работе.


Задачи стилей

Для работы с scss нам нужно будет установить некоторые плагины, которые будут обрабатывать и компилировать наш scss код в готовый css. Прежде всего, установим сам плагин Gulp-sass, который будет компилировать scss файлы в обычный css, понятный браузеру. Так же, для того, чтобы scss-файлы можно было импортировать не по одному, а целыми директориями нам понадобится gulp-sass-bulk-importer, для автоматической расстановки префиксов - gulp-autoprefixer, для очистки лишнего css - gulp-clean-css и для конкатинации ("склеивания" файлов вместе) - gulp-concat. Так же, для того, чтобы в DevTools было понятно, из какого файла взялись стили, установим gulp-sourcemaps. Можно каждый раз не прописывать команду npm i в терминале, а указать перечень устанавливаемых плагинов через пробел, но ключи тогда нужно будет указать перед названиями плагинов: npm i --save-dev gulp-sass gulp-sass-bulk-importer gulp-autoprefixer gulp-clean-css gulp-concat gulp-sourcemaps

Теперь создадим в директории tasks/ файл модуля, в котором опишем, что нужно делать галпу с scss-файлами. От gulp нам понадобятся src и dest, а остальные плагины подключим полностью:

const {
	src,
	dest
} = require('gulp');
const sass = require('gulp-sass');
const bulk = require('gulp-sass-bulk-importer');
const prefixer = require('gulp-autoprefixer');
const clean = require('gulp-clean-css');
const concat = require('gulp-concat');
const map = require('gulp-sourcemaps');

Далее экспортируем функцию:

module.exports = function style() {
	return
}

В ней-то мы и будем обрабатывать наши scss файлы. Gulp выполняет последовательность действий .pipe над объектами, указанными в модуле src. Это похоже на конвейер, проходя по которому, код или файлы, которые мы создали в директории src/, превращаются в то, что мы хотим видеть в итоге и складываются в директорию build/.

Давайте определим порядок действий:

  1. Взять файлы scss из директорий scss/

  2. Инициализировать карту исходных файлов (sourcepams)

  3. Скомпилировать в css

  4. Расставить вендорные префиксы

  5. Очистить от лишнего

  6. Склеить в единый файл style.css

  7. Записать карту исходных файлов в получившемся файле

  8. Положить его в build

Итогом (return) нашей функции как раз и будет результат всей последовательности действий, которые мы определили. Это и запишем:

	return src('src/scss/**/*.scss')
		.pipe(map.init())
		.pipe(bulk())
		.pipe(sass())
		.pipe(prefixer())
		.pipe(clean())
		.pipe(concat('style.min.css'))
		.pipe(map.write('../sourcemaps/'))
		.pipe(dest('build/css/'))
Пояснение каждой строки кода отдельно

src('src/scss/**/*.scss') - определяем источник исходного кода (source)

.pipe(map.init()) - инициализируем маппинг, чтобы он отслеживал включаемые файлы

.pipe(bulk()) - проводим код через плагин, который ползволяет использовать директиву @include в scss для директорий, а не только для отдельных файлов

.pipe(sass()) - проводим код через сам компиллятор sass

.pipe(prefixer()) - проводим код через префиксер, который расставит вендорные префиксы

.pipe(clean()) - проводим код через "очиститель" от лишнего css

.pipe(concat('style.min.css')) - склеиваем все исходные файлы в один

.pipe(map.write('../sourcemaps/')) - записываем "карту" источников полученного файла

.pipe(dest('build/css/')) - кладём итоговый файл в директорию

Единственное, чего нам здесь не хватает - настроек и методов для некоторых плагинов. Настройки для плагинов задаются в виде объектов, которые передаются аргументом в функцию плагина. Звучит страшно, а на деле выглядит примерно так: .pipe(sass({outputStyle: 'compressed'}). Для sass я определяю степень сжатия выходного css как compressed, а так же добавляю на событие error логирование ошибки, чтобы было понятно, что не так (если вдруг). И того получаем такой пайп:

.pipe(sass({
  outputStyle: 'compressed'
}).on('error', sass.logError))

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

.pipe(prefixer({
  overrideBrowserslist: ['last 8 versions'],
  browsers: [
    'Android >= 4',
    'Chrome >= 20',
    'Firefox >= 24',
    'Explorer >= 11',
    'iOS >= 6',
    'Opera >= 12',
    'Safari >= 6',
  ],
}))

С клинером вообще всё просто: выставляю уровень очистки (level) в значение 2 и готово.

Что такое bs и для чего он нужен я опишу ниже, в соответствующем разделе.

Как итог, в файле tasks/style.js у нас будет следующее:
const {
	src,
	dest
} = require('gulp');
const sass = require('gulp-sass');
const bulk = require('gulp-sass-bulk-importer');
const prefixer = require('gulp-autoprefixer');
const clean = require('gulp-clean-css');
const concat = require('gulp-concat');
const map = require('gulp-sourcemaps');
const bs = require('browser-sync');

module.exports = function style() {
	return src('src/scss/**/*.scss')
		.pipe(map.init())
		.pipe(bulk())
		.pipe(sass({
			outputStyle: 'compressed'
		}).on('error', sass.logError))
		.pipe(prefixer({
			overrideBrowserslist: ['last 8 versions'],
			browsers: [
				'Android >= 4',
				'Chrome >= 20',
				'Firefox >= 24',
				'Explorer >= 11',
				'iOS >= 6',
				'Opera >= 12',
				'Safari >= 6',
			],
		}))
		.pipe(clean({
			level: 2
		}))
		.pipe(concat('style.min.css'))
		.pipe(map.write('../sourcemaps/'))
		.pipe(dest('build/css/'))
    .pipe(bs.stream())
}

Приблизитекльно аналогичным образом поступаем со стилями библиотек и плагинов, которые будем подключать к будущим проектам. Создаём константу plugins, в которой у нас будет массив файлов-источников из node_modules const plugins = []; . Путь к файлам стилей будем писать в кавычках через запятую - получится массив строк с путями к файлам плагинов. Подключаем в файл gulp.src и gulp.dest, плагины gulp-concat и gulp-sourcemaps аналогично предыдущей задаче и прописываем наш "конвейер":

  1. Взять файлы из источников (в данном случае - из константы)

  2. Инициализировать маппинг

  3. Прогнать всё через sass

  4. Подчистить неиспользуемый css (в библиотеках это особенно важно)

  5. Сконкатенировать файлы в один

  6. Записать маппинг

  7. Сложить в директорию build/css

Но тут есть одна особенность - добавим условие, если длина массива plugins будет равна нулю - эта функция и вернёт коллбэк done(), который просто выдаст сообщение в терминал, что плагинов нет без выполнения других действий. Без этого, наша функция будет ругаться на отсутствие плагинов. А они нужны далеко не всегда.

А так же нам понадобится плагин chalk для раскрашивания сообщения в консоли, чтобы мы обратили на неё внимание. Принципиально этот плагин не будет влиять на работу задачи, только разукрашивать консоль, примерно так:

В результате, файл tasks/libs_style.js будет выглядеть так:
const plugins = [];

const {
	src,
	dest
} = require('gulp');
const sass = require('gulp-sass');
const concat = require('gulp-concat');
const map = require('gulp-sourcemaps');
const chalk = require('chalk');

module.exports = function libs_style(done) {
	if (plugins.length > 0) {
		return src(plugins)
			.pipe(map.init())
			.pipe(sass({
				outputStyle: 'compressed'
			}).on('error', sass.logError))
			.pipe(concat('libs.min.css'))
			.pipe(map.write('../sourcemaps/'))
			.pipe(dest('build/css/'))
	} else {
		return done(console.log(chalk.redBright('No added CSS/SCSS plugins')));
	}
}

Задачи для JavaScript

C JavaScript всё будет немного сложнее. Для разработки нам понадобится чистый, не редактированный и не минимизированный код. При этом, в готовом проекте мы будем прогонять код через babel, который не только приводит код к стандарту ES5, но и скоращает его, путём замены имён переменных и функций на более короткие (одно-двух символьные). Поэтому, нам потребуется три разные задачи: для нашего JS-кода, для плагинов/библиотек и для билда.

Для обработки JS нам понадобится gulp-uglify-es - для минификации JS-кода и gulp-babel для оптимизации. С остальными плагинами, которыми мы будем обрабатывать наш код вы уже знакомы.

Для начала, опишем процесс работы с кодом:

  1. Определить источники (sources)

  2. Инициализировать маппинг

  3. Склеить в один файл

  4. Минифицировать полученны файл

  5. Записать источники в файл

  6. Положить итоговый файл в build/js/

Для итоговой (билдовой) версии после 4 пункта мы ещё прогоним его через babel для оптимизации. И для этого нам понадобится установить @babel/core , а так же @babel/preset-env. После установки, в корне проекта нужно будет создать файл .babelrc , который указывает на пресет настроек Babel со следующим содержимым:

{
  "presets": ["@babel/preset-env"]
}

Теперь можем приступать к написанию файлов задач. Они будут максимально похожи друг на друга, поэтому нет смысла описывать каждый отдельно. Разница будет лишь в задаче build, как я уже писал выше.

Файл tasks/dev_js.js
const {
	src,
	dest
} = require('gulp');
const uglify = require('gulp-uglify-es').default;
const concat = require('gulp-concat');
const map = require('gulp-sourcemaps');
const bs = require('browser-sync');

module.exports = function dev_js() {
	return src(['src/components/**/*.js', 'src/js/01_main.js'])
		.pipe(map.init())
		.pipe(uglify())
		.pipe(concat('main.min.js'))
		.pipe(map.write('../sourcemaps'))
		.pipe(dest('build/js/'))
    .pipe(bs.stream())
}
Файл tasks/libs_js.js
const plugins = [];
const {
	src,
	dest
} = require('gulp');
const uglify = require('gulp-uglify-es').default;
const concat = require('gulp-concat');
const map = require('gulp-sourcemaps');
const chalk = require('chalk');

module.exports = function libs_js(done) {
	if (plugins.length > 0)
		return src(plugins)
			.pipe(map.init())
			.pipe(uglify())
			.pipe(concat('libs.min.js'))
			.pipe(map.write('../sourcemaps'))
			.pipe(dest('build/js/'))
	else {
		return done(console.log(chalk.redBright('No added JS plugins')));
	}
}
Файл tasks/build__js.js
const {
	src,
	dest
} = require('gulp');
const uglify = require('gulp-uglify-es').default;
const babel = require('gulp-babel');
const concat = require('gulp-concat');

module.exports = function build_js() {
	return src(['src/components/**/*.js', 'src/js/01_main.js'])
		.pipe(uglify())
		.pipe(babel({
			presets: ['@babel/env']
		}))
		.pipe(concat('main.min.js'))
		.pipe(dest('build/js/'))
}

Как видим, тут есть указанный напрямую файл src/js/01_main.js который должен присутствовать для выполнения задач, поэтому создадим его в директории src/js.

Так же стоит отметить ещё пару мелких особенностей. Во-первых, как видите, подключение плагина gulp-uglify-es я сделал сразу с параметром (свойством) default: const uglify = require('gulp-uglify-es').default - указание какого-либо параметра обязательно для успешной работы плагина. Я использую стандартную настройку. Вы-же можете порыться в документации к плагину и использовать другие настройки, если хотите. Во-вторых, в build-задаче я не использовал sourcemaps, поскольку они там и не нужны. Эта карта необходима при разработке, чтобы видеть источники кода в итоговом файле.


Задачи для HTML/PHP

Следующим этапом будет обработка HTML или PHP, в зависимости от того, на чём вы пишете. Мы не будем рассматривать препроцессоры, такие как Pug или Nunjucks по той простой причине, что с появлением Emmet они перестали значительно ускорять процесс разработки на HTML, а шаблоны мы вполне можем строить другим плагином: gulp-file-include, который умеет, как мне кажется, нечто большее - включать текстовое представление любых файлов в любые файлы. Далее, вы поймёте, почему я считаю это важным. Для разработки на PHP это вообще не имеет никакого значения. Там можно использовать require и другие возможности PHP из коробки. Если же вы привыкли пользоваться препроцессорами - вы легко сможете настроить Gulp для их обработки, я считаю.

Для минификации HTML можно использовать gulp-htmlmin, добавив его в виде .pipe(htmlmin({ collapseWhitespace: true })) в свою задачу html. Однако, как показывает практика, вёрстка - не конечный этап разработки в 90% случаев, поэтому добавлять его в сборку я не буду. При необходимости, сможете установить и добавить в задачу по аналогией с другими плагинами.

Устанавливаем gulp-file-include и пишем задачу для html:

Файл tasks/html.js
const {
	src,
	dest
} = require('gulp');
const include = require('gulp-file-include');
const bs = require('browser-sync');

module.exports = function html() {
	return src(['src/**/*.html', '!!src/components/**/*.html'])
		.pipe(include())
		.pipe(dest('build'))
    .pipe(bs.stream())
}

Здесь вы увидели немного новую конструкцию. В src аргумент стал массивом, в котором один из элементов обозначени восклицательным знаком(!) в начале. Это на языке JavaScript буквально означает "не". То есть мы берём все файлы html 'src/**/*.html', но только не те, которые '!src/components/**/*.html' находятся в директории src/components. В дальнейшем это позволит нам создавать файлы модулей, которые не должны попасть в build директорию, а служат, так сказать, служебными шаблонами. Об этих шаблонах мы поговорим ниже.

Создадим аналогичную задачу для php. Абсолютно такую же, один в один., кроме исключений. Они нам в этой задаче не потребуются.

Файл tasks/php.js
const {
	src,
	dest
} = require('gulp');
const include = require('gulp-file-include');
const bs = require('browser-sync');

module.exports = function php() {
	return src('src/**/*.php')
		.pipe(include())
		.pipe(dest('build'))
    .pipe(bs.stream())
}

Задачи для изображений

Здесь всё будет несколько сложнее. Нам потребуется как минимум две задачи для svg, две для растровых изображений. Что нам нужно:

  1. Сжимать растровые изображения.

  2. Конвертировать растровые изображения в webp.

  3. Объединять svg в спрайт.

  4. Включать svg в виде класа с фоном в CSS и cоздавать для svg класс с псевдоэлементами ::after и ::before

Это всё мы будем делать разными задачами и тут нам понадобится просто тонна разных плагинов! Их так много, что я решил оформить их названия в отдельный список и убрать под спойлер.

Плагины для изображений
  • gulp-changed - понадобится нам для отслеживания изменения в файле. Если файл не изменился, дальнейшие действия с ним не производятся.

  • gulp-multi-dest - понадобится нам для складывания результатов обработки в несколько директорий.

  • gulp-imagemin - сжимает изображения

  • imagemin-jpeg-recompress - тоже сжимает изображения

  • imagemin-pngquant - и этот тоже сжимает изображения

  • gulp-webp - конвертирует растровые форматы (png, jpeg) в webp

  • gulp-svgmin - сжимает svg

  • gulp-svg-css-pseudo - добавляет svg фоном в css и сразу же создаёт псевдоэлементы

  • gulp-svg-sprite - склеивает все svg в один спрайт. Лично я им пользуюсь крайне редко, но это ввиду особенностей проектов. Вообще весьма полезен для снижения запросов к серверу.

Теперь создадим под всё это файлы задач: tasks/rastr.js, tasks/webp.js, tasks/svg_css.js и tasks/svg_sprite.js. Самая сложная задача будет для растровых изображений, ввиду того, что там много настроек, для объяснения значаний которых нужна отдельная статья. Здесь я детали всех настроек описывать не буду. Just belive me, что я долго сидел подбирал эти настройки с дизайнером так, чтобы качество графики сильно снижало трафик и не сильно резало глаз. В итоге у нас получился вот такой монстр:

tasks/rastr.js
const {
	src,
	dest
} = require('gulp');
const changed = require('gulp-changed');
const imagemin = require('gulp-imagemin');
const recompress = require('imagemin-jpeg-recompress');
const pngquant = require('imagemin-pngquant');
const bs = require('browser-sync');

module.exports = function rastr() {
	return src('src/img/**/*.+(png|jpg|jpeg|gif|svg|ico)')
		.pipe(changed('build/img'))
		.pipe(imagemin({
				interlaced: true,
				progressive: true,
				optimizationLevel: 5,
			},
			[
				recompress({
					loops: 6,
					min: 50,
					max: 90,
					quality: 'high',
					use: [pngquant({
						quality: [0.8, 1],
						strip: true,
						speed: 1
					})],
				}),
				imagemin.gifsicle(),
				imagemin.optipng(),
				imagemin.svgo()
			], ), )
		.pipe(dest('build/img'))
  	.pipe(bs.stream())
}

В этой задаче мы применили фильтр по расширениям файлов таким образом, чтобы обрабатывались только конкретные расширения: src('src/img/**/*.+(png|jpg|jpeg|gif|svg|ico)'). В данном случае прямой слеш (знак |) означает буквально "или". Таким образом, при обработке эта функция выберет файлы с данными расширениями, а все остальные просто проигнорирует. Так же с помощью gulp-changed мы запретим обработку старых изображений - это ускорит выполнение задачи. Для того же, чтобы выполнение задачи не вызывало ошибку, если входящих файлов для конвертации нет - используем gulp-plumber.

Теперь создадим webp-дубликаты этих изображений. Эти дубликаты мы будем делать одновременно и в директорию src, и в build, банально для того, чтобы path-intellisense подсказывал нам пути к ним.

tasks/webp.js
const {
	src
} = require('gulp');
const webpConv = require('gulp-webp');
const changed = require('gulp-changed');
const multiDest = require('gulp-multi-dest');
const plumber = require('gulp-plumber');

module.exports = function webp() {
	return src('build/img/**/*.+(png|jpg|jpeg)')
		.pipe(plumber())
		.pipe(changed('build/img', {
			extension: '.webp'
		}))
		.pipe(webpConv())
		.pipe(multiDest(['src/img', 'build/img']))
}

Следующий этап - обработка SVG. Здесь у нас будет две задачи: добавление svg в качестве отдельного класса фоном прямо в css и создание svg-спрайта. У этих задач, хоть и похожее, но всё-же разное назначение. Я сейчас не буду вдаваться в подробности, поскольку они не предмет данной статьи. Если вам интересно, зачем нужны svg-спрайты и как этим пользоваться, то об этом писали тут и тут. Давайте перейдём к установке нужных плагинов и написанию задач.

Прежде всего, нам нужно очистить наш svg от всего лишнего. Для этого я буду использовать gulp-svgmin.

Для добавления svg в качестве background-image я буду использовать плагин gulp-svg-css-pseudo. Точнее это форк плагина gulp-svg-css, который я сделал после того, как авторы оригинального плагина не добавили мой пропоуз в свой плагин. Разница между ними заключается в том, что он создаёт для каждого класса псевдоэлементы ::before и ::after. Вы можете точно так же использовать оригинальный плагин, но он не будет работать с псевдоэлементами. На входе мы в определённую директорию кладём файл, например myicon.svg, а на выходе получаем классы --svg__myicon, --svg__myicon-before и --svg__myicon-after, у которых background-image уже задан в виде нашей картинки. Это очень просто и удобно, если картинку не нужно менять (анимировать или изменять цвет):

.--svg__myicon,.--svg__myicon-before::before,.--svg__myicon-after::after{
    background-image: url("data:image/svg+xml;charset=utf8, %3Csvg width='8' height='6' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M.228 1.635A1 1 0 011 0h6a1 1 0 01.772 1.635L4.808 5.589a.999.999 0 01-1.616 0L.228 1.635z' fill='%232C2D2E'/%3E%3C/svg%3E");
}
.--svg__myicon-before::before {
    content:'';
}
.--svg__myicon-after::after {
    content:'';
}

И так, мы будем брать файлы из директории src/svg/css и обрабатывать нашими плагинами, получая на выходе файл svg.scss и положим его в директорию src/scss/base. Полный файл этой задачи будет выглядеть так:

tasks/svg_css.js
const {
	src,
	dest
} = require('gulp');
const svgmin = require('gulp-svgmin');
const svgCss = require('gulp-svg-css-pseudo');

module.exports = function svg_css() {
	return src('src/svg/css/**/*.svg')
		.pipe(svgmin({
			plugins: [{
					removeComments: true
				},
				{
					removeEmptyContainers: true
				}
			]
		}))
		.pipe(svgCss({
			fileName: '_svg',
			fileExt: 'scss',
			cssPrefix: '--svg__',
			addSize: false
		}))
		.pipe(dest('src/scss/global'))
}

Теперь напишем задачу для svg-спрайта. Смысл спрайта в том, что он объединяет svg в один файл, который содержит набор векторных картинок. К этим картинкам можно обращаться по ID и вставлять их в нужное место с возможностью изменять их (например цвет) средствами css. Установим плагин gulp-svg-sprite и создадим под него вот такую задачу:

tasks/svg_sprite.js
const {
	src,
	dest
} = require('gulp');
const svgmin = require('gulp-svgmin');
const sprite = require('gulp-svg-sprite');

module.exports = function svg_sprite() {
	return src('src/svg/**/*.svg')
		.pipe(svgmin({
			plugins: [{
					removeComments: true
				},
				{
					removeEmptyContainers: true
				}
			]
		}))
		.pipe(sprite({
			mode: {
				stack: {
					sprite: '../sprite.svg'
				}
			}
		}))
		.pipe(dest('src/img'))
}

На выходе мы получим файл src/img/sprite.svg внутри которого и будут все наши svg. Обратиться к ним в html можно будет так: <img src="sprite.svg#myIconFileName"> или так:

<svg class="img">
    <use xlink:href="sprite.svg#myIconFileName"></use>
</svg>

Если же иконка находилась во вложенной директории (например, как в нашем случае, в директории css), то перед именем файла нужно поставить имя этой директории и два дефиса. Вот так: <img src="sprite.svg#css--myIconFileName">

Чуть позже покажу ещё и третий способ вставлять svg в html с помощью сборки. Вы можете использовать любой из этих способов или их комбинировать, в зависимости от задач, которые нужно решить.


Задачи для шрифтов

Здесь у нас будет две задачи. Первая направлена на конвертацию шрифтов из формата ttf в форматы woff и woff2. Во всех остальных форматах в 2021 году не вижу никакого смысла, поскольку поддержка формата woff простирается аж до Internet Explorer 9. Давно ли вы видели компьютеры с IE9 на борту? Конвертиролвать будем с помощью двух плагинов: gulp-ttftowoff2 и gulp-ttf2woff, соответственно. Файл задачи для конвертации шрифтов будет выглядеть так:

tasks/ttf.js
const {
	src,
	dest
} = require('gulp');
const changed = require('gulp-changed');
const ttf2woff2 = require('gulp-ttftowoff2');
const ttf2woff = require('gulp-ttf2woff');

module.exports = function ttf(done) {
	src('src/fonts/**/*.ttf')
		.pipe(changed('build/fonts', {
			extension: '.woff2',
			hasChanged: changed.compareLastModifiedTime
		}))
		.pipe(ttf2woff2())
		.pipe(dest('build/fonts'))

	src('src/fonts/**/*.ttf')
		.pipe(changed('build/fonts', {
			extension: 'woff',
			hasChanged: changed.compareLastModifiedTime
		}))
		.pipe(ttf2woff())
		.pipe(dest('build/fonts'))
	done();
}

После того, как шрифты сконвертированы, нам нужно их подключить. Я так и не смог добиться полной автоматизации подключения шрифтов, объединения их по жирности и стилю начертания, в зависимости от имени файла и присвоения нескольких вариантов локального имени. Тем не менее, процесс подключения тоже можно немного автоматизировать. Для этого, во-первых, создадим в директороии src/scss/base файл _mixins.scss. В дальнейшем, он нам ещё понадобится и для других миксинов. Напишем в этом файле следующий миксин:

src/scss/base/_mixins.scss
@mixin font-face($name, $file, $weight: 400, $style: normal) {
	@font-face {
		font-family: "#{$name}";
		src: local("#{$file}"),
		url('../fonts/#{$file}.woff2') format('woff2'),
		url('../fonts/#{$file}.woff') format('woff');
		font-weight: $weight;
		font-style: $style;
		font-display: swap;
	}
}

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

  • $name - имя шрифтового семейства;

  • $file - имя файла;

  • $weight - жирность шрифта (по-умолчанию установлено значение 400, но если мы передадим параметр, то значение по-умолчанию будет проигнорировано)

  • $style - стиль начертания (тоже установлен по-умолчанию normal)

На выходе мы получим уже подключенный файл шрифта, но с ним потребуются некоторые манипуляции вручную, которые я опишу чуть ниже.

Далее нам нужно написать задачу, которая будет подключать наши шрифты. По сути эта задача будет циклом проходиться по файлам, брать имя каждого файла, отсекать расширение и на основе имени генерировать код для его подключения через миксин. Для этого нам потребуется модуль nodeJS для работы с файлами, который называется fs. Он уже установлен вместе с nodejs, поэтому устанавливать его нет необходимости - нужно только подключить: const fs = require('fs');

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

let srcFonts = 'src/scss/_local-fonts.scss';
let appFonts = 'build/fonts/';

Дальнейший код этой функции я честно скопировал у Максима Васяновича ( @MaxGraph ), с его урока про Gulp и немного переписал. В итоге, у нас получится вот такая функция:

tasks/fonts.js
const fs = require('fs');
const chalk = require('chalk');

let srcFonts = 'src/scss/_local-fonts.scss';
let appFonts = 'build/fonts/';
module.exports = function fonts(done) {
	fs.writeFile(srcFonts, '', () => {});
	fs.readdir(appFonts, (err, items) => {
		if (items) {
			let c_fontname;
			for (let i = 0; i < items.length; i++) {
				let fontname = items[i].split('.'),
					fontExt;
				fontExt = fontname[1];
				fontname = fontname[0];
				if (c_fontname != fontname) {
					if (fontExt == 'woff' || fontExt == 'woff2') {
						fs.appendFile(srcFonts, `@include font-face("${fontname}", "${fontname}", 400);\r\n`, () => {});
						console.log(chalk `
{bold {bgGray Added new font: ${fontname}.}
----------------------------------------------------------------------------------
{bgYellow.black Please, move mixin call from {cyan src/scss/_local-fonts.scss} to {cyan src/scss/_fonts.scss} and then change it!}}
----------------------------------------------------------------------------------
`);
					}
				}
				c_fontname = fontname;
			}
		}
	})
	done();
}

Эта функция на выходе создаст нам файл src/scss/_local-fonts.scss, в котором под каждый файл шрифтов будет содержаться вызов миксина для его подключения. Так же, при выполнении функции в консоль будет выдаваться следующее сообщение:

Added new font: YoyrFont.
----------------------------------------------------------------------------------
Please, move mixin call from src/scss/_local-fonts.scss to src/scss/_fonts.scss and then change it.
----------------------------------------------------------------------------------

Как видите, в сообщении написано следующее: "Добавлен новый шрифт: названиешрифта. Пожалуйста, переместите вызов миксина из src/scss/_local-fonts.scss в src/scss/_fonts.scss после чего измените его". Давайте разберёмся, что куда нужно переместить и как изменить. Но прежде создадим тот самый файл src/scss/_fonts.scss - он будет содержать уже реальные вызовы миксина для подключения шрифтов, которыми мы и будем пользоваться.

Когда мы добавляем шрифты, чаще всего, у нас есть отдельные файлы для разной жирности шрифта и для разных стилей начертания (наклонные и прямые, тонкие и жирные и т.п.). Каждый такой файл шрифта создаст отдельный вызов миксина и на выходе мы получим файл src/scss/_local-fonts.scss примерно такого содержания:

@include font-face("Arial", "Arial", 400);
@include font-face("ArialBold", "ArialBold", 400);
@include font-face("ArialItalic", "ArialItalic", 400);

Как видим, мы подключаем шрифты отдельными названиями. Это неправильно с точки зрения читабельности и удобства использования шрифта, поскольку вместо того, чтобы прописывать font-weight: 700, нам каждый раз придётся указывать font-family: "ArialBold". Это плохая практика, поэтому нам нужно переписать эти вызовы. Как помним из миксина, который мы написали, первым параметром он принимает имя шрифтового семейства. В данном случае это "Arial". Второй параметр мы не меняем - это имя файла без расширения (расширения подставит миксин). Третий параметр отвечает за жирность. Изменим во второй строке 400 на 700. Четвёртый параметр, который здесь не указан, отвечает за стиль начертания. В третей строке миксин вызывается для наклонного шрифта, поэтому четвёртым параметром нужно это указать. В итоге получим:

@include font-face("Arial", "Arial", 400);
@include font-face("Arial", "ArialBold", 700);
@include font-face("Arial", "ArialItalic", 400, italic);

Теперь это всё нужно переместить в файл src/scss/_fonts.scss, который, естественно, необходимо предварительно создать. Перемещать нужно в новый файл по той причине, что при следующем запуске эта функция перезапишет файл src/scss/_local-fonts.scss и все наши изменения затрутся.


Автоматическое обновление и синхронизация браузеров

Нам нужно настроить Gulp таким образом, чтобы он при запуске открывал наш проект в браузере и автоматически обновлял его при каких-либо изменениях. Я для этого использую browser-sync. Этот плагин создаёт сервер средствами nodejs и позволяет подключать к нему различные браузеры. Для него у нас будет две задачи - для html-разработки и для php соответственно. Установим этот плагин и создадим два соответствующих файла задач. В первом файле мы укажем настройки для разработки html.

tasks/bs_html.js
const bs = require('browser-sync');

module.exports = function bs_html() {
	bs.init({
		server: {
			baseDir: 'build/',
			host: '192.168.0.104',
		},
		callbacks: {
			ready: function (err, bs) {
				bs.addMiddleware("*", function (req, res) {
					res.writeHead(302, {
						location: "404.html"
					});
					res.end("Redirecting!");
				});
			}
		},
		browser: 'chrome',
		logPrefix: 'BS-HTML:',
		logLevel: 'info',
		logConnections: true,
		logFileChanges: true,
		open: true
	})
}

Параметр host отвечает за IP-адрес компьютера, с которого сайт будет раздаваться на другие устройства в сети. Важно, чтобы там был указан именно IP-адрес вашего компьютера в локальной сети, если хотите иметь доступ к разрабатываемому сайту со всех устройств. Коллбэк-функция отвечает за открытие страницы 404, если она есть и если не найдена запрашиваемая (по умолчанию это страница index.html). Все остальные параметры вы вольны настраивать как вашей душе угодно, согласно документации.

Для php мы напишем гораздо более простую задачу. Она будет только обновлять браузер в случае изменений, а локальный сервер Вы будете запускать с помощью сторонней программы: openserver, wamp или любой другой.

tasks/bs_php.js
const bs = require('browser-sync');

module.exports = function bs_php() {
	bs.init({
		browser: ['chrome'],
		watch: true,
		proxy: '',
		logLevel: 'info',
		logPrefix: 'BS-PHP:',
		logConnections: true,
		logFileChanges: true,
	})
}

Теперь нам нужно подключить в ранее написанные задачи browser-sync и pipe для обновления браузера в конце выполнения задачи. Как подключить - вы уже знаете: так же, как и др угие плагины. А добавлять pipe мы будем следуюший: .pipe(bs.stream()).

Перезагружать страницу нам придётся при изменении html, scss и js, php, а так же растровых картинок. Соответственно, в эти задачи я уже добавил нужный pipe.

Опционально!

Лично мне нравится, когда в консоли показывается исходный размер изображений до сжатия и итоговый - после сжатия, поэтому в сборку я добавил плагин gulp-size, но поскольку я почти не нашёл единомшленников в данном вопросе "засорения терминала лишней инфой", я не описывал его здесь. При желании вы можете добавить соответствующий pipe к нужным задачам. В публично доступной версии сборки его так же нет.

Далее нам следует заняться функциями вотчинга. Вотчинг - это состояние, когда вся наша программа запущена, следит за файлами и их изменением и выполняет те или иные функции, если эти файлы изменились. И так, за чем будем следить: html, php, scss, js, json (ниже объясню, для чего), изображения в src, изображения в build (помните, что у нас webp конвертируются из уже сжатых изображений?), svg, шрифты. Давайте напишем соответствующую задачу:

tasks/watch.js
const {
	watch,
	parallel,
	series
} = require('gulp');

module.exports = function watching() {
	watch('src/**/*.html', parallel('html'));
	watch('src/**/*.php', parallel('php'));
	watch('src/**/*.scss', parallel('style'));
	watch('src/**/*.js', parallel('dev_js'));
	watch('src/**/*.json', parallel('html'));
	watch('src/img/**/*.+(png|jpg|jpeg|gif|svg|ico)', parallel('rastr'));
	watch('build/img/**/*.+(png|jpg|jpeg)', parallel('webp'));
	watch('src/svg/css/**/*.svg', series('svg_css', 'style'));
	watch('src/svg/sprite/**/*.svg', series('svg_sprite', 'rastr'));
	watch('src/fonts/**/*.ttf', series('ttf', 'fonts'));
}

Здесь мы использовали два варианта запуска и выполнения задач - parallel и series. Разница между ними в том, что в первом случае задачи запускаются одновременно и выполняются параллельно друг другу, а во втором - последовательно, одна за другой в том порядке, в котором мы указали.

Обратите внимание на имя нашей функции. Поскольку переменная watch уже занята методом галпа, мы назвали свою функцию несколько иначе.

Вот и всё. Мы научили Gulp следить за файлами и выполнять те или иные задачи в тот момент, когда эти файлы изменились.


Деплой на хостинг по FTP

Для этого нам потребуется отдельная задача и плагин. Я использую vinyl-ftp, хотя есть аналоги, без проблем делающие то же самое. Установим его и напишем задачу. На хостинг будем грузить только директорию build/, в которой находятся готовые, скомпилированные файлы проекта. Для вывода результата в консоль я буду использовать тот же chalk.

tasks/deploy.js
const {
	src
} = require('gulp');
const ftp = require('vinyl-ftp');
const ftpSettings = require('../tasks/ftp_settings');
const chalk = require('chalk');
const connect = ftp.create(ftpSettings);

module.exports = function deploy() {
	return src(['build/**/*.*', '!build/**/*.map'])
		.pipe(connect.newer('public_html/'))
		.pipe(connect.dest('public_html/'))
		.on('success', () => console.log(`Finished deploing ./build to https://${chalk.blueBright(ftpSettings.host)}`))
}

Константа connect - это функция, в которую аргументом передаётся объект с настройками хостинга - адрес сервера, логин, пароль. Будьте аккуратны, выкладывая подобные данные на Github или другое хранилище кода. Я создал отдельный файл tasks/ftp_settings.json с настройками и рекомендую вам добавить его в gitignore, чтобы случайно не забыть и не вывалить свои логин и пароль к хостингу на весь мир:

{
	"host": "yourhosting.com",
	"user": "username",
	"pass": "*********",
	"parallel": 10
}

Не забудьте так же указать правильную директорию на ftp-сервере вместо public_html/, которая указана в моих настройках newer и dest.


Написание GULP- и NPM-сценариев

Теперь нам нужно составить несколько gulp-сценариев, то есть поочерёдного запуска задач для того, чтобы собирать воедино наш проект.

Для этого у нас будет две gulp-задачи: default и php, с той лишь разницей, что мы будем запускать тот или иной сервер, а так же несколько сценариев npm для запуска разработки, запуска сборки проекта и для деплоя на хостинг.

Для начала нам необходимо задать все написанные ранее функции в gulpfile.js, чтобы они были доступны в gulp. Делать мы это будем так же, как мы делали с функцией hello:

exports.style = tasks.style;
exports.libs_style = tasks.libs_style;
exports.build_js = tasks.build_js;
exports.libs_js = tasks.libs_js;
exports.dev_js = tasks.dev_js;
exports.html = tasks.html;
exports.php = tasks.php;
exports.rastr = tasks.rastr;
exports.webp = tasks.webp;
exports.svg_css = tasks.svg_css;
exports.svg_sprite = tasks.svg_sprite;
exports.ttf = tasks.ttf;
exports.fonts = tasks.fonts;
exports.bs_html = tasks.bs_html;
exports.bs_php = tasks.bs_php;
exports.watch = tasks.watch;
exports.deploy = tasks.deploy;

После этого создадим сценарии, которые будем выполнять при запуске Gulp с теми или иными параметрами:

Сценарий для html (он же - сценарий по-умолчанию)
exports.default = gulp.parallel(
	exports.libs_style,
	exports.style,
	exports.libs_js,
	exports.dev_js,
	exports.rastr,
	exports.webp,
	exports.svg_css,
	exports.svg_sprite,
	exports.ttf,
	exports.fonts,
	exports.html,
	exports.bs_html,
	exports.watch
)
Сценарий для php
exports.dev_php = gulp.parallel(
	exports.libs_style,
	exports.svg_css,
	exports.fonts,
	exports.style,
	exports.libs_js,
	exports.dev_js,
	exports.rastr,
	exports.webp,
	exports.svg_sprite,
	exports.ttf,
	exports.php,
	exports.bs_php,
	exports.watch
)

Итоговый gulpfile.js будет выглядеть так:

gulpfile.js
const gulp = require('gulp');
const requireDir = require('require-dir');
const tasks = requireDir('./tasks');

exports.style = tasks.style;
exports.libs_style = tasks.libs_style;
exports.build_js = tasks.build_js;
exports.libs_js = tasks.libs_js;
exports.dev_js = tasks.dev_js;
exports.html = tasks.html;
exports.php = tasks.php;
exports.rastr = tasks.rastr;
exports.webp = tasks.webp;
exports.svg_css = tasks.svg_css;
exports.svg_sprite = tasks.svg_sprite;
exports.ttf = tasks.ttf;
exports.fonts = tasks.fonts;
exports.bs_html = tasks.bs_html;
exports.bs_php = tasks.bs_php;
exports.watch = tasks.watch;
exports.deploy = tasks.deploy;

exports.default = gulp.parallel(
	exports.libs_style,
	exports.style,
	exports.libs_js,
	exports.dev_js,
	exports.rastr,
	exports.webp,
	exports.svg_css,
	exports.svg_sprite,
	exports.ttf,
	exports.fonts,
	exports.html,
	exports.bs_html,
	exports.watch
)
exports.dev_php = gulp.parallel(
	exports.libs_style,
	exports.style,
	exports.libs_js,
	exports.dev_js,
	exports.rastr,
	exports.webp,
	exports.svg_css,
	exports.svg_sprite,
	exports.ttf,
	exports.fonts,
	exports.php,
	exports.bs_php,
	exports.watch
)

Теперь перейдём в файл package.json и напишем несколько npm-сценариев, которые будут последовательно выполнять задачи и запускать сборку. В объекте "scripts" мы можем задавать в формате ключ-значение имя и наборы задач для запуска. Я сделаю пять сценариев:

  "scripts": {
    "html": "gulp",
    "php": "gulp dev_php",
    "build": "gulp libs_style && gulp svg_css && gulp ttf && gulp fonts && gulp style && gulp libs_js && gulp build_js && gulp rastr && gulp webp && gulp svg_sprite && gulp html && gulp php",
    "ftp": "gulp deploy",
    "build-ftp": "npm run build && npm run ftp"
  },
  • html будет запускать Gulp для разработки в html-версии;

  • php будет запускать Gulp для разработки в php-версии;

  • build будет поочерёдно запускать задачи для сборки проекта в build без запуска вотчинга и сервера;

  • ftp отправит содержимое пдиректории build кроме файловых карт (файлы с расширением .map) на ftp-сервер;

  • build-ftp поочерёдно выполнит build и ftp сценарии.


Создание базовых шаблонов

Для разработки нам потребуются стартовые шаблоны html, кое-какие стили и несколько миксинов, которыми мы будем пользоваться в разработке. Начнём с html. Создадим индексный файл (index.html) со стандартной базовой разметкой:

<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="UTF-8">
	<meta http-equiv="X-UA-Compatible" content="IE=edge">
	<meta name="viewport" content="width=device-width, initial-scale=1.0">
	<title>Document</title>
</head>
<body>
  <main>
    
  </main>
</body>
</html>

Теперь уберём в компонент то, что будет повторяться на всех страницах:

src/components/page-blocks/_head.html
<html lang="@@lang">

<html lang="@@lang">

<head>
	<meta charset="UTF-8">
	<meta name="viewport"
				content="width=device-width, initial-scale=1.0">
	<meta name="description"
				content="@@description">
	<meta name="keywords"
				content="@@keywords">
	<link rel="shortcut icon"
				href="@@favicon.svg"
				type="image/svg+xml">
	<link rel="shortcut icon"
				href="@@favicon.webp"
				type="image/webp">
	<link rel="shortcut icon"
				href="@@favicon.png"
				type="image/x-icon">
	<link rel="stylesheet"
				href="css/libs.min.css">
	<link rel="stylesheet"
				href="css/style.min.css">
	<title>@@title</title>
</head>

...и подключим этот компонент вместо обычного head страницы:

<!DOCTYPE html>
@@include('components/page-blocks/_head.html',{
"lang":"en",
"title":"Easy-webdev-startpack v3.0",
"description":"Gulp pack for easy html/php markup development",
"keywords":"",
"favicon":"img/favicons/favicon",
})

<body>
	<main class="main">
    
  </main>
</body>

</html>

Как можете видеть, ничего сложного нет. Функция @@include принимает два параметра. Первый параметр - путь к файлу, который мы импортируем, а второй - json-объект (тут может быть путь к файлу json, содержащему объект или массив), в котором в формате ключ-значение указаны те переменные, которые нам нужно подставить в этом инклюде. Создадим в директории компонентов так же шаблоны хедера, футера и подключения скриптов:

src/components/page-blocks/...

_header.html

<header class="header"></header>

_footer.html

<footer class="footer"></footer>

_scripts.html

<script src="js/libs.min.js"></script>
<script src="js/main.min.js"></script>

И теперь подключим их все в индексный файл:

<!DOCTYPE html>
@@include('components/page-blocks/_head.html',{
"lang":"en",
"title":"Easy-webdev-startpack v3.0",
"description":"Gulp pack for easy html/php markup development",
"keywords":"",
"favicon":"img/favicons/favicon",
})

<body>
	@@include('components/page-blocks/_header.html')
	<main class="main"></main>
	@@include('components/page-blocks/_footer.html')
	@@include('components/page-blocks/_scripts.html')
</body>

</html>

Теперь при запуске Gulp на выходе мы получим готовую страницу, с прописанным head, header, footer и подключенными скриптами. Если после запуска gulp посмотреть на индексный файл в директории build, то мы увидим вполне обычный html-файл:

build/index.html
build/index.html

Теперь перейдём к созданию базовых стилей. Для начала, я подключу все необходимые директории с файлами в style.scss. Здесь важен порядок импорта стилей, поскольку стили, которые будут ниже в итоговом файле имеют приоритет:

@import 'base/*.scss';
@import 'global/*.scss';
@import '../components/**/*.scss';
@import '**/_*.scss';

Далее создам в директории src/scss/base файл _01_normalize.scss Именно так: с подчёркиванием и номером файла. Это нужно для того, чтобы стили из этого файла оказались в самом верху и их можно было перебить другими стилями, если это понадобится. Далее в этот файл я скопирую стили из normalize.css. Безусловно, можно подключить normalize в качестве плагина, но я выбрал именно этот способ, в том числе, для демонстрации принципов подключения файлов со стилями.

Теперь перейдём к файлу src/scss/base/_mixins.scss. Добавим сюда несколько миксинов, чтобы было удобно задавать быстрые стили:

@mixin bg ($size:"contain", $position: "center") {
	background-size: #{$size};
	background-position: #{$position};
	background-repeat: no-repeat;
}

Этот миксин поможет нам с фоновыми картинками. Достаточно нужному классу написать @include bg и на выходе мы получим вот это:

body {
	background-size: contain;
	background-position: center;
	background-repeat: no-repeat;
}

Следующий миксин, который я использую - это анимация кнопок (при наведении, фокусе и нажатии):

@mixin btn_anim($scaleMax:1.05, $scaleMin:0.95) {
	transform-origin: center center;
	transition: all ease-out 240ms;

	&:hover {
		transform: scale($scaleMax);
	}

	&:focus{
		outline: transparent;
	}

	&:focus-visible {
		transform: scale($scaleMax) translateY(-5%);
	}

	&:active {
		transform: scale($scaleMin);
	}
}

Далее идёт миксин для сброса стилей кнопки. Очень часто такой необходим.

@mixin no-btn ($display:"inline-block") {
	padding: 0;
	margin: 0;
	border: 0;
	background-color: transparent;
	border-radius: 0;
	cursor: pointer;
	appearance: none;
	display: #{$display};
}

Следующий миксин достаточно спорный, но я исплоьзую. Он делает transition:

@mixin transit ($tr:0.24) {
	transition: all #{$tr}s;
}

И ещё один миксин для контейнеров:

@mixin container($max-width:"120rem",$padding:"2rem"){
	width: 100%;
	max-width: #{$max-width};
	padding: 0 #{$padding};
	margin: 0 auto;
}

Теперь перейдём в файл src/scss/global/_global.scss и создадим несколько "служебных" классов и общих стилей. Я и не припомню проекта, где они не были бы мне нужны.

Во-первых, установим box-sizing: border-box для всех элементов страницы:

*,
*::before,
*::after {
	box-sizing: border-box;
}

Во-вторых, распишем стили для флексов:

.flex {
	display: flex;
	align-items: flex-start;
	justify-content: flex-start;
}
.--just-space {
	justify-content: space-between;
}

.--just-center {
	justify-content: center;
}

.--just-end {
	justify-content: flex-end;
}

.--align-str {
	align-items: stretch;
}

.--align-center {
	align-items: center;
}

.--align-end {
	align-items: flex-end;
}

.--dir-col{
	flex-direction: column;
}

В-третьих, для html и body заранее установим некоторые стили:

html{
	font-size: 16px;
}

html,
body {
	min-height: 100%;
	position: relative;
}

body{
	font-size: 1rem;
}

И далее момент спорный, но я использую. Это сброс отступов у некоторых блочных элементов:

ul,
ol,
li,
p,
h1,
h2,
h3,
h4,
h5,
h6 {
	margin: 0;
}

И того, у нас должны получиться файлы:

src/scss/base/_mixins.scss
@mixin font-face($name, $file, $weight: 400, $style: normal) {
	@font-face {
		font-family: "#{$name}";
		src: local("#{$file}"),
		url('../fonts/#{$file}.woff2') format('woff2'),
		url('../fonts/#{$file}.woff') format('woff');
		font-weight: $weight;
		font-style: $style;
		font-display: swap;
	}
}

@mixin bg ($size:"contain", $position: "center") {
	background-size: #{$size};
	background-position: #{$position};
	background-repeat: no-repeat;
}

@mixin btn_anim($scaleMax:1.05, $scaleMin:0.95) {
	transform-origin: center center;
	transition: all ease-out 240ms;

	&:hover {
		transform: scale(#{$scaleMax});
	}

	&:focus {
		outline: transparent;
	}

	&:focus-visible {
		transform: scale(#{$scaleMax}) trahslateY(-5%);
	}

	&:active {
		transform: scale(#{$scaleMin});
	}
}

@mixin no-btn ($display:"inline-block") {
	padding: 0;
	margin: 0;
	border: 0;
	background-color: transparent;
	border-radius: 0;
	cursor: pointer;
	appearance: none;
	display: #{$display};
}

@mixin transit ($tr:0.24) {
	transition: all #{$tr}s;
}

@mixin container($max-width:"120rem", $padding:"2rem") {
	width: 100%;
	max-width: #{$max-width};
	padding: 0 #{$padding};
	margin: 0 auto;
}
src/scss/global/_global.scss
*,
*::before,
*::after {
	box-sizing: border-box;
}

.flex {
	display: flex;
	align-items: flex-start;
	justify-content: flex-start;
}

.--just-space {
	justify-content: space-between;
}

.--just-center {
	justify-content: center;
}

.--just-end {
	justify-content: flex-end;
}

.--align-str {
	align-items: stretch;
}

.--align-center {
	align-items: center;
}

.--align-end {
	align-items: flex-end;
}

.--dir-col {
	flex-direction: column;
}

html {
	font-size: 16px;
}

html,
body {
	min-height: 100%;
	position: relative;
}

body {
	font-size: 1rem;
}

ul,
ol,
li,
p,
h1,
h2,
h3,
h4,
h5,
h6 {
	margin: 0;
}

Вот, собственно и всё. Поздравляю! Наша сборка готова к работе.


Краткое howto

Первое что следует сказать, что с помощью @@include-функции вы можете вставлять не только html файлы, но и любые другие, в которых есть текстовая информация. Например, вы можете вставлять инлайновые svg в html, не загромождая код.

Второе. В функции @@include есть особый режим @@loop, который принимает вторым аргументом массив объектов и повторяет элемент столько раз, сколько объектов есть в массиве. То есть можно создать, например, слайдер с разными картинками, скормив функции @@loop массив путей к картинкам. ВАЖНОЕ ОГРАНИЧЕНИЕ! loop не работает внутри includ'а - только в корневых файлах.

Третье. Старайтесь дробить свой код на как можно большее количество компонентов. Так вам будет удобнее разрабатывать свои проекты. В директории components можно создавать вложенные директории, в которых хранить шаблоны html, scss и js отдельного компонента (например, слайдера). Scss и js из этой директории подтянутся в выходные файлы, можете об этом не переживать.


Вместо послесловия

Если вы дочитали аж до сюда, значит статья была для вас интересной и познавательной. Поэтому, пожалуйста, пройдите по этой ссылке и поставьте звезду моей сборке. Вам это 10 секунд времени, а мне будет приятно и я буду понимать, что есть запрос на дальнейшее развитие сборки.

Если же Вы знаете, как можно улучшить сборку, отрефакторить какую-то задачу, то напишите об этом в комментариях, а лучше, оставьте свой pull-request в репозитории. Я всегда открыт к улучшениям и готов обсуждению.