Пост посвящается тем, кто имеет больше одного проекта с множеством js/css файлов, но ещё не знаком с героями этого дня: Grunt, Yeoman, Bower.

Зачем нужен Grunt

Хорошо, когда у проекта (в данном случае этим гордым словом мы обозначим сайт и в первую очередь его «внешние» составляющие: html, css, javascript) есть версия для разработки, где код имеет вполне читабельный вид, и версия для боевого сервера, где все данные сжаты по-максимуму для ускорения быстродействия.

И по-большому счёту можно сжимать скрипты и css вручную, если такой сайт один. А если нет? Каждый раз при каждом небольшом изменении в коде идти на какие-то страницы и пережимать файлы? Как был прав высказавшийся в комментариях к указанной выше заметке vonavi. Да, «онлайн-сервисы для сжатия CSS/JS — это всё же моветон» и непозволительная роскошь.

В принципе, само сжатие можно успешно (и, разумеется, автоматически) производить при помощи css-препроцессоров, таких как less или sass. Но только ли этим может быть полезен Grunt? Не только. Этот замечательный инструмент позволяет создавать задачи для проекта и имеет множество доступных плагинов.

Помимо всего прочего, сам Грант и плагины к нему написаны на JavaScript. Если читатель имеет честь быть знакомым с этим языком, то без труда сможет начать писать свои собственные дополнения.

Установка Grunt

Тут мы не обойдёмся без предварительной установки Node.js, которая притянет с собой пакетный менеджер npm. При помощи последнего можно установить сам грант.

# emerge -pav net-libs/nodejs
# npm install -g grunt-cli

Следующим шагом будет создание двух файлов: package.json и Gruntfile.js. Первый представляет собой описание проекта для npm, второй — файл конфигурации Grunt.

$ mkdir project && cd project
$ touch Gruntfile.js
$ npm init

Для описания можно запустить команду nmp init и воспользоваться интерактивным диалогом. Вот пример того, что можно получить на выходе:

{
  "name": "project",
  "version": "0.0.0",
  "description": "test project with grunt",
  "main": "Gruntfile.js",
  "keywords": [
    "css",
    "js"
  ],
  "author": "me",
  "license": "MIT"
}

На версию проекта можно не обращать большого внимания. Дело в том, что Grunt применяют в работе те крутые ребята, что пишут js-библиотеки. Эта настройка скорее относится к ним.

Основное содержимое грантфайла имеет вид:

module.exports = function(grunt) {
  'use strict'; // включить строгий режим

  // автоматически загружать задачи (хотя можно и вручную)
  require('load-grunt-tasks')(grunt);

  // во избежание «очепяток»
  // желательно определить директории разработки и деплоя
  var config = {
    app: 'app',
    dist: 'dist'
  };

  grunt.initConfig({
    config: 'config',

    // дальше следует описание задач (см. ниже)
    less: {},
    uglify: {},
    watch: {}

  });

  // «загрузка» задач вручную
  grunt.loadNpmTasks('grunt-contrib-connect');
  // регистрация задач, default-задача должна быть обязательно
  grunt.registerTask('default', ['jshint']);
};

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

Ещё хотелось бы предостеречь торопыг: не спешите выполнять всё описанное последовательно, это бессмысленно. Рекомендуется дочитать заметку до логического завершения (либо пропустить описание отдельных настроек Grunt, перейдя сразу к секции о bower) и понять концепцию, идею.

Так какие задачи можно автоматизировать при помощи Гранта?

Сжатие изображений

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

$ npm install grunt-contrib-imagemin --save-dev

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

imagemin: {
  options: {
    optimizationLevel: 3,
    progressive: true,
    interlaced: true,
    pngquant: true
  },
  dynamic: {
    files: [{
      expand: true,
      cwd: '<%= config.app %>/images',
      src: ['**/*.{png,jpg,gif}'],
      dest: '<%= config.app %>/images'
    }]
  }
}
//
grunt.loadNpmTasks('grunt-contrib-imagemin');

Как нетрудно догадаться, путь <%= config.app %>/images ведёт к каталогу app/images. В качестве пути для оптимизированных изображений (dest) мы взяли ту же самую директорию. Иначе говоря, исходные изображения в примере перезаписываются.

В этот пример включим также рассмотрение задач (tasks). Иногда их ещё называют целями (targets). Так, imagemin — это задача, результатом выполнения которой должны быть оптимизированные картинки. Тогда imagemin:dynamic будет подзадачей, а imagemin:options — опциями, одинаковыми для всех подзадач.

Подзадачи позволяют запускать одну задачу для разных исходных файлов.

Проверка и минимизация JavaScript

Плагин jshint проверит наличие ошибок во всех javascript-файлах указанной директории. А вот uglify будет полезен при необходимости собрать все файлы, склеить их в единственный файл, код которого оптимизировать (читай: сжать).

uglify: {
  production: {
    src: ['<%= config.app %>/scripts/*.js'],
    dest: '<%= config.dist %>/scripts/scripts.min.js'
  },
jshint: {
  all: [
    'Gruntfile.js',
    ['<%= config.app %>/scripts/*.js']
  ]
}
//
grunt.loadNpmTasks('grunt-contrib-uglify');

Препроцессоры и CSS

Для тех, кто использует препроцессоры, тоже есть хорошая новость. Да, автор в курсе, что less и sass автоматически могут обновлять страницу, если css-код, над которым вы работаете, был изменён. Увы, они ещё не так умны, чтобы отслеживать изменения в javascript и html. А вот grunt’у это по плечу. Без шуток. Но об этом чуть ниже, а пока пример включения less.

less: {
  dev: { // подзадача для разработки
    options: {
      sourceMap: true
    },
    files: { // в .tmp less компилируется в css
      '.tmp/styles/main.css': '<%= config.app %>/styles/main.less'
    }
  },
  dist: { // подзадача для сборки в production
    options: {
      compress: true, // сжимать css
      report: true  // предупреждать об ошибках
    },
    files: { // куда и откуда копировать
      'dist/styles/main.css':'<%= config.app %>/styles/main.less'
    }
  }
}
//
grunt.loadNpmTasks('grunt-contrib-less');

Предполагается, что в данном случае main.less содержит импорты на различные файлы проекта как то: header, content, etc. В таком случае Grunt соберёт все эти «кусочки» и склеит в один файл, после чего оптимизирует и скопирует в директорию для production.

Этот пример наглядно иллюстрирует то, о чём мы уже говорили вначале: подзадачи. Как они могут быть использованы?

$ grunt less:dev  # выполнит подзадачу для разработки
$ grunt less:dist # выполнит подзадачу для production

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

Посмотреть настройки для первого можно, прогулявшись по этому адресу. Что касается csscomb, на странице Build Coding надо выбрать настройки под себя, скопировать получившийся код и положить его рядом с грантфайлом, в файл .csscomb.json. Grunt увидит и прочтёт его, не сомневайтесь. Разумеется, чтобы это работало, следует также создать соответствующие задачи. Это несложно.

Генерация спрайтов

Не смотря на распространение иконочных шрифтов и победного шествия SVG по планете, очень часто приходится работать с обычными png-картинками. Они невелики по размеру, но очень многочисленны. В таком случае поспешим сделать из них старый добрый спрайт.

"devDependencies": {
  "grunt-spritesmith": "*"
}

В конфигурационном файле добавим ещё несколько строк:

// Gruntfile.js
sprite: {
all: {
    'algorithm': 'top-down',
    src: '<%= config.app %>/sprites/*.png',
    destImg: '<%= config.app %>/sprites/sprite.png',
    destCSS: '<%= config.app %>/less/sprite.less'
  }
}

В настройках можно указать как именно генерировать спрайт. Доступны значения алгоритма top-down, left-right, diagonal, alt-diagonal, и binary-tree.

А теперь ещё немного уменьшим важность ручного труда.

Автоматизация в действии

Самое вкусное: слежение за изменившимися файлами и автоматическое обновление страницы браузера. Для этих действий используются цели watch, connect и concurrent. Честно говоря, сложно описать и объяснить весь процесс, да и строк кода это займёт не так уж мало, поэтому ограничимся теорией, а ссылка на рабочий пример будет дана в конце заметки.

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

  • поставить «смотрящего» и давать указание на выполнение этих задач при необходимости
  • соединиться с сервером
  • очищать временные файлы при каждом новом соединении

Здесь watch будет проверять произошли ли какие-то изменения в файлах. И для разных типов файлов лучше назначать разные подзадачи: если вы укажете watch присматривать за html, он будет это делать. Отследить изменившийся код javascript? Да пожалуйста! Ну, и разумеется css/sass/less…

Как это может выглядеть:

watch: {
  options: {
    livereload: true
  },
  less: {
    files: ['<%= config.app %>/styles/{,*/}*.less'],
    tasks: ['less:dev']
  },
  js: {
    files: ['<%= config.app %>/scripts/{,*/}*.js']
  },
...

За соединение с сервером отвечает task connect. А concurrent может запускать несколько задач одновременно.

Кстати, если вы установите time-grunt, то сможете замерить производительность task’ов.

Разложить по полочкам

Когда итоговый грантфайл не вызывает у читателя положительных эмоций в силу своего большого размера, вполне можно разложить его по полочкам. Для этого нужно прописать в нём строку require('load-grunt-config')(grunt); и установить соответствующий модуль.

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

- myproject/
-- Gruntfile.js
-- grunt/
--- aliases.yaml
--- jshint.js
--- uglify.js
--- imagemin.js

Как видно из данного выше примера, мы создали поддиректорию grunt, куда бережно сложили наши task’и. Осталось привести записи в самом грантфайле и модулях к соответствующему виду.

// Gruntfile
module.exports = function(grunt) {
  require('load-grunt-config')(grunt);
};

// grunt/jshint.js
module.exports = {
  all: [
    'Gruntfile.js',
    ['app/scripts/*.js']
  ]
}

Возможно, придётся прописать в aliases.yaml имена модулей, вот так:

default:
  - 'jshint'
  - 'uglify'
  - 'imagemin'

Впрочем, автор не использует эту возможность, но хотелось показать, что она тоже есть.

Bower

Итак, npm — это менеджер пакетов, bower — это менеджер пакетов. Так что для чего предназначено?!

Npm используется для таких вещей как Grunt + его плагины. Соответственно, пакеты а-ля grunt-contrib-* предназначены для него; после их установки появится каталог node_modules. Bower же эффективно управляется с html, css, js; его каталог — bower_components.

Если при помощи npm в предыдущих примерах мы устанавливали разные полезные штуки, то при помощи bower можно установить (и добавить в проект) библиотеку jQuery.

Управление пакетами с bower выглядит следующим образом:

$ bower search fotorama
$ bower install fotorama --save
$ cat bower.json | grep fotorama
    "fotorama": "~4.6.2"
$ ls bower_components
fotorama/  jquery/
$ bower uninstall fotorama --save
$ bower install fotorama --save-dev

Здесь описана установка (и последующее удаление) jQuery-плагина Fotorama. Параметр --save сделает соответствующую запись в файле bower.json и пометит установленный пакет как зависимость проекта.

Зависимости могут быть как для публичного проекта, так и для разработки. Запись о первых вносится в строку «dependencies», запись о вторых — в строку «devDependencies». Для удаления как пакета, так и записи из файла используются указанные в примере ключи. То же касается и управления пакетами с npm.

Yeoman + Grunt + Bower

Возможно, надо было сразу начать с этой связки, не пускаясь в объяснения о Grunt и Bower. Ведь если вам подходит «умолчательная» конфигурация, зачем нужно что-то ещё?

Допустим, мы не ставили Грант. Как быстро развернуть готовую среду для front-end’а? Yeoman спешит на помощь. Если нужен sass и bootstrap, подойдёт данная ниже последовательность команд. Установка yeoman, grunt + bower. Генерация при помощи всего этого добра готовой среды для разработки.

# npm install -g yo
# npm install -g grunt-cli bower
# npm install --global generator-webapp
$ yo webapp

Для проектов на less + bootstrap, less + boilerplate можно сделать так:

# npm install -g generator-lessapp
$ yo lessapp
$ grunt serve

Вместо webapp поставить lessapp. Настраивать ничего не надо: просто отвечайте на предложенные вопросы.

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

Заключение

Таблица полезностей для Grunt (все подробности о них можно найти на gruntjs.com):

чтозачем
grunt-autoprefixerдобавить в css префиксы (-moz, -webkit)
grunt-csscombупорядочить селекторы в css
grunt-contrib-connectзапустить веб-сервер
grunt-contrib-watchотслеживать изменения в файлах
grunt-contrib-cleanочищать директорию
grunt-contrib-copyкопировать файлы
grunt-contrib-lessработать с less
grunt-contrib-sassработать с sass, scss
grunt-contrib-uglifyсжимать js
grunt-contrib-jshintпроверять код js
grunt-contrib-imageminоптимизировать размер изображений
grunt-concurrentодновременный запуск задач
grunt-spritesmithгенерирует спрайт и css-код
grunt-newerсборка только изменившихся файлов
grunt-notifydesktop’ное уведомление об ошибках
load-grunt-tasksавтоматически подгружать задачи
time-gruntрассчёт и отображение времени выполнения task’ов

Список ссылок на упомянутые инструменты:

Да, ещё надо заметить, что грантфайл может быть написан на CoffeeScript вместо JavaScript, что сделает его стиль куда проще и лаконичнее. ⤧  Следующая запись Blur-эффект на чистом css