本文概述
如今, 前端开发人员正在使用多种工具来使例行操作自动化。最受欢迎的三种解决方案是Grunt, Gulp和Webpack。这些工具中的每一个都基于不同的理念, 但它们有一个共同的目标:简化前端的构建过程。例如, Grunt是配置驱动的, 而Gulp几乎不执行任何操作。实际上, Gulp依靠开发人员编写代码来实现构建过程的流程-各种构建任务。
当选择这些工具之一时, 我个人最喜欢的是Gulp。总而言之, 这是一个简单, 快速且可靠的解决方案。在本文中, 我们将通过刺探实现我们自己的类似Gulp的工具来了解Gulp的工作原理。
Gulp API
Gulp仅有四个简单功能:
- gulp.task
- gulp.src
- 吞食
- gulp.watch
这四个简单功能以各种组合提供了Gulp的所有功能和灵活性。在版本4.0中, Gulp引入了两个新功能:gulp.series和gulp.parallel。这些API允许任务以串行或并行方式运行。
在这四个功能中, 前三个对于任何Gulp文件都是绝对必要的。允许从命令行界面定义和调用任务。第四个是通过允许在文件更改时运行任务而真正使Gulp自动化的原因。
Gulp文件
这是一个基本的gulpfile:
gulp.task('test', function{
gulp.src('test.txt')
.pipe(gulp.dest('out'));
});
它描述了一个简单的测试任务。调用时, 当前工作目录中的文件test.txt应该复制到目录./out。通过运行Gulp进行尝试:
touch test.txt # Create test.txt
gulp test
请注意, 方法.pipe不是Gulp的一部分, 它是节点流API, 它将可读取流(由gulp.src(‘test.txt’)生成)与可写流(由gulp.dest(‘out)生成)连接’))。 Gulp和插件之间的所有通信均基于流。这使我们可以以一种优雅的方式编写gulpfile代码。
认识插头
现在我们对Gulp的工作方式有了一些了解, 让我们构建自己的类似Gulp的工具:Plug。
我们将从plug.task API开始。它应该让我们注册任务, 并且如果任务名称在命令参数中传递, 则应该执行任务。
var plug = {
task: onTask
};
module.exports = plug;
var tasks = {};
function onTask(name, callback){
tasks[name] = callback;
}
这将允许任务被注册。现在我们需要使此任务可执行。为简单起见, 我们不会制作单独的任务启动器。相反, 我们会将其包括在我们的插件实现中。
我们需要做的就是运行命令行参数中命名的任务。在所有任务都注册之后, 我们还需要确保在下一个执行循环中尝试这样做。最简单的方法是在超时回调中运行任务, 或者最好在process.nextTick中运行:
process.nextTick(function(){
var taskName = process.argv[2];
if (taskName && tasks[taskName]) {
tasks[taskName]();
} else {
console.log('unknown task', taskName)
}
});
像这样编写plugfile.js:
var plug = require('./plug');
plug.task('test', function(){
console.log('hello plug');
})
…并运行它。
node plugfile.js test
它将显示:
hello plug
子任务
Gulp还允许在任务注册时定义子任务。在这种情况下, plug.task应该带有3个参数, 即名称, 子任务数组和回调函数。让我们实现它。
我们将需要像这样更新任务API:
var tasks = {};
function onTask(name) {
if(Array.isArray(arguments[1]) && typeof arguments[2] === "function"){
tasks[name] = {
subTasks: arguments[1], callback: arguments[2]
};
} else if(typeof arguments[1] === "function"){
tasks[name] = {
subTasks: [], callback: arguments[1]
};
} else{
console.log('invalid task registration')
}
}
function runTask(name){
if(tasks[name].subTasks){
tasks[name].subTasks.forEach(function(subTaskName){
runTask(subTaskName);
});
}
if(tasks[name].callback){
tasks[name].callback();
}
}
process.nextTick(function(){
if (taskName && tasks[taskName]) {
runTask(taskName);
}
});
现在, 如果我们的plugfile.js看起来像这样:
plug.task('subTask1', function(){
console.log('from sub task 1');
})
plug.task('subTask2', function(){
console.log('from sub task 2');
})
plug.task('test', ['subTask1', 'subTask2'], function(){
console.log('hello plug');
})
…运行它
node plugfile.js test
…应显示:
from sub task 1
from sub task 2
hello plug
请注意, Gulp并行运行子任务。但是为了简单起见, 在我们的实现中, 我们按顺序运行子任务。 Gulp 4.0允许使用它的两个新API函数对此进行控制, 我们将在本文稍后实现。
来源和目的地
如果我们不允许读写文件, 则Plug几乎没有用。接下来, 我们将实现plug.src。 Gulp中的此方法需要一个参数, 该参数可以是文件掩码, 文件名或文件掩码数组。它返回可读的Node流。
现在, 在我们的src实现中, 我们将只允许使用文件名:
var plug = {
task: onTask, src: onSrc
};
var stream = require('stream');
var fs = require('fs');
function onSrc(fileName){
var src = new stream.Readable({
read: function (chunk) {
}, objectMode: true
});
//read file and send it to the stream
fs.readFile(path, 'utf8', (e, data)=> {
src.push({
name: path, buffer: data
});
src.push(null);
});
return src;
}
请注意, 我们使用objectMode:true, 这是一个可选参数。这是因为默认情况下, 节点流与二进制流一起使用。如果需要通过流传递/接收JavaScript对象, 则必须使用此参数。
如你所见, 我们创建了一个人工对象:
{
name: path, //file name
buffer: data //file content
}
…并将其传递到信息流中。
另一方面, plug.dest方法应接收目标文件夹名称, 并返回可写流, 该流将接收来自.src流的对象。一旦接收到文件对象, 它将被存储到目标文件夹中。
function onDest(path){
var writer = new stream.Writable({
write: function (chunk, encoding, next) {
if (!fs.existsSync(path)) fs.mkdirSync(path);
fs.writeFile(path +'/'+ chunk.name, chunk.buffer, (e)=> {
next()
});
}, objectMode: true
});
return writer;
}
让我们更新我们的plugfile.js:
var plug = require('./plug');
plug.task('test', function(){
plug.src('test.txt')
.pipe(plug.dest('out'))
})
…创建test.txt
touch test.txt
…并运行它:
node plugfile.js test
ls ./out
应将test.txt复制到./out文件夹。
Gulp本身的工作方式大致相同, 但是它使用Vinyl对象代替了人工文件对象。它更加方便, 因为它不仅包含文件名和内容, 还包含其他元信息, 例如当前文件夹名称, 文件的完整路径等。它可能不包含整个内容缓冲区, 但它具有可读的内容流。
Vinyl:比文件更好
有一个出色的Vinyl库, 可以让我们操作以Vinyl对象表示的文件。从本质上讲, 它使我们可以基于文件掩码来创建可读, 可写的流。
我们可以使用Vinyl-fs库重写插件函数。但是首先我们需要安装Vinyl-fs:
npm i vinyl-fs
安装此程序后, 新的Plug实现将如下所示:
var vfs = require('vinyl-fs')
function onSrc(fileName){
return vfs.src(fileName);
}
function onDest(path){
return vfs.dest(path);
}
// ...
…并尝试一下:
rm out/test.txt
node plugFile.js test
ls out/test.txt
结果应该仍然相同。
Gulp插件
由于我们的插件服务使用Gulp流约定, 因此我们可以将本机Gulp插件与我们的插件工具一起使用。
让我们尝试一下。安装gulp重命名:
npm i gulp-rename
…并更新plugfile.js以使用它:
var plug = require('./app.js');
var rename = require('gulp-rename');
plug.task('test', function () {
return plug.src('test.txt')
.pipe(rename('renamed.txt'))
.pipe(plug.dest('out'));
});
你猜到, 现在运行plugfile.js应该仍然会产生相同的结果。
node plugFile.js test
ls out/renamed.txt
监控变更
最后但并非最不重要的方法是gulp.watch。此方法使我们可以注册文件侦听器, 并在文件更改时调用已注册的任务。让我们实现它:
var plug = {
task: onTask, src: onSrc, dest: onDest, watch: onWatch
};
function onWatch(fileName, taskName){
fs.watchFile(fileName, (event, filename) => {
if (filename) {
tasks[taskName]();
}
});
}
要尝试, 请将以下行添加到plugfile.js:
plug.watch('test.txt', 'test');
现在, 每次更改test.txt时, 文件将被复制到out文件夹中, 其名称将更改。
串联vs并联
现在已经实现了Gulp API的所有基本功能, 现在让我们迈出更进一步。即将推出的Gulp版本将包含更多API函数。这个新的API将使Gulp更加强大:
- gulp.parallel
- 口香糖系列
这些方法允许用户控制任务运行的顺序。要并行注册子任务, 可以使用gulp.parallel, 这是当前的Gulp行为。另一方面, gulp.series可以用于依次运行子任务。
假设我们在当前文件夹中有test1.txt和test2.txt。为了将这些文件并行复制到out文件夹, 让我们制作一个插件文件:
var plug = require('./plug');
plug.task('subTask1', function(){
return plug.src('test1.txt')
.pipe(plug.dest('out'))
})
plug.task('subTask2', function(){
return plug.src('test2.txt')
.pipe(plug.dest('out'))
})
plug.task('test-parallel', plug.parallel(['subTask1', 'subTask2']), function(){
console.log('done')
})
plug.task('test-series', plug.series(['subTask1', 'subTask2']), function(){
console.log('done')
})
为了简化实现, 使子任务回调函数返回其流。这将帮助我们跟踪流的生命周期。
我们将开始修改API:
var plug = {
task: onTask, src: onSrc, dest: onDest, parallel: onParallel, series: onSeries
};
我们还需要更新onTask函数, 因为我们需要添加其他任务元信息来帮助我们的任务启动器正确处理子任务。
function onTask(name, subTasks, callback){
if(arguments.length < 2){
console.error('invalid task registration', arguments);
return;
}
if(arguments.length === 2){
if(typeof arguments[1] === 'function'){
callback = subTasks;
subTasks = {series: []};
}
}
tasks[name] = subTasks;
tasks[name].callback = function(){
if(callback) return callback();
};
}
function onParallel(tasks){
return {
parallel: tasks
};
}
function onSeries(tasks){
return {
series: tasks
};
}
为简单起见, 我们将使用async.js, 这是一个用于处理异步功能的实用程序库, 以并行或串行方式运行任务:
var async = require('async')
function _processTask(taskName, callback){
var taskInfo = tasks[taskName];
console.log('task ' + taskName + ' is started');
var subTaskNames = taskInfo.series || taskInfo.parallel || [];
var subTasks = subTaskNames.map(function(subTask){
return function(cb){
_processTask(subTask, cb);
}
});
if(subTasks.length>0){
if(taskInfo.series){
async.series(subTasks, taskInfo.callback);
}else{
async.parallel(subTasks, taskInfo.callback);
}
}else{
var stream = taskInfo.callback();
if(stream){
stream.on('end', function(){
console.log('stream ' + taskName + ' is ended');
callback()
})
}else{
console.log('task ' + taskName +' is completed');
callback();
}
}
}
我们依赖于节点流”结束”, 该节点流在处理完所有消息并关闭时发出, 这表明子任务已完成。使用async.js, 我们不必处理大量的回调。
要尝试一下, 让我们首先并行运行子任务:
node plugFile.js test-parallel
task test-parallel is started
task subTask1 is started
task subTask2 is started
stream subTask2 is ended
stream subTask1 is ended
done
并依次运行相同的子任务:
node plugFile.js test-series
task test-series is started
task subTask1 is started
stream subTask1 is ended
task subTask2 is started
stream subTask2 is ended
done
总结
就这样, 我们已经实现了Gulp的API, 现在可以使用Gulp插件了。当然, 不要在实际项目中使用Plug, 因为Gulp不仅仅是我们在此实现的内容。我希望这个小练习可以帮助你了解Gulp的工作原理, 并让我们更流畅地使用它并使用插件进行扩展。
相关:使用Gulp的JavaScript自动化简介