作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.
米哈伊尔·安格诺夫
验证专家 在工程
21 Years 的经验

Mikhail holds a Master’s in Physics. He’s run the gamut with Node.js, Go, JavaScript SPAs, React.. js, Flux/Redux, RIOT.js和AngularJS.

专业知识

Share

如今,前端开发人员正在使用多种工具来自动化日常操作. Three of the most popular solutions are Grunt, Gulp and Webpack. Each of these tools are built on different philosophies, 但是它们有一个共同的目标:简化前端构建过程. 例如,Grunt是配置驱动的,而Gulp几乎没有强制. In fact, Gulp relies on the 开发人员 编写代码来实现构建流程流——各种构建任务.

Gulp Under the Hood:构建一个基于流的任务自动化工具

在选择这些工具时,我个人最喜欢的是Gulp. All in all it’s a simple, fast and reliable solution. 在本文中,我们将通过尝试实现我们自己的类似于Gulp的工具来了解Gulp是如何工作的.

吞咽API

Gulp的意思是just 四个简单的功能:

  • gulp.task
  • gulp.src
  • gulp.dest
  • gulp.watch

这四个简单的功能以不同的组合方式提供了Gulp的所有功能和灵活性. 在版本4中.0, Gulp introduced two new functions: gulp.串连及串连.平行. These APIs allow tasks to be run in series or in 平行.

在这四个函数中,前三个对于任何Gulp文件都是绝对必要的. 允许从命令行界面定义和调用任务. 第四个是允许在文件更改时运行任务,从而使Gulp真正实现自动化.

Gulpfile

This is an elementary gulpfile:

gulp.任务(“测试”,函数{
    gulp.src(“测试.txt')
          .管(吞咽.桌子('从'));
});

It describes a simple test task. 调用时,文件 test.txt 在当前工作目录中应该复制到的目录 ./out. Give it a try by running Gulp:

触摸测试.创建test.txt
杯测试

注意这个方法 .pipe 不是Gulp的一部分,它是节点流API,它连接一个可读流(由 gulp.src(“测试.txt')) with a writable stream (generated by gulp.桌子(出来的)). Gulp和插件之间的所有通信都是基于流的. This lets us write gulpfile code in such an elegant way.

满足塞

现在我们对Gulp的工作原理有了一些了解,让我们构建自己的类似Gulp的工具:Plug.

我们从插头开始.task API. It should let us register tasks, 如果任务名称在命令参数中传递,则应该执行任务.

Var插头= {
    任务:onTask
};

module.出口=插头;

Var tasks = {};
function onTask(name, 回调){
    Tasks [name] = callback;
}

This will allow tasks to be registered. Now we need to make this task executable. 为了简单起见,我们不会制作一个单独的任务启动器. Instead we will include it in our plug implementation.

我们所需要做的就是运行命令行参数中指定的任务. 我们还需要确保在下一个执行循环中尝试这样做, after all tasks are registered. 最简单的方法是在超时回调中运行任务,或者最好是在进程中运行.nextTick:

process.nextTick(函数(){
    var taskName =进程.argv [2];
    如果(taskName && 任务(taskName)) {
        任务(taskName) ();
    } else {
        console.log('unknown task', taskName)
    }
});

组成plugfile.就像这样:

Var plug = require('./插头');

plug.任务(“测试”,函数(){
    console.日志(“你好塞”);
})

然后运行它.

节点plugfile.js test

它将显示:

你好,塞

子任务

Gulp also allows to define subtasks at task registration. 在这种情况下,插入.Task应该有3个参数,名称,子任务数组和回调函数. 让我们来实现这个.

我们需要更新 the task API as such:

Var tasks = {};
函数onTask(name) {
	如果数组.isArray(参数[1]) && typeof arguments[2] === "function"){
	    	任务[name] = {
        			子任务:参数[1],
        			回调:参数[2]
	    	};
	} else if(typeof arguments[1] === "function"){
	    	任务[name] = {
        			子任务:[],
        			回调:参数[1]
    		};
	} else{
    		console.log('invalid task registration')
	}
}

函数runTask(名字){
	如果任务[名称].子任务){
	    	任务(名字).子任务.forEach(function(subTaskName){
        			runTask (subTaskName);    
	    	});
	}
	如果任务[名称].回调){
    		任务(名字).回调();
	}
}
process.nextTick(函数(){
	如果(taskName && 任务(taskName)) {
    		runTask (taskName);
	}
});

现在如果我们的plugfile.Js看起来是这样的:

plug.task('subTask1', function(){
    console.日志('从子任务1');
})
plug.task('subTask2', function(){
    console.Log ('from subtask 2');
})
plug.task('test', ['subTask1', 'subTask2'], function(){
    console.日志(“你好塞”);
})

运行它

节点plugfile.js test

应该显示:

从子任务1
从子任务2
你好,塞

Note that Gulp runs subtasks in 平行. 但为了简单起见,在我们的实现中,我们按顺序运行子任务. Gulp 4.0 allows this to be controlled using its two new API函数, which we will implement later in this article.

源和目的

如果我们不允许文件的读写,那么插件就没什么用了. 接下来我们要实现 plug.src. Gulp中的这个方法需要一个参数,要么是一个文件掩码, a filename or an array of file masks. It returns a readable Node stream.

For now, in our implementation of src, we will just allow filenames:

Var插头= {
    任务:onTask,
    src: onSrc
};

var stream = require('stream');
Var fs = require('fs');
函数onSrc(文件名){
    Var SRC =新流.可读({
        读取:function (chunk) {
        },
        objectMode:真
    });
    //read file and send it to the stream
    fs.readFile(path, 'utf8', (e,data)=> {
        src.push({
            名称:路径,
            缓冲区:数据
        });
        src.推动(空);
    });
    返回src;
}

注意,我们使用 objectMode:真, an optional parameter here. 这是因为节点流默认使用二进制流. 如果我们需要通过流传递/接收JavaScript对象,我们必须使用这个参数.

As you can see, we created an artificial object:

{
  名称:路径,//文件名
  缓冲区:数据 //file content
}

… and passed it into the stream.

在另一端,插入.Dest方法应该接收一个目标文件夹名称,并返回一个可写流,该流将从中接收对象 .src流. 一旦接收到文件对象,它就会被存储到目标文件夹中.

函数波形(路径){
    Var writer =新流.可写({
        write: function (chunk, encoding, next) {
            if (!fs.existsSync fs(路径)).mkdirSync(路径);
            fs.writeFile(path +'/'+ chunk.名字,块.buffer, (e)=> {
                next()
            });
        },
        objectMode:真
    });

    返回的作家;
}

Let us update our plugfile.js:

Var plug = require('./插头');

plug.任务(“测试”,函数(){
    plug.src(“测试.txt')
    .管(塞.桌子(了))
})

…创建 test.txt

触摸测试.txt

然后运行它:

节点plugfile.js test
ls  ./out

test.txt 应该复制到 ./out folder.

Gulp本身的工作方式大致相同,但它使用的不是我们的人工文件对象 vinyl objects. It is much more convenient, 因为它不仅包含文件名和内容,还包含额外的元信息, such as the current folder name, 文件的完整路径, 等等....... 它可能不包含整个内容缓冲区,但它具有内容的可读流.

黑胶唱片:比文件好

There is an excellent library vinyl-fs that lets us manipulate files represented as vinyl objects. 它本质上允许我们基于文件掩码创建可读、可写的流.

We can rewrite plug functions using vinyl-fs library. But first we need to install vinyl-fs:

NPM I乙烯基-fs

安装了这个插件后,我们的新插件实现看起来像这样:

var vfs = require('vinyl-fs')

函数onSrc(文件名){
    返回vfs.src(文件名);
}

函数波形(路径){
    返回vfs.桌子(路径);
}

// ...

并尝试一下:

rm /测试.txt
节点plugFile.js test
ls /测试.txt

The results should still be the same.

吞咽插件

Since our Plug service uses Gulp stream convention, we can use native Gulp plugins together with our Plug tool.

我们来试一个. 安装gulp-rename:

我重命名NPM

并更新plugfile.如何使用它:

Var plug = require('./app.js');
var rename = require('gulp-rename');

plug.task('test', function () {
    返回塞.src(“测试.txt')
        .管(重命名(“重命名.txt'))
        .管(塞.桌子('从'));
});

运行plugfile.js now should still, you guessed it, produce the same result.

节点plugFile.js test
ls /重命名.txt

监测变化

The last but not least method is gulp.watch 该方法允许我们注册文件侦听器,并在文件更改时调用已注册的任务. 让我们来实现它:

Var插头= {
    任务:onTask,
    src: onSrc,
    桌子:波形,
    看:onWatch
};

function onWatch(fileName, taskName){
    fs.watchFile(fileName, (event, filename) => {
        If (filename) {
            任务(taskName) ();
        }
    });
}

To try it out, add this line to plugfile.js:

plug.手表(“测试.txt”、“测试”);

每一次变化 test.txt,该文件将被复制到out文件夹中,并更改其名称.

串联vs并联

现在吞咽API的所有基本功能都实现了, let’s take things one step further. The upcoming version of Gulp will contain more API函数. This new API will make Gulp more powerful:

  • gulp.平行
  • gulp.series

这些方法允许用户控制任务的运行顺序. To register subtasks in 平行 gulp.平行 may be used, which is the current Gulp behavior. 另一方面,吞咽.可以使用Series以顺序的方式一个接一个地运行子任务.

假设我们有 test1.txt and test2.txt 在当前文件夹中. 为了将这些文件并行复制到我们的文件夹中,让我们创建一个plugfile:

Var plug = require('./插头');

plug.task('subTask1', function(){
    返回塞.src(“test1.txt')
    .管(塞.桌子(了))
})

plug.task('subTask2', function(){
    返回塞.src(“test2.txt')
    .管(塞.桌子(了))
})

plug.任务(test-平行,插头.平行(['subTask1', 'subTask2']), function(){
    console.日志(“做”)
})

plug.任务(测试系列,插头.series(['subTask1', 'subTask2']), function(){
    console.日志(“做”)
})

为了简化实现,使用子任务回调函数返回其流. This will help us to track stream life cycle.

We will begin amending our API:

Var插头= {
    任务:onTask,
    src: onSrc,
    桌子:波形,
    并行:onParallel,
    系列:onSeries
};

我们需要更新 onTask 功能也一样, 因为我们需要添加额外的任务元信息来帮助我们的任务启动器正确处理子任务.

function onTask(name, 子任务, 回调){
    如果参数.length < 2){
        console.error('invalid task registration',arguments);
        return;
    }
    如果参数.长度=== 2){
        if(typeof arguments[1] === 'function'){
            callback = 子任务;
            子任务 = {series: []};
        }
    }

    任务(名字) = 子任务;
    任务(名字).Callback = function(){
        if(callback) return 回调();
    };
}

function onParallel(tasks){
    返回{
        并行:任务
    };
}

函数onSeries(任务){
    返回{
        系列:任务
    }; 
}

To keep things simple, we will use async.js,用于处理异步函数以并行或串行方式运行任务的实用程序库:

var async = require('async')

function _processTask(taskName, 回调){
            var taskInfo = tasks[taskName];
            console.log('task ' + taskName + ' is started');

            var subTaskNames = taskInfo.series || taskInfo.Parallel || [];
            var 子任务 = subTaskNames.地图(函数(子任务){
                返回函数(cb) {
                    _processTask(subTask, cb);
                }
            });

            如果(子任务.length>0){
                如果(taskInfo.系列){
                    async.taskInfo系列(子任务.回调);
                }else{
                    async.平行(子任务, taskInfo.回调);
                }
            }else{
                var stream = taskInfo.回调();
                如果(流){
                    stream.(“结束”,函数(){
                        console.log('stream ' + taskName + ' is ended');
                        回调()
                    })
                }else{
                    console.log('task ' + taskName +' is completed');
                    回调();
                }
            }

}

我们依赖于节点流' end ',它在流处理完所有消息并关闭时发出, which is an indication that the subtask is complete. 与异步.js, we do not have to deal with a big mess of callbacks.

To try it out, let us first run the subtasks in 平行:

节点plugFile.js test-平行
task test-平行 is started
已启动任务subTask1
已启动任务subTask2
流subTask2结束
流subTask1结束
done

And run the same subtasks in series:

节点plugFile.js系列
task test-series is started
已启动任务subTask1
流subTask1结束
已启动任务subTask2
流subTask2结束
done

结论

就这样,我们已经实现了Gulp的API,现在可以使用Gulp插件了. 当然,不要在实际项目中使用Plug,因为Gulp不仅仅是我们在这里实现的. 我希望这个小练习能帮助你理解Gulp的工作原理,让我们更流畅地使用它,并通过插件对它进行扩展.

Hire a Toptal expert on this topic.
现在雇佣
米哈伊尔·安格诺夫

米哈伊尔·安格诺夫

验证专家 在工程
21 Years 的经验

Nizhny Novgorod, Nizhny Novgorod Oblast, Russia

2015年7月6日加入

作者简介

Mikhail holds a Master’s in Physics. He’s run the gamut with Node.js, Go, JavaScript SPAs, React.. js, Flux/Redux, RIOT.js和AngularJS.

作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.

专业知识

World-class articles, delivered weekly.

Subscription implies consent to our 隐私政策

World-class articles, delivered weekly.

Subscription implies consent to our 隐私政策

Toptal开发者

加入总冠军® 社区.