Введение

Если на минуту забыть о скорости и удобстве конфигурации, то любой сборщик, будь то Grunt, Gulp, Broccoli, Brunch или даже Fly, — отличный вариант сэкономить кучу времени, конечно, если под вашу задачу уже написан соответствующий плагин. Однако, любой из представленных здесь сборщиков имеет один важный недостаток — количество файлов в каждом плагине и их суммарный вес. Последнее время мейнтейнеры плагинов начали хоть как-то уделять внимание оптимизации, но пока это лишь стрельба пушкой по воробьям, и серьезных изменений в ближайшее время точно не будет

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

Какие проблемы решаем?

Итак, предположим, что вы используете шаблон для быстрого старта разработки веб-приложения RWK, тогда количество файлов в директории только что созданного проекта — 18065, а их суммарный вес равен 82,8 МБ. При этом сама директория app, где хранятся less, js, jade и другие файлы, занимает 29,6 КБ. Пузатый случай.

Это не единичная проблема. Например, вы можете оценить вес популярного решения на Gulp — Web Starter Kit. Файлов в проекте — 29752, а вес — 134 МБ.

Так какие же проблемы будет пытаться решить эта статья? Бегло взглянем на список:

  • Максимальное снижение количества файлов в проектах и, как следствие, их суммарного веса.
  • Упрощение действий (один запущенный Gulp, обслуживающий несколько проектов, без необходимости переключения директорий).
  • Меньше проблем от 260 символов для путей в Windows при удалении директории проекта.

Обо всех минусах поговорим в конце статьи, чтобы читатель сразу не закрыл вкладку :)

Какие проблемы получаем?

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

Во-первых, у каждого проекта могут быть свои зависимости в менеджере Bower, или их может не быть вовсе. Здесь либо приходится мириться с тем, что все зависимости в bower_components общие для всех (непозволительный вариант), либо для каждого проекта придется иметь файл настроек, в котором будут указаны необходимые зависимости. Да, немного сложно, но об этом речь идет только тогда, когда у вас имеется задача автоматической вставки Bower-зависимостей в HTML-файлы.

Во-вторых, появляется новая развилка — BrowserSync, или смежные с ним более примитивные пакеты. Тут все проще, так как есть возможность создания общей для всех проектов директории, в которой будут доступны их билды (скомпилированные файлы).

В-третьих, придется разбираться с универстальностью задач, так как теперь просто так вызвать задачу нельзя (она не знает о контексте заранее). Получается, что мы не можем написать в консоли gulp jade или gulp build, так как Gulp не знает, о каком проекте идет речь. Придется приучаться к флажкам в консоли, например, gulp build —canonium или gulp jade —yellfy. Однако, этого можно избежать, назначив проект по умолчанию, например, используя сам gulp-файл или, на худой конец, маркер в названии директории (app-canonium--default).

Архитектура

Окей, имеем следующую архитектуру из трех проектов (canonium, yellfy):

projects/
├── app-canonium/
│   ├── less/
│   ├── templates/
│   ├── ...
│   └── settings.json
├── app-yellfy/
│   ├── less/
│   ├── templates/
│   ├── ...
│   └── settings.json
├── build/
│   ├── app-canonium/
│   ├── app-other/
│   └── ...
├── bower_components/
├── node_modules/
├── ...
├── .editorconfig
├── bower.json
├── gulpfile.js
└── package.json

Каждая директория — независимый проект, внутри которого находятся пользовательские файлы и служебный файл настроек settings.json. К сожалению, без файла настроек побороть идею автоматической вставки bower-зависимостей в HTML-файлы я не смог. Если вдруг кто-то предложит идею получше, то welcome to comments.

Директория templates содержит файлы HTML-препроцессора Jade (шаблоны) и, как не сложно догадаться, директория less — файлы CSS-препроцессора Less.

Gulpfile

Теперь поинтереснее — работа с gulp-файлом, в котором находится вся логика управления проектами. Отличаться от стандартного gulp-файла он будет лишь наличием функционала для определения текущего контекста и, если есть settings.json, то определением пользовательских настроек (о файле настроек речь в статье не идет).

Думаю, чтобы статья была актуальна и через год, нужно использовать Gulp 4.0.0-alpha.1, который имеет некоторые отличия от текущей версии Gulp 3.9.0. Чтобы установить альфа-версию, нужно сначала разобраться с консольной утилитой:

// Удалить текущую версию
npm uninstall gulp -g
// Установить глобально новую версию консольной утилиты
npm i gulpjs/gulp-cli#4.0 -g

и, после установить Gulp локально в директорию проекта:

npm i gulpjs/gulp.git#4.0 -D

а также пару плагинов для примера:

npm i gulp-jade gulp-less -D

Подсказка

Напомню, что флаг -D — это альтернатива флага --save-dev.

Шаг первый. Начальная точка

Для начала напишем gulp-файл, представив, что проект в директории один, и его директория - app-canonium. Тогда задача компиляции jade- и less-файлов будет иметь следующий вид:

var gulp = require('gulp');
var jade = require('gulp-jade');
var less = require('gulp-less');

gulp.task('jade', function() {
  return gulp.src('app-canonium/templates/*.jade')
    .pipe(jade({ pretty: true }))
    .pipe(gulp.dest('build'))
});

gulp.task('less', function() {
  return gulp.src('app-canonium/less/*.less')
    .pipe(less())
    .pipe(gulp.dest('build'))
});

gulp.watch('app-canonium/templates/*.jade', gulp.series('jade', function() {
  console.log('Компиляция файлов шаблона завершена');
}));

gulp.watch('app-canonium/less/*.less', gulp.series('less', function() {
  console.log('Компиляция файлов стилей завершена');
}));

gulp.task('default', gulp.parallel('jade', 'less'));

Что за gulp.series() и gulp.parallel(), вероятно спросите вы? Ответ очевиден — читаем документацию. Наверное, это единственный правильный ответ, но все же:

  • gulp.parallel(...tasks) — запускает указанные задачи максимально параллельно, при этом, если возникает ошибка, то все выполнение будет завершено.
  • gulp.series(...tasks) — запускает задачи последовательно в указанном порядке, при этом, если возникает ошибка, то все выполнение будет завершено.

Хорошо, у нас есть начальная точка, с которой мы потихоньку начинаем сдвигаться в нужную сторону. Идем дальше.

Замечание

Полный листинг кода на этом этапе статьи можно посмотреть на Gist.

Шаг второй. Понимание контекста

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

...
var path = require('path');

// Глобальные настройки контекста для всех задач
//
// Используется для определения контекста по умолчанию
// и хранения текущнго контекста при прослушке
var options = {
  project: 'app-' + getDefaultContext('canonium')
};

Далее нужно избравиться от жестко прописанного пути до проекта в задачах. Просто добавим конкатенацию пути, полученного контекста и пути, взятого из структуры, описанной в разделе «Архитектура»:

gulp.task('jade', function() {
  return gulp.src(path.join(options.project, 'templates/*.jade'))
    .pipe(jade({ pretty: true }))
    .pipe(gulp.dest('build/' + options.project))
});

Для задачи, компилирующей less-файлы все аналогично, поэтому копируем предыдущую версию и добавляем в нее path.join(), как и у задачи jade:

gulp.task('less', function() {
  return gulp.src(path.join(options.project, 'less/*.less'))
    .pipe(less())
    .pipe(gulp.dest('build/' + options.project))
});

А теперь... объявим две небольшие вспомогательные функции, которые и будут определять контекст запуска задач:

// Функция получения контекста по умолчанию
//
// Определяет контекст, исходя из переданных аргументов при запуске. Если
// первый аргумент undefined, то берется второй и, если он имеет `--` в
// начале, то он считается проектом. Это сделано для поддержки запуска задач,
// например, `gulp jade --yellfy`
function getDefaultContext(defaultName) {
  var argv = process.argv[2] || process.argv[3];
  if (typeof argv !== 'undefined' && argv.indexOf('--') < 0) {
    argv = process.argv[3];
  }
  return (typeof argv === 'undefined') ? defaultName : argv.replace('--', '');
};

// Функция перехода в контекст
//
// На основе пути изменившегося файла определяет каталог проекта,
// выводит имя проекта, к которому относится изменение и путь до него
function runInContext(filepath, cb) {
  var context = path.relative(process.cwd(), filepath);
  var projectName = context.split(path.sep)[0];

  // Console
  console.log(
    '[' + chalk.green(projectName.replace('app-', '')) + ']' +
    ' has been changed: ' + chalk.cyan(context)
  );

  // Set project
  options.project = projectName;

  cb();
};

Внимание

Пакет Chalk необходим лишь для раскрашивания выводимой в консоль информации.

В конце не забываем про прослушку события изменения файлов и задачу по умолчанию:

gulp.watch('app-*/templates/*.jade').on('change', function(filepath) {
  runInContext(filepath, gulp.series('jade'));
});

gulp.watch('app-*/less/*.less').on('change', function(filepath) {
  runInContext(filepath, gulp.series('less'));
});

gulp.task('default', gulp.parallel('jade', 'less'));

Здесь для удобства имеется вывод в консоль информации о том, что один из файлов (указывается имя проекта и путь до файла) был изменен. Это довольно удобная опция, учитывая, что можно работать сразу с двумя проектами.

Замечание

Полный листинг кода на этом этапе статьи можно посмотреть на Gist.

Шаг третий. Попытка запуска

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

Задача по умолчанию

В случае, если указан аргумент, совпадающий с именем уже созданного проекта, например gulp --yellfy, то задача по умолчанию будет работать с проектом, находящимся в директории app-yellfy:

Задача по умолчанию

Если же изменить любой из файлов в созданных проектах (templates/*.jade или less/*.less), то задачи будут выполняться в том проекте, в котором было зафиксировано изменение:

Отслеживание файлов

Цель достигнута! Happy end :)

Выводы

О плюсах этого решения говорилось в начале статьи, поэтому самое время поговорить о получаемых минусах:

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

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

Однако, если у вас много однотипных проектов этот минус можно опустить.

Теряется магия Gulp

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

Проблемы с Bower

В начале я говорил о том, как можно решить проблему с зависимостями менеджера Bower. Однако, представьте, что кроме команды bower i jquery —save-dev, вам нужно идти в директорию проекта и добавлять название пакета в файл настроек. Разумеется, что это будет необходимо, если лень написать скрипт, альтернативный вызову команды bower i и автоматически добавляющий устанавливаемую зависимость в указанные проекты (например, npm bower i jquery —canonium —other).

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

В любом случае решать вам.