Gulp 学习小结

gulp 任务怎么写

const gulp = require('gulp');

gulp.task('default', function() {
    gulp.src('js/*.js')
        .pipe(uglify())
        .pipe(gulp.dest('dist'));
});

注意:默认的,task 将以最大的并发数执行,gulp 会一次性运行所有的 task 并且不做任何等待。如果你想要创建一个序列化的 task 队列,并以特定的顺序执行,你需要做两件事:

  • 给出一个提示,来告知 task 什么时候执行完毕
  • 并且再给出一个提示,来告知一个 task 依赖另一个 task 的完成
var gulp = require('gulp');

// 返回一个 callback,因此系统可以知道它什么时候完成
gulp.task('one', function(cb) {
    // 做一些事 -- 异步的或者其他的
    cb(err); // 如果 err 不是 null 或 undefined,则会停止执行,且注意,这样代表执行失败了
    这里也可以使用 return 代替 cb()
});

// 定义一个所依赖的 task 必须在这个 task 执行之前完成
gulp.task('two', ['one'], function() {
    // 'one' 完成后
});

gulp.task('default', ['one', 'two']);

gulp 核心知识

个人认为了解 gulp 的核心知识,主要从三个方面来了解:

  • 文件系统
  • 任务管理

上面三点分别对应了gulp的 pipe(这样写不太严谨,因为pipe是流的api),src和task 这三个API,了解了这三部分,gulp的使用,插件的编写都会比较得心应手。

流(Stream)

说实话到现在也没有对流的所有知识十分了解,之前查阅了一些 node 相关的书,感觉很多书写的东西和例子都很浅,后来看了一些文章,算是了解了一点皮毛。这是网传的学习流必看的文章,有兴趣的可以看看。

stream-handbook的完整中文版本

本文默认读者对流有了一定的了解,因此这里对流的种类,相关API和使用不会说的很详细。

流的好处

stream不会占用大量内存。例如 fs.readFile 在读取文件时,会把整个文件读取到内存当中,当文件很大时,会很占用内存,甚至会超出v8的内存限制,导致程序退出。流则是读一部分,写一部分,而且能够充分地利用 Buffer 不受 V8 内存控制的特点,利用堆外内存完成高效地传输。

gulp 中的流

gulp 内部使用了 through2 封装好的 transform 流(后面会展开说),这一步是在 gulp.src 的时候完成的,因此我们可以看到这样的代码:

gulp.src(xxx).pipe(xxx)...

我个人理解的 pipe 就是一根水管,左边是可读流(readStream),右边是可写流(writeStream)。正因为 gulp 帮我们封装好了流,因此,我们可以直接使用 pipe 这个 api 来传输数据。

gulp 插件的编写

学习插件的编写能让我们对gulp中流的使用有更深入的认识,有的时候可能我们需要一些定制话的功能来处理文件,这时就需要自己编写gulp的插件了,我们先来看一个官方的例子:

点击打开
var through = require('through2');
var gutil = require('gulp-util');
var PluginError = gutil.PluginError;

// 常量
const PLUGIN_NAME = 'gulp-prefixer';

function prefixStream(prefixText) {
  var stream = through();
  stream.write(prefixText);
  return stream;
}

// 插件级别函数 (处理文件)
function gulpPrefixer(prefixText) {

  if (!prefixText) {
    throw new PluginError(PLUGIN_NAME, 'Missing prefix text!');
  }
  prefixText = new Buffer(prefixText); // 预先分配

  // 创建一个让每个文件通过的 stream 通道
  return through.obj(function(file, enc, cb) {
    if (file.isNull()) {
      // 返回空文件
      cb(null, file);
    }
    if (file.isBuffer()) {
      file.contents = Buffer.concat([prefixText, file.contents]);
    }
    if (file.isStream()) {
      file.contents = file.contents.pipe(prefixStream(prefixText));
    }

    cb(null, file);

  });

};

// 暴露(export)插件主函数
module.exports = gulpPrefixer;

对于 gulp 插件的编写我认为这两点是比较重要的:

  • through2 做了什么
  • through2.obj 中的 function 怎么写

带着这些疑问,我们先看下 through2 的源码:源码地址

点击打开
// 前面的代码创建了一个 Transform 流,并给这个流添加了一个 destroy 方法
function through2 (construct) {
  return function (options, transform, flush) {
    if (typeof options == 'function') {
      flush     = transform
      transform = options
      options   = {}
    }

    if (typeof transform != 'function')
      transform = noop

    if (typeof flush != 'function')
      flush = null

    return construct(options, transform, flush)
  }
}

module.exports.obj = through2(function (options, transform, flush) {
  var t2 = new DestroyableTransform(Object.assign({ objectMode: true, highWaterMark: 16 }, options))

  t2._transform = transform

  if (flush)
    t2._flush = flush

  return t2
})

看完代码,我们应该能回答上面的两个问题了。through2 会返回一个封装好的带有 destroy 方法的 Transform 流,而我们使用时传入的 function,实际是 Transform 流的 _transform 方法,用于数据的处理。这里我们有几点要注意一下:
  • 编写插件时要使用 through2.obj 方法
  • _transform 方法最后要执行一下 callback 后才能接收下一个数据块

文件系统

文件系统这个名字说着有点悬乎,其实这部分就是介绍一下 gulp.src 是怎么把文件加工成流的。

未完待续。。。