Создаем первый плагин для Grunt

grunt_logo

В связи с усложнением процесса фронтенд разработки, системы сборки клиентской части набрали большую популярность. Существуют две причины для подобного усложнения: миграция функциональных задач на клиентскую сторону и улучшение возможностей представления. Самая взрослая и, пожалуй, самая известная из таких систем разработки - Grunt. Благодаря своей популярности, она развила большую экосистему, где каждый разработчик может найти плагин для своих целей.

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

Начнем с grunt-init

Самый простой путь, чтобы начать создавать плагины для Grunt — использование grunt-init. Предполагаем, что вы уже установили Node (и npm), теперь установите с помощью командной строки

npm install -g grunt-init

Шаблон для плагина выложен на gitHub, давайте его клонируем:

git clone https://github.com/gruntjs/grunt-init-gruntplugin.git ~/.grunt-init/gruntplugin

Далее нужно запустить grunt-init в папке, в которой будет находится плагин:

grunt-init gruntplugin

Скафолдер задаст множество вопросов, которые помогут определить, как лучше создать структуру папки для плагина:

code_01

Grunt-init способен создавать множество разных вещей, связанных с Grunt. Полный список шаблонов и дополнительных инструкций Вы можете найти здесь.

Регистрируем задачу

Генерируемые файлы включают большое количество дополнительных ресурсов, таких как: readme, .jshintrc, license, .gitignore, package.json. Что нас на самом деле интересует, так это файл plugin.js находящийся в папке tasks.

Код самого плагина содержит registerMultiTask функция:

grunt.registerMultiTask(taskName, [description, ] taskFunction)

Мультизадачной она называется потому, что содержит множество конфигурационных объектов для разных задач. Строка taskName будет аналогичной свойствам основного объекта grunt.initConfig.

API Grunt отлично документировано, так что в статье мы рассмотрим только самое важные для нашей задачи. Внутри функции задачи this содержит множество полезных методов и свойств, например:

this.options()

Он возвращает объект

options

определенный в Gruntfile, кроме того используя его можно определять значения по умолчанию настроек:

var options = this.options({
enabled: false
});

Массив this.files содержит список всех пар, соответствующих паттернам конфигурации для src и dest, все глобальные паттерны должны быть обработаны до момента обращения к массиву. Большинству из вас понадобится цикл для этих файлов, таким образом, сгенерированный плагин будет содержать следующее:

this.files.forEach(function(f) {
var src = f.src.filter(function(filepath) {
  if (!grunt.file.exists(filepath)) {
    grunt.log.warn('Source file "' + filepath + '" not found.');
    return false;
  } else {
    return true;
  }
}).map(function(filepath) {
  return grunt.file.read(filepath);
}).join(grunt.util.normalizelf(options.separator));
 
grunt.file.write(f.dest, src);
grunt.log.writeln('File "' + f.dest + '" created.');
});

Как вы могли убедиться, массив this.files имеет свойство src, которое на самом деле является массивом файлов. В самой обычной ситуации Вы будете фильтровать данные файлы по их расширению, содержанию или с помощью каких-то других критериев.

grunt.file (http://gruntjs.com/api/grunt.file) содержит несколько полезных методов для работы с файловой системой.

grunt.util (http://gruntjs.com/api/grunt.util) содержит вспомогательные функции.

grunt.log (http://gruntjs.com/api/grunt.log) обеспечивает необходимые методы для загрузки информации в консоль для различных групп настроек.

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

var done = this.async();
someAsyncFunction(function(result){
  done(result);
});

Метод this.async возвращает callback done, который должен вызываться при выполнении задачи. Если результатом является ошибка или false, задача считается завершенной «провалом».

Примеры плагинов

После того как вы освоите основы внедрения плагинов Grunt, я покажу Вам несколько своих плагинов и опишу их особенности.

Grep

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

Заметка: в статье Эдди Османи (Addy Osmani) есть список схожих плагинов, решающих задачу сборки проектов с учетом особенностей окружения.

Вот пример использования grep, при сборке для продакшена:

Источник

<link rel="stylesheet" href="./style.css"> <!--@grep dev-->
<link rel="stylesheet" href="http://some.cdn/style.css"> <!--@grep production-->

Результат

<link rel="stylesheet" href="http://some.cdn/style.css"> <!--@grep production-->

В отличии от большинства плагинов Grunt, grep не имеет никаких зависимостей, таким образом, это не просто задача, которую выполняет Grunt, но, скорее, отдельный модуль.

Его исходники можно найти здесь.

Главный функционал grep реализован путем поиска регулярных выражений. Если хотите работать с содержимым файла в Grunt-плагине, следующий сниппет вам пригодится:

var src = grunt.util.normalizelf(src);
var lines = src.split(grunt.util.linefeed);

Он позволяет комфортно работать с текстовыми файлами, так как при разбиении на строки учитывает различные символы перевода строки, которые обычно сложно держать в голове. Grunt больше не поддерживает хеш grunt.util, поэтому привожу исходный код этих свойств:

util.normalizelf = function(str) {
  return str.replace(/\r\n|\n/g, util.linefeed);
};
util.linefeed = process.platform === 'win32' ? '\r\n' : '\n';

У grep масса преимуществ по сравнению с Grunt API. Все операции с файлами - чтение, запись, проверка присутствия, сравнение файлов из папок - реализованы с использованием grunt.file. Если планируете работать с содержимым файла, исходный код grep будет вам полезен.

Capo

Grunt-capo тонкая обертка вокруг модуля Capo. Если коротко, плагин разработан для продолжительной интеграции модулей, помогая своевременно обновлять генерируемые файлы. Сам по себе Capo упрощает работу с архитектурой, управляемой событиями в JavaScript, более подробно об этом можно прочесть в этой статье.

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

Similar

Grunt-similar служит удобству выполнения grunt-задач из консоли, он ищет имя введенной задачи в Gruntfile и, в случае если вы ошиблись при вводе её имени, проверяет, существуют ли зарегистрированные задачи со схожими именами.

code_02

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

if (grunt.cli.tasks.length === 1){
  var taskParts = grunt.cli.tasks[0].split(':');
  var tasks = Object.keys(grunt.task._tasks);
  if (tasks.indexOf(taskParts[0]) === -1){
    grunt.registerTask(grunt.cli.tasks[0], ['similar']);
  }
}

grunt.cli.tasks массив задач, которые были введены через консоль и grunt.task._tasks массив всех задач зарегистрированных в конфигурации.

Метод grunt.registerTask принимает имя задачи, которую нужно зарегистрировать, и массив заданий, который нужно будет вызвать. Таким образом проксируется выполнение похожих similar задач.

Еще следует обратить внимание на то, как плагин реализует выполнение задачи с заданным именем:

grunt.task.run(similarTaskName);

Заключение

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

В завершении я бы хотел обратить внимание, что уже достаточно много сказано про другие системы сборки и про гонки производительности между ними, и что разработчики Grunt поставили себе цель выпуска новой, улучшенной версии проекта. Информацию о проекте и его развитии можно найти в репозитории grunt-next.