Предисловие


В данной статье я поделюсь с вами опытом, как быстро и безболезненно создавать простые плагины для gulp. Статья ориентирована на таких же чайников, как и я. На тех, кто до сих пор лишь использовал готовые плоды gulp, срывая их с великого Древа Познания NPM, и не имел серьезного опыта работы с Node JS и его стримами.

Я не буду отвечать на вопросы вида «А зачем создавать свои плагины, если уже написано все, что только возможно?». Придет время, и вам за полчаса нужно будет написать что-то очень специфичное для вашего проекта. Перерыв весь npm, вы найдете один заброшенный плагин с убогим функционалом, автор которого недоступен, код ужасен и так далее. А может быть, это будет настолько специфичная задача, что вы не найдете абсолютно ничего.

Такой задачей для меня стала визуализация большого проекта, использующего Angular JS. Было огромное количество angular-модулей, связи между которыми были для меня уже не столь очевидными. Плюс мне хотелось видеть «диверсантов» — модули, которые каким-либо образом нарушали общую концепцию проекта (например, лезли в другой модуль не через провайдера, а напрямую).

Поискав, я нашел такое решение своей задачи. В принципе, запускать grunt плагины в gulp достаточно просто, но реализация в этом плагине меня не слишком впечатлила. Мне не хотелось использовать сторонние программы, а именно graphviz в качестве средства визуализации графа. Плюс ко всему, кто знает, что мне потребуется еще, а зависимость от сторонних библиотек всегда налагает ограничения.

Если читателя интересует лишь этот плагин, а не сама статья, то вот ссылка на проект на github и на npm. Всем остальным — добро пожаловать под кат.

С чего начать?


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

Краткий конспект философии gulp-плагинов:
  • ваш плагин всегда принимает набор Vinyl объектов
  • ваш плагин всегда должен отдавать набор Vinyl объектов (вы можете этого и не делать, но с результатом вашего плагина потом невозможно будет работать другим плагинам. Это обязательно выстрелит)
  • что за винил? Vinyl file object — в простонародье просто файл. В свойстве path хранит filename — полный путь до файла, в свойстве contents — буфер или стрим с содержанием файла
  • никогда не пишите плагины, которые будут делать то же самое, что и существующие node пакеты. Вы попадете в блэклист. И вполне справедливо

Плюс ко всему разработчики советуют ознакомиться с хорошо написанными простыми плагинами. Я бы советовал посмотреть на код gulp-replace

Реализуем свои идеи


Я приведу наиболее устоявшийся шаблон построения gulp-плагинов, который используется в большинстве хороших плагинов. Детальное описание реализации моей задачи — не есть цель данной статьи. Основная цель в том, чтобы каждый мог быстро «въехать» на примере и пойти создавать свой плагин.

Итак, начнём. Предполагается, что node js уже стоит в системе глобально.
npm init

Main файл проекта пусть будет index.js. После заполнения основной информации, устанавливаем следующее
npm install --save through2 gulp-util vinyl

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

Займемся проектированием. Итак, я хочу получить два файла. Первый — это описание графа в формате dot для поддержки graphviz, если вдруг что. Второй — это html файл, открыв который я увижу красивый граф, нарисованный с помощью d3. Итого в моей задаче есть 3 основных действия:
  1. получить массив всех ангуляр-модулей объявленных в файлах, которые примет в себя плагин
  2. создать .dot файл графа на основе массива модулей
  3. создать визуальное представление графа (html файл c d3-скриптом)

Создаем index.js, чистый как холст, и бросаем на него побольше красок:

var through = require('through2'),
    gutil = require('gulp-util'),

    //ты будешь извлекать массив ангуляр-модулей
    ModulesReader = require('./lib/modules-reader'),

    //ты будешь строить граф
    GraphBuilder = require('./lib/graph-builder'),

    //а ты его визуализировать
    GraphVisualizer = require('./lib/graph-visualizer');

//экспортируем функцию, вызывая которую в тасках gulp, пользователь инициирует наш плагин
module.exports = function(options) {

    //#section инициализация
    var modulesReader;
    var graphBuilder;
    var graphVisualizer;

    options = options || {};
    if (!modulesReader) {
        modulesReader = new ModulesReader();
    }
    if (!graphBuilder) {
        graphBuilder = new GraphBuilder();
    }
    if (!graphVisualizer) {
        graphVisualizer = new GraphVisualizer();
    }
    //#endsection инициализация
    
    //функция, которую будет вызывать through для каждого файла
    function bufferContents(file, enc, callback) {
        if (file.isStream()) {
            //бросим ошибку с помощью gulp-util
            this.emit('error', new gutil.PluginError('gulp-ng-graph', 'Streams are not supported!'));
            return callback();
        }
        if (file.isBuffer()) {
            //отдадим файл на чтение нашему читателю модулей ангуляра
            modulesReader.read(file.contents, enc);
        }
        callback();
    }
    
    //функция вызывающаяся перед закрытием стрима
    function endStream(callback) {
        var modules = modulesReader.getModules();
        if (!modules || !modules.length) {
            return;
        }
        //соберем dot файл и объект графа
        var builderData = graphBuilder.build({
            modules: modules,
            dot: options.dot || 'ng-graph.dot',
        });
        //соберем html файл на основе объекта графа
        var htmlFile = graphVisualizer.render({
            graph: builderData.graph,
            html: options.html || 'ng-graph.html',
        });
        //отправляем результат в стрим
        this.push(builderData.dotFile);
        this.push(htmlFile);
        callback();
    }

    return through.obj(bufferContents, endStream);
};


Важно помнить, что если вы планируете возвращать обработанные входные файлы, то необходимо в функции bufferContents вызывать this.push(file) после манипуляций с контентом файла. Но если вы планируете (как в моей задаче) генерировать новые файлы на основе входных, то вам обязательно потребуется функция endStream, где стрим еще не закрыт и вы сможете добавить ваши файлы в пустой стрим.

Так как основная цель статьи — научиться писать плагины gulp на конкретном примере, то я не буду приводить здесь реализации ModulesReader, GraphBuilder и GraphVisualizaer, являющиеся специфичными для моей конкретной задачи. Если кого-то заинтересует их реализация, то добро пожаловать на гитхаб

Результат работы плагина — вот такой вот приятный граф проекта на d3 с возможностью зумирования.


Комментарии (1)


  1. piumosso
    08.05.2015 11:08

    Тут всё про стримы да про стримы, а вот

    if (file.isStream()) {
      //бросим ошибку с помощью gulp-util
      this.emit('error', new gutil.PluginError('gulp-ng-graph', 'Streams are not supported!'));
      return callback();
    }