Инкрементальная сборка без глотка

В моем офисе мы используем gulp для создания меньшего количества файлов. Я хотел улучшить задачу сборки, так как сборка большого проекта, над которым мы недавно работали, занимала больше секунды. Идея заключалась в том, чтобы кэшировать файлы и передавать только тот, который изменился. Поэтому я начал с google и нашел инкрементные сборки для javascript и подумал, что будет легко переписать их за меньшие деньги. Вот тот, с которого я начал: https://github.com/gulpjs/gulp/blob/master/docs/recipes/incremental-builds-with-concatenate.md

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

var gulp            = require('gulp');
var less            = require('gulp-less');
var concat          = require('gulp-concat');
var remember        = require('gulp-remember');
var cached          = require('gulp-cached');

var fileGlob = [
    './bootstrap/**/*.less',
    '!./bootstrap/bootstrap.less',
    '!./bootstrap/mixins.less'
];

gulp.task('less', function () {
    return gulp.src(fileGlob)
        .pipe(cached('lessFiles'))
        .pipe(remember('lessFiles'))
        .pipe(less())
        .pipe(gulp.dest('output'));
});

gulp.task('watch', function () {
    var watcher = gulp.watch(fileGlob, ['less']);
    watcher.on('change', function (e) {
        if (e.type === 'deleted') {
            delete cached.caches.scripts[e.path];
            remember.forget('lessFiles', e.path);
        }
    });
});

Но это передает только измененный файл, и компилятор less дает сбой из-за отсутствия определений переменных. Если я передаю плагин concat перед задачей less, gulp застревает (на первый взгляд) в бесконечном цикле.

gulp.task('less', function () {
    return gulp.src(fileGlob)
        .pipe(cached('lessFiles'))
        .pipe(remember('lessFiles'))
        .pipe(concat('main.less')
        .pipe(less())
        .pipe(gulp.dest('output'));
});

Есть ли у кого-нибудь опыт работы с этими плагинами или ему удалось создать инкрементную сборку less другим способом. Вот (беспорядочный) репозиторий github для тестирования: https://github.com/tuelsch/perfect-less-build

PS: я планирую добавить linting, sourcemaps, minification, evtl. очистка кеша и автопрефикс позже.


person Philipp Gfeller    schedule 29.10.2014    source источник
comment
Я исследовал точно такое же дело. К сожалению, похоже, что нет удобного решения. Однако я нашел статью, посвященную этой теме (хотя она все еще бесполезна): io.pellucid.com/blog/   -  person nirazul    schedule 30.10.2014
comment
Копаясь, я наткнулся на брокколи (solitr.com/blog/ 2014/02/broccoli-first-release), еще один таск-раннер. Это все еще молодой проект, но они, кажется, реализуют вышеупомянутую идею как основную функцию. Буду следить за этим.   -  person Philipp Gfeller    schedule 30.10.2014
comment
cached нужно? Заставляет ли удаление его из конвейера работать? Они работают на меня, но я не знаю, делаю ли я то же самое, что и вы. Не могли бы вы предоставить шаг для дублирования ваших ошибок?   -  person pgreen2    schedule 21.12.2014


Ответы (3)


Как и Эшвелл, я счел полезным использовать импорт, чтобы убедиться, что все мои файлы LESS имеют доступ к переменным и миксинам, которые им нужны. Я также использую файл LESS с импортом для целей объединения. Это имеет несколько преимуществ:

  1. Я могу использовать функции LESS для выполнения сложных задач, таких как переопределение значений переменных для создания нескольких тем или добавление класса перед каждым правилом в другом файле LESS.
  2. Плагин concat не нужен.
  3. Такие инструменты, как Web Essentials для Visual Studio, могут предоставить справку по синтаксису и предварительный просмотр выходных данных, поскольку каждый файл LESS полностью способен обрабатываться сам по себе.

Если вы хотите импортировать переменные, примеси и т. д., но не хотите на самом деле выводить все содержимое другого файла, вы можете использовать:

@import (reference) "_colors.less";

После нескольких дней усилий я, наконец, смог получить инкрементную сборку, которая правильно перестраивает все объекты, зависящие от измененного мной LESS-файла. Я задокументировал результаты здесь. Это последний глоток:

/*
 * This file defines how our static resources get built.
 * From the StaticCommon root folder, call "gulp" to compile all generated
 * client-side resources, or call "gulp watch" to keep checking source 
 * files, and rebuild them whenever they are changed. Call "gulp live" to 
 * do both (build and watch).
 */

/* Dependency definitions: in order to avoid forcing everyone to have 
 * node/npm installed on their systems, we are including all of the 
 * necessary dependencies in the node_modules folder. To install new ones,
 * you must install nodejs on your machine, and use the "npm install XXX" 
 * command. */
var gulp = require('gulp');
var less = require('gulp-less');
var LessPluginCleanCss = require('less-plugin-clean-css'),
    cleanCss = new LessPluginCleanCss();
var sourcemaps = require('gulp-sourcemaps');
var rename = require('gulp-rename');
var cache = require('gulp-cached');
var progeny = require('gulp-progeny');
var filter = require('gulp-filter');
var plumber = require('gulp-plumber');
var debug = require('gulp-debug');

gulp.task('less', function() {
    return gulp
        // Even though some of our LESS files are just references, and 
        // aren't built, we need to start by looking at all of them because 
        // if any of them change, we may need to rebuild other less files.
        .src(
        ['Content/@(Theme|Areas|Css)/**/*.less'],
        { base: 'Content' })
        // This makes it so that errors are output to the console rather 
        // than silently crashing the app.
        .pipe(plumber({
            errorHandler: function (err) {
                console.log(err);
                // And this makes it so "watch" can continue after an error.
                this.emit('end');
            }
        }))
        // When running in "watch" mode, the contents of these files will 
        // be kept in an in-memory cache, and after the initial hit, we'll
        // only rebuild when file contents change.
        .pipe(cache('less'))
        // This will build a dependency tree based on any @import 
        // statements found by the given REGEX. If you change one file,
        // we'll rebuild any other files that reference it.
        .pipe(progeny({
            regexp: /^\s*@import\s*(?:\(\w+\)\s*)?['"]([^'"]+)['"]/
        }))
        // Now that we've set up the dependency tree, we can filter out 
        // any files whose
        // file names start with an underscore (_)
        .pipe(filter(['**/*.less', '!**/_*.less']))
        // This will output the name of each LESS file that we're about 
        // to rebuild.
        .pipe(debug({ title: 'LESS' }))
        // This starts capturing the line-numbers as we transform these 
        // files, allowing us to output a source map for each LESS file 
        // in the final stages.
        // Browsers like Chrome can pick up those source maps and show you 
        // the actual LESS source line that a given rule came from, 
        // despite the source file's being transformed and minified.
        .pipe(sourcemaps.init())
        // Run the transformation from LESS to CSS
        .pipe(less({
            // Minify the CSS to get rid of extra space and most CSS
            // comments.
            plugins: [cleanCss]
        }))
        // We need a reliable way to indicate that the file was built
        // with gulp, so we can ignore it in Mercurial commits.
        // Lots of css libraries get distributed as .min.css files, so
        // we don't want to exclude that pattern. Let's try .opt.css 
        // instead.
        .pipe(rename(function(path) {
            path.extname = ".opt.css";
        }))
        // Now that we've captured all of our sourcemap mappings, add
        // the source map comment at the bottom of each minified CSS 
        // file, and output the *.css.map file to the same folder as 
        // the original file.
        .pipe(sourcemaps.write('.'))
        // Write all these generated files back to the Content folder.
        .pipe(gulp.dest('Content'));
});

// Keep an eye on any LESS files, and if they change then invoke the 
// 'less' task.
gulp.task('watch', function() {
    return gulp.watch('Content/@(Theme|Areas|Css)/**/*.less', ['less']);
});

// Build things first, then keep a watch on any changed files.
gulp.task('live', ['less', 'watch']);

// This is the task that's run when you run "gulp" without any arguments.
gulp.task('default', ['less']);

Теперь мы можем просто запустить gulp live для сборки всех наших LESS-файлов один раз, а затем позволить каждому последующему изменению создавать только те файлы, которые зависят от измененных файлов.

person StriplingWarrior    schedule 05.02.2015
comment
Это потрясающе, потомство и использование импорта в каждом файле кажутся теми частями, которых мне не хватало. Позвольте мне запустить несколько тестовых сборок, прежде чем принять это как правильный ответ. - person Philipp Gfeller; 09.02.2015
comment
@phippu: Рад, что мои усилия могут помочь другим. Не стесняйтесь обращаться ко мне, если вы столкнетесь с какими-либо препятствиями. - person StriplingWarrior; 09.02.2015
comment
@phippu: я обновил свой пост в блоге и приведенный выше код некоторыми дополнительными строками обработки ошибок, необходимыми для поддержания работы watch после возникновения ошибки. - person StriplingWarrior; 10.02.2015
comment
Я обновил git (github.com/tuelsch/perfect-less-build) с помощью рабочий пример инкрементных сборок благодаря include и progeny. Хотя я не могу заставить автопрефикс меньше расширений и чистый css работать правильно. Я получаю сообщение об ошибке Object has no method run. - person Philipp Gfeller; 10.02.2015
comment
@StriplingWarrior это работает нормально, но когда вы останавливаете задачу gulp и запускаете ее снова, она снова компилирует все меньше файлов. - person classydraught; 09.02.2021
comment
@classydraught: Да, с момента моего последнего редактирования (на сегодняшний день) прошло шесть лет, и я как бы перешел на другие технологии. Если у вас есть решение, которое выясняет, как кэшировать результаты между запусками, не стесняйтесь добавлять свой собственный ответ. :-) - person StriplingWarrior; 09.02.2021
comment
@StriplingWarrior Конечно, так и будет :) Также не могли бы вы сообщить мне, какую технологию вы используете. - person classydraught; 10.02.2021
comment
Сейчас мы используем Angular, у которого есть собственная система сборки. Если бы я использовал фреймворк без собственной системы сборки, я бы сегодня выбрал Webpack. - person StriplingWarrior; 10.02.2021

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

// Create a function that does just the processing
var runCompile = function( src, dest, opts ){
  return gulp.src( src )
    .pipe(less( opts ))
    .pipe(gulp.dest( dest ));
};

// Leverage the function to create the task
gulp.task( 'less', function(){
  return runCompile( fileGlob, 'output', {} );
});

// Use it again in the watch task
gulp.task( 'less:watch', function(){
  return gulp.watch( fileGlob )
    .on( "change", function( event ){
      // might need to play with the dest dir here
      return runCompile( event.path, 'output', {} );
    });
});

Это отлично работает для меня, и я использую этот шаблон во всех своих задачах с глотком. Однако я заметил, что иногда gulp будет раздавливать пути во время просмотра «при изменении», если он получает один файл. В этом случае я сам манипулирую путем, что-то вроде path.dirname(srcPath.replace( srcDir, outputDir )) в качестве аргумента dest для функции runCompile.

Изменить: только что понял, что это, вероятно, не решит вашу проблему «потерянных переменных». У меня нет ничего на уме, чтобы решить эту проблему, поскольку я организую свои файлы LESS с интенсивным использованием импорта, поэтому каждый файл, которому потребуется набор переменных, будет иметь оператор импорта, гарантирующий, что они там.

person ashwell    schedule 24.01.2015

На самом деле мы можем использовать gulp-newer и gulp-progeny-mtime для этой задачи. Подход Стриплинга является почти лучшим, ожидайте, что каждый раз, когда вы запускаете задачу без глотка, он будет снова компилировать все с нуля, а затем начнет просматривать файлы. Это будет стоить вам много времени, если вы работаете с меньшим количеством таблиц стилей. gulp-progeny-mtime похож на gulp-progeny, за исключением того, что он делает настоящие хардкорные вещи. Каждый раз, когда файлы проходят через gulp-progeny-mtime, он проверяет любые изменения в импорте, и если это так, он корректирует mtime текущего файла в потоке, что приводит к его прохождению через >gulp-новее. Я чувствую, что это лучше, потому что мы даже ничего не кешируем.

   //Compile less for deployment 
   gulp.task("less", () => {
      return gulp
        .src(["static/less/**/*.less"])
        .pipe(progenyMtime())
        .pipe(
          plumber({
            errorHandler: function (err) {
              log(chalk.bgRed.white.bold(err.message));
            },
          })
        )
        .pipe(filter(["**/*.less", "!**/_*.less", "!static/less/includes*/**"]))
        .pipe(newer({ dest: "static/css/", ext: ".css" }))
        .pipe(debug({ title: "LESS" }))
        .pipe(
          less({
            plugins: [cleanCss, autoprefix],
          })
        )
        .pipe(gulp.dest("static/css/"));
    });

    //Watch changes is less and compile if changed.
    gulp.task("watch-less", () => {
      return gulp.watch("static/less/**/*.less", gulp.series("less"));
    });
    
    //Compile all less files on first run ( if changed ) then compile only modified files from next run
    gulp.task("live-less", gulp.series("less", "watch-less"));
person classydraught    schedule 10.02.2021
comment
Это довольно умно. Кроме того, этот вопрос действительно старый, и у меня больше нет возможности его проверить. - person Philipp Gfeller; 10.02.2021
comment
@PhilippGfeller Спасибо :), в моем случае это отлично работает. - person classydraught; 10.02.2021