Готовим ASP.NET 5: подробнее про работу с Gulp
Мы продолжаем нашу колонку по теме ASP.NET 5 публикацией от Дмитрия Сикорского (DmitrySikorsky) — разработчика из Украины. В этой статье Дмитрий подробнее рассказывает о сценариях применения с ASP.NET 5 популярного средства Gulp. Предыдущие статьи из колонки всегда можно прочитать по ссылке #aspnetcolumn — Владимир Юнев
До появления ASP.NET 5 я никогда не использовал такие инструменты, как Gulp, поэтому пришлось уделить некоторое время и разобраться, что же это такое, когда я создал свой первый проект на этой платформе (правда, тогда там еще был Grunt, но это не важно). Не стану вдаваться в базовые вещи, которые уже и так везде достаточно подробно описаны (подразумеваю, что в вашем проекте уже есть Gulpfile.js и вы можете выполнять задания из него, используя диспетчер выполнения задач Visual Studio 2015), а сразу перейду к делу и на практике покажу, как можно использовать Gulp для автоматизации всего-всего в вашем проекте на ASP.NET 5.
В статье будут приведены фрагменты файла Gulpfile.js тестового проекта AspNet5Gulpization, который целиком лежит тут: https://github.com/DmitrySikorsky/AspNet5Gulpization.
Вступление
Вы наверняка знаете, для чего используется новая папка wwwroot. На самом деле, с ее появлением я немного по-новому взглянул на скрипты, стили и картинки. А именно, как и серверный код сайта, теперь я разделяю их на исходники и готовые к публикации объекты.
Подготовка
Совет! Вы можете попробовать все самостоятельно или загрузив исходный код из GitHub https://github.com/DmitrySikorsky/AspNet5Gulpization.
Для начала, нам необходимо перечислить в нашем Gulp-файле все пакеты, которые мы будем использовать в своих заданиях (и убедиться, что все они присутствуют в package.json):
var gulp = require("gulp"),
autoprefixer = require("gulp-autoprefixer"),
concat = require("gulp-concat"),
del = require("del"),
minifyCss = require("gulp-minify-css"),
rename = require("gulp-rename"),
runSequence = require("run-sequence"),
sass = require("gulp-sass"),
tsc = require("gulp-tsc"),
uglify = require("gulp-uglify");
Далее, очень удобно представлять пути где лежат исходные и результирующие файлы в виде объекта, чтобы иметь возможность редактировать их все в одном месте:
var paths = {
frontend: {
scss: {
src: [
"styles/*.scss"
],
dest: "wwwroot/css"
},
ts: {
src: [
"scripts/*.ts"
],
dest: "wwwroot/js"
}
},
shared: {
bower: {
src: "bower_components",
dest: "wwwroot/lib"
}
}
}
И наконец, опишем основную Gulp-задачу, которая будет производить перестроение всех скриптов и стилей, их обработку и копирование в результирующую папку:
gulp.task(
"rebuild",
function (cb) {
runSequence(
"clean",
"build",
"minify",
"delete-unminified",
"rename-temp-minified",
"delete-temp-minified",
cb
);
}
);
Пакет runSequence позволяет выполнять задачи одну за другой, последовательно, что в нашем случае очень важно. Также, следует обязательно передавать функцию обратного вызова cb в качестве последней задачи последовательности, чтобы вызывающий код мог быть уведомленным о завершении процесса в целом.
Кстати, чтобы не выполнять задание rebuild вручную, можно либо заставить студию выполнять его при сборке или открытии проекта, либо прямо в Gulp-файле дописать еще одно задание (и выполнять его автоматически при открытии проекта), которое будет устанавливать слушателей на изменение исходных файлов и выполнять задание, когда такое изменение будет обнаружено.
Скрипты
Я очень любил (и продолжаю) JavaScript за его простоту и изящество. Теперь я полюбил еще и TypeScript. (Это замечательный инструмент, рекомендую обратить на него внимание.) Все ts-файлы я обычно храню в папке Scripts, которая игнорируется при публикации проекта. Это исходники клиентских скриптов. Я настроил несколько заданий в моем Gulp-файле, которые сначала компилируют TypeScript в JavaScript, затем сжимает его, затем склеивают в один файл и, наконец, копируют полученный файл в папку wwwroot/js, откуда он и используется в приложении. (Если вы не используете TypeScript, то можно просто пропустить этап его превращения в JavaScript — остальные задания будут работать без изменений.)
По умолчанию, Visual Studio 2015 компилирует ts-файлы в момент их сохранения и складывает полученные js-файлы в ту же папку. Нам это поведение не нужно, поэтому отключаем компиляцию TypeScript в настройках проекта.
Вот так выглядит задание, компилирующее TypeScript:
gulp.task(
"frontend:build-ts", function (cb) {
return gulp.src(paths.frontend.ts.src)
.pipe(tsc())
.pipe(gulp.dest(paths.frontend.ts.dest));
}
);
А вот так можно сжать полученный JavaScript и склеить его в один результирующий файл:
gulp.task(
"frontend:minify-js", function (cb) {
return gulp.src(paths.frontend.ts.dest + "/*.js")
.pipe(uglify())
.pipe(concat("aspnet5gulpization.min.js.temp"))
.pipe(gulp.dest(paths.frontend.ts.dest));
}
);
При склейке многих js-файлов в один возможна ситуация, когда они будут добавлены в результирующий файл в неправильном порядке и вы получите сообщения, что что-то не определено, т. к. определение будет расположено после вызова. Если так случится, можно легко задать порядок следования скриптов в результирующем файле вручную. Можно даже перечислить в необходимом порядке те скрипты, которые должны следовать первыми, а затем последним элементом массива просто указать ту же самую папку, и Gulp догадается, что нужно сначала обработать явно указанные файлы из папки, а затем все остальные.
Кстати, если вдруг при восстановлении пакетов NPM вы получаете сообщение об ошибке (опционально, с формулировкой, состоящей из бессвязной последовательности символов), или же Visual Studio 2015 просто падает при попытке восстановить пакеты, возможно это связано с глубиной вложенности вашего проекта в файловой системе. (Частично, я нашел информацию об этом тут: https://github.com/Microsoft/nodejstools/issues/336.) Потратив некоторое время, я просто создал пустой проект в менее вложенной папке, скопировал туда свой package.json, восстановил там пакеты и затем перенес их вместе с папкой node_modules в свой проект. Также, в процессе падения студии в папку npm-cache может попасть испорченный пакет, поэтому стоит иметь это в виду и при необходимости его оттуда удалить.
Стили
Относительно недавно я решил попробовать SCSS. Основной целью было иметь возможность редактировать такие вещи, как цвета, в одном месте, а не с использованием поиска и замены. Также я решил, что было бы здорово, заодно, разделять огромные css-файлы на части, чтобы их было удобнее сопровождать (на мой взгляд, сопровождать стили, избегая при этом их замусоривания, достаточно сложно, т. к. результат изменений сложно поддается тестированию и не всегда очевиден с первого взгляда). Использование SCSS дало хороший результат.
Аналогично TypeScript, мой Gulp-файл содержит задания для компиляции SCSS в CSS, добавления вендорных префиксов, сжатия, склейки и копирования полученного файла в папку wwwroot/css.
Компиляция SCSS в CSS выглядит следующим образом:
gulp.task(
"frontend:build-scss", function (cb) {
return gulp.src(paths.frontend.scss.src)
.pipe(sass())
.pipe(gulp.dest(paths.frontend.scss.dest));
}
);
Ну и сжатие, склейка (с одновременной расстановкой вендорных префиксов):
gulp.task(
"frontend:minify-css", function (cb) {
return gulp.src(paths.frontend.scss.dest + "/*.css")
.pipe(minifyCss())
.pipe(autoprefixer("last 2 version", "safari 5", "ie 8", "ie 9"))
.pipe(concat("aspnet5gulpization.min.css.temp"))
.pipe(gulp.dest(paths.frontend.scss.dest));
}
);
Библиотеки
Если в проекте на ASP.NET 5 вам потребуется, например, JQuery, скорее всего вы загрузите его при помощи Bower, и, в отличии от NuGet, который использовался ранее, вы получите немного больше, чем просто файл jquery.min.js и парочки других. В папке bower_components будет создана папка jquery, в которой, кроме упомянутого выше файла, будет несжатый вариант библиотеки, а также ее исходники (которые, само собой, при публикации будут игнорироваться).
Если задуматься, мы можем использовать эти библиотеки как минимум двумя способами.
Во-первых, можно просто подключить на страницу файл jquery.min.js, предварительно скопировав его в папку wwwroot/lib/jquery. Так поступил я (не знаю, возможно правильнее использовать сервисы, вроде Google Hosted Libraries, чтобы в некоторых случаях браузер брал закешированный при посещении другого сайта вариант библиотеки, а не скачивал ее вновь, но чаще всего я так не делаю).
Во-вторых, можно взять несжатый вариант библиотеки (jquery.js) и обработать его таким же образом, каким обрабатываются другие скрипты в приложении. Т. е. в результате добавить его в единственный общий js-файл, таким образом уменьшив количество запросов к веб-серверу.
Вот задание, копирующее необходимые файлы для трех библиотек:
gulp.task(
"lib-copy", function (cb) {
var lib = {
"/jquery": "/jquery/dist/jquery*.{js,map}",
"/jquery-validation": "/jquery-validation/dist/jquery.validate*.js",
"/jquery-validation-unobtrusive": "/jquery-validation-unobtrusive/jquery.validate.unobtrusive*.js"
};
for (var $package in lib) {
gulp
.src(paths.shared.bower.src + lib[$package])
.pipe(gulp.dest(paths.shared.bower.dest + $package));
}
cb();
}
);
Выводы
Мне нравится то, что в итоге получилось. Мне нравится, что можно легко и удобно работать со стилями и скриптами и так же легко и удобно оптимизировать их перед публикацией, обладая полным контролем над процессом. Это, конечно, не единственное, в чем может помочь Gulp, но думаю этого достаточно, чтобы полностью вникнуть в его возможности.
Авторам
Друзья, если вам интересно поддержать колонку своим собственным материалом, то прошу написать мне на vyunev@microsoft.com для того чтобы обсудить все детали. Мы разыскиваем авторов, которые могут интересно рассказать про ASP.NET и другие темы.
Об авторе
Дмитрий Сикорский,
Украина
DmitrySikorsky