Оптимизация и автоматизация

1. Проблема
На данный момент мы изучили лишь самые основы вёрстки макетов, впереди ещё огромное количество информации, но мы уже сталкиваемся со сложностями разработки.
Так, у нас уже имеется 3 HTML-файла для разных страниц нашего сайта, и каждый из них содержит повторяющиеся элементы, такие как блок <head>
с мета-информацией, шапка и подвал сайта. И даже в одном HTML-файле мы видим повторяющиеся однотипные блоки, такие как карточки в листингах. Поддерживать код становится очень утомительно — ведь любое внесение изменений требует одновременной правки во множестве мест. Хотелось бы этого избежать.
Наш CSS-код стремительно разрастается, и чем дальше, тем сложнее в нём ориентироваться. Назревает потребность разбить всё на отдельные файлы-блоки и максимально повысить читаемость. А ещё мы постоянно сталкиваемся с повторяющимися значениями свойств, например основными цветами сайта, которые не хотелось бы постоянно держать в голове. Есть и другая проблема — дизайнер в любой момент может подправить какое-либо из глобальных значений, и было бы здорово для нас внести изменение в одном единственном месте, а не править все свойства, которых могут быть сотни.
Кроме того очень хочется, чтобы браузер самостоятельно перезагружал вкладку с нашим проектом при каждом изменении в коде, а не делать это вручную.
Помимо автоматизации разработки огромнейшее значение имеют производительность конечного кода, оптимизация и минификация всех ресурсов. Чем быстрее загрузиться наш сайт, тем быстрее потенциальный пользователь получит доступ к нашему контенту. А ещё это напрямую влияет на SEO (Search Engine Optimization). Быстрые сайты получают приоритет в поисковой выдаче.
Оптимизация — понятие комплексное. Многое зависит от того, какой код вы пишете, какие ресурсы используете, но не малую часть задач здесь можно автоматизировать. Поэтому начнём именно с автоматизации, а об оптимизации будем говорит параллельно, и на этом, и на следующих уроках.
2. Автоматизация разработки
Структура проекта
Первое, что нам нужно сделать, изменить структуру нашего проекта. Создайте в корне учебного проекта каталог src и все вложенные каталоги, как показано в примере ниже:
my_project/
src/
assets/
fonts/
icons/
images/
svg-sprite/
html/
blocks/
scripts/
dev/
vendor/
styles/
blocks/
global/
vendor/
Каталог src
будет содержать наш исходный код и ресурсы.
В подкаталоге assets
мы разместим наши шрифты, иконки, изображения.
В подкаталоге html
будут лежать html-файлы всех наших страничек. Переместите сюда наши файлы index.html
, events.html
и event.html
. В подкаталоге html/blocks/
в дальнейшем мы будем размещать переиспользуемые HTML-блоки, такие как шапка и футер сайта, карточки листингов и т.п.
В подкаталоге scripts/dev/
будет располагаться наш js-код, а в подкаталоге scripts/vendor/
— js-код сторонних разработчиков.
В подкаталоге styles
разместим наши стили. Пока переместите сюда имеющийся файл styles.css
, а что с ним делать дальше, разберёмся чуть позже.
Node.js
Далее нам понадобится Node.js. Это среда выполнения JavaScript-кода, построенная на основе JavaScript-движка V8 с открытым исходным кодом, который в свою очередь был создан компанией Google. Node.js прежде всего предназначена для создания серверных приложений на языке JavaScript.
Перейдите по предложенной выше ссылке и установите последнюю рекомендованную версию. Желательно работать именно с LTS-версией (Long-Term Support), имеющей максимальное время технической поддержки. Версия «Current» имеет все последние наработки и обновления платформы, но также она может содержать в себе ошибки.
Чтобы удостовериться, что установка Node.js прошла успешно, выполните в командной строке следующее:
node -v
В консоли отобразится номер установленной версии Node.js.
npm
npm (Node Package Manager) — это менеджер пакетов, который управляет модулями и зависимостями проекта. В Node.js npm имеется по умолчанию, его отдельная установка не требуется.
Все пакеты хранятся в облачном репозитории на официальном сайте. Вы можете создать свой модуль и опубликовать его в репозитории для общего использования. В рамках данного курса нам это не потребуется, но знать о такой возможности нужно.
Внимание: прежде чем выполнить следующие шаги, в корне проекта создайте файл .gitignore
со следующим содержимым:
node_modules
Таким образом, при последующих коммитах в удалённый репозиторий установленные пакеты npm не попадут в ваш git.
Для описания модулей, от которых зависит наше приложение, в npm используется файл package.json
. Создайте такой файл в корне учебного проекта, рядом с каталогом src
, откройте текстовым редактором и добавьте следующее содержимое:
{
"name": "course_front",
"version": "1.0.0",
"description": "course junior frontend-developer",
"author": "si@tagree.ru",
"devDependencies": {
"autoprefixer": "^10.4.2",
"browser-sync": "^2.27.7",
"del": "^6.0.0",
"gulp-csso": "^4.0.1",
"gulp-file-include": "^2.3.0",
"gulp-format-html": "^1.2.5",
"gulp-imagemin": "^8.0.0",
"gulp-less": "^5.0.0",
"gulp-plumber": "^1.2.1",
"gulp-postcss": "^9.0.1",
"gulp-rename": "^2.0.0",
"gulp-svgmin": "^4.1.0",
"gulp-svgstore": "^9.0.0",
"gulp-terser": "^2.1.0",
"imagemin-gifsicle": "^7.0.0",
"imagemin-mozjpeg": "^10.0.0",
"imagemin-optipng": "^8.0.0",
"postcss": "^8.4.5",
"postcss-sort-media-queries": "^4.2.1"
},
"scripts": {
"build": "gulp build",
"serve": "gulp serve",
"start": "gulp start"
}
}
Запись devDependencies
содержит наименования и версии всех пакетов, которые нам понадобятся в дальнейшей работе. Эти пакеты не попадут в финальную сборку приложения и будут использоваться только в служебных целях на этапе разработки.
Чтобы установить новый пакет в devDependencies
, нужно открыть терминал, перейти в рабочую папку проекта и набрать следующую команду:
npm install [package-name] --save-dev
Если установить пакет без флага --save-dev
, или с флагом --save
, то пакет попадёт в запись dependencies
. В этой записи должны содержаться те пакеты, от которых зависит финальный код приложения. В рамках данного учебного проекта нам это не понадобится.
Так как все необходимые пакеты уже прописаны в нашем package.json
, то ставить их по отдельности нет никакой необходимости. Поэтому откройте терминал, перейдите в папку проекта и просто выполните команду:
npm install
Все необходимые пакеты установятся автоматически.
Как правило, обновлять установленные пакеты нет никакой необходимости, кроме того обновление пакета может сломать ваше приложение, если автор пакета внёс какие-либо серьёзные изменения в свой код.
После установки хотя бы одного npm-модуля рядом с package.json
будет автоматически создан файл package-lock.json
, который будет обновляться каждый раз при добавлении новой зависимости.
В package-lock.json
содержится описание текущей иерархии используемых в приложении модулей: их точные версии и точные версии используемых в свою очередь ими модулей. При развертывании приложения в новой среде наличие файла package-lock.json
гарантирует, что будут установлены версии тех пакетов, которые использовались в разработке в последний раз.
Описание файла package-lock.json
имеет более высокий приоритет в сравнении с описанием зависимостей package.json
.
Иногда всё же возникает необходимость обновить пакеты. Чтобы обновить все пакеты, описанные в package.json
, можно использовать следующую команду:
npm update
Команда для обновления конкретного пакета:
npm update [package-name]
Команда для удаления пакета (может быть выполнена с флагами --save
и --save-dev
):
npm uninstall [package-name]
Gulp
Итак, мы установили целый набор пакетов из каталога npm. Давайте разбираться, что с ними делать, и как из наших исходников получить готовый билд, который в свою очередь сможет использовать backend-разработчик.
Для наших целей в рамках курса (а очень часто и в коммерческой разработке) нет смысла использовать сложно-конфигурируемые системы сборки проектов вроде webpack. Нам идеально подойдёт быстрый таск-менеджер для автоматического выполнения часто используемых задач. Одним из них является Gulp.
Gulp — это инструмент, который помогает автоматизировать такие рутинные задачи веб-разработки как:
- создание локального веб-сервера и автоматическая перезагрузка страницы в браузере при изменении кода или файлов проекта;
- использование различных JavaScript, CSS и HTML препроцессоров (об этом чуть позже);
- оптимизация и конкатенация (объединение) отдельных файлов проекта;
- минификация CSS и JS кода;
- автоматическое создание вендорных префиксов — приставок к названию CSS свойства, которые добавляют производители браузеров для нестандартных или неустоявшихся CSS-свойств;
- обработка изображений — автоматическое сжатие, создание спрайтов, ресайз и т.п.;
- деплой проекта — процедура переноса вашего сайта на сервер;
- и многое другое.
Давайте установим Gulp.js. Сначала нам понадобится установить саму утилиту. Для этого в командной строке наберите:
npm install --global gulp-cli
Данную команду достаточно выполнить один раз, а в дальнейшем для новых проектов делать это уже не нужно.
Теперь, находясь в директории нашего проекта, нужно установить зависимость в devDependencies
, которой нам не хватало:
npm install --save-dev gulp
Проверьте версию Gulp с помощью команды:
gulp --version
Должно получиться примерно следующее:
$ gulp --version
CLI version: 2.3.0
Local version: 4.0.2
В вашем package.json
также должна появиться соответствующая запись.
Мы установили наш такс-менеджер, теперь нужно его настроить. Конфигурация Gulp пишется на обычном JavaScript, основы которого мы ещё будем разбирать в рамках данного курса, поэтому те моменты, которые сейчас будут непонятны, прояснятся уже через несколько уроков.
Конфигурация Gulp
Создайте в корне проекта файл gulpfile.mjs
, добавьте в него следующее содержимое:
import gulp from "gulp";
import del from "del";
import include from "gulp-file-include";
import formatHtml from "gulp-format-html";
import less from "gulp-less";
import plumber from "gulp-plumber";
import postcss from "gulp-postcss";
import autoprefixer from "autoprefixer";
import sortMediaQueries from "postcss-sort-media-queries";
import minify from "gulp-csso";
import rename from "gulp-rename";
import terser from "gulp-terser";
import imagemin from "gulp-imagemin";
import imagemin_gifsicle from "imagemin-gifsicle";
import imagemin_mozjpeg from "imagemin-mozjpeg";
import imagemin_optipng from "imagemin-optipng";
import svgmin from "gulp-svgmin";
import svgstore from "gulp-svgstore";
import server from "browser-sync";
const resources = {
html: "src/html/**/*.html",
jsDev: "src/scripts/dev/**/*.js",
jsVendor: "src/scripts/vendor/**/*.js",
images: "src/assets/images/**/*.{png,jpg,jpeg,webp,gif,svg}",
less: "src/styles/**/*.less",
svgSprite: "src/assets/svg-sprite/*.svg",
static: [
"src/assets/icons/**/*.*",
"src/assets/favicons/**/*.*",
"src/assets/fonts/**/*.{woff,woff2}",
"src/assets/video/**/*.{mp4,webm}",
"src/assets/audio/**/*.{mp3,ogg,wav,aac}",
"src/json/**/*.json",
"src/php/**/*.php"
]
};
// Gulp Tasks:
function clean() {
return del("dist");
}
function includeHtml() {
return gulp
.src("src/html/*.html")
.pipe(plumber())
.pipe(
include({
prefix: "@@",
basepath: "@file"
})
)
.pipe(formatHtml())
.pipe(gulp.dest("dist"));
}
function style() {
return gulp
.src("src/styles/styles.less")
.pipe(plumber())
.pipe(less())
.pipe(
postcss([
autoprefixer({ overrideBrowserslist: ["last 4 version"] }),
sortMediaQueries({
sort: "desktop-first"
})
])
)
.pipe(gulp.dest("dist/styles"))
.pipe(minify())
.pipe(rename("styles.min.css"))
.pipe(gulp.dest("dist/styles"));
}
function js() {
return gulp
.src("src/scripts/dev/*.js")
.pipe(plumber())
.pipe(
include({
prefix: "//@@",
basepath: "@file"
})
)
.pipe(gulp.dest("dist/scripts"))
.pipe(terser())
.pipe(
rename(function (path) {
path.basename += ".min";
})
)
.pipe(gulp.dest("dist/scripts"));
}
function jsCopy() {
return gulp
.src(resources.jsVendor)
.pipe(plumber())
.pipe(gulp.dest("dist/scripts"));
}
function copy() {
return gulp
.src(resources.static, {
base: "src"
})
.pipe(gulp.dest("dist/"));
}
function images() {
return gulp
.src(resources.images)
.pipe(
imagemin([
imagemin_gifsicle({ interlaced: true }),
imagemin_mozjpeg({ quality: 100, progressive: true }),
imagemin_optipng({ optimizationLevel: 3 })
])
)
.pipe(gulp.dest("dist/assets/images"));
}
function svgSprite() {
return gulp
.src(resources.svgSprite)
.pipe(
svgmin({
js2svg: {
pretty: true
}
})
)
.pipe(
svgstore({
inlineSvg: true
})
)
.pipe(rename("symbols.svg"))
.pipe(gulp.dest("dist/assets/icons"));
}
const build = gulp.series(
clean,
copy,
includeHtml,
style,
js,
jsCopy,
images,
svgSprite
);
function reloadServer(done) {
server.reload();
done();
}
function serve() {
server.init({
server: "dist"
});
gulp.watch(resources.html, gulp.series(includeHtml, reloadServer));
gulp.watch(resources.less, gulp.series(style, reloadServer));
gulp.watch(resources.jsDev, gulp.series(js, reloadServer));
gulp.watch(resources.jsVendor, gulp.series(jsCopy, reloadServer));
gulp.watch(resources.static, { delay: 500 }, gulp.series(copy, reloadServer));
gulp.watch(resources.images, { delay: 500 }, gulp.series(images, reloadServer));
gulp.watch(resources.svgSprite, gulp.series(svgSprite, reloadServer));
}
const start = gulp.series(build, serve);
export {
clean,
copy,
includeHtml,
style,
js,
jsCopy,
images,
svgSprite,
build,
serve,
start
};
Объём кода может немного испугать, но давайте поэтапно разберём, что здесь происходит.
В начале мы видим серию строк, начинающихся словом import
:
import gulp from "gulp";
Это всего-навсего автоматическое подключение установленных нами npm-модулей из папки node_modules
. Используются они ниже.
Далее идёт const resources
. Это константа, то есть тип данных, который не подлежит дальнейшему изменению. Её значением является объект, представляющий из себя набор свойств, состоящих из пар «ключ: значение»:
const resources = {
html: "src/html/**/*.html",
};
Здесь мы просто указываем пути до наших ресурсов, изменение которых будем отслеживать. В примере выше для ключа html
мы указали любые HTML-файлы в любых подкаталогах, расположенных в src/html/
.
Давайте перейдём чуть ниже к нашим задачам, которые представляют из себя JavaScript-функции. Это специальный тип объектов, позволяющий формализовать средствами языка определённую логику поведения и обработки данных.
Вначале идёт ключевое слово function
, после него имя
функции, затем в круглых скобках список перечисленных через запятую параметров, которые передаются в данную функцию (их может и не быть) и, наконец, тело функции внутри фигурных скобок. Функция может что-либо вернуть с помощью ключевого слова return
.
Очистка сборки
Первая задача, которую мы видим — clean
.
function clean() {
return del("dist");
}
Задача «возвращает» вызов функции del
с текстовым параметром "dist"
.
del
является одной из наших зависимостей, импорт которой можно видеть на второй строке нашего конфига gulpfile.mjs
.
Всё, что делает данный пакет, — удаляет указанную нами директорию, в данном случае dist
. Эта директория будет автоматически создаваться при сборке проекта и содержать готовую сборку проекта. Нам нужно регулярно её чистить, чтобы не накапливать «мусор».
Обработка HTML
Следующая задача — includeHtml
. Здесь мы обрабатываем все наши HTML-файлы.
function includeHtml() {
return gulp
.src("src/html/*.html")
.pipe(plumber())
.pipe(
include({
prefix: "@@",
basepath: "@file"
})
)
.pipe(formatHtml())
.pipe(gulp.dest("dist"));
}
Вначале в src
мы обращаемся ко всем HTML-файлам, лежащим непосредственно в src/html/
. Как мы помним, здесь у нас расположены наши страницы.
Далее по цепочке благодаря встроенному Gulp-методу pipe
выполняется следующая подзадача — вызов плагина plumber
. Этот плагин предотвращает прерывание исполняемой функции в том случае, если мы совершили какую-то ошибку и наш код нельзя правильно обработать. В консоль будет выведено соответствующее уведомление.
Далее вызывается include
. Этот плагин позволяет нам включать HTML-блоки, вынесенные в отдельные файлы.
Пора сделать первую оптимизацию процесса разработки.
Теперь разметка всех наших страниц в src/html/
будет выглядеть так:
<!DOCTYPE html>
<html lang="ru">
@@include('blocks/head.html')
<body class="body">
@@include('blocks/page-header.html')
<main class="main">
<!-- содержимое страницы -->
</main>
@@include('blocks/page-footer.html')
</body>
</html>
Соответственно в подкаталоге src/html/blocks/
нам необходимо создать файлы:
head.html
page-header.html
page-footer.html
Далее просто переносим туда соответствующую разметку, которую ранее мы дублировали на каждой странице.
Следующая подзадача — вызов formatHtml
. Плагин просто автоматически форматирует нашу разметку.
Наконец, вызывается gulp.dest("dist")
— обработанные и собранные HTML-файлы сохраняются в каталог dist
в корне нашего проекта.
Обработка стилей
За обработку стилей отвечает задача style
.
function style() {
return gulp
.src("src/styles/styles.less")
.pipe(plumber())
.pipe(less())
.pipe(
postcss([
autoprefixer({ overrideBrowserslist: ["last 4 version"] }),
sortMediaQueries({
sort: "desktop-first"
})
])
)
.pipe(gulp.dest("dist/styles"))
.pipe(minify())
.pipe(rename("styles.min.css"))
.pipe(gulp.dest("dist/styles"));
}
Работать мы будем не с обычным CSS, а использовать CSS-препроцессор Less. Подробно об этом поговорим в следующем разделе данного урока. Пока в каталоге src/styles/
создайте пустой файл styles.less
.
Благодаря плагину less
мы обрабатываем наш less-файл и автоматически конвертируем его в обычный CSS, после чего обрабатываем код CSS-процессором PostCSS.
C помощью autoprefixer
автоматически и там, где это необходимо, добавляем вендорные префиксы CSS-свойств для различных браузеров, что позволяет значительно расширить поддержку многих плохо-поддерживаемый CSS-свойств.
В качестве примера, наш исходный CSS-код:
.element {
display: flex;
}
Скомпилированный CSS:
.element {
display: -webkit-box;
display: -ms-flexbox;
display: flex;
}
Далее sortMediaQueries
автоматически сортирует медиа-выражения. Пока опустим этот момент, но подробно к нему вернёмся, когда будем изучать адаптивную вёрстку.
Сохраняем наши стили в каталог dist/styles/
, тут же производим очередную оптимизацию — минифицируем наш CSS-файл плагином minify
, переименовываем в styles.min.css
и также сохраняем в dist/styles/
.
Обработка JavaScript
За обработку наших js-файлов отвечает задача js
.
function js() {
return gulp
.src("src/scripts/dev/*.js")
.pipe(plumber())
.pipe(
include({
prefix: "//@@",
basepath: "@file"
})
)
.pipe(gulp.dest("dist/scripts"))
.pipe(terser())
.pipe(
rename(function (path) {
path.basename += ".min";
})
)
.pipe(gulp.dest("dist/scripts"));
}
Мы ещё не изучали, JavaScript, однако давайте в общих чертах разберём что тут происходит.
Мы обрабатываем все js-файлы, лежащие непосредственно в src/scripts/dev/
, инклудим js-блоки по аналогии с HTML, если в этом есть острая необходимость (в случае с JavaScript делать это крайне не рекомендуется), сохраняем всё в dist/scripts/
, а затем с помощью плагина terser
минифицируем наш js-код.
Ниже мы видим задачу jsCopy
.
function jsCopy() {
return gulp
.src(resources.jsVendor)
.pipe(plumber())
.pipe(gulp.dest("dist/scripts"));
}
Всё, что она делает — копирует без какой либо обработки js-файлы, лежащие в resources.jsVendor
и сохраняет в dist/scripts/
.
Обработка статичных файлов
Все пути для статичных файлов, которые мы не хотим никак обрабатывать, мы указываем в resources.static
.
С помощью задачи copy
все файлы с соответствующими путями сохраняются в папку dist
.
function copy() {
return gulp
.src(resources.static, {
base: "src"
})
.pipe(gulp.dest("dist/"));
}
Так, шрифты, лежащие в подкаталоге src/assets/fonts
/ будут скопированы в dist/assets/fonts/
.
Обработка изображений
С помощью задачи images
мы обрабатываем изображения.
function images() {
return gulp
.src(resources.images)
.pipe(
imagemin([
imagemin_gifsicle({ interlaced: true }),
imagemin_mozjpeg({ quality: 100, progressive: true }),
imagemin_optipng({ optimizationLevel: 3 })
])
)
.pipe(gulp.dest("dist/assets/images"));
}
Берём, всё, что указано в resources.images
, оптимизируем и сжимаем в соответствии с указанными параметрами плагином imagemin
и далее сохраняем в dist/assets/images/
.
При этом в папке src
у нас остаются исходные изображения.
SVG-спрайт.
Одна из самых замечательных оптимизаций — автоматическое создание внешнего символьного спрайта. Об особенностях и ограничениях поговорим на одном из следующих уроков. А пока сделайте следующее.
Скопируйте в каталог src/assets/svg-sprite/
ранее экспортированный из макета шапки логотип logo.svg
. Откройте файл текстовым редактором.
Если в нём имеются атрибуты ширины и высоты (width
и height
), удалите их. Должно остаться следующее:
<svg viewBox="0 0 202 147" fill="none" xmlns="http://www.w3.org/2000/svg">
<!-- остальное содержимое файла -->
</svg>
Теперь надо изменить разметку нашего логотипа в шапке на следующую:
<a href="#" class="page-header__logo">
<svg class="page-header__logo-icon" width="202" height="147">
<use xlink:href="assets/icons/symbols.svg#logo"></use>
</svg>
</a>
Обратите внимание, насколько наша разметка стала меньше и чище. Мы обращаемся к внешнему файлу symbols.svg
, лежащему в каталоге dist/assets/icons/
, и к конкретной его части — символу logo
.
Сам SVG-спрайт генерируется благодаря задаче svgSprite
.
function svgSprite() {
return gulp
.src(resources.svgSprite)
.pipe(
svgmin({
js2svg: {
pretty: true
}
})
)
.pipe(
svgstore({
inlineSvg: true
})
)
.pipe(rename("symbols.svg"))
.pipe(gulp.dest("dist/assets/icons"));
}
Мы берём все SVG-файлы, расположенные в resources.svgSprite
, объединяем в один символьный спрайт, оптимизируем его и сохраняем по указанному пути.
Наш сгенерированный файл будет иметь примерно следующее содержимое:
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<symbol id="logo" viewBox="0 0 202 147">
<!-- код логотипа -->
</symbol>
<!-- прочие символы -->
</svg>
Сборка проекта и отслеживание изменений
Мы разобрали все отдельные задачи, которые нам нужны, чтобы собрать готовый проект. Но выполнять их все по очереди очень неудобно. Воспользуемся встроенным Gulp-методом series
, который позволяет выполнить серию задач друг за другом, поставив их в очередь:
const build = gulp.series(
clean,
copy,
includeHtml,
style,
js,
jsCopy,
images,
svgSprite
);
Таким образом мы создали команду build
, последовательно выполняющую все написанные нами задачи.
Осталось совсем чуть-чуть. Развернём локальный сервер, используя директорию dist
, плагин server
и новую задачу serve
.
function serve() {
server.init({
server: "dist"
});
gulp.watch(resources.html, gulp.series(includeHtml, reloadServer));
gulp.watch(resources.less, gulp.series(style, reloadServer));
gulp.watch(resources.jsDev, gulp.series(js, reloadServer));
gulp.watch(resources.jsVendor, gulp.series(jsCopy, reloadServer));
gulp.watch(resources.static, { delay: 500 }, gulp.series(copy, reloadServer));
gulp.watch(resources.images, { delay: 500 }, gulp.series(images, reloadServer));
gulp.watch(resources.svgSprite, gulp.series(svgSprite, reloadServer));
}
При выполнении команды gulp serve
в консоли по адресу http://localhost:3000/
запустится наше приложение.
Благодаря встроенному функционалу gulp.
watch
мы будем отслеживать все изменения по указанным путям, выполнять указанные задачи, а затем автоматически перезагружать наш сервер с помощью reloadServer
.
function reloadServer(done) {
server.reload();
done();
}
Создадим ещё одну задачу, которая объединит в себе сборку проекта и запуск сервера:
const start = gulp.series(build, serve);
Последнее, что нам осталось — сделать экспорт всех наших задач, которые мы хотим выполнять:
export {
clean,
copy,
includeHtml,
style,
js,
jsCopy,
images,
svgSprite,
build,
serve,
start
};
Три основные задачи мы также прописали в package.json
:
"scripts": {
"build": "gulp build",
"serve": "gulp serve",
"start": "gulp start"
}
На этом с автоматизацией всё. Теперь, находясь в директории проекта, выполняем в консоли команду:
gulp start
Ждём сборки проекта, запуска сервера и работаем дальше уже в автоматическом режиме.
Остановить обработку и отслеживание можно сочетанием клавиш ctrl + c
.
Примечание: на данный момент у нас не прописаны стили, так как файл styles.less
пустой. Скоро мы это исправим.
3. CSS-препроцессор Less
О препроцессорах
CSS-препроцессор — это инструмент, расширяющий возможности стандартного CSS, его целями являются облегчение и ускорение процесса разработки за счёт подходов и приёмов, характерных для языков программирования:
- импорт файлов;
- вложенные CSS-правила;
- переменные;
- функции;
- математические операции;
- операции с цветом;
- циклы.
Новые возможности постепенно появляются и в стандартном CSS (например переменные и некоторые функции), но они несравнимы с возможностями препроцессоров, развивающихся намного быстрее.
Следует помнить, что CSS-препроцессор — это всего лишь инструмент разработки, и если вы не умеете писать CSS-код, то он вам не поможет.
Кроме того, не стоит пытаться «программировать» на препроцессорах. Ваш код должен быть простым и максимально понятным, иначе в дальнейшем такое «упрощение разработки» создаст проблемы вам или другому разработчику, которому доведётся работать с вашим кодом. Сложные функции и вычисления хороши для демонстрации возможностей препроцессора, но не в реальной жизни.
Браузеры не поддерживают препроцессоры, поэтому в конечном итоге весь наш код благодаря средствам автоматизации преобразовывается в обычный CSS.
Самые популярные CSS-препроцессоры это Less и SASS. Оба препроцессора во многом похожи, но имеют и свои различия. В целом, если вы освоили работу с одним, разобраться со вторым сможете очень быстро. В рамках данного курса мы будем работать с Less.
Структура файлов
В каталоге проекта src/styles/
у нас лежит файл styles.less
. Это основной файл, в котором будут подключаться все наши стили. Он не должен содержать какой-либо CSS-код, здесь будут только импорты прочих файлов.
Порядок импорта важен. В начале мы будем подключать общие стили, которые нельзя отнести к какому-либо выделенному БЭМ-блоку, локальные шрифты, переменные и прочее. Такие файлы будут лежать у нас в каталоге src/styles/global/
. Давайте их создадим:
- common.less
- fonts.less
- variables.less
В каталоге src/styles/vendor/
мы будем хранить стили сторонних плагинов. Например стили слайдера, который у нас есть на главной странице. Пока оставим этот каталог пустым, но запомним, что такие файлы лучше подключать следующими, т.е. после общих стилей.
Наконец, в каталоге src/styles/blocks/
будут располагаться стили наших БЭМ-блоков. Правило простое: название БЭМ-блока всегда должно соответствовать названию файла.
Таким образом, в данном каталоге у нас будут лежать:
- btn.less
- event-card.less
- event.less
- events.less
- listing.less
- page-footer.less
- page-header.less
- reserve.less
- section-title.less
- и все прочие блоки.
При этом подключать БЭМ-блоки следует от меньшего к большему: сначала подключаем самые маленькие, такие как заголовки, кнопки и прочее, а далее более крупные, которые могут в себе содержать предыдущие блоки.
Подключаются файлы с помощью правила @import
. Содержание нашего файла styles.less
на данный момент должен быть примерно таким:
@import "global/variables.less";
@import "global/fonts.less";
@import "global/common.less";
// @import "vendor/swiper.less";
@import "blocks/btn.less";
@import "blocks/section-title.less";
@import "blocks/event-card.less";
@import "blocks/listing.less";
@import "blocks/reserve.less";
@import "blocks/events.less";
@import "blocks/event.less";
@import "blocks/page-header.less";
@import "blocks/page-footer.less";
Препроцессор Less хорош тем, что внутри less-файлов мы можем писать обычный CSS-код. А это значит, что всё, что находится у нас в файле styles.css
, можно распределить по только что созданным less-файлам, а сам CSS-файл можно удалить, он нам больше не нужен.
С БЭМ-блоками должно быть всё более-менее понятно, variables.less
пока оставим пустым, в fonts.less
локально подключим ещё один шрифт, который нам нужен, но отсутствует на Google Fonts:
@font-face {
font-family: "mak";
src: local("MAK-bold"), url("../assets/fonts/MAK-bold.woff") format("woff");
font-weight: 700;
font-style: normal;
}
@font-face {
font-family: "mak";
src: local("MAK-light"), url("../assets/fonts/MAK-light.woff") format("woff");
font-weight: 300;
font-style: normal;
}
Скачайте файлы шрифтов и разместите их в каталоге src/assets/fonts/
.
Содержание common.less
может быть примерно таким:
html,body {
width: 100%;
margin: 0;
padding: 0;
}
body {
box-sizing: border-box;
display: flex;
flex-direction: column;
min-height: 100vh;
background-color: #dedede;
background-image: url(../assets/images/noise.png), url(../assets/images/bg.png);
background-position: 0 0, 0 0;
background-size: auto, 100% auto;
background-repeat: repeat, repeat-y;
color: #1F1E1E;
font-family: "Open Sans", sans-serif;
font-size: 16px;
font-weight: 400;
font-style: normal;
line-height: 1.375;
overflow-x: hidden;
padding-top: 147px;
}
.main {
flex: 1 0 auto;
}
.page-footer {
flex: 0 0 auto;
}
a {
color: #1F1E1E;
outline: none;
}
img {
max-width: 100%;
height: auto;
object-fit: cover;
}
Мы задали здесь основную структуру и стили нашего сайта, кроме того прописали цвет текста ссылкам по умолчанию и запретили изображениям быть больше, чем их контейнер.
В дальнейшем в целях оптимизации добавим сюда ещё некоторые фиксы.
Теперь наш учебный проект можно собрать и запустить с помощью команды gulp start
. Если вы всё сделали правильно, запустится локальный сервер, а в браузере по адресу http://localhost:3000/
можно будет увидеть итог вашей работы в текущем состоянии.
С этого момента уже можно приступать к полноценной разработке, но прежде стоит сделать ещё несколько оптимизаций.
Переменные
Здесь всё достаточно просто. Переменные в Less объявляются с помощью символа @
, затем идёт имя переменной, оно может включать в себя латинские символы, символ подчёркивания и дефис. Затем после двоеточия указывается допустимое значение в CSS, имя другой переменной или специальное выражение.
@black: #1f1e1e;
@black_80: fade(@black, 80%);
@font: "Roboto", sans-serif;
@font_size: 16px;
@fonts_size_s: (@font_size / 1.25);
Теперь заданные переменные мы можем использовать в качестве значений для любых CSS-правил:
body {
color: @black;
font-family: @font;
font-size: @font_size;
}
figcaption {
color: @black_80;
font-size: @fonts_size_s;
}
В конечном итоге наш код будет скомпилирован в обычный CSS:
body {
color: #1f1e1e;
font-family: "Roboto", sans-serif;
font-size: 16px;
}
figcaption {
color: rgba(31,30,30,.8);
font-size: 12.8px;
}
Обратите внимание, благодаря функции fade
мы преобразовали цвет для figcaption
с HEX в RGBA, добавив 20% прозрачности.
Хранить переменные мы будем в файле src/styles/global/variables.less
. Наш файл на данный момент может выглядеть примерно так:
// Цветовая палитра
@white: #ffffff;
@black: #1f1t1t;
@red: #bc3324;
@red_dark: #a51505;
@gray: #dedede;
@gray_dark: #bfbfbf;
// Шрифты
@font1: "Open Sans", sans-serif;
@font2: "Lora", serif;
@font3: "mak", serif;
Дело осталось за малым: пройтись автозаменой по всем прочим less-файлам и заменить совпадающие значения на наши переменные. Так, common.less
к данному моменту будет выглядеть следующим образом:
html,body {
width: 100%;
margin: 0;
padding: 0;
}
body {
box-sizing: border-box;
display: flex;
flex-direction: column;
min-height: 100vh;
background-color: @gray;
background-image: url(../assets/images/noise.png), url(../assets/images/bg.png);
background-position: 0 0, 0 0;
background-size: auto, 100% auto;
background-repeat: repeat, repeat-y;
color: @black;
font-family: @font1;
font-size: 16px;
font-weight: 400;
font-style: normal;
line-height: 1.375;
overflow-x: hidden;
padding-top: 147px;
}
.main {
flex: 1 0 auto;
}
.page-footer {
flex: 0 0 auto;
}
a {
color: @black;
outline: none;
}
img {
max-width: 100%;
height: auto;
object-fit: cover;
}
Комментарии
В обычном CSS единственный способ оставить комментарий выглядит так:
/* Комментарий */
/*
Многострочный
комментарий
*/
В less мы можем делать точно так же, или же использовать построчное комментирование, которое мы уже могли видеть в примерах выше:
// Шрифты
@font1: "Open Sans", sans-serif;
Закомментированные строки не попадут в конечный CSS-код. Однако при необходимости (например нам нужно указать авторские права) мы можем сохранить комментарий в скомпилированном файле:
/*! Этот комментарий будет сохранён. !*/
Вложенные CSS-правила
Обычный CSS-код писать не очень удобно, нет никакой группировки правил, а в случае с БЭМ мы постоянно пишем одинаковые части названий элементов блоков. Давайте вспомним, как выглядели наши для шапки сайта:
.page-header {
/* Стили блока шапки */
}
.page-header__container {
/* Стили контейнера шапки */
}
.page-header__nav-link {
/* Стили ссылок навигации */
}
.page-header__nav-link:hover {
/* Стили ссылок по наведению мыши */
}
В less мы будем писать код, объединяя всё внутри БЭМ-блоков:
.page-header {
/* Стили блока шапки */
&__container {
/* Стили контейнера шапки */
}
&__nav-link {
/* Стили ссылок навигации */
&:hover {
/* Стили ссылок по наведению мыши */
}
}
}
В данном случае амперсанд &
будет соединять наименования всех уровней вложенности CSS-правил. Специфичность правил, о которой мы говорили на втором уроке, останется той же. Таким образом, мы не только сохраняем все преимущества БЭМ, но и объединяем весь CSS-код внутри каждого БЭМ-блока.
Для псевдоэлементов и модификаторов работает всё точно так же:
.element {
/* CSS-стили */
&::before,
&::after {
/* CSS-стили псевдоэлемента */
}
&--red {
/* CSS-стили модификатора*/
&::before,
&::after {
/* псевдоэлемент с модификатором*/
}
}
}
Аналогично мы можем прописывать CSS-правила для вложенных элементов, повышая специфичность правил.
CSS-код:
.element {
/* CSS-стили */
}
.element section {
/* CSS-стили */
}
.element section.child {
/* CSS-стили */
}
Соответствующий less-код:
.element {
/* CSS-стили элемента */
section {
/* CSS-стили секции внутри .element */
&.child {
/* CSS-стили секции с классом .child */
}
}
}
Впрочем, делать так следует только в случае крайней необходимости.
Примеси
Примеси, или миксины, ещё одна очень важная особенность препроцессора, позволяющая нам значительно быстрее и проще писать код.
Давайте вспомним, как мы задавали контейнер для шапки сайта:
.page-header {
// Стили блока шапки
&__container {
box-sizing: border-box;
display: flex;
align-items: center;
width: 100%;
max-width: 1414px;
margin: 0 auto;
padding-left: 40px;
padding-right: 40px;
}
}
Если внимательно посмотреть на макеты, то мы увидим, что подобный контейнер нам нужен не только в шапке, но и во многих других блоках, так как сетка сайта выдерживается на каждой странице в едином стиле. Дублировать и поддерживать подобный код очень неудобно. Решить проблему можно с помощью примесей.
В каталоге src/styles/global
создайте файл mixins.less
со следующим содержанием:
.container() {
box-sizing: border-box;
width: 100%;
max-width: 1414px;
margin-left: auto;
margin-right: auto;
padding-left: 40px;
padding-right: 40px;
}
Как видим, после названия миксина идут круглые скобки.
При этом подключать данный файл в styles.less
следует самым последним. Чуть ниже поймём почему, а пока давайте посмотрим, как будет выглядеть теперь контейнер шапки:
.page-header {
/* Стили блока шапки */
&__container {
.container();
display: flex;
align-items: center;
}
}
В конечном CSS-коде стили миксина будут объединены со стилями элемента, к которому мы его применили, при этом сам миксин не попадёт в CSS-файл.
Однако, Less позволяет нам использовать любое CSS-правило в качестве миксина или же БЭМ- микса (вспоминаем второй урок).
Мы можем задать стили в нашем mixins.less
, к примеру, следующим образом:
.container {
box-sizing: border-box;
width: 100%;
max-width: 1414px;
margin-left: auto;
margin-right: auto;
padding-left: 40px;
padding-right: 40px;
}
.display-none {
display: none;
}
.color-red {
color: @red;
}
Такие правила попадут в скомпилированный CSS-файл, следовательно мы сможем их использовать в разметке в качестве обычных CSS-классов при необходимости. При этом там, где мы применяли миксины (контейнер шапки), всё останется без изменений, стили точно так же будут объединены.
Такие примеси-классы нужны, чтобы точечно перебить какие-либо свойства элементов. Например, нам может понадобиться временно скрыть какой-либо блок с помощью JavaScript. Для этого мы просто добавим данному элементу класс display-none
. Именно поэтому подключать mixins.less
следует самым последним. Это позволит перебить родные стили элемента, но сохранить специфичность на том же уровне.
Примеси с параметром
Иногда возникает потребность использовать с разными элементами один и тот же миксин, в котором должны отличаться некоторые свойства. Чтобы не дублировать наш код, мы можем использовать параметры:
.mixin(@width: 100px, @height: 50px) {
width: @width;
height: @height;
/* прочие свойства */
}
Мы задали два параметра — ширину и высоту, и указали им значения по умолчанию. Теперь мы можем использовать данный миксин следующим образом:
.element_1 {
.mixin();
}
.element_2 {
.mixin(80px);
}
.element_3 {
.mixin(80px 70px);
}
Скомпилированный CSS-код:
.element_1 {
width: 100px;
height: 50px;
}
.element_2 {
width: 80px;
height: 50px;
}
.element_3 {
width: 80px;
height: 70px;
}
Расширения
Примеси добавляют свои свойства к каждому CSS-правилу, в котором мы их вызываем. То есть в конечном CSS-коде идёт дублирование стилей, что увеличивает размер файла. Есть ещё один способ, позволяющий повторно использовать одни CSS-правила в других — расширения.
Рассмотрим less-код:
.inline {
color: red;
}
.nav {
&:extend(.inline);
background: blue;
width: 100%;
}
Конечный CSS-код будет выглядеть так:
.inline,
.nav {
color: red;
}
.nav {
background: blue;
width: 100%;
}
Расширения вместо того, чтобы добавлять свой код другим правилам, объединяют различные селекторы с одними и теми же CSS-свойствами.
Примечание: Задумка отличная. Однако уменьшение кода в конечном счёте оказывается мнимым. Дело в том, что при сборке проекта мы автоматически сжимаем, минифицируем наш CSS с помощью gulp-postcss, а все правила объединяются наиболее оптимальным образом. Таким образом, никакого смысла использовать less-расширения для нас нет, следует применять именно примеси.
На этом с препроцессорами мы заканчиваем, более подробно с документацией предлагаем ознакомиться самостоятельно.
4. Оптимизация HTML-разметки
Листинг мероприятий
Мы уже научились создавать и включать отдельные HTML-блоки с помощью задачи includeHtml
для Gulp. Теперь на примере листинга мероприятий разберёмся, как ещё сильнее оптимизировать нашу вёрстку.
Используемый нами Gulp-плагин позволяет передавать переменные. Воспользуемся этим.
В каталоге src/html/blocks/
создайте файл event-card.html
, скопируйте разметку карточки мероприятия из events.html
и измените её следующим образом:
<article class="event-card @@class">
<a href="#" class="event-card__link">
<picture class="event-card__picture">
<img src="@@img" alt="@@alt" class="event-card__img" />
</picture>
<h4 class="event-card__title">@@title</h4>
<p class="event-card__description">@@descr</p>
<time datetime="@@datetime" class="event-card__time">@@time</time>
</a>
</article>
Мы вынесли все необходимые значения в переменные, которые теперь зададим непосредственно в листинге, перечислив все карточки через метод loop()
следующим образом:
<div class="events">
<section class="listing events__listing">
<h2 class="listing__title section-title">Мероприятия</h2>
<div class="listing__events-list">
<!-- prettier-ignore -->
@@loop('blocks/event-card.html', [
{
"class": "listing__event-card",
"img": "assets/images/events/nirvana.jpg",
"alt": "nirvana",
"title": "Собираемся и слушаем альбом Nirvana",
"descr": "Это третий альбом группы...",
"time": "07.12.2022 | начало 18.00",
"datetime": "2022-12-07T18:00"
},
{
"class": "listing__event-card",
"img": "assets/images/events/stuff.jpg",
"alt": "stuff",
"title": "Есть тема, нужно обсудить с HowStuffWorks",
"descr": "Один из самых популярных подкастов в мире...",
"time": "14.12.2022 | начало 18.30",
"datetime": "2022-12-14T18:30"
}
])
</div>
<!-- Кнопка "Показать всё" -->
</section>
<!-- Остальная разметка -->
</div>
Комментарий <!-- prettier-ignore -->
вам понадобится в том случае, если вы используете автоматическое форматирование разметки с помощью расширения prettier для вашего редактора кода.
Теперь HTML-код карточки мы можем править в одном единственном месте. Переменные для оставшихся карточек пропишите самостоятельно.
Листинг статей блога
На главной странице у нас имеется листинг статей блога, сверстаем его.

Чтобы разметка самой страницы осталась максимально простой, создадим в каталоге src/html/blocks
новый файл blog-listing.html
и подключим его:
<!DOCTYPE html>
<html lang="ru">
@@include('blocks/head.html')
<body class="body">
@@include('blocks/page-header.html')
<main class="main">
<div class="home">
<!-- прочая разметка -->
<!-- prettier-ignore -->
@@include('blocks/blog-listing.html', {"class": "home__listing"})
<!-- прочая разметка -->
</div>
</main>
@@include('blocks/page-footer.html')
</body>
</html>
БЭМ-блок home
нам нужен, чтобы задать внутренние отступы на странице, а БЭМ-элементу home__listing
зададим внешний нижний отступ в соответствии с дизайном:
.home {
padding: 100px 0;
&__listing {
margin-bottom: 95px;
}
}
В качестве листинга мы используем уже имеющийся у нас после вёрстки списка мероприятий БЭМ-блок listing
.
А для самих карточек необходимо создать новый БЭМ-блок article-card
и соответствующий HTML-файл. По аналогии с мероприятиями сразу же сделаем цикл по карточкам:
<section class="listing @@class">
<h2 class="listing__title section-title">Блог</h2>
<div class="listing__article-list">
<!-- prettier-ignore -->
@@loop('article-card.html', [
{
"class": "listing__article-card",
"img": "assets/images/blog/theatre.jpg",
"alt": "Theatre Photosession",
"title": "Фотосессия в стенах Театра драмы",
"descr": "Организовывая фотосессию для...",
"time": "26.11.2021",
"datetime": "2021-11-26T00:00"
}
])
</div>
<a href="#" class="listing__link link">Смотреть все</a>
</section>
В примере заданы переменные для одной карточки, добавьте ещё для двух самостоятельно.
Кнопка «Смотреть всё» вынесена в отдельный БЭМ-блок, так как используется повторно.
Карточка статьи блога
Карточку в очередной раз сверстаем с помощью Flexbox. Карточка довольно крупная. Как правило, в таких случаях ссылкой делают не всю карточку а её отдельные элементы. Чаще всего это изображение и заголовок. Однако в правой части у нас имеется стрелка, которая также должна быть кликабельной. Мы можем избежать лишней ссылки в нашей разметке, разместив стрелку в разметке ссылки-заголовка, но спозиционировав её абсолютно в правом нижнем углу карточки.
В отдельных случаях стрелка может быть ссылкой или кнопкой, но в данном случае она уже находится внутри ссылки, а значит мы должны использовать <div>
или <span>
.
Изображение мы снова обернём в тег <picture>
, в дальнейшем он нам понадобится для адаптивных изображений, а затем всё обернём в ссылку.
Таким образом в карточке мы имеем два выделенных блока — ссылку-изображение и прочую информацию:
<article class="article-card @@class">
<a href="#" class="article-card__link">
<picture class="article-card__picture">
<img src="@@img" alt="@@alt" class="article-card__img" />
</picture>
</a>
<div class="article-card__info">
<time datetime="@@datetime" class="article-card__time">@@time</time>
<a href="#" class="article-card__title-link">
<h4 class="article-card__title">@@title</h4>
<div class="article-card__arrow arrow">
<svg class="arrow__icon" width="92" height="62">
<use xlink:href="assets/icons/symbols.svg#arrow"></use>
</svg>
</div>
</a>
<p class="article-card__description">@@descr</p>
</div>
</article>
Если обратить внимание на UI-kit проекта в Figma, то по наведению на карточку мыши вокруг изображения должна появляться тень, подсвечивая таким образом карточку.
.article-card {
box-sizing: border-box;
display: flex;
width: 100%;
&:hover {
.article-card__picture {
box-shadow: 0 0 30px fade(@black, 55%);
}
}
&__link {
flex-shrink: 0;
align-self: flex-start;
display: block;
width: 467px;
margin-right: 60px;
font-size: 0;
text-decoration: none;
}
&__picture {
display: block;
position: relative;
width: 100%;
transition: box-shadow 0.2s;
overflow: hidden;
&::after {
content: "";
display: block;
width: 100%;
padding-top: 62.3126%;
}
}
&__img {
display: block;
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
object-fit: cover;
}
}
Для информации в правой части карточки зададим внутренний отступ справа чуть больше, чем ширина стрелки, вынесенной абсолютом, заголовку зададим автоматический отступ сверху, чтобы прибить весь текстовый контент к нижней части карточки. Здесь нам снова поможет Flexbox.
.article-card {
&__info {
box-sizing: border-box;
position: relative;
display: flex;
flex-direction: column;
width: 100%;
padding-top: 30px;
padding-bottom: 30px;
padding: 30px 0;
padding-right: 150px;
}
&__time {
margin: 0 0 40px;
font-weight: 300;
font-size: 16px;
line-height: 22px;
}
&__title-link,
&__description {
max-width: 470px;
}
&__title-link {
margin: auto 0 40px;
color: @black;
text-decoration-color: transparent;
transition: color 0.2s, text-decoration-color 0.2s;
&:hover {
color: @red;
text-decoration-color: @red;
}
}
&__title {
margin: 0;
color: inherit;
font-weight: 600;
font-size: 24px;
line-height: 33px;
}
&__description {
margin: 0;
font-size: 16px;
line-height: 22px;
}
&__arrow {
position: absolute;
right: 0;
bottom: 30px;
color: inherit;
transition: none;
}
}
Мы огранили ширину заголовка и описания максимальной шириной 470px
, по наведению мыши на ссылку меняем её цвет на красный и добавляем нижнее подчёркивание у заголовка.
Через урок мы поговорим о графике, научимся менять цвет у наших символьных SVG, находящихся в спрайте, после чего цвет стрелки по наведению на ссылку также будет становится красным. Пока оставим так.
В нашей карточке две ссылки, и будет очень хорошо, если цвет заголовка мы будем менять на красный не только по наведению на сам заголовок, но и по наведению на изображение:
.article-card {
/* прочие стили */
&__link {
/* прочие стили */
&:hover {
& ~ .article-card__info {
.article-card__title-link {
color: @red;
text-decoration-color: @red;
}
}
}
}
}
Осталось последнее: между карточками имеется полоса-разделитель. Реализовать это можно по-разному, но в данном случае разумно будет отступить от методологии БЭМ, и с помощью селектора последующего элемента (+
) для карточки, идущей вслед за такой же карточкой, задать двойной внешний отступ, а разделителем выступит псевдоэлемент, спозиционированный абсолютно между ними. Таким образом разделитель будет появляться только в том случае, если в нашем списке имеется две или более карточек, идущих подряд.
.article-card {
position: relative;
/* прочие стили */
& + .article-card {
margin-top: 120px;
&::before {
content: "";
display: block;
position: absolute;
margin: 0 auto 60px;
bottom: 100%;
left: 0;
right: 0;
width: 100%;
max-width: 1254px;
height: 1px;
background-color: @black;
pointer-events: none;
}
}
}
Резюме
Мы разобрались, каким образом автоматизировать наш рабочий процесс, познакомились с препроцессорами, поговорили об оптимизации ресурсов и кода. На следующем уроке нас ждёт ещё один очень важный компонент веб страниц — форма обратной связи.
5. Материалы для самостоятельного изучения
- Руководство по Node.js:
https://nodejs.org/ru/docs/guides/ - Документация npm:
https://docs.npmjs.com/ - Документация Gulp:
https://gulpjs.com/docs/en/getting-started/quick-start - Документация Less:
https://lesscss.org/features/