增量式的gulp less构建

15
在我的办公室里,我们使用gulp来构建less文件。我想改进构建任务,因为在最近处理的一个大型项目上,构建任务需要超过一秒钟的时间。我的想法是缓存文件并只传递已更改的文件。于是我开始在Google上查找JavaScript的增量构建,并认为重写它们用于Less应该很容易。下面是我开始使用的一个例子:https://github.com/gulpjs/gulp/blob/master/docs/recipes/incremental-builds-with-concatenate.md
经过几次不成功的尝试,我最终得到了以下代码(使用最新的Bootstrap分发进行测试):
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 编译器失败。如果我在 less 任务之前使用 concat 插件进行管道处理,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:我计划稍后添加代码检查、源映射、压缩、evt.缓存破坏和自动前缀。


1
我正在调查完全相同的事情。不幸的是,似乎没有方便的解决方案。 但是我找到了一篇涵盖该主题的文章(尽管仍然无济于事): http://io.pellucid.com/blog/tips-and-tricks-for-faster-front-end-builds - nirazul
在搜寻过程中,我偶然发现了Broccoli(http://www.solitr.com/blog/2014/02/broccoli-first-release/),又一个“任务运行器”。虽然这是一个年轻的项目,但他们似乎将上述想法作为核心特性来实现。我会继续关注这个项目的进展。 - Philipp Gfeller
“cached” 是否必要?将其从流水线中移除会使事情正常吗?对我而言,它们是有效的,但我不知道我是否在做与您相同的事情。您能否提供重现您错误的步骤? - pgreen2
3个回答

20
像Ashwell一样,我发现使用导入可以确保所有的LESS文件都可以访问它们需要的变量和混合。我还使用一个带有导入的LESS文件进行捆绑。这有一些优点:
  1. 我可以利用LESS的功能来完成复杂任务,比如覆盖变量值以生成多个主题,或在另一个LESS文件中的每个规则前面添加一个类。
  2. 不需要concat插件。
  3. 像Visual Studio的Web Essentials这样的工具可以提供语法帮助和输出预览,因为每个LESS文件都可以完全自行渲染。
当您想要导入变量、混合等,但又不想实际输出另一个文件的全部内容时,可以使用:
@import (reference) "_colors.less";

经过几天的努力,我终于能够获得一个增量构建,它正确地重新构建了所有依赖于我更改的LESS文件的对象。我在这里记录了结果(链接)。这是最终的gulpfile:

/*
 * 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文件,然后允许每个后续更改只构建依赖于已更改文件的文件。

1
这太棒了,子孙和在每个文件中使用导入似乎是我缺失的部分。在接受这个答案之前,让我运行几个测试构建。 - Philipp Gfeller
1
@phippu:我在我的博客文章和上面的代码中更新了一些额外的错误处理行,以便在watch遇到错误后仍然能够正常工作。 - StriplingWarrior
1
@StriplingWarrior 这个可以正常工作,但是当你停止 gulp 任务并重新启动它时,它会再次编译所有的 less 文件。 - classydraught
1
@classydraught:是的,距离我上次编辑已经六年了(就在今天),而且我已经转向其他技术了。如果你有一个解决方案可以在运行之间缓存结果,请随意添加你自己的答案。 :-) - StriplingWarrior
1
@StriplingWarrior 当然会做的 :) 另外,您能告诉我您正在使用哪种技术吗? - classydraught
显示剩余3条评论

2

当我想在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任务中经常使用这种模式。但是我注意到有时候gulp会在监视“更改”期间压缩路径,如果它得到一个单文件。在这种情况下,我自己进行路径操作,类似于将path.dirname(srcPath.replace(srcDir,outputDir))作为runCompile函数的dest参数。
编辑:刚才才意识到这可能无法解决你的“丢失变量”的问题。我没有头脑中首先想到的东西来解决这个问题,因为我使用了大量的导入来组织我的LESS文件,所以需要一组变量的每个文件都有一个导入语句,确保它们存在。

1
你也可以升级并使用类似于 LazyPipe 的东西,https://www.npmjs.com/package/lazypipe - ashwell
谢谢你的建议,但我不明白如何只编译更改的文件并仍然得到一个单一的CSS文件。虽然我喜欢使用导入的想法。 - Philipp Gfeller
是的,我很抱歉,我应该更仔细地阅读问题。 - ashwell

1
我们实际上可以使用 gulp-newergulp-progeny-mtime 来完成这个任务。Stripling 的方法几乎是最好的,但每次运行 gulp less 任务时,它都会从头开始编译所有内容,然后开始监视文件。如果您正在使用大量 less 样式表,这将浪费很多时间。 gulp-progeny-mtime 类似于 gulp-progeny,但它做了真正的核心工作。每当一个文件通过 gulp-progeny-mtime 时,它都会检查导入文件是否有任何修改,如果有,则会调整流中当前文件的 mtime,从而使其通过 gulp-newer。我认为这更好,因为我们甚至没有缓存任何东西。
   //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"));

1
这非常聪明。此外,这个问题真的很老了,我已经没有测试它的设置了。 - Philipp Gfeller

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接