Декабрь 2015
ТОМ 30, НОМЕР 13
Visual Studio - Современные средства для веб-разработки: Grunt и Gulp
Адам Тьюлипер | Декабрь 2015
Продукты и технологии:
Visual Studio 2015, Grunt, Gulp, Node.js, ASP.NET 5
В статье рассматриваются:
- установка и использование Grunt;
- установка и использование Gulp;
- Task Runner Explorer;
- Gulp и ASP.NET 5.
Современному веб-разработчику доступно много инструментов. Два из наиболее часто встречающихся в сегодняшних веб-проектах — Grunt и Gulp, которые представляют собой JavaScript-средства запуска задач (JavaScript task runners). Применение JavaScript для запуска задачи может показаться странной концепцией, если вы никогда не делали такого или если вы привыкли к веб-разработке исключительно средствами Visual Studio, но есть веские причины на то, чтобы опробовать ее. JavaScript-средства запуска задач, работающие вне браузера и обычно использующие Node.js в командной строке, позволяют легко выполнять задачи, связанные с разработкой клиентской части, включая минимизацию (minification) , конкатенацию нескольких файлов, определение зависимостей скрипта и встраивание соответствующих ссылок в должном порядке в HTML-страницы, создание средств модульного тестирования (unit test harnesses), обработку скриптов на TypeScript или CoffeeScript и др.
Что выбрать — Grunt или Gulp?
Выбор средства запуска задач (task runner) обычно зависит от личных предпочтений или специфики проекта, если только вы не используете плагин, поддерживающий лишь конкретное средство запуска задач. Основные отличия в том, что Grunt управляется конфигурационными параметрами в формате JSON и каждая задача Grunt, как правило, должна создавать промежуточные файлы для передачи чего-либо в другие задачи, тогда как Gulp управляется исполняемым JavaScript-кодом (т. е. не только JSON) и может направлять результаты от одной задачи следующей без использования временных файлов. Gulp является более новым инструментом и, как таковой, часто используется в более новых проектах. Тем не менее, Grunt имеет множество общеизвестных средств поддержки, например jQuery, который использует его для создания… jQuery. И Grunt, и Gulp работают через плагины — модули, которые вы устанавливаете для обработки конкретной задачи. Существует огромная экосистема плагинов, и часто встречаются пакеты задач, поддерживающие как Grunt, так и Gulp, поэтому использование одного или другого средства вновь сводится, по сути, к персональному выбору.
Установка и использование Grunt
Установщиком для Grunt и Gulp является Node Package Manager (npm), о котором я кратко рассказал в своей статье за октябрь 2015 года (msdn.com/magazine/mt573714). Команда установки Grunt на самом деле состоит из двух частей. Первая — это разовая установка интерфейса командной строки Grunt, а вторая — установка Grunt в папку вашего проекта. Такая установка из двух частей позволяет использовать несколько версий Grunt в системе и применять интерфейс командной строки Grunt по любому пути:
#Это делается лишь раз, чтобы глобально установить
#средство запуска командной строки grunt
npm install –g grunt-cli
#Один раз выполняется в папке вашего проекта
#для создания package.json. Этот файл будет отслеживать
#устанавливаемые npm зависимости вроде grunt и его плагинов
#по аналогии с bower.json (см. предыдущую статью).
npm init
#Установка grunt как зависимости этапа разработки в ваш проект
#(вновь см. предыдущую статью). Однако для настройки grunt
#нам все равно понадобится gruntfile.js.
npm install grunt –save-dev
Конфигурация Grunt
1 Под этим термином подразумевается сокращение кода. — Прим. ред.
Конфигурационный файл Grunt — это просто JavaScript-файл с функцией-оболочкой, содержащей конфигурацию, список загружаемых плагинов и определение задачи (рис. 1).
Рис. 1. Конфигурационный файл Grunt
module.exports = function (grunt) {
// Откуда берется uglify? Устанавливается через:
// npm install grunt-contrib-uglify --save-dev
grunt.initConfig({
uglify: {
my_target: {
files: {
'dest/output.min.js': '*.js'
}
}
}
});
// Внимание: убедитесь, что вы загружаете свои задачи
// из установленных пакетов!
grunt.loadNpmTasks('grunt-contrib-uglify');
// Для запуска Grunt из командной строки без параметров
// нужно зарегистрировать задачу по умолчанию,
// поэтому используйте uglify
grunt.registerTask('default', ['uglify']);
// Вы можете включать собственный код прямо в свою задачу,
// а также использовать приведенные выше плагины
grunt.registerTask('customtask', function () {
console.log("\r\nRunning a custom task");
});
};
Вы можете запускать задачи на рис. 1 в командной строке простым вызовом:
#Отсутствие параметров означает выбор задачи "по умолчанию"
#(которой является uglify)
grunt
#Или указывать конкретную задачу по имени
grunt customtask
После этого вызова вы найдете минимизированный (uglified, или minified) и объединенный результат в wwwroot/output-min.js. Если вы пользовались минимизацией и объединением (bundling) в ASP.NET, то заметите, что этот процесс отличается: он не привязан к запуску вашего приложения или даже к компиляции, и существует намного больше вариантов, которые вы можете выбирать для задач вроде минимизации (uglifying). Лично я нахожу этот рабочий процесс более прямолинейным и понятным.
Задачи можно объединять в цепочки с помощью Grunt так, чтобы они зависели друг от друга. Эти задачи будут выполняться синхронно, так как сначала должна быть выполнена первая задача и только потом можно переходить к следующей:
#Указываем, что uglify должна выполняться первой,
#а затем concat. Поскольку grunt удаляет временные файлы,
#многие задачи должны ожидать выполнения предыдущих задач.
grunt.registerTask('default', ['uglify', 'concat']);
Установка и использование Gulp
Установка Gulp аналогична установке Grunt. Я расскажу о Gulp немного подробнее, но заметьте, что вы можете делать похожие вещи с помощью любого из инструментов; я просто не хочу слишком часто повторяться. Gulp можно устанавливать глобально (чтобы использовать по любому пути в системе) и локально в папку проекта (чтобы использовать в конкретном проекте конкретную версию). Глобально установленный Gulp будет передавать управление тому, который был установлен локально в ваш проект, если найдет его, тем самым сохраняя версию Gulp в проекте:
#Это делается лишь раз, чтобы глобально установить gulp
npm install –g gulp
#Запускается в папке вашего проекта один раз,
#чтобы создать package.json. Этот файл будет отслеживать
#устанавливаемые npm зависимости вроде gulp и его плагинов.
npm init
#Установка gulp как зависимости этапа разработки в ваш проект.
#Однако для настройки gulp нам понадобится gulpfile.js.
npm install gulp --save-dev
Конфигурация Gulp и API
Конфигурация Gulp значительно отличается от таковой для Grunt. Конфигурационный файл gulpfile.js, который обычно имеет структуру, показанную на рис. 2, содержит выражения require для загрузки плагинов и последующего определения задач. Заметьте, что здесь я не использую конфигурационные параметры JSON; вместо этого задачи управляются кодом.
Конфигурационный файл Grunt — это просто JavaScript-файл с функцией-оболочкой, содержащей конфигурацию, список загружаемых плагинов и определение задачи.
Рис. 2. Конфигурационный файл Gulp
// Все выражения require для загрузки
// ваших плагинов и самого gulp
var gulp = require('gulp');
var concat = require('gulp-concat');
// Собственная задача, запускается через gulp customtask
gulp.task('customtask', function(){
// Какая-то собственная задача
});
// Определяем задачу по умолчанию, запускается просто как gulp
gulp.task('default', function () {
gulp.src('./lib/scripts/*.js')
.pipe(concat('all-scripts.js'))
.pipe(gulp.dest('./wwwroot/scripts'));
});
Gulp работает, используя несколько ключевых API и концепций: src, dest, pipe, task и globs. Gulp.src API сообщает Gulp, какие файлы следует открыть для работы, и эти файлы потом, как правило, отправляются по конвейеру какой-то другой функции вместо создания временных файлов. Это главное отличие от Grunt. Ниже показано несколько базовых примеров gulp.src без конвейеризации результатов, о которой я расскажу позже. Этот API-вызов принимает glob как параметр. Glob — это, по сути, шаблон (нечто вроде регулярного выражения), который вы можете ввести, чтобы, например, указать путь к одному или более файлам (подробнее о globs см. в github.com/isaacs/node-glob):
#Tell gulp about some files to work with
gulp.src('./input1.js');
#This will represent every html file in the wwwroot folder
gulp.src('./wwwroot/*.html')
#You can specify multiple expressions
gulp.src(['./app/**/*.js', './app/*.css']
Dest (destination) API, как можно представить себе, указывает получателя (destination) и также принимает glob. Поскольку globs очень гибки для определения путей, вы можете осуществлять вывод либо в индивидуальные файлы, либо в папку:
#Сообщаем dest, что будем использовать файл в качестве вывода
gulp.dest ('./myfile.js');
#Или записывать результаты в папку
gulp.dest ('./wwwroot');
Задачи в Gulp — это просто код, который вы пишете для выполнения каких-либо операций. Формат задач весьма несложен, но задачи можно использовать несколькими способами. Самый прямолинейный способ — создание задачи по умолчанию и одной или более других задач:
gulp.task('customtask', function(){
// Какая-то собственная задача для выполнения/чтения файлов,
// добавления заголовков, конкатенации
});
gulp.task('default', function () {
// Некая задача по умолчанию, которая запускается
// при вызове gulp без параметров
});
Visual Studio обеспечивает поддержку Gulp и Grunt через Task Runner Explorer, который включен в Visual Studio 2015 и доступен как Visual Studio Extension.
Задачи могут выполняться параллельно или могут быть зависимы друг от друга. Если порядок выполнения задач не важен, их можно просто объединить в цепочку, например:
gulp.task('default', ['concatjs', 'compileLess'], function(){});
В этом примере определяется задача по умолчанию, которая ничего не делает, но запускает отдельные задачи для конкатенации JavaScript-файлов и компиляции файлов LESS (предполагая, что код был в задачах, перечисленных здесь). Если требования диктуют завершение одной задачи до выполнения другой, вам нужно сделать задачи зависимыми друг от друга, а затем запустить несколько задач. В следующем коде задача по умолчанию сначала ожидает завершения concat, которая в свою очередь ожидает завершения uglify:
gulp.task('default', ['concat']);
gulp.task('concat', ['uglify'], function(){
// Concat
});
gulp.task('uglify', function(){
// Uglify
});
Pipe API используется для конвейерной передачи результатов от одной функции другой, используя stream API из Node.js. Рабочий процесс обычно таков: чтение src, конвейеризация в task, а потом конвейеризация результатов в dest. Следующий полный пример gulpfile.js считывает все JavaScript-файлы в /scripts, соединяет их в один файл и записывает вывод в другую папку:
// Определяем плагины – сначала надо выполнить
// npm install gulp-concat --save-dev
var gulp = require('gulp');
var concat = require('gulp-concat');
gulp.task('default', function () {
#Получаем все файлы .js в /scripts,
#соединяем их и пишем в папку
gulp.src('./lib/scripts/*.js')
.pipe(concat('all-scripts.js'))
.pipe(gulp.dest('./wwwroot/scripts'));
}
Возьмем практический пример. Вам будет часто требоваться объединять файлы и/или добавлять дополнительные заголовки в файлы исходного кода. Это можно легко делать в несколько этапов, добавив пару файлов в корень вашего веб-сайта (то же самое можно сделать в коде — см. документацию на gulp-header). Сначала создайте файл copyright.txt, содержащий добавляемые заголовки:
/*
MyWebSite Version <%= version %>
https://twitter.com/adamtuliper
Copyright 2015, licensing, etc
*/
Затем создайте файл version.txt, содержащий номер текущей версии (существуют плагины вроде gulp-bump и grunt-bump, которые увеличивают номера версий):
1.0.0
Теперь установите плагины gulp-header и gulp-concat в корень своего проекта:
npm install gulp-concat gulp-header --save-dev
В качестве альтернативы можно вручную добавить их в файл package.json и позволить Visual Studio выполнить восстановление пакетов за вас.
Наконец, вам нужен файл gulpfile.js, чтобы сообщить Gulp, что именно делать, как показано на рис. 3. Если вы не хотите объединять все свои скрипты и вместо этого предпочитаете просто добавить заголовки в каждый файл, закомментируйте строку pipe(concat). Ничего сложного, правда?
Рис. 3. Gulpfile.js
var gulp = require('gulp');
var fs = require('fs');
var concat = require("gulp-concat");
var header = require("gulp-header");
// Считываем файлы *.js, объединяем в один файл,
// записываем заголовки, выводим в /processed
gulp.task('concat-header', function () {
var appVersion = fs.readFileSync('version.txt');
var copyright =fs.readFileSync('copyright.txt');
gulp.src('./scripts/*.js')
.pipe(concat('all-scripts.js'))
.pipe(header(copyright, {version: appVersion}))
.pipe(gulp.dest('./scripts/processed'));
});
Затем вы можете запустить задачу приведенной ниже командой, и вуаля — вы объединили все файлы .js, добавили пользовательский заголовок и записали вывод в папку ./scripts/processed:
gulp concat-header
Task Runner Explorer
Visual Studio обеспечивает поддержку Gulp и Grunt через Task Runner Explorer, который включен в Visual Studio 2015 и доступен как Visual Studio Extension. Вы найдете его в View | Other Windows | Task Runner Explorer. Task Runner Explorer весьма впечатляет, так как он распознает, есть ли в вашем проекте gulpfile.js или gruntfile.js, разбирает задачи и предоставляет UI для выполнения найденных задач (рис. 4). Более того, вы можете определять задачи для запуска, когда в вашем проекте происходят заранее определенные операции, о чем я расскажу далее, поскольку ASP.NET 5 использует эту функциональность в своих шаблонах по умолчанию. Просто щелкните правой кнопкой мыши задачу, чтобы выполнить ее или связать с определенной операцией, например для запуска при открытии проекта.
Шаблоны ASP.NET 5, включенные в Visual Studio 2015, используют Gulp, и они устанавливают Gulp в папку node_components вашего проекта, так что все это будет заранее готово для применения в проекте.
Рис. 4. Task Runner Explorer показывает задачи Grunt и Gulp с параметрами
Как иллюстрирует рис. 5, Grunt выводит параметры, когда вы выбираете какую-либо задачу Grunt, которую вы не увидите в случае Gulp, в частности параметры Force, чтобы игнорировать предупреждения, и Verbose (буквы «F» и «V» слева вверху). Это просто параметры, передаваемые в командную строку Grunt. Замечательное качество Task Runner Explorer в том, что он показывает команды, передаваемые им в командную строку Grunt или Gulp (в окне вывода задачи), поэтому вам не придется гадать, что происходит «за кулисами».
Рис. 5. Дополнительные команды Grunt и командная строка
Gulp и ASP.NET 5
Шаблоны ASP.NET 5, включенные в Visual Studio 2015, используют Gulp, и они устанавливают Gulp в папку node_components вашего проекта, так что все это будет заранее готово для применения в проекте. Конечно, вы можете по-прежнему пользоваться Grunt в проекте ASP.NET 5; просто вам придется установить его в проект с помощью npm или добавлением в packages.json в раздел devDependencies с последующим автоматическим восстановлением пакетов в Visual Studio. Хочу подчеркнуть: все это делается в командной строке или в самой Visual Studio — выбор за вами.
На момент написания этой статьи текущие шаблоны ASP.NET 5 включают пару задач для минимизации и конкатенации файлов .css и .js. В предыдущих версиях ASP.NET эти задачи обрабатывались в скомпилированном коде в период выполнения, т. е. когда эти задачи запускать совершенно не нужно. Как видно на рис. 6, задачи с именами clean и min вызывают свои методы css и js для минимизации этих файлов или очистки ранее минимизированных файлов.
Рис. 6. Готовые задачи в шаблонах предварительной версии ASP.NET 5
Следующая строка в gruntfile.js демонстрирует другой пример одновременного запуска нескольких задач:
gulp.task("clean", ["clean:js", "clean:css"]);
У вас есть возможность связывать задачи Grunt и Gulp с четырьмя разными действиями в Visual Studio. В случае MSBuild для выполнения различных задач часто определяли командную строку для обработки перед компиляцией и после нее. При использовании Task Runner Explorer вы можете определить события Before Build, After Build, Clean и Project Open, чтобы выполнять такие задачи. При этом в файл gulpfile.js или gruntfile.js просто добавляются комментарии, которые не влияют на выполнение, но распознаются Task Runner Explorer. Чтобы увидеть привязку clean в gulpfile.js из ASP.NET 5, взгляните на следующую строку в начале этого файла:
// <binding Clean='clean' />
И это все, что требуется для подключения события.
Заключение
Grunt и Gulp — отличное пополнение в вашем арсенале средств веб-разработки. Оба инструмента прекрасно поддерживаются и имеют колоссальную экосистему плагинов. Любой веб-проект может выиграть от того, что они предлагают. За более подробной информацией обратитесь к следующим ресурсам:
- документации Grunt «Getting started» (bit.ly/1dvvDWq);
- инструкциям по Gulp (bit.ly/1L8SkUC);
- «Package Management and Workflow Automation: Machine Package Managers» (bit.ly/1FLwGW8);
- «Mastering Node.js Modules and Packages» (bit.ly/1N8UKon);
- блогу Adam’s Garage (bit.ly/1NSAYxK).
Адам Тьюлипер (Adam Tuliper) — старший идеолог Microsoft, живет в солнечной Калифорнии. Занимается веб-разработкой, созданием игр, является автором на Pluralsight и интересуется разнообразной техникой. С ним можно связаться через Twitter (@AdamTuliper) или через блог Adam’s Garage (bit.ly/1NSAYxK).
Выражаю благодарность за рецензирование статьи эксперту Microsoft Майклу Палермо (Michael Palermo).