Grunt.js


В жизни front-end web разработчика довольно много рутинной работы. Наиболее типичные примеры — это объединение js файлов, проверка их на синтаксические ошибки, минификация js и css кода, создание спрайтов, компиляция файлов sass, less или других препроцессоров в css и прочие задачи. Даже одно автоматическое обновление страницы в браузере многого стоит. С одной стороны — это всего лишь пара кнопок — нужно переключитсья на браузер и нажать на обновление, но, учитывая какое бесчисленное количество раз за время разработки приходится это делать, эта функция является очень востребованной.

Часто функции автоматизации выполняются либо самим приложением (к примеру sass может отслеживать изменения в собственных файлах и обновлять итоговые css файлы на лету), либо средой разработки или плагинами и скриптами для текстовых редакторов разной степени костыльности. У данного подхода есть ряд недостатков. Во-первых, не все приложения умеют отслеживать изменения в своих файлах (тот же less например). Во-вторых, несмотря на удобства средств разработки, они являются менее отзывчивыми и более ресурсоемкими, чем текстовые редакторы, а это подходит далеко не всем. И в-третьих, нет универсальности — каждая среда разработки и каждый текстовый редактор решают проблемы хоть и схожими способами, но по-своему. К примеру, если часть вашей команды пользуется Sublime Text, и они уже нашли все необходимые плагины для автоматизации с учетом используемых технологий в проекте, то члены команды, использующие Vim, не смогут применить их решение, и будут вынуждены искать плагины уже для своего редактора и т.д.

Поэтому намного выгоднее иметь универстальное решение, которое работает одинаково вне зависимости от ОС и используемых текстовых редакторов. Одним из таких решений является Grunt.js. Grunt.js использует Node.js, но даже если вы не имели с ним дел раньше, то это вам не помешает успешно использовать Grunt. Достаточно лишь общих знаний Javascript.

Установка

Прежде всего вам необходимо установить Node.js, если его еще нет в вашей системе, а также grunt-cli. Для Windows и Mac есть пакеты для установки, а пользователи Linux найдут Node.js в своих репозиториях. Затем необходимо установить grunt-cli. Это небольшая утилита, которая ищет ближайший уставновленный Grunt в папке с проектом и запускает его. Для этого небоходимо в консоли написать следующее.

Для пользователей Mac и Linux:
sudo npm install -g grunt-cli

Для пользователей Windows в консоли запущенной с правами администратора:
npm install -g grunt-cli

После этого для доступа к локальным копиям Grunt достаточно будет обращаться с помощью команды grunt, находясь в директории проекта. Административные права нужны только для установки. Запуск самого Grunt осуществаляется с правами обычного пользователя.

Подготовка нового проекта

В любом проекте, использующем Grunt, в корневом каталоге должно быть 2 файла — package.json и Gruntfile.js или Gruntfile.coffee.

package.json

Этот файл хранит данные о пакете для Node.js. И даже если мы занимаемся front-end разработкой, а значит скорее всего не собираемся публиковать свой код для других пользователей Node.js, он нам нужен для хранения информации о зависимостях, которые будут у нас загружаться в наш проект. Это как минимум сам Grunt.js, а также его плагины.

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

Создадим директрию для нашего тестового проекта:
mkdir test_project
cd test_project

Создадим package.json
npm init

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

{
	"name": "test_project",
		"version": "0.0.0",
		"description": "A test project to get familiar with Grunt.js",
		"main": "index.js",
		"scripts": {
			"test": "echo \"Error: no test specified\" && exit 1"
		},
		"author": "Dmitry Gavrikov",
		"license": "MIT"
}

После этого можем устанавливать локальную копию Grunt и его плагины в наш проект. Установка осуществляется с правами обычного пользователя и имеет следующий вид

npm install module --save-dev

Вместо module пишется имя нужного модуля, который будет устанавливаться локально в директории нашего проекта. Обратите внимание. что, когда мы устанавливали grunt-cli, то мы использовали опцию -g, которая устанавливала программу глобально, поэтому и нужны были права администратора.

Опция --save-dev означает, что устанавливаемый модуль будет прописан в package.json, как зависимость необходимая для разработки. Это нужно, чтобы другие члены команды могли установить те же модули, что и у вас при помощи всего одной команды npm install.

Примечание: к сожалению на практике не все так просто, и зачастую выгоднее включать чужие модули в код вашего проекта сразу. В таком случае другим разработчикам достаточно будет только получить доступ к вашему репозиторию и устанавливать модули самим при помощи npm install уже не понадобится. Есть отличная статья на тему, стоит ли включать код чужих модулей в свой репозиторий от Addy Osmani. К сожалению, она только на английском.

Установим Grunt.

npm install grunt --save-dev

После этого появится папка node_modules, в которую будет установлен Grunt и будут впоследствии устанавливаться все его плагины, а также обновится файл package.json

{
	"name": "test_project",
		"version": "0.0.0",
		"description": "A test project to get familiar with Grunt.js",
		"main": "index.js",
		"scripts": {
			"test": "echo \"Error: no test specified\" && exit 1"
		},
		"author": "Dmitry Gavrikov",
		"license": "MIT",
		"devDependencies": {
			"grunt": "~0.4.5"
		}
}

Обратите внимание на секцию devDependencies. Сюда будут включаться все модули, которые устанавливаются с помощью опции --save-dev.

Gruntfile.js

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

module.exports = function(grunt) {

	// 1) Project configuration.
	grunt.initConfig({
	});

	// 2) Load plugins
	grunt.loadNpmTasks('some-plugin-name');

	// 3) Task(s) registration
	grunt.registerTask('default', ['some-task']);

};

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

Вторая часть отвечает за подгрузку плагинов в Grunt. Т.е. недостаточно установить плагины с помощью npm, нужно еще указать Grunt, что они доступны. Например, для подключения плагина, отвечающего за сжатие Javascript необходимо прописать:

grunt.loadNpmTasks('grunt-contrib-uglify');

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

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

Третья часть отвечает за регистрацию задач. В данном случае зарегистрирована только одна задача — default. Эта задача выполняется всегда, когда пользователь запускает в консоли команду grunt. Если будет зарегистрирована другая задача, то ее запуск будет выполняться при помощи команды:

grunt task

где вместо task будет название зарегистрированной задачи.

Регистрация задачи выполняется с помощью функции grunt.registerTask. Первым аргументом идет название задачи, вторым — массив, перечисляющий другие задачи, которые будут выполнены в том порядке, в котором они идут в массиве. Таким образом, получается, что зарегистрированные (или их можно назвать собственные) задачи выступают чем-то вроде макрозадач, объединяющих в себе подзадачи. В качестве подзадач могут выступать другие собственные задачи, а также задачи, созданные плагинами Grunt. Если на данный момент немного не понятно, о чем идет речь, то когда дойдем до примеров, все встанет на свои места.

grunt.initConfig

Разберем эту секцию Gruntfile более подробно. Единственным аргументом этой функции является объект. Свойствами данного объекта являются задачи (tasks). Как правило это задачи, предоставленные плагинами, но возможно передавать значения и собственным задачам. Эти свойства сами хранят объекты, свойствами которых уже являются цели (targets). Цель определяет к каким файлам будет применена задача и с какими параметрами. Любая цель может иметь неограниченное число целей, но, как правило, определяют всего несклько целей. Также может содержаться необязательный объект options, в котором указываются параметры плагина, которые надо поменять, в случае, если не устраивают те, которые были выставлены по умолчанию.

В общем виде ситуция выглядит следующим образом:

grunt.initConfig({
  task1: {
    target1: {
    },
    target2: {
    }
  },
  task2: {
    options: {
    },
    target4: {
    },
    target5: {
      options: {
      }
    },
    target6: {
    }
  },
  task3: {
    target7: {
    }
  }
});

Рассмотрим простой пример с плагином grunt-contrib-uglify. На его странице в секции «Read Me» есть краткое описание работы плагина, затем идет команда на установку, функция для регистрации плагина внутри Gruntfile, описание опций и примеры. В таком виде сделано описание всех плагинов, поэтому, разобравшись с применением одного плагина, довольно просто начать использовать и другие.

Установим плагин в наш проект.

npm install grunt-contrib-uglify --save-dev

Исправим наш Gruntfile, чтобы он уже был не в общем виде, а его уже можно было реально использовать.

module.exports = function(grunt) {

  // 1) Project configuration.
  grunt.initConfig({
    uglify: {
      src: {
        files: {
          'dest/output.min.js': 'src/input.js'
        }
      }
    }
  });

  // 2) Load plugins
  grunt.loadNpmTasks('grunt-contrib-uglify');

  // 3) Task(s) registration
  grunt.registerTask('default', ['uglify']);

};

Вторую часть мы взяли целиком из «Read Me» плагина. В третьей части прописали, что при выполнении задачи default у нас будет выполняться задача uglify.

Как мы уже знаем, в первой части мы прописываем настройки для плагина.

uglify — это задача, предоставленная плагином. Ее имя мы можем подсмотреть в примерах в секции «Read Me».

src — это цель. Имя цели может быть абсолютно любым, но лучше давать осмысленные имена.

files — это предопределенное имя, которое определяет какие файлы следует брать и куда записывать. В данном случае указан 1 входящий файл и 1 исходящий.

Однако, это далеко не идинственный способ работы с файлами.

Работа с файлами

Компактный формат

Этот формат использует свойства src и dest вместо files. Применительно к нашему примеру, код выглядел бы так:

grunt.initConfig({
  uglify: {
    src: {
      src: 'src/input.js',
      dest: 'dest/output.min.js'
    }
  }
});

Формат с использованием объекта files

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

grunt.initConfig({
  uglify: {
    src: {
      files: {
        'dest/output1.min.js': 'src/input1.js',
        'dest/output2.min.js': 'src/input2.js',
        'dest/output3.min.js': 'src/input3.js'
      }
    }
  }
});

Формат с использованием массива files

Этот формат сочетает в себе два предыдущих:

grunt.initConfig({
  uglify: {
    src: {
      files: [
        {
          src: 'src/input1.js',
          dest: 'dest/output1.min.js'
        },
        {
          src: 'src/input2.js',
          dest: 'dest/output2.min.js'
        },
        {
          src: 'src/input3.js',
          dest: 'dest/output3.min.js'
        }
      ]
    }
  }
});

Устаревший формат

Этого формата следует избегать, но тем не менее он может попасться, поэтому не лишним будет знать о его существовании.

grunt.initConfig({
  uglify: {
    'dest/output1.min.js': 'src/input1.js',
    'dest/output2.min.js': 'src/input2.js',
    'dest/output3.min.js': 'src/input3.js'
  }
});

В этом формате имя исходящего файла является также именем цели.

Практика работы с файлами

Какой формат предпочесть? Компактный формат удобен, если есть только одна связь одного или многих входящих файлов к одному исходящему. Если у нас есть несколько исходящих файлов, то применяют либо формат с использованием объекта files, либо массива files. Однако, по-моему мнению формат с массивом files является синтаксически избыточным.

Не все плагины создают исходящий файл. Это относится к информирующим плагинам (например, grunt-contrib-jshint, который показывает синтаксические ошибки в Javascript). С такими плагинами используют компактный формат, и свойство dest при этом не используют.

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

grunt.initConfig({
  uglify: {
    src: {
      src: ['src/first.js', 'src/second.js'],
      dest: 'dest/output.min.js'
    }
  }
});

Паттерны в файловых путях

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

  • * — означает любое количество символов, но не /
  • ? — означает любой одиночный символ, но не /
  • ** — означает любое количество символов, включая /, если этот символ является единственным в файловом пути
  • {} — внутри этих скобок можно перечислять через запятую выражения, которые соотносятся через «ИЛИ»
  • ! — если стоит перед началом выражения, инвертирует его

Рассмотрим наиболее типичные примеры:

grunt.initConfig({
  uglify: {
    src: {
      files: {

        'dest/output1.min.js': 'src/*.js',
      	// любые js файлы в директории src

        'dest/output2.min.js': 'src/**/*.js',
      	// любые js файлы в директории src, включая поддиректории

        'dest/output3.min.js': 'src/*.{js, coffee}'
        // любые js и coffee файлы в директории src (в данном случае глупость полная, но смысл вы уловили)

        'dest/output4.min.js': 'src/app*.js',
        // любые js файлы в директории src, начинающиеся на app

        'dest/output5.min.js': ['src/*.js', '!src/linter.js']
        // любые js файлы в директории src, кроме файла linter.js
      }
    }
  }
});

Запуск Grunt

Вернемся к нашему исходному Gruntfile.js

module.exports = function(grunt) {

  // 1) Project configuration.
  grunt.initConfig({
    uglify: {
      src: {
        files: {
          'dest/output.min.js': 'src/input.js'
        }
      }
    }
  });

  // 2) Load plugins
  grunt.loadNpmTasks('grunt-contrib-uglify');

  // 3) Task(s) registration
  grunt.registerTask('default', ['uglify']);

};

Как мы уже знаем, для запуска задачи default достаточно выполнить команду

grunt

Однако мы можем вызвать задачу uglify и напрямую

grunt uglify

В Grunt выделяют обычные задачи и мультизадачи. Обычне задачи подразумевают одну цель, поэтому цели у них не указываются. Задачи регистрируемые с помощью функции grunt.registerTask() являются обычными. Большинство задач, которые предоставляют плагины, являются мультизадачами, а потому могут иметь несколько целей и отдельные блоки options, определенные для каждой цели отдельно. К примеру, задача uglify являетмя мультизадачей.

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

module.exports = function(grunt) {

  // 1) Project configuration.
  grunt.initConfig({
    uglify: {
      src1: {
        files: {
          'dest/output1.min.js': 'src/input1.js'
        }
      },
      src2: {
        files: {
          'dest/output2.min.js': 'src/input2.js'
        }
      }
    }
  });

  // 2) Load plugins
  grunt.loadNpmTasks('grunt-contrib-uglify');

  // 3) Task(s) registration
  grunt.registerTask('default', ['uglify']);

};

В таком случае при выполнении команды grunt uglify будут выполнены и цель src1, и цель src2. Однако для мультизадач мы можем напрямую указать какую цель нам нужно выполнить. Это делается через двоеточие. К примеру, чтобы выполнить задачу uglify применительно только к src2, то необходимо выполнить:

grunt uglify:src2

Также доступны дополнительные опции при запуске Grunt. Наиболее полезной является --help, так как помимо общей информации выводит список доступных задач в проекте с их описанием.

Продолжение следует…

Добавить комментарий