Пишем плагин под webpack

О webpack уже писали на хабре, рассказывали на moscowjs и есть несколько статей на других ресурсах, которые описывают общие возможности webpack«a, его достоинства и недостатки.

Поэтому в этой статье мы расскажем о самом webpack лишь вкратце и более подробно о разработке плагина под него.

image

1. Коротко о Webpack


Для систем сборки, вроде популярных grunt или gulp написано большое количество вспомогательных библиотек, а вот webpack в данный момент похвастаться таким изобилием не может и было бы здорово иметь возможность использовать необходимую библиотеку. Если вы решили внедрить webpack в ваш проект, но готового решения не нашлось, то выход остается один — написать самим.

И так, что же такое webpack, в чем его особенность и как он работает?

Webpack — это module bundler, так его позиционируют сами разработчики (http://webpack.github.io).

image

Из коробки он поддерживает AMD, CommonJS и ES6 модули, есть возможность live reload«a, реализованый в дополнении webpack-dev-server и работающий через socket.io. Это означает, что вам не нужно каждый раз после внесения правок в исходные файлы — запускать команды сборки заново.

Также у webpack«a достаточно простой config, не столь прозрачный, как у gulp, но гораздо более лаконичный и аккуратный.

Терминология:

2. Loaders & Plugins

Loader — это мощный инструмент, используемый для загрузки, обработки и преобразования файлов. Т.е. если в нашем проекте используются такие технологии как, например, jade, sass, coffee, es6 или наши svg слишком большие и их нужно пережать — все подобные задачи по обработке, трансформации и минификации будут решать как раз loader«ы. Их уже написано достаточно большое количество, ознакомиться можно здесь здесь.

Написать свой loader просто. Для этого на оффициальном сайте проекта есть отдельный раздел — How to write a loader и в целом по первым строчкам кода можно сразу понять насколько это просто:

// Identity loader
module.exports = function(source) {
  return source;
};

// Identity loader with SourceMap support
module.exports = function(source, map) {
  this.callback(null, source, map);
};


Loader представляет из себя module.exports функцию, которая принимает на вход содержимое файла и возвращает результат обработки, также может вернуть и sourcemap при наличии в конфигурации нужного аттрибута.

Возвращая текущий контекст, появляется возможность использования цепочки (chaining). Соответственно каждый из них должен выполнять только одну, свою задачу, разработчики webpack«a призывают нас писать отдельный loader под каждый шаг. В целом это общая модульная идеология, когда каждый модуль инкапсулирован, за счет чего гораздо проще работать с этим и производить отладку.

Plugin — это объект, у которого есть метод apply всего с одним параметром — compiler. Через него можно связываться с различными этапами компиляции webpack«a. Выполнять необходимые преобразования, так же существуют различные типы интерфейсов плагина, в качестве примера на официальном сайте приводится следующий код: 


compiler.plugin("compile", function(params) {
  // Just print a text
  console.log("Compiling...");
});

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

Когда мы запустили процесс сборки с помощью webpack — код сработает и в терминале мы увидим: «Compiling…
»

Внешне — это понятная механика, но примеров подобной работы немного, а в документации примеров практически нет.


От части это и побудило нас к написанию статьи, т.к. раздел How to write a loader есть и примеров loader«ов достаточно много, а вот раздел How to write a plugin скрыт (а возможно и вовсе убран) с официального сайта на неопределенный срок. По большому счету мы имеем только список и текстовое описание работы так называемых interface types (upd: теперь и этот раздел скрыт) и возможность изучить код уже написанных плагинов, посмотреть как это используется и работает.

Конечно же мы не сомневаемся, что разработчики webpack«а вскоре предоставят нам и этот раздел, но попробуем разобраться в этой теме сейчас для решения текущих задач, а когда раздел откроют — найдем для себя что-то новое, а может и вовсе переосмыслим работу с плагинами.

3. Пример решения проблемы — склейка спрайтов svg

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


И переписывать проект, который собирается grunt«ом или gulp«ом вместе со всеми тасками и плагинами на механику webpack«а — дело не 15 минут. 
Особенно учитывая, что до сих пор под webpack нет того множества библиотек, которыми могут похвастаться более старые и ходовые системы сборки.

Предположим, что мы решились на это и начали обновлять наш проект. И как раз в процессе этого можно наткнуться на ту проблему, которая была описаны выше — в webpack«e может не оказаться того инструмента, который бы дублировал или выполнял похожую механику задачи, которая ранее была у нас в проекте и отлично работала.


Так случилось с grunt-svgstore — библиотеки, которая собирает все svg из нужной нам директории в один спрайт, проставляя префикс для подключения нужных svg в нужных местах станицы через use xlink: href=»#logo».


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

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

4. Процесс написания кода

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


Поэтому тут мы пойдем таким путем: за основу возьмем код библиотеки grunt-svgstore, немного изменим ее, чтобы она начала работать как плагин под webpack и добавим какие-то новые возможности, при этом в самом верху кода плагина укажем всю правду: 


/**
 * Webpack SVGstore plugin based on grunt-svg-store
 * @see https://github.com/FWeinb/grunt-svgstore/blob/master/tasks/svgstore.js
 */


Таким образом мы никого не обманем и не обидим разработчика, писавшего svgstore под grunt. В целом мы взялись за этот плагин не для одноразового использования в конкретном проекте, а чтобы при удачном исходе выложить его на npm, таким образов сделав свой вклад в open source сообщество.

Чтобы преобразовать код task«и в плагин мы фактически убираем из кода весь grunt, сохраняя только готовый механизм непосредственно сборки и склеивания svg в спрайт. Но далее мы натыкаемся на ряд проблем, которые необходимо исправить:

5. Неожиданные проблемы


1) Спрайт получается слишком большим, и нам нужно добавить минификацию.

Первое что нашлось — под webpack есть готовый svg лоадер (https://github.com/rpominov/svgo-loader) под капотом которого svgo, через который можно прогнать все наши svg«шки.

Подключаем в плагине модуль svgo:

var SVGO = require(‘svgo');


И добавляем функцию минификации наших svg:

SvgStore.prototype.svgMin = function(file, loop) {
  var svgo = new SVGO();
  var source = file;
  var i;

  function svgoCallback(result) {
    source = result.data;
  }

  // optimize loop
  for (i = 1; i <= loop; i++) {
    svgo.optimize(source, svgoCallback);
  }

  return source;
};


Бывает что минификация может испортить некоторые svg, также мы стремимся к тому, чтобы процесс добавления новой svg был минимальным по трудозатратам. Добиться этого мы можем, добавив опция в плагин min: true/false, по которому мы понимаем, будет ли мы минифицировать определенные svg«шки, или мы соберем все svg ровно в таком виде, в котором они есть.

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

/app/assets/svg

А те svg, которые мы хотим минифицировать мы кладем во вложенную директорию
:
/app/assets/svg/min

Таким образом минифицироваться у нас будут только svg лежащие во внутренней директории min и отказываться от всей минификации, если она нам ломает несколько иконок, не нужно.

Еще тут есть нюанс из-за которого функция у нас принимает 2 параметра, один из которых содержимое svg, а второй — loop.
Дело в том, что одно из достоинств svg — это то, что мы может через css управлять стилями нашего изображение, менять цвет, ширину линий и т.д.


Но после 1-ого прогона svgo не удаляет inline стили, из-за которых мы не можем их переопределять через css, т.е. если нам надо например на ховер менять цвет — это в некоторых случаях работать не будет. Но уже после 2 прогонов эти атрибуты стираются и переопределение начинает работать.


Естественно не всем это надо, поэтому по умолчанию опции loop равна единице (1 прогон), но мы можем задать любое количество прогонов через svgo.

2) Необходимо с помощью плагина собирать несколько спрайтов. Это было реализовано с помощью префиксов довольно примитивным и не особо красивым (но рабочим) кодом, опция назвалась filter и работает следующим образом:

1. «except-name» — если значением начинается со слов except-, то в спрайт будут собраны все svg, кроме тех, которые в данном случае начинаются с name
2. «name» — в спрайт будут собраны svg, которые начинаются с заданной строки, в данном случае с name

output: [
  {
    filter: 'Logo-',
    sprite: 'svg/[hash].logo_sprite.svg'
  },
  {
    filter: 'except-Logo-',
    sprite: 'svg/[hash].sprite.svg'
  }
]


3. «all» — если нам не нужно ничего разделять и мы хотим получить на выходе только 1 спрайт, в котором будут находиться все svg.

output: [
  {
    filter: 'all',
    sprite: 'svg/[hash].sprite.svg'
  }
]


Кроме фильтра мы также указываем директорию, куда собирается все наши ассеты, а также была добавлена опции [hash].
В самом методе хеширование мы опять же не изобретали велосипед, а взяли самый стандартный способ хеширования, который использует модуль crypto:

Name.replace('[hash]', crypto.createHash(‘md5').update(source).digest('hex'))


Изначально не планировалось добавлять хеш к собранным спрайтам, потому что вроде как такие вещи должен делать webpack и наша задача только подсунуть в процессе компиляции собранный спрайт на этапе ДО добавления хешов, а все остальное сделает сборщик. Но, увы, заставить его заработать именно так не получилось, поэтому хеш добавляем внутри плагина сами.

3) Еще одна большая проблема — добавление собранных спрайтов в манифест.


Манифест — это json в которым указано соответствие именнам ассетов к собранным названиям с хешами и путями. Это делается для подключения ассетов, так как мы не знаем какой хеш будет у каждой сборки. При помощи манифеста мы получаем удобную таблицу связей. Для это используем плагин манифеста под вебпак.

С помощью этого манифеста бек нашего проекта понимает, что надо подключать после новой сборки.
И здесь есть еще одна проблема, с которой мы столкнулись: compiler отдает 2 объекта, в одном из которых массив ассетов, во втором — модулей.

И после добавление ассета внутри плагина

compiler.plugin('emit', function(compilation, callback) {
  compilation.assets[key.sprite] = {
    source: function() { return new Buffer(source); },
    size: function() { return Buffer.byteLength(source, 'utf8'); }
  };
  callback();
});


он появляется в массиве compiler.assets, но этот массив не дает нам соответствия между именем файла — и его путем с хешом.


Именно поэтому разработчик, написавший manifest-webpack плагин, использует внутри своего плагина именно compiler.modules, а не comiler.assets, хотя второе было бы гораздо логичнее.


Т.е. после того как мы добавили новый ассет, по примеру кода, который подсказал нам Тобиас (один из разработчиков webpack), этот ассет появляется в массиве assets, но этот массив не содержит исходного имени или пути, поэтому мы не можем использовать его.

Мы пытались вставить поле для имени через pull request, чтобы мы могли использовать assets массив для выстраивания манифеста, но он не был принят. И в целом эта идея была забракована.


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

compiler.plugin('done', function(compilation, callback) {
  // code

})



И дописывать в манифест руками спрайты через fs.readFile/fs.writeFile, что не есть хорошо.

6. Примеры и перспективы использования

Потыкать и попробовать получившийся плагин в своем проекте на webpack вы можете по ссылке.


В перспективе планируется сделать плагин наиболее универсальным, провести рефакторинг кода, убрать всё лишнее и покрыть его работу тестами — в общем оставить минимум того, что необходимо и сделать его максимально стабильным.

7. Ссылки

Demo с результатом работы плагина — все иконки на сайте motor.ru
Репозиторий плагина (ждем ваши issue и pull requests) — github.com/lgordey/webpack-svgstore-plugin
Плагин в NPM — www.npmjs.com/package/webpack-svgstore-plugin
Документация по webpack — webpack.github.io/docs
Репозиторий webpack — github.com/webpack/webpack

© Habrahabr.ru