Ноябрь 2016
Том 31 номер 11
Работающий программист - MEAN: исследуем Gulp
Тэд Ньюард | Ноябрь 2016
С возвращением, дорогие «министы».
Если вы читали мою статью в прошлом номере, где рассказывалось о «перезагрузке» кодовой базы (с помощью Yeoman для генерации базового шаблонного кода и «склеивающего» кода), то, вероятно, заметили, что в нашу смесь попал новый инструмент без особых пояснений. Я, конечно, имею в виду утилиту Gulp, которая использовалась, чтобы запустить сервер и открыть в клиентском браузере сгенерированный каркас клиентского приложения (msdn.com/magazine/mt742874).
Если вы пропустили ту статью, наверстать упущенное несложно. Сначала убедитесь, что Yeoman и генераторы angular-fullstack установлены в вашей среде разработки Node.js (наряду с локальной копией MongoDB):
npm install –g yeoman angular-fullstack-generator
yo angular-fullstack
Потом, ответив на вопросы по поводу того, какие средства Yeoman должен задействовать (в этой рубрике выбор делался по большей части произвольно), и после того, как Yeoman выполнил npm install для получения всех зависимостей для исполняющей среды и разработки, генератор сообщает о готовности приложения к проверке командами gulp test или gulp start:server.
Очевидно, что бы ни представлял собой Gulp, это своего рода инструмент сборки, похожий по своему духу на Make, MSBuild или Ant. Но работает он несколько иначе, чем любой из этих трех инструментов, а потому заслуживает отдельного обсуждения.
Немного Gulp для начала
Хотя называть Gulp инструментом сборки для языка, который на самом деле не компилируется, не совсем правильно (вспомните: ECMAScript в целом является интерпретируемым языком), это все же лучший термин из того, что у нас есть, для инструмента, предназначенного для запуска после (или во время) разработки и позволяющего убедиться, что все выправлено и готово к работе. Возможно, было бы лучше называть все такие средства инструментами автоматизации разработки, поскольку так точнее. Однако, раз уж к нему приклеился такой термин, давайте пока продолжим называть Gulp инструментом сборки.
Чтобы приступить к работе с Gulp, давайте удалимся от сгенерированного шаблонного кода (scaffolded code) и начнем все с начала, сосредоточившись на том, что и как делает Gulp. Установите глобальные средства командной строки Gulp (командой npm install --g gulp-cli). Затем в новом каталоге создайте пустой проект Node.js, выполнив следующие команды:
npm init
npm install --save-dev gulp
Благодаря этим командам на Gulp появится ссылка в файле package.json как на зависимость разработчика (developer dependency), так что, если вы извлечете проект, вам не придется помнить о предварительной установке Gulp — он просто будет в вашем распоряжении при следующей команде вида npm install в новой среде.
Теперь в своем любимом редакторе кода создайте новый файл, назвав его gulpfile.js, со следующим содержимым:
const gulp = require('gulp');
gulp.task('default', function() {
console.log("Gulp is running!");
});
Затем из того же каталога введите стандартную команду Gulp, которая (по-видимому, не удивительно) представляет собой просто gulp. Gulp подумает около секунды и выдаст следующее:
{Для верстки: в самом конце листинга есть греческая буква "мю"}
[18:09:38] Using gulpfile ~/Projects/code/gulpdemo/gulpfile.js
[18:09:38] Starting 'default'...
Gulp is running!
[18:09:38] Finished 'default' after 142 μs
Неплохо. Кажется перебором на данный момент, но неплохо.
Решаем некоторые задачи с помощью Gulp
Gulp, как и большинство инструментов сборки, оперирует понятием «задачи» (tasks) и, что важнее, тем, как отслеживать зависимости между этими задачами. Поэтому здесь в простом gulpfile он видит только одну задачу с именем default (по соглашению, эту задачу следует выполнять, если никаких других задач в командной строке не указывается) и выполняет тело связанного литерала функции. Назвать задачу по-другому тривиально:
const gulp = require('gulp');
gulp.task('echo', function() {
console.log("Gulp is running!");
});
Должно быть очевидно, что Gulp — это просто код, поэтому все, что можно сделать в коде, можно сделать и в теле задачи Gulp. Это открывает уйму невероятных возможностей вроде чтения из базы данных для нахождения элементов, которые должны быть сгенерированы кодом, взаимодействия с другими онлайновыми сервисами для конфигурирования или простого вывода текущей даты и времени:
const gulp = require('gulp');
gulp.task('echo', function() {
console.log("Gulp is running on " + (new Date()));
});
Это дает следующее:
{Для верстки: в самом конце листинга есть греческая буква "мю"}
Teds-MacBook-Pro:gulpdemo tedneward$ gulp echo
[18:16:24] Using gulpfile ~/Projects/code/gulpdemo/gulpfile.js
[18:16:24] Starting 'echo'...
Gulp is running on Wed Sep 14 2016 18:16:24 GMT-0700 (PDT)
[18:16:24] Finished 'echo' after 227 μs
Зависимые задачи просто перечисляются как строковый массив между именем основной задачи и телом литерала ее функции, так что задача echo, зависимая от другой задачи, выглядит так:
const gulp = require('gulp');
const child_process = require('child_process');
gulp.task('gen-date', function() {
child_process.exec('sh date > curdate.txt');
});
gulp.task('echo', ['clean', 'gen-date'], function() {
console.log("Gulp is running on " + (new Date()));
});
Здесь задача gen-date использует стандартный Node.js-пакет child_process, чтобы запустить внешнюю утилиту для записи даты в файл — просто для проверки того, что вы можете это сделать. Это, конечно, здорово, но, по правде говоря, обычно от средств сборки ожидают выполнения более серьезных задач, чем элементарный вывод в консоль и выяснения текущей даты и времени.
Еще немного Gulp
Давайте углубимся немного дальше. Создайте файл index.js со следующим ECMAScript-кодом в нем, включая неважнецкие части кода:
// index.js
function main(args) {
for (let arg in args) {
if (arg == "hello")
console.log("world!");
console.log("from index.js!");
}
}
console.log("Hello, from index.js!")
main()
Да, это несколько бессмысленно, и, да, в этом коде есть некоторые проблемы, но в том и суть: было бы неплохо, если бы был какой-то инструмент, который выявлял такие проблемы и сообщал о них. (Будет ли мне позволено сказать о «проверке на этапе компиляции»?) Такой инструмент существует в JSHint (jshint.com), но он устанавливается по умолчанию как утилита командной строки, и все время помнить, что каждый раз его надо запускать отдельно, очень быстро станет головной болью.
К счастью, именно для этого и предназначен инструмент сборки. Возвращаясь к gulpfile, давайте заставим Gulp запускать JSHint для каждого исходного файла (прямо сейчас у нас всего один такой файл), сообщая ему о нужных исходных файлах, а затем прося его выполнять JSHint для каждого из этих файлов:
// В этом файле он не нужен, но нам требуется его установка
require('jshint');
require('jshint-stylish');
const gulp = require('gulp');
const jshint = require('gulp-jshint');
gulp.task('jshint', function() {
return gulp.src('*.js')
.pipe(jshint())
.pipe(jshint.reporter('jshint-stylish'));
});
После выполнения этого кода утилита сообщит, что для всех файлов .js в текущем каталоге есть некоторые предложенные изменения, включая сам gulpfile. Вот это да! На самом деле нас не интересует проверка gulpfile, поэтому отсеем его из набора файлов:
gulp.task('jshint', function() {
return gulp.src(['*.js', '!gulpfile.js'])
.pipe(jshint())
.pipe(jshint.reporter('jshint-stylish'));
});
Это исключит gulpfile.js из текущего списка файлов до его передачи на следующую стадию конвейера. В действительности вы, по-видимому, захотите, чтобы большая часть кода находилась в каталоге src (или в каталогах server и client на соответствующих сторонах), так что добавьте эти и любые другие подкаталоги в список обрабатываемых файлов:
gulp.task('jshint', function() {
return gulp.src(['*.js', '!gulpfile.js', 'server/**/*.js', 'client/**/*.js'])
.pipe(jshint())
.pipe(jshint.reporter('jshint-stylish'));
});
Две звездочки в каждом пути инициируют рекурсивное поведение для подхвата любых файлов .js в каждом из этих подкаталогов.
На первый взгляд, это замечательно, но на самом деле вы по-прежнему много делаете сами: все так же вручную вводите gulp jshint (или просто gulp, если связали его с задачей default как зависимость) всякий раз, когда хотите увидеть, что нуждается в исправлении. А почему бы не заставить его выполняться при каждом внесении изменений в код, как это делается во всех IDE?
Разумеется, это возможно, как показано на рис. 1.
Рис. 1. Постоянная автоматизация с помощью Gulp
// В этом файле он не нужен, но нам требуется его установка
require('jshint');
const gulp = require('gulp');
const jshint = require('gulp-jshint');
gulp.task('default', ['watch']);
gulp.task('watch', function() {
gulp.watch(['*.js', '!gulpfile.js', 'client/**/*.js', 'server/**/*.js'],
['jshint']);
});
gulp.task('jshint', function() {
return gulp.src(['*.js', '!gulpfile.js', 'client/**/*.js', 'server/**/*.js'])
.pipe(jshint())
.pipe(jshint.reporter('default'));
});
Теперь, когда вы запускаете команду gulp, командная строка просто «ставится на паузу» и ждет. Сейчас Gulp находится в режиме «наблюдения», в котором он отслеживает любые файлы, передаваемые в вызове gulp.watch, и, если какой-то из этих файлов изменяется (подразумевается, что они сохраняются, поскольку Gulp, увы, не в состоянии влезть в текстовые редакторы), он немедленно запускает задачу jshint применительно ко всему набору файлов. А потом опять переходит в режим наблюдения.
И еще
Один из ключей к пониманию того, как задачи Gulp обрабатывают файлы, скрыт в вызове канала (pipe). Gulp «мыслит» в терминах потоков данных, а не задач или файлов. Например, простая задача Gulp, которая скопировала бы файлы из каталога src в каталог dest, выглядела бы как-то так:
gulp.task('copy-files', function() {
gulp.src('source/folder/**')
.pipe( gulp.dest('dest/folder/**') );
});
По сути, каждый файл подхватывается gulp.src и помещается в неизменном виде в каталог-приемник, указанный gulp.dest. Все, что должно произойти с этими файлами, просто выполняется как стадия конвейера, и каждый файл проходит эту стадию до того, как попадет на следующую стадию конвейера. Это архитектурный стиль каналов и фильтров Unix, привнесенный экосистемой Node.js невероятно элегантным образом. Windows PowerShell опирается на тот же тип архитектуры.
Поэтому, например, если вы хотите просто увидеть, как Gulp обращается с каждым файлом на конвейере, есть плагин для этого (gulp-filelogger), и он выводит в консоль информацию о каждом таком файле:
gulp.task('copy-files', function () {
gulp.src(srcFiles)
.pipe(filelogger())
.pipe(gulp.dest(destDir));
});
Это дает следующий результат:
Teds-MacBook-Pro:gulpdemo tedneward$ gulp copy-files
[20:14:01] Using gulpfile ~/Projects/code/gulpdemo/gulpfile.js
[20:14:01] Starting 'copy-files'...
[20:14:01] Finished 'copy-files' after 14 ms
[20:14:01] [/Users/tedneward/Projects/code/gulpdemo/index.js]
Teds-MacBook-Pro:gulpdemo tedneward$
Обратите внимание на то, как появляется вывод после того, как Gulp сообщает о завершении работы. Gulp может большую часть времени обрабатывать эти потоки асинхронно (и делает это), сокращая время «сборки». В большинстве случаев разработчики либо не знают, либо не хотят знать то, что операции выполняются параллельно, но в моменты, когда выполнение в точной последовательности крайне важно, некоторые плагины от сообщества Gulp будут сериализовать выполнение и обеспечивать строгую последовательность операций. В Gulp 4.0 будет добавлено две новые функции, parallel и serial, чтобы внести больше ясности, но, поскольку эта версия пока не поставляется, вам придется обходиться тем, что есть.
Кстати, сам Gulp состоит исключительно из четырех функций, которые вы уже видели: gulp.task, gulp.watch, gulp.src и gulp.dest. Все остальное заключено в плагинах, npm-модулях или пишется самостоятельно. Это само по себе делает Gulp чрезвычайно простым в понимании. Я бы даже сказал так: простым до тоски для авторов статей, которым платят по количеству слов в тексте.
И все разом
Сам по себе Gulp совсем не сложный инструмент, но как и в случае любого инструмента такой природы, его реальная сила в огромном массиве плагинов и дополняющих инструментов, создаваемых его сообществом. Их полный список доступен на gulpjs.com/plugins, но рис. 2 демонстрирует репрезентативный пример рецепта Gulp, показывая, как автоматизировать выпуск проекта на GitHub, включая команды Git для помещения в главную ветвь (master).
Рис. 2. Один из рецептов Gulp
var gulp = require('gulp');
var runSequence = require('run-sequence');
var conventionalChangelog = require('gulp-conventional-changelog');
var conventionalGithubReleaser = require('conventional-github-releaser');
var bump = require('gulp-bump');
var gutil = require('gulp-util');
var git = require('gulp-git');
var fs = require('fs');
gulp.task('changelog', function () {
return gulp.src('CHANGELOG.md', {
buffer: false
})
.pipe(conventionalChangelog({
preset: 'angular' // или по другому соглашению коммита
}))
.pipe(gulp.dest('./'));
});
gulp.task('github-release', function(done) {
conventionalGithubReleaser({
type: "oauth",
token: '' // измените это на свой маркер в GitHub
}, {
preset: 'angular' // или по другому соглашению коммита
}, done);
});
gulp.task('bump-version', function () {
// "Зашиваем" в код тип изменения версии как "patch",
// но может быть неплохой идеей использование
// minimist (bit.ly/2cyPhfa) для определения с помощью
// аргумента команды, какой тип изменения вы вносите -
// "major", "minor" или "patch"
return gulp.src(['./bower.json', './package.json'])
.pipe(bump({type: "patch"}).on('error', gutil.log))
.pipe(gulp.dest('./'));
});
gulp.task('commit-changes', function () {
return gulp.src('.')
.pipe(git.add())
.pipe(git.commit('[Prerelease] Bumped version number'));
});
gulp.task('push-changes', function (cb) {
git.push('origin', 'master', cb);
});
gulp.task('create-new-tag', function (cb) {
var version = getPackageJsonVersion();
git.tag(version, 'Created Tag for version: ' + version, function (error) {
if (error) {
return cb(error);
}
git.push('origin', 'master', {args: '--tags'}, cb);
});
function getPackageJsonVersion () {
// Разбираем JSON-файл вместо использования require,
// поскольку require кеширует множество вызовов, из-за чего
// номер версии не будет обновляться
return JSON.parse(fs.readFileSync('./package.json', 'utf8')).version;
};
});
gulp.task('release', function (callback) {
runSequence(
'bump-version',
'changelog',
'commit-changes',
'push-changes',
'create-new-tag',
'github-release',
function (error) {
if (error) {
console.log(error.message);
} else {
console.log('RELEASE FINISHED SUCCESSFULLY');
}
callback(error);
});
});
Этот пример демонстрирует целый ряд вещей: как выполнять задачи в определенной последовательности, используя плагины Gulp для генерации файла с набором правил соглашения, выдачи сообщений о релизе в стиле GitHub и многого другого.
Заключение
В этой статье мы не писали никакого нового кода и, тем не менее, «перезагрузили» все приложение, получили уйму функциональности и, по сути, вывели на тот же уровень (и даже выше) приложение, которым занимались в течение года или около того. Вам понравится скаффолдинг!
Еще важнее, что создавая все части одна за другой вручную до применения скаффолдинга, становится гораздо проще понять код в целом и то, что и где в нем происходит. Так, содержимое routes.js напомнит вам содержимое таблицы маршрутизации (routing table), созданной вами ранее вручную, а содержимое package.json (в корне каталога проекта) будет больше, но его суть никак не изменится.
Фактически единственное, что здесь нового, помимо использования самого Yeoman, — это введение инструмента для сборки всех частей воедино, и это будет темой следующей статьи. Ну а тем временем… удачи в кодировании!
Тэд Ньюард (Ted Neward) — глава фирмы Neward & Associates, предоставляющей консалтинговые услуги по самым разнообразным технологиям. Автор и соавтор многочисленных книг, в том числе «Professional F# 2.0» (Wrox, 2010), более сотни статей, часто выступает на многих конференциях по всему миру; кроме того, имеет звание Microsoft MVP в области F#. С ним можно связаться по адресу ted@tedneward.com или почитать его блог blogs.tedneward.com.
Выражаю благодарность за рецензирование статьи эксперту Шону Уайлдермуту (Shawn Wildermuth).