Gulp сборка для верстки с использованием Pug шаблонизатора

Создадим Seo оптимизированную, многофункциональную Gulp сборку для комфортной верстки многостраничных сайтов с использованием технологий Pug, Scss, Webpack, Linter (линтеров), SVG спрайтов. Оптимизируем изображения, а так же сгенерируем Favicons (фавиконки).

Оглавление статьи

  1. Вводная часть
  2. Базовая инициализация
  3. Установка Gulp
  4. Установка линтеров
  5. Настройка линтеров
  6. Проверка линтера
  7. Структура каталогов
  8. Создание config.js
  9. Редактируем package.json
  10. Таск clear
  11. Таск server
  12. Таск webpack
  13. Таск styles
  14. Таск sprites
  15. Таск images
  16. Таск assets
  17. Таск pug
  18. Таск favicons
  19. Файл manifest.json
  20. Файл robots.txt
  21. Подведем итоги

Вводная часть

Основная идея заключается в создании сборщика для верстки сайтов и автоматизации рутинных действий. Часть используемых идей такие как базовая SEO оптимизация, заготовки в виде скриптов Google Analytics и Yandex Metrika которые мы использовали при создании простого HTML шаблона, будут так же использованы в данной сборке.

Каждый шаг создания Gulp сборки я старался комментировать в репозитории сборки. Обязательно загляните туда если походу статьи вам будет что-то не понятно.

Основной функционал

Основной функционал gulp сборки будет включать в себя:

  • Оптимизация под Page Speed
    • Отложенная загрузка
    • Критические стили
  • Базовая SEO оптимизация
    • Open Graph / Twitter Cards
    • JSON-LD микроразметка
    • Google Analytics / Yandex Metrika
  • Pug шаблонизатор
    • Улучшенный фильтр markdown-it
    • Возможность добавлять свои фильтры на примере фильтра special-chars
    • Работа с json данными
  • SCSS препроцессор
  • Webpack сборщик
    • Возможность использовать новый формат JS по максимуму
    • Оптимизация кода через Babel
  • SVG спрайты
    • Цветные
    • Черно-белые
  • Оптимизированные изображения
    • Генерация webp и avif форматов
  • Настроенные линтеры
    • Stylelint
    • Pug-lint
    • ES-Lint
    • Prettier
    • Editorconfig
  • Автоматическая генерация favicons

Базовая инициализация

Создадим базовые файлы для проекта, такие как конфиги, инициализируем git и npm. О том как установить и настроить git говорилось в соответствующих статьях, так же и про установку npm

Cоздание README.md

В корень проекта добавим файл README.md для вводной информации о сборке.

README.MD
# Gulp сборка

Gulp сборка c использованием pug, scss, webpack.

Инициализация git

Git нам понадобится в качестве системы учета контроля версий. В дальнейшем мы будем делать commit предварительно пропуская сохраняемые файлы через linter, об этом более подробно далее.

BASH
git init

Создание .gitignore

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

.idea
.vscode
node_modules
build

В дальнейшем мы сможем дополнить этот файл другими правилами.

Создание .gitattributes

В корне проекта создадим файл .gitattribute, в нем мы будем задавать правила как git должен обрабатывать различные расширения файлов.

# Общие настройки, которые обязательно должны быть прописаны.
# Автоматическое определение текстовых файлов и выполнение нормализации LF.
* text eol=lf

# Изображения
*.png   binary
*.jpg   binary
*.jpeg  binary
*.gif   binary
*.webp  binary
*.tif   binary
*.tiff  binary
*.ico   binary
# SVG по дефолту рассматривается как бинарный.
# Для распознавания как текcт, закомментируйте нижнюю строку и раскомментируйте следующую за ней.
*.svg   binary
#*.svg  text
*.eps   binary
*.mo    binary
*.po    binary

# Шрифты
*.woff  binary
*.woff2 binary

# Документы
*.doc     diff=astextplain
*.docx    diff=astextplain
*.dot     diff=astextplain
*.pdf     diff=astextplain
*.rtf     diff=astextplain
*.md      text
*.tex     text
*.adoc    text
*.textile text
*.mustache  text
*.csv       text
*.tab       text
*.tsv       text
*.sql       text

# Исключить файлы из экспорта
.gitattributes export-ignore
.gitignore export-ignore

Инициализация npm

Удостоверьтесь что у вас установлен Node.js. Инициализируем пакетный менеджер зависимостей npm, с его помощью мы будем устанавливать необходимые зависимости для нашего проекта.

BASH
npm init -y

В корне проекта создастся файл package.json, примерное содержимое файла:

JSON
{
  "name": "gulp-template",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}

Установка Gulp

Установим Gulp сборщик, для этого перейдем на официальный сайт и выполним предложенные команды в терминале.

BASH
// Установит глобально для всей системы gulp-cli, чтобы можно было использовать команду gulp в терминале.
npm i --global gulp-cli

// Установит gulp для текущего проекта в режиме --save-dev
npm i -D gulp

Имеются два режима установки npm зависимостей:

  • Зависимость используемая только при разработке --save-dev или -D
  • Зависимость используемая в готовом варианте проекта --save или -S (по умолчанию всегда ставится этот режим если явно его не переопределить)

Создание gulpfile.babel.js

В корне проекта создадим файл gulpfile.babel.js.

Это главный файл для сборки, именно его будет запускать Gulp. Существует несколько вариантов gulpfile, вариант с babel означает, что при проектировании сборки мы сможем использовать все прелести ES6 и сделать ее модульной и структурированной. О вариантах gulpfile можно почитать здесь.

Так же в корне проекта создадим каталог gulp в котором будем хранить все наши таски.

BASH
// Установит глобально для всей системы gulp-cli, чтобы можно было использовать команду gulp в терминале.
npm i --global gulp-cli

// Установит gulp для текущего проекта в режиме --save-dev
npm i -D gulp

Установка babel

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

BASH
npm i -D @babel/core @babel/register

@babel/core - это основной пакет babel, который содержит основные компоненты для транспиляции кода (перевода кода из нового формата ES6 в старый формат ES5, для поддержки старых браузеров), включая лексический анализатор (парсер), генератор кода, правила трансформации и другие инструменты, вместе с другими плагинами и пресетами.

@babel/register, этот пакет перехватывает операции чтения файлов с расширением .js и .jsx в процессе выполнения. После перехвата он трансформирует код на лету, используя babel, и передает его исполнителю.

Подробнее о babel можно почитать здесь.

Установка пресета и полифилла

Пресет и полифиллы необходимы для обратной совместимости новых возможностей JavaScript с старыми браузерами.

BASH
npm i -D @babel/preset-env core-js

@babel/preset-env - используется для транспиляции кода на этапе компиляции и core-js для добавления необходимых полифиллов на этапе выполнения, чтобы обеспечить оптимальную совместимость с различными окружениями.

При совместном использовании @babel/preset-env c core-js, babel автоматически добавит необходимые полифиллы из core-js в код на этапе компиляции.

Подробнее о пресете и полифилле можно почитать здесь.

Настройка пресета

Чтобы пресет заработал ему необходимо задать определенные правила по которым он должен действовать. Правила для пресета должны быть указаны в файле .babelrc. В корне проекта создадим файл .babelrc со следующим содержимым.

.BABELRC
{
  "presets": [
    [
      "@babel/preset-env",
      {
        "debug": false,
        "useBuiltIns": "usage",
        "corejs": "3"
      }
    ]
  ]
}

Разберем каждую настройку по отдельности:

presets - это опция babel, которая указывает список наборов плагинов (пресетов), которые будут использоваться в процессе транспиляции.

@babel/preset-env - это пресет, который включает набор плагинов, необходимых для транспиляции кода с использованием современного синтаксиса JavaScript и поддержки различных окружений.

debug - это опция пресета @babel/preset-env, которая определяет, будет ли выводиться отладочная информация в консоль. Значение false указывает на то, что отладочная информация не будет выводиться.

useBuiltIns - это опция пресета @babel/preset-env, которая указывает, каким образом следует добавлять полифиллы из core-js. Значение "usage" означает, что Babel будет добавлять только те полифиллы, которые необходимы в вашем коде, на основе его актуального использования. Это позволяет уменьшить размер финального бандла и добавлять только необходимый функционал.

corejs - это опция пресета @babel/preset-env, которая указывает версию core-js, которая будет использоваться для предоставления полифиллов. В данном случае указана версия "3", что означает использование core-js версии 3.

Тестируем gulpfile.babel.js

В файле gulpfile.babel.js пропишем следующий код.

JS
import { dest } from 'gulp'

export default (done) => {
  console.log('Test message')

  done()
}

Выполним команду gulp в терминале.

BASH
gulp

Мы должны увидеть следующий вывод.

BASH
[time] Requiring external module @babel/register
[time] Using gulpfile ~/gulp-template/gulpfile.babel.js
[time] Starting 'default'...
Test message
[time] Finished 'default' after 2.24 ms

Установка линтеров

Линтеры позволяют поддерживать весь код проекта в единообразном стиле, придерживаясь определенных правил написания кода. Линтер не позволит пользователю создать commit до тех пор, пока код не будет отредактирован согласно правилам.

В рамках сборки мы будем использовать линтеры для файлов js, pug и scss, а так же будем проверять соответствие кода правилам, прописанным в файлах .editorconfig и .prettierrc.

Перед установкой зависимостей удостоверьтесь, что у вас установлены расширения для редактора кода или IDE, которые работают с линтерами. Это нужно для того, чтобы ошибки в коде автоматически подсвечивались в вашем редакторе. Например, для VSCode нужно будет установить расширения: Pug Linter (pug-lint), stylelint, ESLint. Расширения и плагины были рассмотрены в предыдущих статьях.

Линтер для JS

Установим соответствующие зависимости.

BASH
npm i -D eslint eslint-config-airbnb-base eslint-plugin-import @babel/eslint-parser eslint-config-prettier

Это инструменты и пакеты, используемые для статического анализа кода JavaScript с помощью ESLint.

eslint - это инструмент статического анализа кода для JavaScript, который помогает выявить и предотвратить потенциальные ошибки, стилистические проблемы и проблемы безопасности в вашем коде.

eslint-config-airbnb-base - это предустановленный конфигурационный набор правил ESLint, созданный и поддерживаемый Airbnb компанией, которая активно использует и поддерживает JavaScript код стандарта Airbnb.

eslint-plugin-import - это плагин для ESLint, который предоставляет правила и функциональность, связанные с импортами и использованием модулей. Этот плагин часто используется с eslint-config-airbnb-base, чтобы улучшить поддержку правил импортов в стандарте Airbnb.

@babel/eslint-parser - это парсер для ESLint, который позволяет использовать Babel для разбора синтаксиса JavaScript файлов вместо стандартного парсера, который используется по умолчанию.

eslint-config-prettier - гарантирует, что правила форматирования, установленные Prettier, не будут нарушены правилами ESLint. Это позволяет легко интегрировать ESLint и Prettier в проект, чтобы управлять как стилем, так и качеством кода.

Линтер для Pug

Установим соответствующие зависимости.

BASH
npm i -D pug-lint

Это инструмент для статического анализа кода шаблонов Pug.

pug-lint - позволяет выявлять потенциальные проблемы, стилистические ошибки и несоответствия соглашениям о форматировании в коде Pug.

Линтер для стилей CSS и Scss

Установим соответствующие зависимости.

BASH
npm i -D stylelint stylelint-config-rational-order stylelint-config-recommended-scss stylelint-config-standard stylelint-order stylelint-scss

Это инструменты и пакеты, используемые для статического анализа кода CSS (или его препроцессоров, таких как SCSS) с помощью Stylelint.

stylelint - помогает выявить и предотвратить потенциальные ошибки, стилистические проблемы и проблемы форматирования в вашем CSS коде.

stylelint-config-rational-order - упорядочивает свойства CSS в определенном логическом порядке, что улучшает читаемость и облегчает поддержание кода.

stylelint-config-recommended-scss - предоставляет рекомендуемые правила для SCSS кода (расширение CSS, поддерживающее дополнительные возможности, такие как переменные и вложенные стили).

stylelint-config-standard - предоставляет стандартные правила форматирования CSS, соответствующие общим стандартам и соглашениям о кодировании.

stylelint-order - позволяет настроить, каким образом должны быть упорядочены свойства CSS в вашем коде.

stylelint-scss - это плагин для Stylelint, который добавляет поддержку для SCSS синтаксиса и дополнительных функций, таких как вложенные правила и переменные.

Линтер для Editorconfig и Prettier

Установим соответствующие зависимости.

BASH
npm i -D editorconfig-checker prettier

editorconfig-checker - это инструмент для проверки соответствия файлов вашего проекта настроенным правилам EditorConfig.

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

Установим Lint staged

Установим соответствующие зависимости.

BASH
npx mrm lint-staged

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

lint-staged - это инструмент, который позволяет запускать линтеры (например, ESLint или Stylelint) только для тех файлов, которые были изменены или добавлены в систему контроля версий, перед фиксацией изменений (commit). Это позволяет оптимизировать процесс проверки кода, чтобы проверять только измененные файлы, а не весь проект.

Когда вы запускаете npx mrm lint-staged, команда mrm будет временно устанавливать и запускать задачу lint-staged. Задача lint-staged, в свою очередь, проверит измененные файлы в вашем проекте с помощью настроенных линтеров перед фиксацией изменений (commit). Это позволяет убедиться, что ваш код соответствует стандартам и соглашениям о форматировании, прежде чем он попадет в систему контроля версий.

Настройка линтеров

После установке в файле package.json появятся следующие изменения.

JSON
{
  ...
  "scripts": {
    ...
    "prepare": "husky install"
  },
  ...
  "devDependencies": {
    ...
    "husky": "^8.0.3",
    "lint-staged": "^13.2.3",

    "editorconfig-checker": "^5.1.1",
    "eslint": "^8.45.0",
    "eslint-config-airbnb-base": "^15.0.0",
    "eslint-plugin-import": "^2.27.5",
    "prettier": "^3.0.0",
    "pug-lint": "^2.7.0",
    "stylelint": "^15.10.2",
    "stylelint-config-rational-order": "^0.1.2",
    "stylelint-config-recommended-scss": "^12.0.0",
    "stylelint-config-standard": "^34.0.0",
    "stylelint-order": "^6.0.3",
    "stylelint-scss": "^5.0.1"
  },
  "lint-staged": {
    "*.js": "eslint --cache --fix",
    "*.css": "stylelint --fix",
    "*.{js,css,md}": "prettier --write"
  }
}

Как видим установились нужные нам зависимости, а так же были добавлены husky и lint-staged.

husky - предназначен для управления хуками Git в вашем проекте. Хуки Git - это скрипты, которые можно запускать автоматически при определенных событиях Git, таких как commit, push и т.д. Husky позволяет легко настроить и управлять хуками, чтобы выполнять определенные действия перед или после выполнения операций с Git.

В конструкции scripts можно заметить, что было добавлено правило "prepare": "husky install". Данная команда автоматически была вызвана при инициализации npx mrm lint-staged, но если предположим вы захотите на основе данной сборке создать новый проект, вам понадобится установить husky вручную командой:

BASH
npm run prepare

Посмотрим на следующий фрагмент в файле package.json.

JSON
"lint-staged": {
  "*.js": "eslint --cache --fix",
  "*.css": "stylelint --fix",
  "*.{js,css,md}": "prettier --write"
}

Данное правило позволяет при создании commit, автоматически запускать eslint для всех измененных файлов с расширением .js, stylelint для всех измененных файлов с расширением .css и prettier для всех измененных файлов с расширениями js, css, md.

Если линтеры обнаружат проблемы, они будут пытаться исправить их автоматически (--fix), и если исправления возможны, они будут включены в commit. Если исправления невозможны, lint-staged не позволит сделать commit, что поможет поддерживать качество кода и согласованность в проекте. Решить найденные проблемы нужно будет вручную, отредактировав соответсвующее место.

А теперь давайте изменим, дополним эти правила:

JSON
"lint-staged": {
  "*": "editorconfig-checker --exclude '.git|.husky|node_modules'",
  "src/pug/**/*.pug": "pug-lint --fix",
  "src/scss/**/*.scss": "stylelint --fix",
  "*.js": [
    "eslint --cache --fix",
    "prettier --write"
  ]
}

Каждый прописанный линтер будет сверять, найденные файлы с определенными, заданными правилами. На данном этапе мы еще создавали файлы с правилами. Ниже я опишу, что это за файлы, а после мы их создадим.

editorconfig-checker - проверит все файлы в проекте (согласно правилам прописанным в файле .editorconfig), кроме тех которые находятся в каталогах .git, .husky, node_modules.

prettier - проверит все js файлы в проекте, согласно правилам прописанным в файле .prettierrc.

pug-lint - проверит все pug файлы в проекте, согласно правилам прописанным в файле .pug-lintrc.

stylelint - проверит все scss файлы в проекте, согласно правилам прописанным в файле .stylelintrc.

eslint - проверит все js файлы в проекте, согласно правилам прописанным в файле .eslintrc.js.

Создание .editorconfig

В корне проекта создадим файл .editorconfig, в нем мы будем задавать правила того как редактор кода, IDE должны стандартизировать содержимое файлов.

.EDITORCONFIG
# Корневой файл .editorconfig
root = true

# Все файлы
[*]
charset = utf-8 # кодировка
indent_style = space # отступ через пробел
indent_size = 2 # размер отступа 2 символа
end_of_line = lf # unix стиль новой строки
trim_trailing_whitespace = true # удалять пробелы в конце строк
insert_final_newline = true # оставлять в конце файла пустую строку

# Pug файлы
[*.pug]
trim_trailing_whitespace = false # удалять пробелы в конце строк

# Md файлы
[*.md]
trim_trailing_whitespace = false # удалять пробелы в конце строк

Создание .prettierrc

В корне проекта создадим файл .prettierrc, в нем мы будем задавать дополнительные правила того как редактор кода, IDE должны стандартизировать содержимое js файлов.

.PRETTIERRC
{
  "singleQuote": true, // одинарные кавычки
  "printWidth": 80, // максимальная длина строки, после которой Prettier будет автоматически переносить код на новую строку.
  "semi": false, // автоматически добавлять точки с запятой в конце каждого выражения
  "tabWidth": 2, // количество пробелов, которые будут заменять один символ табуляции
  "trailingComma": "all", // нужна ли запятая в конце объекта
  "useTabs": false, // использовать символы табуляции, вместо пробелов, в качестве отступов
  "endOfLine": "lf", // указывает символ новой строки для использования в конце файла
  "bracketSpacing": true, // добавить пробелы вокруг скобок в объектах и массивах
  "arrowParens": "avoid" // определяет, следует ли оборачивать параметры стрелочных функций в круглые скобки. "avoid" означает, что скобки будут опущены, если возможно.
}

Создание .pug-lintrc

В корне проекта создадим файл .pug-lintrc, в нем мы будем задавать дополнительные правила того как редактор кода, IDE должны стандартизировать содержимое pug файлов.

.PUG-LINTRC
{
  "validateExtensions": true, // проверяет расширения файлов Pug и обнаруживает файлы с неправильными расширениями
  "validateDivTags": true,// проверяет использование тегов <div> и рекомендует заменить их на более специфичные теги или сокращенную форму, если это возможно
  "validateAttributeSeparator": { "separator": ", ", "multiLineSeparator": ",\n " }, // проверяет разделитель атрибутов и рекомендует использовать запятую и пробел между атрибутами на одной строке, а при многострочных атрибутах, использовать запятую и перевод строки
  "validateAttributeQuoteMarks": "\"", // проверяет кавычки в атрибутах и рекомендует использовать двойные кавычки для атрибутов
  "requireSpecificAttributes": [ { "img": [ "src", "width", "height", "alt" ] } ], // требует, чтобы для тега <img> были указаны обязательные атрибуты "src", "width", "height", "alt"
  "requireSpaceAfterCodeOperator": true, // требует пробел после оператора кода (например, =) в Pug-шаблонах
  "requireLowerCaseTags": true, // требует использовать нижний регистр для имен тегов в Pug-шаблонах
  "requireClassLiteralsBeforeAttributes": true, // требует, чтобы классы (атрибуты с классами) шли перед другими атрибутами в тегах
  "disallowSpacesInsideAttributeBrackets": true, // запрещает использование пробелов внутри квадратных скобок атрибутов (например, [attr= "value"])
  "disallowLegacyMixinCall": true, // запрещает использование устаревшего синтаксиса вызова миксинов в Pug
  "disallowDuplicateAttributes": true // запрещает использование повторяющихся атрибутов в одном теге
}

Создание .stylelintrc

В корне проекта создадим файл .stylelintrc, в нем мы будем задавать дополнительные правила того как редактор кода, IDE должны стандартизировать содержимое scss файлов.

.STYLELINTRC
{
  "extends": [
    "stylelint-config-recommended-scss", // предоставляет рекомендуемые правила для SCSS-кода (расширение CSS, поддерживающее дополнительные возможности, такие как переменные и вложенные стили).
    "stylelint-config-rational-order" // упорядочивает свойства CSS в определенном логическом порядке, что улучшает читаемость и облегчает поддержание кода.
  ],
  "plugins": [
    "stylelint-scss" // это плагин добавляет поддержку для SCSS-синтаксиса и дополнительных функций, таких как вложенные правила и переменные
  ],
  "rules": {
    "at-rule-no-unknown": null, // отключает правило проверки на неизвестные @ правила
    "scss/at-if-no-null": null, // отключает правило проверки на использование null в SCSS @if
    "scss/at-rule-no-unknown": [ // правило для проверки на неизвестные SCSS @ правила, за исключением правила @tailwind, которое игнорируется
      true,
      {
        "ignoreAtRules": [
          "tailwind"
        ]
      }
    ],
    "declaration-empty-line-before": null, // отключает правило проверки на пустую строку перед объявлениями
    "order/properties-order": [], // перечень порядка свойств, которые в данной конфигурации пустой, что означает отсутствие жестких правил о порядке свойств
    "plugin/rational-order": [ // используется плагин stylelint-order для проверки порядка свойств CSS
      true,
      {
        "empty-line-between-groups": true // добавить пустую строку между группами свойств
      }
    ],
    "no-descending-specificity": null, // отключает правило проверки на уменьшение специфичности селекторов
    "block-no-empty": null, // отключает правило проверки на пустые блоки CSS
    "import-notation": null, // отключает правило проверки импорта модулей
    "string-quotes": "double", // устанавливает двойные кавычки (") в качестве предпочтительного стиля для строк
    "selector-class-pattern": "^(?:(?:o|c|u|t|s|is|has|_|js|qa)-)?[a-zA-Z0-9]+(?:-[a-zA-Z0-9]+)*(?:__[a-zA-Z0-9]+(?:-[a-zA-Z0-9]+)*)?(?:--[a-zA-Z0-9]+(?:-[a-zA-Z0-9]+)*)?(?:\\[.+\\])?$", // устанавливает шаблон для проверки наименований классов. В данном случае, это шаблон для БЭМ-стиля и анализирует структуру именования классов.
    "selector-no-vendor-prefix": null, // отключает правило проверки на использование вендорных префиксов в селекторах
    "scss/no-global-function-names": null // отключает правило проверки на использование глобальных функций в SCSS
  }
}

Создание .eslintrc.js

В корне проекта создадим файл .eslintrc, в нем мы будем задавать дополнительные правила того как редактор кода, IDE должны стандартизировать содержимое js файлов.

.PUG-LINTRC
module.exports = {
  root: true, // указывает, что это корневой файл конфигурации ESLint
  env: {
    browser: true, // предназначен для браузера
    es2023: true, // код использует возможности ECMAScript 2023
    jquery: false, // код не использует jquery
  },
  extends: [
    'airbnb-base', // предоставляет набор рекомендованных правил от Airbnb для JavaScript
    'prettier', // гарантирует, что правила форматирования, установленные Prettier, не будут нарушены правилами ESLint
  ],
  parser: '@babel/eslint-parser', // здесь указан парсер, используемый для анализа JavaScript-кода, который позволяет ESLint работать с кодом, написанным с использованием Babel
  rules: {
    indent: ['off', 2], // отключает правило проверки отступов и разрешает использовать два пробела в качестве отступа
    'import/no-extraneous-dependencies': [ // отключает правило проверки на наличие лишних зависимостей
      'off',
      {
        devDependencies: [
          'gulpfile.babel.js',
          'gulp/**/*',
        ],
      },
    ],
    'import/no-import-module-exports': [ // отключает правило проверки на использование import вместо module.exports
      'off',
      {
        exceptions: [
          'gulpfile.babel.js',
          'gulp/**/*',
        ],
      },
    ],
    'import/resolver': [ // отключает правило проверки путей к импортированным модулям
      'off',
      {
        exceptions: [
          'gulpfile.babel.js',
          'gulp/**/*',
        ],
      },
    ],
    'implicit-arrow-linebreak': [ // отключает правило проверки на использование неявных стрелочных функций
      'off',
      {
        exceptions: [
          'gulpfile.babel.js',
          'gulp/**/*',
        ],
      },
    ],
    'no-var': 'error', // устанавливает запрет на использование var для объявления переменных
    'object-curly-newline': 'error', // включает правило проверки на новую строку после открывающейся фигурной скобки
    'max-len': 'off', // отключает правило проверки максимальной длины строки кода
    'no-multi-assign': 'off', // отключает правило проверки на множественное присваивание
    'no-unused-vars': 'error', // включает правило проверки на неиспользуемые переменные
    'no-undef': 'off', // отключает правило проверки на неопределенные переменные
    'no-console': 'error', // устанавливает ошибку при использовании console.log и других методов console
    quotes: [2, 'single'], // устанавливает одинарные кавычки для строковых литералов
    'import/no-dynamic-require': 'off', // отключает правило проверки на использование динамических require
    'global-require': 'off', // отключает правило проверки на использование require вне глобальной области видимости
    semi: ['error', 'never'], // устанавливает запрет на использование точек с запятой
    'arrow-parens': [ // устанавливает использование круглых скобок для стрелочных функций только тогда, когда это необходимо
      'error',
      'as-needed',
    ],
    'no-underscore-dangle': 'off', // проверяет и предупреждает о использовании символа подчеркивания (underscore) в идентификаторах переменных, методов и свойств объектов
  },
}

Создание .eslintignore

В корне проекта создадим файл .eslintignore. Он будет использоваться для указания файлов и каталогов, которые необходимо игнорировать во время анализа ESLint.

Изначальное содержимое файла .eslintignore будет пустым.

Создание .stylelintignore

В корне проекта создадим файл .stylelintignore. Он будет использоваться для указания файлов и каталогов, которые необходимо игнорировать во время анализа Stylelint.

Изначальное содержимое файла .stylelintignore будет пустым.

Проверка линтера

Если мы откроем файл gulpfile.babel.js, то можем увидеть что у нас в нем сразу два не соответствия правилам no-unused-vars и no-console (см. правила в .eslintrc.js).

Изменим текст на “Test message 2” и попробуем сделать новый commit, если мы все настроили правильно, то commit не должен выполниться, пока мы не исправим ошибки.

BASH
git add .
git commit -m 'add lint staged'

✖ eslint --cache --fix:
~/gulp-template/gulpfile.babel.js
1:10  error  'dest' is defined but never used  no-unused-vars
4:5   error  Unexpected console statement      no-console
✖ 2 problems (2 errors, 0 warnings)
husky - pre-commit hook exited with code 1 (error)

Так же мы можем заметить, что в корне проекта появился файл .eslintcache. Он используется для ускорения работы eslint путем кэширования предыдущих результатов. Добавим .eslintcache в файл .gitignore:

.GITIGNORE
.idea
.vscode
node_modules
build
.eslintcache

А так же в файл package.json.

JSON
"lint-staged": {
  "*": "editorconfig-checker --exclude '.git|.husky|node_modules|.eslintcache'",
  ...
}

Вернемся к исправлению ошибок, удалим не используемую переменную и console.log.

JS
export default (done) => {
  done()
}

Пробуем еще раз произвести commit.

BASH
git add .
git commit -m 'add lint staged'

✔ Preparing lint-staged...
✔ Running tasks for staged files...
✔ Applying modifications from tasks...
✔ Cleaning up temporary files...
✔ Preparing lint-staged...
✔ Running tasks for staged files...
✔ Applying modifications from tasks...
✔ Cleaning up temporary files...
[main 9840a2a] add lint staged
11 files changed, 8835 insertions(+), 1931 deletions(-)
create mode 100644 .editorconfig
create mode 100644 .eslintcache
create mode 100644 .eslintrc.js
create mode 100755 .husky/pre-commit
create mode 100644 .prettierrc
create mode 100644 .pug-lintrc
create mode 100644 .stylelintrc

Если у вас так же не проходит commit, попробуйте удалить файл .eslintcache и повторить коммит.

Мы успешно настроили линтер, позже мы протестируем его на других файлах таких как pug и scss.

Структура каталогов

Создадим структуру нашей сборки следующим образом.

BASH
mkdir -p ./src/{assets,js,pug,scss}

# assets
mkdir -p ./src/assets/{fonts,icons,images}
mkdir -p ./src/assets/icons/{mono,multi}
mkdir -p ./src/assets/images/favicons
touch ./src/assets/manifest.json
touch ./src/assets/robots.txt

# js
mkdir -p ./src/js/components
touch ./src/js/main.js

# pug
mkdir -p ./src/pug/{components,data,layouts,markdown,pages,parts}

# scss
mkdir -p ./src/scss/{core,scaffolds}
mkdir -p ./src/scss/core/{base,helpers}
mkdir -p ./src/scss/scaffolds/{components,sections}
touch ./src/scss/main.scss
touch ./src/scss/critical.scss

# libs
mkdir -p ./src/libs

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

BASH
touch ./src/assets/fonts/.gitkeep
touch ./src/assets/icons/mono/.gitkeep
touch ./src/assets/icons/multi/.gitkeep
touch ./src/assets/images/favicons/.gitkeep

touch ./src/js/components/.gitkeep

touch ./src/pug/components/.gitkeep
touch ./src/pug/data/.gitkeep
touch ./src/pug/layouts/.gitkeep
touch ./src/pug/markdown/.gitkeep
touch ./src/pug/pages/.gitkeep
touch ./src/pug/parts/.gitkeep

touch ./src/scss/core/base/.gitkeep
touch ./src/scss/core/helpers/.gitkeep
touch ./src/scss/core/scaffolds/components/.gitkeep
touch ./src/scss/core/scaffolds/sections/.gitkeep

touch ./src/libs/.gitkeep

Описание каталогов

  • ./src/assets - содержит все оставшиеся ресурсы проекта, не вошедшие в каталоги js, scss, pug, libs
  • ./src/js - содержит js скрипты
  • ./src/pug - содержит разметку
  • ./src/scss - содержит стили
  • ./src/assets/fonts - содержит шрифты
  • ./src/assets/icons/mono - содержит черно-белые svg иконки
  • ./src/assets/icons/multi - содержит цветные svg иконки
  • ./src/assets/images/favicons - содержит фавиконки
  • ./src/assets/manifest.json - содержит настройки для PWA
  • ./src/assets/robots.txt - содержит правила для поисковых роботов
  • ./src/js/components - содержит логику отдельных компонентов
  • ./src/js/main.js - основной исполняемый файл
  • ./src/pug/components - содержит разметку отдельных компонентов (например кнопки, иконки)
  • ./src/pug/data - содержит различные настройки и данные (например список ссылок для навигации)
  • ./src/pug/layouts - содержит шаблоны верстки
  • ./src/pug/markdown - содержит markdown разметку страниц, альтернатива pug разметке
  • ./src/pug/pages - содержит pug разметку, альтернатива markdown разметке, на самом деле markdown разметку можно внедрять в pug разметку для удобства наполнения страниц контентной информацией, но об этом позже
  • ./src/pug/parts - содержит отдельные секции разметки (например шапка, подвал)
  • ./src/scss/core/base - содержит базовые стили, шрифты
  • ./src/scss/core/helpers - содержит переменные, миксины, функции
  • ./src/scss/core/scaffolds/components - содержит стили компонентов верстки (что то не большое)
  • ./src/scss/core/scaffolds/sections - содержит стили секций верстки (что то крупное)
  • ./src/scss/critical.scss - содержит подключение критических стилей
  • ./src/scss/main.scss - содержит подключение всех остальных стилей
  • ./src/libs - содержит файлы сторонних библиотек (например слайдеры, бутстрап)

Сохраняем изменения

Если сейчас попытаться закоммитить изменения, то линтер нас не пропустит так как имеются пустые scss файлы. Чтобы все же добавить пустые scss файлы в репозиторий добавим в каждый scss файл следующий комментарий.

SCSS
/* stylelint-disable */

Либо в файле .eslintignore пропишем путь к каталогу со стилями.

.ESLINTIGNORE
./src/scss/**/*

Это вынужденная мера, так как мне хочется сохранить структуру в репозиторий. В дальнейшем, когда начнем наполнять каталог src файлами мы удалим временные файлы .gitkeep и комментарии /* stylelint-disable */.

Создание config.js

Внутри каталога gulp создадим файл config.js и каталог tasks.

BASH
mkdir -p ./gulp/tasks
touch ./gulp/config.js
touch ./gulp/tasks/.gitkeep
  • config.js - содержит все конфигурации связанные со сборкой
  • tasks - содержит модули сборки, которые будут автоматизировать процесс

Изначальное содержимое файла config.js будет таким:

JS
/**
 * Конфигурационный файл
 *
 * Содержит пути к каталогам и параметры для тасков
 */

const srcPath = 'src' // ресурсы для разработки проекта
const buildPath = 'build' // готовый продакшен проект

const config = {
  proxy: 'http://localhost', // url виртуального хоста
  port: 3000, // порт виртуального хоста

  // Пути к каталогам для разработки проекта
  src: {
    root: srcPath, // корневой каталог

    // Шаблонизатор pug
    pug: {
      root: `${srcPath}/pug`, // корневой каталог pug
      pages: `${srcPath}/pug/pages`, // pug страницы для компиляции в html
    },

    // Препроцессор Scss
    scss: `${srcPath}/scss`, // scss стили

    // Скрипты
    js: {
      root: `${srcPath}/js`, // корневой каталог js
      components: `${srcPath}/js/components`, // компоненты js
    },

    libs: `${srcPath}/libs`, // сторонние библиотеки

    // Различные ресурсы
    assets: {
      root: `${srcPath}/assets`, // корневой каталог js
      images: `${srcPath}/assets/images`, // изображения
      favicons: `${srcPath}/assets/favicons`, // фавиконки
      icons: {
        root: `${srcPath}/assets/icons`, // корневой каталог svg иконок
        mono: `${srcPath}/assets/icons/mono`, // черно-белые иконки
        multi: `${srcPath}/assets/icons/multi`, // цветные иконки
      },
      fonts: `${srcPath}/assets/fonts`, // шрифты
    },
  },

  // Пути к каталогам для продакшен проекта
  build: {
    root: buildPath, // корневой каталог js
    css: `${buildPath}/css`, // стили
    js: `${buildPath}/js`, // скрипты
    images: `${buildPath}/img`, // изображения
    fonts: `${buildPath}/fonts`, // шрифты
  },

  // Определение окружения сборки проекта
  setEnv() {
    this.isProd = process.argv.includes('--prod') // true если сборка проекта выполнена с ключом --prod
    this.isDev = !this.isProd // false если сборка проекта выполнена без ключа --prod
  },
}

export default config

Редактируем gulpfile.babel.js

Чтобы проверить работоспособность, созданного файла config.js, изменим содержимое gulpfile.babel.js на.

JS
// Пользовательские скрипты
import config from './gulp/config'

// Устанавливаем окружение сборки dev или prod
config.setEnv()

export default done => {
  console.log(config.isDev)
  console.log(config.isProd)

  done()
}

Теперь введем в терминале.

BASH
gulp
# Результат
# true
# false

gulp --prod
# Результат
# false
# true

Как видим файл config.js успешно отрабатывает, чтобы понять, что тут произошло, ознакомьтесь с кодом внутри файла config.js.

JS
setEnv() {
  this.isProd = process.argv.includes('--prod') // true если сборка проекта выполнена с ключом --prod
  this.isDev = !this.isProd // false если сборка проекта выполнена без ключа --prod
},

Сохраняем изменения

Если сейчас сделать commit, то линтер нас не пропустит так как имеется console.log в коде. Чтобы все же коммит прошел, пропишем в файл .eslintignore.

.ESLINTIGNORE
gulp
gulpfile.babel.js

Это вынужденная мера в дальнейшем, когда начнем создавать таски мы очистим файл .eslintignore и уберем console.log.

Редактируем package.json

Удалим не нужные конструкции и добавим новые. Я собираюсь добавить описание, ключевые слова, путь до репозитория, удалить из блока script команду test и т.п. Так будет выглядеть обновленный файл.

.ESLINTIGNORE
{
  "name": "gulp-template",

  // Изменяем версию проекта
  "version": "0.0.4",

  // Описание проекта
  "description": "Gulp сборка для продуктивной верстки",
  "keywords": ["gulp", "pug", "webpack", "scss", "svg-sprite"],

  "scripts": {
    "prepare": "husky install"
  },

  // Browserslist - это конфигурационный файл или опция, используемая различными
  // инструментами разработки, такими как Babel, Autoprefixer, ESLint, Stylelint,
  // PostCSS и другими, для определения списка целевых браузеров и окружений,
  // которые должны поддерживаться вашим веб-приложением.
  "browserslist": [
    "last 2 version", // поддерживаются последние 2 версии браузера
    "not dead" // старание браузеры такие как IE не поддерживаются
  ],

  // Путь до репозитория с текущей сборкой
  "repository": {
    "type": "git",
    "url": "git+https://github.com/Eliofery/gulp-template.git"
  },

  // Путь до раздела вопрос/ответ в репозитории
  "bugs": {
    "url": "https://github.com/Eliofery/gulp-template/issues"
  },

  // Путь до основной страницы сборки
  "homepage": "https://github.com/Eliofery/gulp-template#readme",

  // Имя автора сборки
  "author": "Sergio Eliofery",

  "license": "ISC",
  "devDependencies": {
    "@babel/core": "^7.22.9",
    "@babel/preset-env": "^7.22.9",
    "@babel/register": "^7.22.5",
    "core-js": "^3.31.1",
    "editorconfig-checker": "^5.1.1",
    "eslint": "^8.45.0",
    "eslint-config-airbnb-base": "^15.0.0",
    "eslint-plugin-import": "^2.27.5",
    "gulp": "^4.0.2",
    "husky": "^8.0.3",
    "lint-staged": "^13.2.3",
    "prettier": "^3.0.0",
    "pug-lint": "^2.7.0",
    "stylelint": "^15.10.2",
    "stylelint-config-rational-order": "^0.1.2",
    "stylelint-config-recommended-scss": "^12.0.0",
    "stylelint-config-standard": "^34.0.0",
    "stylelint-order": "^6.0.3",
    "stylelint-scss": "^5.0.1"
  },
  "lint-staged": {
    "*": "editorconfig-checker --exclude '.git|.husky|node_modules|.eslintcache'",
    "src/pug/**/*.pug": "pug-lint --fix",
    "src/scss/**/*.scss": "stylelint --fix",
    "*.js": [
      "eslint --cache --fix",
      "prettier --write"
    ]
  }
}

Таск clear

Данный такс будет удалять каталог с собранным проектом. Установим зависимость.

BASH
npm i -D gulp-clean

Внутри каталога ./gulp/tasks создадим файл clear.js который будет содержать код таска.

BASH
touch ./gulp/tasks/clear.js

Так же удалим файл .gitkeep из каталога tasks он нам больше не нужен:

BASH
rm ./gulp/tasks/.gitkeep

Изначальное содержимое файла clear.js будет таким.

JS
/**
 * Удаления продакшен версии проекта
 *
 * Полностью удаляет каталог build
 *
 * @link https://gulpjs.com/docs/en/api/src#options
 * @link https://www.npmjs.com/package/gulp-clean
 */

// Сторонние библиотеки
import { src } from 'gulp' // gulp плагин
import clean from 'gulp-clean' // плагин для удаления каталогов

// Конфиги
import config from '../config'

// Таск
const clear = () =>
  src(config.build.root, { // указываем путь к каталогу который хотим удалить
    read: false, // запрещаем читать содержимое файлов
    allowEmpty: true, // отключаем ошибки о не существующем каталоге
  }).pipe(
    clean({ // удаляем каталог
      force: true, // разрешаем удаление каталогов и файлов за пределами каталога с тасками
    }),
  )

export default clear

Конструкция const clear = означает создание таска с именем clear. Это то же самое как если бы написали gulp.task(’clear’), подробнее об этом ниже.

Добавляем таск clear в gulpfile.babel.js

Изменим содержимое файла gulpfile.babel.js на.

JS
// Сторонние библиотеки
import { series } from "gulp"

// Таски
import clear from './gulp/tasks/clear'

// Конфиги
import config from "./gulp/config"

// Определяем окружения сборки dev или prod
config.setEnv()

// Сборка проекта
export const build = series(
  clear,
)

// Слежение за изменением файлов
export const watch = series(
  build,
)

export default watch

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

Мы создали две конструкции build и watch с использованием метода series, рассмотрим их поподробней.

gulp.series - это метод в инструменте Gulp, который позволяет создавать последовательные серии задач (тасков) для выполнения в определенном порядке. То есть сперва выполнится задача clear потом следующая за ней и т.д. по цепочке. Пока не выполнится до конца впереди идущая задача, последующая не начнется.

Раньше в основном gulp таски создавались через gulp.task(’build’, …), но с появлением варианта gulpfile.babel.js таски можно создавать используя синтаксис ES6 вот так export const build.

export const build тоже самое что и gulp.task(’build’). Где build - это имя gulp таска, оно может быть абсолютно любым, вы сами его задаете.

Чтобы запустить таск необходимо выполнить команду.

BASH
gulp build

# Выполнится конструкция:

#export const build = series(
  #clear,
#)

# Или

gulp watch

# Выполнится конструкция:

#export const watch = series(
  #build,
#)

Так же можно заметить, что в самом низу мы добавили строку.

JS
export default watch

Она означает что по умолчанию будет запускаться таск с именем watch, например если набрать в терминале команду gulp без названий тасков.

BASH
gulp

# Выполнится конструкция:

#export const watch = series(
  #build,
#)

Добавляем запуск тасков в package.json

Хорошей практикой считается выносить запуск тасков в отдельные команды, в файле package.json.

JSON
...

"scripts": {
    ...
    "build": "gulp build --prod",
    "dev": "gulp watch"
  },

...

Здесь мы добавили две команды "build": "gulp build --prod" и "dev": "gulp watch".

  • build будет запускать команду gulp build --prod
  • dev будет запускать команду gulp watch

Пример как вызвать команды build и dev.

BASH
# Для сборки проекта в продакшен версию
# Для продакшн версии картинки проекта оптимизируются, код минифицируется.
npm run build

# Для сборки проекта в режиме разработки
# При режиме разработки проект собирается без оптимизации и минификации.
npm run dev

Конечно сейчас преимущества в использовании команды npm run dev по сравнению с gulp watch не заметны, напротив кажется что команда gulp watch короче. Но так как знакомство с новым проектом обычно начинается с просмотра файла package.json, очень удобно видеть сразу те команды которые участвуют в сборке проекта.

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

BASH
gulp build --prod --arg2 --arg3 --arg4

То каждый раз набирать это вручную муторно, а тем более запомнить, гораздо проще набрать npm run build. Причем наименования build и dev общепринятые в сообществе разработчиков и запустив не известный проект можно с высокой долей вероятности предположить, что они там есть и отвечают за одну и туже логику.

Тестируем таск clear

Чтобы протестировать работоспособность таска clear, создадим в корне проекта каталог build.

Теперь запустим команду сборки или разработки проекта.

BASH
npm run build
npm run dev

Как видим каталог build в корне проекта удалился, таск успешно отработал.

Таск server

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

BASH
npm i -D browser-sync

Внутри каталога ./gulp/tasks создадим файл server.js который будет содержать следующий код таска.

JS
/**
 * Виртуальный сервер
 *
 * Создает виртуальный сервер, автоматически обновляет браузер при изменении файлов
 *
 * @link https://browsersync.io/docs/gulp
 * @link https://browsersync.io/docs/options
 */

// Конфиги
import config from '../config'

// Создаем browserSync
global.browserSync = require('browser-sync').create()

const server = done => {
  browserSync.init({
    // proxy: `${config.proxy}:${config.port}`, // хост по заданной ссылке
    // port: config.port, // использовать заданный порт

    server: config.build.root, // хост по заданному каталогу

    open: true, // автоматически открыть страницу в браузере после запуска таска
    notify: false, // показать уведомление
    cors: true, // добавить HTTP заголовок CORS
    ui: false, // включить доступ к интерфейсу настроек browser-sync
  })

  done() // скрипт завершен
}

export default server

По умолчанию виртуальный сервер хостует каталог build, если необходимо слушать какой-то конкретный адрес расскоментируйте строки в файле config.js

JS
proxy: `${config.proxy}:${config.port}`, // хост по заданной ссылке
port: config.port, // использовать заданный порт

// server: config.build.root, // хост по заданному каталогу

Добавляем таск server в gulpfile.babel.js

Изменим содержимое файла gulpfile.babel.js.

JS
// Сторонние библиотеки
import { series } from 'gulp'

// Таски
import clear from './gulp/tasks/clear'
import server from './gulp/tasks/server'

// Конфиги
import config from './gulp/config'

// Определяем окружения сборки dev или prod
config.setEnv()

// Запуск виртуального сервера
export const proxy = server

// Сборка проекта
export const build = series(clear)

// Слежение за изменением файлов
export const watch = series(build, server)

export default watch

Дополнительно с добавлением таска server был добавлен таск proxy, который отдельно запускает виртуальный сервер, без полной сборки проекта.

Так же добавим в файл package.json, в scripts команду "proxy": "gulp proxy".

Тестируем таск server

Чтобы протестировать работоспособность таска server, запустим проект в dev режиме.

BASH
npm run dev

При запуске виртуального сервера в браузере откроется вкладка с содержимым "Cannot GET /". Чтобы это исправить создадим вручную каталог build и внутри него файл index.html:

BASH
mkdir build
touch ./build/index.html

Добавим в index.html любое содержимое и обновим страницу. В браузере должно отобразиться, введенное нами в index.html содержимое. После чего сбросим подключение к виртуальному серверу нажав в терминале Ctrl+C и введем команду.

BASH
npm run proxy

Должна открыться страница с содержимым файла index.html.

Таск webpack

Данный такс будет объединять js файлы в один общий и минифицировать его. Установим зависимости.

BASH
npm i -D babel-loader eslint-import-resolver-alias webpack-stream gulp-strip-comments vinyl-named-with-path

Внутри каталога ./gulp/tasks создадим файл webpack.js который будет содержать код таска.

JS
/**
 * JS Webpack
 *
 * Объединяет указанные js файлы в один общий и минифицирует его
 */

// Сторонние библиотеки
import { dest, src, watch } from 'gulp' // gulp плагин
import plumber from 'gulp-plumber' // перехватывает ошибки
import notify from 'gulp-notify' // уведомляет об ошибках
import gulpif from 'gulp-if' // вызывает функции по условию
import webpackStream from 'webpack-stream' // webpack плагин
import named from 'vinyl-named-with-path' // дает возможность использовать ${config.src.js}/*.js конструкцию
import strip from 'gulp-strip-comments' // очищает комментарии

// Конфиги
import config from '../config'
import webpackConfig from '../../webpack.config'

// Сборка таска
export const webpackBuild = () =>
  src([`${config.src.js.root}/*.js`]) // входящие файлы
    .pipe(
      // Отлавливаем и показываем ошибки в таске
      plumber({
        errorHandler: notify.onError(err => ({
          title: 'Ошибка в задаче webpackBuild', // заголовок ошибки
          sound: false, // уведомлять звуком
          message: err.message, // описание ошибки
        })),
      }),
    )
    .pipe(named()) // меняем [name] на имя файла
    .pipe(webpackStream(webpackConfig)) // настройки webpack
    .pipe(gulpif(config.isProd, strip())) // удаляем комментарии в коде
    .pipe(dest(`${config.build.js}`)) // исходящий файл
    .pipe(browserSync.stream()) // обновление страницы в браузере

// Слежение за изменением файлов
export const webpackWatch = () => watch(`${config.src.js.root}/**/*.js`, webpackBuild)

Теперь создадим в корне проекта файл webpack.config, который будет содержать правила и настройки по сборке js файлов.

JS
const path = require('path') // определение абсолютного пути

// Рабочее окружение
const isProd = process.argv.includes('--prod')

module.exports = {
  mode: isProd ? 'production' : 'development', // минификация файла
  devtool: isProd ? false : 'inline-source-map', // если gulp сборка запущена в режиме разработки создаст sourcemap файл
  output: {
    filename: '[name].js', // имя файла после сборки
  },
  // разбиение исходного файла на чанки
  optimization: {
    splitChunks: {
      chunks: 'async', // all
      minSize: 30000, // 10000
      maxSize: 0, // 200000
      minChunks: 1,
      maxAsyncRequests: 5,
      maxInitialRequests: 3,
      automaticNameDelimiter: '~',
      cacheGroups: {
        vendors: {
          test: /[\\/]node_modules[\\/]/,
          priority: -10,
        },
        default: {
          minChunks: 2,
          priority: -20,
          reuseExistingChunk: true,
        },
      },
    },
  },
  module: {
    rules: [
      {
        test: /\.js$/, // файл заканчивается на .js
        exclude: /(node_modules|bower_components)/, // исключаем каталоги node_modules и bower_components
        use: {
          loader: 'babel-loader', // используем в качестве обработчика babel-loader
        },
      },
    ],
  },
  resolve: {
    extensions: ['.js'],
    alias: {
      // короткий путь до js файлов через символ @, например @/components/ButtonComponent
      '@': path.resolve(__dirname, 'src/js'),
    },
  },
}

Для того чтобы текстовый редактор и IDE распознал короткий путь, обозначенный через символ @ дополним правила в файле .eslintrc.js.

JS
module.exports = {
	...

	settings: {
	  'import/resolver': {
	    alias: {
	      map: [
	        ['@', './src/js'],
	      ],
	      extensions: ['.js'],
	    },
	  },
	},
}

Добавляем таск webpack в gulpfile.babel.js

Изменим содержимое файла gulpfile.babel.js на.

JS
// Сторонние библиотеки
import { series, parallel } from 'gulp'

// Таски
import clear from './gulp/tasks/clear'
import server from './gulp/tasks/server'
import { webpackBuild, webpackWatch } from './gulp/tasks/webpack'

// Конфиги
import config from './gulp/config'

// Определяем окружения сборки dev или prod
config.setEnv()

// Запуск виртуального сервера
export const proxy = server

// Сборка проекта
export const build = series(clear, webpackBuild)

// Слежение за изменением файлов
export const watch = series(
  build,
  server,

  parallel(
    webpackWatch,
  ),
)

export default watch

Здесь мы добавили сразу 2 таска webpackBuild и webpackWatch. Первый таск объединяет js файлы, второй таск следит за изменениями js файлов. При изменение какого либо js файла внутри src/js, автоматически будет вызван таск webpackBuild, который заного объединит js файлы.

Так же был добавлен новый метод gulp.parallel, который позволяет вызывать таски асинхронно друг другу, не дожидаясь того пока другой таск завершит свою работу. Сейчас у нас только один таск webpackWatch, следящий за изменением файлов, но вскоре к нему добавятся и другие.

Тестируем таск webpack

Чтобы протестировать работоспособность таска webpack создадим компонент кнопки.

BASH
touch ./src/js/components/ButtonComponent.js

Изначальное содержимое ButtonComponent.js будет таким.

JS
export default class ButtonComponent {
  _element = null

  constructor(selector) {
    this._element = document.querySelector(selector)
  }

  get element() {
    return this._element
  }
}

Так же удалим файл .gitkeep из каталога srs/js/components.

Теперь в файле main.js импортируем созданную кнопку, напишем следующий код.

JS
import ButtonComponent from '@/components/ButtonComponent'

const button = new ButtonComponent('.button')

button.element.addEventListener('click', () => {
  const message = document.createElement('div')

  message.textContent = 'Нажали на кнопку'
  document.body.insertAdjacentElement('beforebegin', message)
})

Запустим сборку в режиме разработки.

BASH
npm run dev

В каталоге build появится каталог js с файлом main.js внутри.

Создадим в корне каталога build файл index.html, со следующим содержимым:

BASH
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Title</title>
</head>
<body>

  <button class="button" type="button">Кнопка</button>

<script src="js/main.js"></script>
</body>
</html>

Перезапустим страницу в браузере и нажмем на кнопку. Появится сообщение “Нажали на кнопку”.

Таск styles

Данный такс будет объединять scss файлы в один общий css и минифицировать его. Установим зависимости.

BASH
npm i -D sass gulp-sass gulp-sass-glob gulp-postcss autoprefixer cssnano postcss-custom-media postcss-discard-comments
npm i -S normalize.css

Внутри каталога ./gulp/tasks создадим файл styles.js который будет содержать код таска.

JS
/**
 * Scss препроцессор
 *
 * Объединяет указанные scss файлы в один общий css файл и минифицирует
 */

// Сторонние библиотеки
import { src, dest, watch } from 'gulp' // gulp плагин
import plumber from 'gulp-plumber' // перехватывает ошибки
import notify from 'gulp-notify' // уведомляет об ошибках
import sassGlob from 'gulp-sass-glob' // позволяет использовать /**/*.scss конструкцию
import dartSass from 'sass' // препроцессор sass
import gulpSass from 'gulp-sass' // препроцессор sass
import postcss from 'gulp-postcss' // позволяет использовать PostCSS для обработки CSS-файлов
import autoPrefixer from 'autoprefixer' // автоматически добавляет префиксы для поддержки старых браузеров
import cssnano from 'cssnano' // минифицирует css файл
import postcssCustomMedia from 'postcss-custom-media' // группирует стили под общими медиа запросами
import comments from 'postcss-discard-comments' // группирует стили под общими медиа запросами

// Пользовательские скрипты
import config from '../config' // конфигурационный файл

// Подключение библиотеки sass
const sass = gulpSass(dartSass)

// Сборка таска
export const stylesBuild = () => {
  const minify = config.isProd ? cssnano() : () => {} // минификация стилей
  const clear = config.isProd ? comments({ removeAll: true }) : () => {} // удаление комментариев

  return src(`${config.src.scss}/**/*.{scss,sass}`, { sourcemaps: config.isDev }) // входящие файлы
    .pipe(
      // Отлавливаем и показываем ошибки в таске
      plumber({
        errorHandler: notify.onError(err => ({
          title: 'Ошибка в задаче stylesBuild', // заголовок ошибки
          sound: false, // уведомлять звуком
          message: err.message, // описание ошибки
        })),
      }),
    )
    .pipe(sassGlob()) // проходит по всем файлам в scss, подключенным через шаблон /**/*
    .pipe(sass.sync({
      includePaths: ['./node_modules'], // ищет зависимости в node_modules, например normalize.css
    }))
    .pipe(postcss([
      postcssCustomMedia(), // объединяет медиа запросы
      autoPrefixer(), // автопрефиксер
      minify, // минификация
      clear, // очистка комментариев
    ]))
    .pipe(dest(config.build.css, { sourcemaps: config.isDev })) // исходящий файл
    .pipe(browserSync.stream()) // обновление страницы в браузере
}

// Слежение за изменением файлов
export const stylesWatch = () => {
  watch(`${config.src.scss}/**/*.{scss,sass}`, stylesBuild)
}

Добавляем таск styles в gulpfile.babel.js

Изменим содержимое файла gulpfile.babel.js на следующий код.

JS
// Сторонние библиотеки
import { series, parallel } from 'gulp'

// Таски
import clear from './gulp/tasks/clear'
import server from './gulp/tasks/server'
import { webpackBuild, webpackWatch } from './gulp/tasks/webpack'
import { stylesBuild, stylesWatch } from './gulp/tasks/styles'

// Конфиги
import config from './gulp/config'

// Определяем окружения сборки dev или prod
config.setEnv()

// Запуск виртуального сервера
export const proxy = server

// Сборка проекта
export const build = series(
  // clear,
  stylesBuild,
  webpackBuild,
)

// Слежение за изменением файлов
export const watch = series(
  build,
  server,

  parallel(
    stylesWatch,
    webpackWatch,
  ),
)

export default watch

Здесь мы добавили сразу 2 таска stylesBuild и stylesWatch. Первый таск объединяет scss файлы, второй таск следит за изменениями scss файлов. При изменении какого либо scss файла внутри src/scss, автоматически будет вызван таск stylesBuild, который снова объединит scss файлы.

Создаем файлы со стилями

В каталоге scss у должна получиться следующая структура.

SCSS
./src/scss/
├── core
│    ├── base
│    │     ├── _base.scss
│    │     ├── _container.scss
│    │     └── _fonts.scss
│    └── helpers
│        ├── _functions.scss
│        ├── _mixins.scss
│        └── _variables.scss
├── critical.scss
├── main.scss
└── scaffolds
    ├── components
    │    └── .gitkeep
    └── sections
        └── _browser-upgrade.scss
  • core - хранит основные, базовые стили для любого проекта который будет создаваться через данную сборку.
  • scaffolds - хранит стили специфичные только для определенного проекта который будет создаваться через данную сборку.
  • base - хранит базовые стили используемые на всех страницах.
  • helpers - хранит вспомогательные файлы такие как переменные, миксины, функции.
  • components - хранит стили компонентов, небольших элементов.
  • sections - хранит стили секций, крупных блоков, внутри которых располагаются компоненты.
  • main.scss - основной файл со стилями подключает внутри себя остальные стили, из выше указанных каталогов.
  • critical.scss - критические стили подключает внутри себя те стили которые должны быть подключены inline внутри <head> тега.

_base.scss

Отвечает за базовые стили проекта. Содержимое файла _base.scss.

SCSS
@use "sass:math";

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

html,
body {
  height: 100vh;
}

html {
  box-sizing: border-box;
}

body {
  position: relative;

  min-width: $min-screen;
  margin: 0;

  color: rgb(var(--theme-text));
  font-size: $fz;
  font-family: $font-primary;
  line-height: $lh;
  text-rendering: optimizeLegibility;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;

  background-color: rgb(var(--theme-bg));
}

img {
  max-width: 100%;
  height: auto;

  object-fit: cover;

  vertical-align: middle;
}

button {
  display: inline-block;

  font-family: inherit;

  cursor: pointer;
}

@each $headline, $size in $headlines {
  $margin: $size * $lh;

  #{$headline} {
    margin: $margin 0 math.div($margin, 2);

    font-weight: 500;
    font-size: $size;
  }
}

strong,
b {
  font-weight: 500;
}

.sr-only {
  @include sr-only;
}

_container.scss

Отвечает за стили центрующего контейнера в проекте. Содержимое файла _container.scss.

SCSS
@use "sass:map";

.container {
  width: 100%;
  max-width: map.get($container-sizes, desktop);
  margin: 0 auto;
  padding: 0 $container-grid;
}

@include from(mobile) {
  .container {
    padding: 0 $container-grid * 2;
  }
}

@include from(desktop) {
  .container {
    padding: 0 $container-grid * 3;
  }
}

_fonts.scss

Отвечает за подключение сторонних шрифтов в стилях проекта. Содержимое файла _fonts.scss.

SCSS
/* stylelint-disable scss/comment-no-empty */

// 100 – Thin.
// 200 – Extra Light (Ultra Light)
// 300 – Light.
// 400 – Normal.
// 500 – Medium.
// 600 – Semi Bold (Demi Bold)
// 700 – Bold.
// 800 – Extra Bold (Ultra Bold)

@include font-face("Roboto", "../fonts/roboto/roboto-regular");

_mixins.scss

Отвечает за миксины проекта. Содержимое файла _mixins.scss.

SCSS
@use "sass:map";

// mobile first
@mixin from($min_width) {
  $min_width: map.get($container-sizes, $min_width);

  @media screen and (min-width: $min_width) {
    @content;
  }
}

// desktop first
@mixin to($max_width) {
  $max_width: map.get($container-sizes, $max_width);

  @media screen and (max-width: $max_width) {
    @content;
  }
}

// only mobile
@mixin only-mobile($max_width: "mobile") {
  $max_width: map.get($container-sizes, $max_width);

  @media screen and (max-width: $max_width - 1) {
    @content;
  }
}

@mixin retina($dpi: 144, $dppx: 1.5) {
  @media (min-resolution: #{$dpi}dpi), (min-resolution: #{$dppx}dppx) {
    @content;
  }
}

@mixin font-face($font-family, $url, $weight: normal, $style: normal) {
  @font-face {
    font-weight: $weight;
    font-family: "#{$font-family}";
    font-style: $style;
    font-display: swap;
    src: url("#{$url}.woff2") format("woff2");
  }
}

@mixin list-reset {
  margin: 0;
  padding: 0;

  list-style: none;
}

@mixin button-reset {
  padding: 0;

  background-color: transparent;
  border: none;
}

@mixin sr-only {
  position: absolute;

  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;

  border: 0;

  clip: rect(0, 0, 0, 0);
  clip-path: inset(100%);
}

@mixin overlay($color: #222) {
  &::before {
    position: fixed;
    top: 0;
    right: 0;
    bottom: 0;
    left: 0;

    width: 100%;
    height: 100%;

    background-color: rgba($color, 0.8);

    content: "";
  }
}

_variables.scss

Отвечает за функции проекта. Содержимое файла _variables.scss.

SCSS
$black: #222;
$white: #fff;

:root {
  --theme-text: #{hextorgb($black)};
  --theme-bg: #{hextorgb($white)};
}

@mixin dark-mode {
  --theme-text: #{hextorgb($white)};
  --theme-bg: #{hextorgb($black)};
}

.dark-mode {
  @include dark-mode;
}

@media (prefers-color-scheme: dark) {
  :root {
    @include dark-mode;
  }
}

$font-primary: Roboto, -apple-system, Arial, sans-serif;

$fz: 16px;
$lh: 1.5;
$font-sizes: (xxs: 12px, xs: 14px, sm: $fz, md: 18px, lg: 24px, xl: 33px, xxl: 48px);
$headlines: (
  h1: map-get($font-sizes, "xxl"),
  h2: map-get($font-sizes, "xl"),
  h3: map-get($font-sizes, "lg"),
  h4: map-get($font-sizes, "md"),
  h5: map-get($font-sizes, "sm"),
  h6: map-get($font-sizes, "xs")
);

$min-screen: 320px;
$container-sizes: (mobile: 768px, tablet: 1180px, desktop: 1700px);
$container-grid: 20px;

$transition: all 0.3s ease;

_button.scss

Отвечает за стили компонента button. Содержимое файла _button.scss.

SCSS
.button {
  display: inline-block;

  vertical-align: middle;
}

_main-header.scss

Отвечает за стили секции main-header. Содержимое файла _main-header.scss.

SCSS
.main-header {
  background-color: #ebebeb;
}

main.scss

Основной файл со стилями. Содержимое файла main.scss.

SCSS
@import "core/helpers/functions";
@import "core/helpers/variables";
@import "core/helpers/mixins";

@import "core/base/fonts";

@import "scaffolds/components/**/*";
@import "scaffolds/sections/**/*";

critical.scss

Файл с критическими стилями. Содержимое файла critical.scss.

SCSS
@import "core/helpers/functions";
@import "core/helpers/variables";
@import "core/helpers/mixins";

@import "normalize.css/normalize";

@import "core/base/base";

@import "core/container/mobile";
//@import "core/container/desktop";

Удаляем все файлы .gitkeep внутри директорий src/scss

BASH
rm src/scss/core/base/.gitkeep
rm src/scss/core/helpers/.gitkeep
rm src/scss/scaffolds/components/.gitkeep
rm src/scss/scaffolds/sections/.gitkeep

Тестируем таск styles

Чтобы протестировать работоспособность таска styles запустим сборку в режиме разработки.

SCSS
npm run dev

В каталоге build появится каталог css с файлом main.css и critical.css внутри. Создадим в корне каталога build файл index.html, со следующим содержимым.

HTML
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Title</title>

  <link rel="stylesheet" href="css/critical.css">
  <link rel="stylesheet" href="css/main.css">
</head>
<body>
</body>
</html>

Откроем файл src/scss/core/helpers/_variables.scss и изменим цвет фона.

SCSS
$white: red;

Страница должна автоматически обновиться, а цвет фона измениться.

Таск sprites

Данный такс будет cоздавать svg спрайт. Установим зависимости.

BASH
npm i -D files-exist gulp-concat gulp-svg-symbol-view

Внутри каталога ./gulp/tasks создадим файл sprites.js.

JS
/**
 * SVG спрайт
 *
 * Создает svg спрайт
 *
 * Пример использования в css:
 * background: url('sprite-mono.svg#icon-name-view') no-repeat;
 * background-size: 20px;
 *
 * Пример использования в html:
 * 
 *
 * @link https://github.com/svg/svgo
 */

// Сторонние библиотеки
import { src, dest, watch, parallel } from 'gulp' // gulp плагин
import filesExist from 'files-exist' // проверяет файл на существование
import concat from 'gulp-concat' // объединяет несколько файлов в один
import svgSprite from 'gulp-svg-symbol-view' // создает спрайт

// Конфиги
import config from '../config'

// Создание черно-белого svg спрайта
const spriteMono = () =>
  // входящие файлы
  src(filesExist(`${config.src.assets.icons.mono}/**/icon-*.svg`, { exceptionMessage: 'Нет ни одного файла svg' }))
    .pipe(
      svgSprite({
        svgo: {
          plugins: [
            { cleanupIDs: true }, // удалить id
            { removeRasterImages: true }, // удалить растровые изображения
            { removeStyleElement: true }, // удалить <style>
            { removeUselessDefs: true }, // удалить <defs>
            { removeViewBox: false }, // удалить ViewBox
            { removeComments: true }, // удалить комментарии
            {
              removeAttrs: {
                attrs: ['class', 'data-name'], // удалить указанные атрибуты, 'fill', 'stroke.*'
              },
            },
          ],
        },
      }),
    )
    .pipe(concat('sprite-mono.svg')) // объединение файлов
    .pipe(dest(config.build.images)) // исходящий файл
    .pipe(browserSync.stream()) // обновление страницы в браузере

// Создание цветного svg спрайта
const spriteMulti = () =>
  // входящие файлы
  src(filesExist(`${config.src.assets.icons.multi}/**/icon-*.svg`, { exceptionMessage: 'Нет ни одного файла svg' }))
    .pipe(
      svgSprite({
        svgo: {
          plugins: [
            { cleanupIDs: true }, // удалить id
            { removeUselessDefs: true }, // удалить <defs>
            { removeViewBox: false }, // удалить ViewBox
            { removeComments: true }, // удалить комментарии
            { removeUselessStrokeAndFill: false }, // удалить атрибуты stroke и fill
            { inlineStyles: true }, // поддержка встроенных стилей <style></style>
            {
              removeAttrs: {
                attrs: ['class', 'data-name'], // удалить указанные атрибуты
              },
            },
          ],
        },
      }),
    )
    .pipe(concat('sprite-multi.svg')) // объединение файлов
    .pipe(dest(config.build.images)) // исходящий файл
    .pipe(browserSync.stream()) // обновление страницы в браузере

// Сборка всех тасков
export const spritesBuild = parallel(spriteMono, spriteMulti)

// Слежение за изменением файлов
export const spritesWatch = () => {
  watch(`${config.src.assets.icons.mono}/**/*.svg`, { ignoreInitial: false }, spriteMono)
  watch(`${config.src.assets.icons.multi}/**/*.svg`, { ignoreInitial: false }, spriteMulti)
}

Добавляем таск sprites в gulpfile.babel.js

Изменим содержимое файла gulpfile.babel.js на следующий код.

SCSS
// Сторонние библиотеки
import { series, parallel } from 'gulp'

// Таски
import clear from './gulp/tasks/clear'
import server from './gulp/tasks/server'
import { webpackBuild, webpackWatch } from './gulp/tasks/webpack'
import { stylesBuild, stylesWatch } from './gulp/tasks/styles'
import { spritesBuild, spritesWatch } from './gulp/tasks/sprites'

// Конфиги
import config from './gulp/config'

// Определяем окружения сборки dev или prod
config.setEnv()

// Запуск виртуального сервера
export const proxy = server

// Сборка проекта
export const build = series(
  // clear,
  spritesBuild,
  stylesBuild,
  webpackBuild
)

// Слежение за изменением файлов
export const watch = series(
  build,
  server,

  parallel(
    spritesWatch,
    stylesWatch,
    webpackWatch
  ),
)

export default watch

Тестируем таск sprites

Чтобы протестировать таск sprites.js скачаем и добавим в каталог ./src/assets/icons/mono черно-белые иконки, а в каталог ./src/assets/icons/multi цветные иконки. Попутно удалим из этих каталогов файл .gitkeep. Запустим сборку в режиме разработки.

BASH
npm run dev

Создадим в каталоге build файл index.html.

SCSS
<!doctype html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>

  <link rel="stylesheet" href="css/critical.css">
  <link rel="stylesheet" href="css/main.css">
</head>
<body>
  <div class="icon icon--snow"></div>
  <div class="icon icon--sun"></div>
  <div class="icon icon--user"></div>
</body>
</html>

Создадим стили для иконок src/scss/scaffolds/components/_icons.scss.

SCSS
.icon {
  display: inline-block;
  width: 50px;
  height: 50px;

  background-size: 50px;
  background-repeat: no-repeat;
}

.icon--snow {
  background-image: url("../img/sprite-mono.svg#icon-snow-view");
}

.icon--sun {
  background-image: url("../img/sprite-mono.svg#icon-sun-view");
}

.icon--user {
  background-image: url("../img/sprite-multi.svg#icon-user-view");
}

Обновим страницу в браузере и увидим добавленные svg иконки.

Таск images

Данный такс будет оптимизировать изображения. Установим зависимости.

BASH
npm i -D gulp-avif gulp-webp gulp-flatten gulp-imagemin@7.1.0 imagemin-pngquant gulp-newer

Внутри каталога ./gulp/tasks создадим файл images.js

JS
/**
 * Оптимизация изображений
 *
 * Уменьшает размер изображений и создает webp, avif форматы
 */

// Сторонние библиотеки
import { src, dest, watch, series } from 'gulp' // gulp плагин
import gulpif from 'gulp-if' // вызывает функции по условию
import newer from 'gulp-newer' // пропускает старые файлы
import flatten from 'gulp-flatten' // настраивает исходную структуру каталогов
import imagemin, { gifsicle, mozjpeg, optipng, svgo } from 'gulp-imagemin' // оптимизирует изображения
import pngQuant from 'imagemin-pngquant' // оптимизирует png изображения
import webp from 'gulp-webp' // создает webp файлы
import avif from 'gulp-avif' // создает avif файлы

// Конфиги
import config from '../config'

// Оптимизация изображений
export const imageOptim = () =>
  src([`${config.src.assets.images}/**/*`]) // входящие файлы
    .pipe(newer(config.build.images)) // только те изображения которые изменились или были добавлены
    .pipe(
      gulpif(
        config.isProd,
        imagemin(
          [
            gifsicle({
              interlaced: true, // чересстрочная загрузка изображения
            }),
            optipng({
              optimizationLevel: 5, // уровень оптимизации png файла
            }),
            pngQuant({
              quality: [0.8, 0.9], // уровень оптимизации png файла
            }),
            mozjpeg({
              quality: 75, // уровень оптимизации jpg файла
              progressive: true, // чересстрочная загрузка изображения
            }),
            svgo({
              plugins: [
                { cleanupIds: true }, // удаляет неиспользуемые id
                { removeUselessDefs: true }, // удаляет <defs>
                { removeViewBox: true }, // удаляет атрибут viewBox
                { removeComments: true }, // удаляет комментарии
                // { inlineStyles: { removeMatchedSelectors: false, onlyMatchedOnce: false } }, // оставляет стили в теге style
                { mergePaths: true }, // объединяет несколько путей в один
                { minifyStyles: false }, // не удаляет @keyframes из тега style
              ],
            }),
          ],
          {
            verbose: config.isProd, // каждая оптимизированная картинка отобразится в терминале
          },
        ),
      ),
    )
    .pipe(flatten({ includeParents: [1, 1] })) // сохранение структуры родительских каталогов
    .pipe(dest(config.build.images)) // исходящие файлы
    .pipe(browserSync.stream()) // обновление страницы в браузере

// Создание Webp изображения
export const toWebp = () =>
  src(`${config.src.assets.images}/**/*.{jpg,png,jpeg}`) // для всех файлов формата jpg,png,jpeg будет создан файл webp
    .pipe(webp({
      quality: 80, // уровень оптимизации webp файла
    }))
    .pipe(dest(config.build.images)) // разместит оптимизированные webp файлы
    .pipe(browserSync.stream()) // обновление страницы в браузере

// Создание Avif изображения
export const toAvif = () =>
  src(`${config.src.assets.images}/**/*.{jpg,png,jpeg}`) // для всех файлов формата jpg,png,jpeg будет создан файл webp
    .pipe(avif({
      quality: 80, // уровень оптимизации avif файла
    }))
    .pipe(dest(config.build.images)) // разместит оптимизированные webp файлы
    .pipe(browserSync.stream()) // обновление страницы в браузере

// Выполнение всех тасков
export const imagesBuild = series(imageOptim, toWebp, toAvif)

// Слежение за изменением файлов
export const imagesWatch = () => watch(`${config.src.assets.images}/**/*`, { ignoreInitial: false }, imagesBuild)

Добавляем таск images в gulpfile.babel.js

Изменим содержимое файла gulpfile.babel.js на следующий код.

JS
// Сторонние библиотеки
import { series, parallel } from 'gulp'

// Таски
import clear from './gulp/tasks/clear'
import server from './gulp/tasks/server'
import { webpackBuild, webpackWatch } from './gulp/tasks/webpack'
import { stylesBuild, stylesWatch } from './gulp/tasks/styles'
import { spritesBuild, spritesWatch } from './gulp/tasks/sprites'
import { imagesBuild, imagesWatch } from './gulp/tasks/images'

// Конфиги
import config from './gulp/config'

// Определяем окружения сборки dev или prod
config.setEnv()

// Запуск виртуального сервера
export const proxy = server

// Сборка проекта
export const build = series(clear, spritesBuild, stylesBuild, webpackBuild, imagesBuild)

// Слежение за изменением файлов
export const watch = series(
  build,
  server,

  parallel(spritesWatch, stylesWatch, webpackWatch, imagesWatch),
)

export default watch

Тестируем таск images

Добавим любую картинку формата jpg или png в каталог src/assets/images и запустим сборку в жиме разработке.

BASH
npm run dev

Создадим файл index.html со следующим содержимым.

HTML
<!doctype html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>

  <link rel="stylesheet" href="css/critical.css">
  <link rel="stylesheet" href="css/main.css">
</head>
<body>

<picture>
  <source type="image/avif" srcset="/img/example.avif">
  <source type="image/webp" srcset="/img/example.webp">
  <img src="/img/example.jpg" width="800" height="600" alt="Стопка монет" loading="lazy">
</picture>

</body>
</html>

Приоритет загрузки изображения идет сверху вниз, если браузер сможет распознать формат avif будет загружен этот файл и так далее. Это можно увидеть открыв инспектор и перейдя во вкладку Сеть. Обновим страницу и посмотрим какого формата была загружена картинка.

Таск assets

Данный такс будет копировать файлы из каталога src в build. Установим зависимости.

JS
/**
 * Перенос файлов в продакшн
 *
 * Копирует файлы из src в build
 */

// Сторонние библиотеки
import { src, dest, watch, series } from 'gulp' // gulp плагин
import merge from 'merge-stream' // слияние gulp потоков

// Конфиги
import config from '../config'

// Переносит в корень build
const rootBuild = () => src(
  [
    `${config.src.assets.favicons}/favicon.ico`,
    `${config.src.assets.root}/manifest.json`,
  ],
).pipe(dest(`${config.build.root}`))

// Переносит в build/fonts
const fontsBuild = () => src(
  [
    `${config.src.assets.fonts}/**/*`,

    `${config.src.libs}/slick-carousel/fonts/*`,
  ],
).pipe(dest(`${config.build.fonts}`))

// Переносит в build/img
const imageBuild = () => src(
  [
    `${config.src.assets.favicons}/**/*`,

    `${config.src.libs}/slick-carousel/ajax-loader.gif`,
  ]
).pipe(dest(`${config.build.images}`))

// Переносит содержимое из библиотек node_modules в src/assets/libs
const componentsBuild = () => {
  // Список библиотек, которые будут переноситься
  const folders = [
    'slick-carousel/slick',
  ]

  const tasks = folders.map(folder => {
    const pathFolder = `node_modules/${folder}` // полный путь до node_modules библиотеки
    const nameFolder = pathFolder.match(/^[a-zA-Z0-9_]*\/([a-zA-Z0-9-_.]*)/)[1] // название библиотеки, например slick-carousel

    return src(`${pathFolder}/**/*`).pipe(dest(`${config.src.libs}/${nameFolder}`))
  })

  return merge(tasks)
}

// Выполнение всех тасков
export const assetsBuild = series(componentsBuild, fontsBuild, rootBuild, imageBuild)

// Слежение за изменением файлов
export const assetsWatch = () => watch([
  `${config.src.assets.fonts}/**/*`,
  `${config.src.assets.images}/**/*`
], assetsBuild)

Добавляем таск assets в gulpfile.babel.js

Изменим содержимое файла gulpfile.babel.js на следующий код.

JS
// Сторонние библиотеки
import { series, parallel } from 'gulp'

// Таски
import clear from './gulp/tasks/clear'
import server from './gulp/tasks/server'
import { webpackBuild, webpackWatch } from './gulp/tasks/webpack'
import { stylesBuild, stylesWatch } from './gulp/tasks/styles'
import { spritesBuild, spritesWatch } from './gulp/tasks/sprites'
import { imagesBuild, imagesWatch } from './gulp/tasks/images'
import { assetsBuild, assetsWatch } from './gulp/tasks/assets'

// Конфиги
import config from './gulp/config'

// Определяем окружения сборки dev или prod
config.setEnv()

// Запуск виртуального сервера
export const proxy = server

// Сборка проекта
export const build = series(
  clear,
  spritesBuild,
  stylesBuild,
  webpackBuild,
  imagesBuild,
  assetsBuild,
)

// Слежение за изменением файлов
export const watch = series(
  build,
  server,

  parallel(
    spritesWatch,
    stylesWatch,
    webpackWatch,
    imagesWatch,
    assetsWatch,
  ),
)

export default watch

Таск pug

Данный такс будет переводить pug разметку в html. Установим зависимости.

BASH
npm i -D gulp-pug jstransformer jstransformer-clean-css jstransformer-markdown-it jstransformer-scss markdown-it-attrs markdown-it-kbd pug-include-glob

Внутри каталога ./gulp/tasks создадим файл pug.js

JS
/**
 * Шаблонизатор pug
 *
 * Компилирует pug разметку в html
 */

// Сторонние библиотеки
import { dest, src, watch } from 'gulp' // gulp плагин
import pug from 'gulp-pug' // шаблонизатор pug
import plumber from 'gulp-plumber' // перехватывает ошибки
import notify from 'gulp-notify' // уведомляет об ошибках
import pugIncludeGlob from 'pug-include-glob' // позволяет использовать /**/*.pug конструкцию
import fs from 'fs' // чтение файлов

// Конфиги
import config from '../config'

// Дополнительные markdown библиотеки
const md = require('jstransformer')(require('jstransformer-markdown-it')) // позволяет компилировать и преобразовывать Markdown-код
const kbd = require('markdown-it-kbd') // позволяет добавлять тег kbd через markdown разметку [[]]
const markdownItAttrs = require('markdown-it-attrs') // позволяет добавлять markdown разметке атрибуты
const jstscss = require('jstransformer')(require('jstransformer-scss')) // компилирует scss в css
const jstminify = require('jstransformer')(require('jstransformer-clean-css')) // минифицирует css

// Формирование объекта с данными
// Далее значение этого объекта в pug файле можно будет получить примерно так:
// #{jsonData.nav.home.link}
const getData = () => {
  const dir = `${config.src.pug.root}/data` // здесь ищем json файлы
  const files = fs.readdirSync(dir) // получаем список всех файлов в каталоге
  const data = {} // хранит данные файлов

  // Проходимся по каждому файлу
  files.forEach(file => {
    if (!file.endsWith('.json')) return // пропускаем только json файлы

    const fileName = file.replace('.json', '') // формируем имя файла без его расширения

    // Создаем ключ с названием файла в объекте data и помещаем в него содержимое файла
    data[fileName] = JSON.parse(fs.readFileSync(`${dir}/${file}`, 'utf8'))
  })

  return data
}

// Сборка таска
export const pugBuild = () => {
  const jsonData = getData() // получаем данные

  return src(`${config.src.pug.pages}/**/*.pug`) // входящие файлы
    .pipe(
      // Отлавливаем и показываем ошибки в таске
      plumber({
        errorHandler: notify.onError(err => ({
          title: 'Ошибка в задаче pugBuild', // заголовок ошибки
          sound: false, // уведомлять звуком
          message: err.message, // описание ошибки
        })),
      }),
    )
    .pipe(
      pug({
        doctype: 'html', // чтобы не было обратного слеша у одиночных тэгов
        pretty: true, // сжатие html разметки
        plugins: [pugIncludeGlob()], // подключаем сторонние pug плагины
        locals: {
          // передаем jsonData в pug, далее используем его примерно так: #{jsonData.nav.home.link}
          jsonData,
        },
        filters: {
          // Переопределяем pug фильтр markdown-it, улучшая его стандартную функциональность
          'markdown-it': (text, options) => {
            // inline режим добавляет/убирает обертку в виде тега <p>
            const inline = options.inline || false

            return md.render(text, {
              plugins: [kbd, markdownItAttrs], // подключаем дополнительные плагины
              inline, // включаем/отключаем inline режим
            }).body
          },

          // Пользовательский фильтр
          // @var options {} имеет один параметр path, хранящий путь до critical.scss
          'critical-css': (text, options) => {
            const css = jstscss.render(options.path).body // компилирует scss в css
            css.replaceAll('"', '"') // jstscss при компиляции заменяет " на " что не есть хорошо

            // минифицирует получившейся css файл, удаляя комментарии
            return jstminify.render(css, {
              level: {
                1: {
                  specialComments: 0,
                },
              },
            }).body
          },

          // Пользовательский фильтр экранирования html тегов
          'special-chars': text =>
            text.replaceAll('<', '<').replaceAll('>', '>'),
        },
      }),
    )
    .pipe(dest(config.build.root)) // исходящие файлы
    .pipe(browserSync.stream()) // обновление страницы в браузере
}

// Слежение за изменением файлов
export const pugWatch = () => {
  watch(
    [`${config.src.pug.root}/**/*.pug`, `${config.src.pug.root}/data/**/*`],
    pugBuild,
  )
}

Добавляем таск pug в gulpfile.babel.js

Изменим содержимое файла gulpfile.babel.js на следующий код.

JS
// Сторонние библиотеки
import { series, parallel } from 'gulp'

// Таски
import clear from './gulp/tasks/clear'
import server from './gulp/tasks/server'
import { webpackBuild, webpackWatch } from './gulp/tasks/webpack'
import { stylesBuild, stylesWatch } from './gulp/tasks/styles'
import { spritesBuild, spritesWatch } from './gulp/tasks/sprites'
import { imagesBuild, imagesWatch } from './gulp/tasks/images'
import { assetsBuild, assetsWatch } from './gulp/tasks/assets'
import { pugBuild, pugWatch } from './gulp/tasks/pug'

// Конфиги
import config from './gulp/config'

// Определяем окружения сборки dev или prod
config.setEnv()

// Запуск виртуального сервера
export const proxy = server

// Сборка проекта
export const build = series(
  clear,
  spritesBuild,
  pugBuild,
  stylesBuild,
  webpackBuild,
  imagesBuild,
  assetsBuild,
)

// Слежение за изменением файлов
export const watch = series(
  build,
  server,

  parallel(
    spritesWatch,
    pugWatch,
    stylesWatch,
    webpackWatch,
    imagesWatch,
    assetsWatch,
  ),
)

export default watch

Таск favicons

Данный такс будет создавать фавиконки различного разрешения и типа из svg файла. Установим зависимости.

BASH
npm i -D gulp-svg2png gulp-rename gulp-image-resize gulp-to-ico

Внутри каталога ./gulp/tasks создадим файл favicons.js.

JS
/**
 * Favicons
 *
 * Создает фавиконки различного разрешения и типа из svg файла
 *
 * Если возникает ошибка для gulp-svg2png
 * Ошибка: DSO support routines:DLFCN_LOAD:could not load the shared library:dso_dlfcn.c:185:filename(libproviders.so):
 * Ввести в терминале: export OPENSSL_CONF=/dev/null
 *
 * @link https://habr.com/ru/articles/672844/
 */

// Сторонние библиотеки
import { src, dest, watch } from 'gulp' // gulp плагин
import plumber from 'gulp-plumber' // перехватывает ошибки
import notify from 'gulp-notify' // уведомляет об ошибках
import svg2png from 'gulp-svg2png' // svg в png
import rename from 'gulp-rename' // переименование файла
import imageResize from 'gulp-image-resize' // изменение разрешения картинки
import ico from 'gulp-to-ico' // png в ico

// Конфиги
import config from '../config'

// Сборка таска
export const faviconBuild = () =>
  src(`${config.src.assets.favicons}/icon.svg`) // входящий файл
    .pipe(
      // Отлавливаем и показываем ошибки в таске
      plumber({
        errorHandler: notify.onError(err => ({
          title: 'Ошибка в задаче faviconBuild', // заголовок ошибки
          sound: false, // уведомлять звуком
          message: err.message, // описание ошибки
        })),
      }),
    )
    .pipe(svg2png()) // svg в png

    // изменение разрешения картинки на 512х512
    .pipe(
      imageResize({
        width: 512,
        height: 512,
        crop: true, // должно ли изображение быть обрезано до заданных размеров
        upscale: false, // может ли изображение быть увеличено, если указанные размеры больше, чем оригинальные размеры
      }),
    )
    .pipe(rename('icon-512.png')) // переименование файла
    .pipe(dest(config.src.assets.favicons)) // исходящий файл

    // изменение разрешения картинки на 192х192
    .pipe(
      imageResize({
        width: 192,
        height: 192,
        crop: true, // должно ли изображение быть обрезано до заданных размеров
        upscale: false, // может ли изображение быть увеличено, если указанные размеры больше, чем оригинальные размеры
      }),
    )
    .pipe(rename('icon-192.png')) // переименование файла
    .pipe(dest(config.src.assets.favicons)) // исходящий файл

    // изменение разрешения картинки на 180х180
    .pipe(
      imageResize({
        width: 180,
        height: 180,
        crop: true, // должно ли изображение быть обрезано до заданных размеров
        upscale: false, // может ли изображение быть увеличено, если указанные размеры больше, чем оригинальные размеры
      }),
    )
    .pipe(rename('apple-touch-icon.png')) // переименование файла
    .pipe(dest(config.src.assets.favicons)) // исходящий файл

    // изменение разрешения картинки на 32х32
    .pipe(
      imageResize({
        width: 32,
        height: 32,
        crop: true, // должно ли изображение быть обрезано до заданных размеров
        upscale: false, // может ли изображение быть увеличено, если указанные размеры больше, чем оригинальные размеры
      }),
    )
    .pipe(ico('favicon.ico')) // переименование файла
    .pipe(dest(config.src.assets.favicons)) // исходящий файл

    .pipe(browserSync.stream()) // обновление страницы в браузере

// Слежение за изменением файлов
export const faviconWatch = () =>
  watch(`${config.src.assets.images}/favicons/icon.svg`, faviconBuild)

Добавляем таск favicons в gulpfile.babel.js

Изменим содержимое файла gulpfile.babel.js на следующий код.

JS
// Сторонние библиотеки
import { series, parallel } from 'gulp'

// Таски
import clear from './gulp/tasks/clear'
import server from './gulp/tasks/server'
import { webpackBuild, webpackWatch } from './gulp/tasks/webpack'
import { stylesBuild, stylesWatch } from './gulp/tasks/styles'
import { spritesBuild, spritesWatch } from './gulp/tasks/sprites'
import { imagesBuild, imagesWatch } from './gulp/tasks/images'
import { assetsBuild, assetsWatch } from './gulp/tasks/assets'
import { pugBuild, pugWatch } from './gulp/tasks/pug'
import { faviconBuild, faviconWatch } from './gulp/tasks/favicons'

// Конфиги
import config from './gulp/config'

// Определяем окружения сборки dev или prod
config.setEnv()

// Запуск виртуального сервера
export const proxy = server

// Сборка проекта
export const build = series(
  clear,
  spritesBuild,
  pugBuild,
  stylesBuild,
  webpackBuild,
  faviconBuild,
  imagesBuild,
  assetsBuild,
)

// Слежение за изменением файлов
export const watch = series(
  build,
  server,

  parallel(
    spritesWatch,
    pugWatch,
    stylesWatch,
    webpackWatch,
    faviconWatch,
    imagesWatch,
    assetsWatch,
  ),
)

export default watch

Тестируем таск favicons

Удалить из каталога ./src/assets/favicons все фавиконки кроме icon.svg.

BASH
npm run dev

Запустить сборку в режиме разработки.

BASH
npm run dev

В каталоге ./src/assets/favicons автоматически должны сгенерироваться, удаленные иконки.

Файл manifest.json

Содержимое файла manifest.json.

JSON
{
  "name": "Название проекта",
  "short_name": "Название проекта",
  "description": "Описание проекта",
  "lang": "ru",
  "dir": "ltr",
  "id": "/",
  "start_url": "/",
  "scope": "/",
  "display": "minimal-ui",
  "orientation": "any",
  "theme_color": "#ссс",
  "background_color": "#ссс",
  "prefer_related_applications": false,
  "icons": [
    {
      "src": "/img/icon-192.png",
      "type": "image/png",
      "sizes": "192x192"
    },
    {
      "src": "/img/icon-512.png",
      "type": "image/png",
      "sizes": "512x512"
    },
    {
      "src": "/img/icon.svg",
      "sizes": "any",
      "type": "image/svg",
      "purpose": "maskable"
    }
  ]
}

Файл robots.txt

Содержимое файла robots.txt.

JS
# robotstxt.org

# Разрешить сканирование всего контента
User-agent: *
Disallow:

Подведем итоги

Мы полностью завершили написание своей Gulp сборки. Было создано множество тасков для автоматизации рутинных процессов при верстки сайта. Статья получилась довольно объемная.

Надеюсь вы подчеркнете для себя какие либо идеи для создании своей сборки, так же не стесняйтесь использовать текущую.

P.S. На данный момент сборка претерпела множественные изменения и не перестает изменяться. Актуальную версию вы всегда можете найти в моем репозитории.

Так же каждый шаг создания Gulp сборки я старался комментировать в репозитории сборки. Обязательно загляните туда если походу статьи вам было что-то не понятно.

Предыдущая статья Простой, легкий HTML шаблон без использования gulp сборки Следующая статья Стартовый Jekyll шаблон для создания статических сайтов