原文链接: The Complete-Ish Guide to Upgrading to Gulp 4
Gulp4虽然一直在开发中,但是将来的某一天你始终会用上它。这篇文章将向你介绍Gulp3.x和Gulp4之间的不同点,以及教你如何相对无痛的迁移到新的版本。
在你开始使用最新版的Gulp之前,你需要确认你当前的Gulp版本。通常,你只需要更新你的 package.json
中的版本号就行了,不过有时候你也有可能碰到一些额外的麻烦。最可能的原因是你分别在当前项目文件夹下和本地全局中都安装了Gulp(如果你读过了这篇文章 the practice of using npm scripts to access the locally installed version of CLI’s ,那就好办多了。虽然在这里它可能还是帮不了你太多)。因此,首先你要把你项目文件夹下的Gulp删除,如果你在全局环境中也安装了Gulp,最好也把它删了。
npm uninstall gulp --save-dev npm uninstall gulp -g
现在你就可以在你的项目中安装Gulp4。因为它还没有正式发布,我们只能直接通过Github来安装它:
npm install gulpjs/gulp.git#4.0 --save-dev
当它在npm上发布后,你就可以像平常一样使用 npm install gulp --save-dev
了。而且,当他最终正式发布时,我们也最好停止从Github上安装,改为直接从npm上进行安装。好了,现在我们还有另一个东西需要安装:命令行工具。跟现在的Grunt类似的,Gulp4把命令行工具从Gulp的核心代码中剥离了。Gulp3和Gulp4都能使用独立出来的命令行工具。
npm install gulp-cli --save-dev
如果你不想在你的项目中使用npm scripts,你需要使用 -g
替换 -save-dev
来进行全局安装。现在你就可以像以前一样使用 gulp
命令了,但是你应该会得到一个错误信息,因为你需要更新你的 gulpfile.js
来兼容最新版的Gulp。
如果你的任务代码十分简单,不依赖其他任何东西。那你将幸运的不需要做任何修改!不过令人哀伤的是,相当一部分人都需要修改他们的代码。Gulp4做的最大的一个改变就是 gulp.task
函数现在只支持两个参数,分别是任务名和运行任务的function。举个例子,下面的任务代码可以很好的运行在Gulp3和Gulp4上面:
gulp.task('clean', function() {...})
但是当你使用三个参数时怎么办?我们该怎么控制任务之间的相互依赖?这时你应该会用到最新的 gulp.series
和 gulp.parallel
函数。这两个方法会接受一个函数列表或一堆任务名,然后组合并返回一个函数。 gulp.series
会返回一个函数用来顺序执行它所接受的任务/函数,而 gulp.parallel
返回的函数则会并行的运行它们。Gulp总算能够让我们自由的选择以串行或并行的方式来执行任务而不再需要其他的第三方依赖(比如常用的 run-sequence ),也不再需要定义一堆让人看不懂的任务依赖。
因此,如果你以前是这么写:
gulp.task('styles', ['clean'], function() { ... });
那你现在可以这样:
gulp.task('styles', gulp.series('clean', function() { ... }));
在改写的时候,不要忘了现在你的主任务函数也是放在gulp.series里面调用,所以不要忘了在结尾加上括号。很多人经常犯这个错误。
注意,由于 gulp.series
和 gulp.parallel
返回的是一个函数,所以他们是可以被嵌套调用的。如果您的任务往往有多个依赖任务,你会经常嵌套调用它们。比如这个例子:
gulp.task('default', ['scripts', 'styles'], function() { ... });
你可以改写成:
gulp.task('default', gulp.series(gulp.parallel('scripts', 'styles'), function() { ... }));
不过,这样代码的可读性将大大受影响。不过考虑到这样会使你代码更加的灵活可控,这点代价也就无所谓了。当然你也可以自己封装一些helper/alias函数来简化的你的代码,提高可读性,我也不反对,但我应该不会这么去做。
在Gulp3中,假设你设定几个有相同的依赖的任务,然后运行它们,Gulp会检测出这些将要运行的任务的依赖是一样的,然后只会运行一次依赖任务。然而现在我们不在指定“依赖任务”,而是通过series和parallel函数来组合任务,Gulp没办法限制那些本应该只运行一次的任务,导致它们可能多次运行。所以我们要改变我们指定依赖任务的方式。
让我们看一下这个Gulp3的例子:
// default任务,需要依赖scripts和styles gulp.task('default', ['scripts', 'styles'], function() {...}); // script折styles任务都依赖clean gulp.task('styles', ['clean'], function() {...}); gulp.task('scripts', ['clean'], function() {...}); // clean任务用来清空目录 gulp.task('clean', function() {...});
我们注意到 styles
和 scripts
任务都依赖 clean
任务。当你运行 default
任务时,Gulp3会率先运行 styles
和 scripts
任务,又因为检测到这两个人物都有各自的依赖,所以需要优先运行它们的依赖任务,这时我们注意到这两个人物都依赖于 clean
这个任务,于是Gulp3将确保在回去运行 styles
和 scripts
任务之前, clean
任务只运行一次。这是特性非常有用!不过可惜的是,我们在尝试新版本的时候没办法运用这个特性。如果你在Gulp4中只下下面的例子一样做了简单的改变, clean
任务将会运行两次:
gulp.task('clean', function() {...}); gulp.task('styles', gulp.series('clean', function() {...})); gulp.task('scripts', gulp.series('clean', function() {...})); gulp.task('default', gulp.parallel('scripts', 'styles'));
这是因为 parallel
和 series
不是用来解决依赖的;他们只是用来把多个函数合并成一个。所以我们需要把共同依赖的任务抽离出来,然后用一个更大的串行任务来包裹它们,以此来模拟任务依赖:
重要提示: 你不能在定义那些用来组合 default
的小任务之前,定义 default
任务。因为在你调用 gulp.series("taskName")
之前,你 必须 已经定义好了一个名为 "taskName"
的任务。所以在Gulp4中我们必须在代码的最后才能定义 default
,而在Gulp3中你可以把它放在任何地方。
// 任务直接不再有依赖 gulp.task('styles', function() {...}); gulp.task('scripts', function() {...}); gulp.task('clean', function() {...}); // default任务,需要依赖scripts和styles gulp.task('default', gulp.series('clean', gulp.parallel('scripts', 'styles')));
如果照这么写,当你单独运行 styles
和 scripts
任务时, clean
任务就不会优先自动完成。不过这问题也不大,单独运行 clean
任务就能把scripts和styles的文件夹清空。总之你想怎么写都行,我也不确定你想怎么定义你的任务。
在Gulp3中,如果你代码里运行的任务都是同步的,就没有额外要做的事情。但是在Gulp4中就不能这样了:现在你必须指定done回调(这点是我最早意识到的一个变化)。另外,在执行异步任务时,你有三个选择来确保Gulp能够检测到你的任务完成了,它们分别是:
你可以在你的任务函数的参数中提供一个回调函数并且在你的任务完成后调用它:
var del = require('del'); gulp.task('clean', function(done) { del(['.build/'], done); });
你也可以返回一个流,通常通过 gulp.src
或 vinyl-source-stream 这个库来创建。这会是一个最常用的方式:
gulp.task('somename', function() { return gulp.src('client/**/*.js') .pipe(minify()) .pipe(gulp.dest('build')); });
Promise已经是一个很重要的技术而且在Node中已经有了完整的实现,所以这也是一个很有用的方式。你只需要放回一个promise对象,Gulp就能知道任务在什么时候完成。
var promisedDel = require('promised-del'); gulp.task('clean', function() { return promisedDel(['.build/']); });
感谢Gulp现在引入了 async-done 库,在最新的版本中我们有更多的方式来确认异步任务的完成。
你可以创建一些子进程并返回!你可以把你的npm scripts放到Gulp中执行,这样你就不需要为你的package.json中加载了百万条命令而烦恼。你也可以通过这样的封装摆脱那些随时可能过时的gulp插件。尽管这看上去像一个反模式,不过你还是有很多可以优化它们的函数。
var spawn = require('child_process').spawn; gulp.task('clean', function() { return spawn('rm', ['-rf', path.join(__dirname, 'build')]); });
我没用过RxJS,它好像挺小众的。不过对于那些RxJS的死忠粉丝,他们会很高兴可以返回一个observable对象。
var Observable = require('rx').Observable; gulp.task('sometask', function() { return Observable.return(42); });
处理文件系统的监听和响应的API也有了一点进步。之前的API中,在我们传入一个glob通配符和那些可选参数后,我们可以再指定一个任务数组或者一个回调函数用来处理事件数据。可是现在,任务队列都是由个serise或者parallel函数合并而成,这样你就无法用一个回调来区分这些任务,所以我们取消了这种简单回调的方式。取而代之的是, gulp.watch
将像之前一样会返回一个的“观察”对象,不过你可以对它添加各种事件监听:
// 旧版 gulp.watch('js/**/*.js', function(event) { console.log('File ' + event.path + ' was ' + event.type + ', running tasks...'); }); // 新版: var watcher = gulp.watch('js/**/*.js' /* 你可以在这里传一些参数或者函数 */); watcher.on('all', function(event, path, stats) { console.log('File ' + path + ' was ' + event + ', running tasks...'); }); // 单个事件的监听 watcher.on('change', function(path, stats) { console.log('File ' + path + ' was changed, running tasks...'); }); watcher.on('add', function(path) { console.log('File ' + path + ' was added, running tasks...'); }); watcher.on('unlink', function(path) { console.log('File ' + path + ' was removed, running tasks...'); });
正如所看到的,在 all
和 change
的事件处理中,你还可以接受一个stats对象。stats对象只在他们可用的时候出现(不确定他们什么时候可用什么时候不可用),不过你可以设置 alwaysStat
选项的值为 true
来让它始终出现。Gulp使用了 chokidar 库来实现这些东西,阅读chokidar的文档能让你了解的更多,尽管chokidar并不支持在事件回调中指定第三个参数。
由于现在每个任务基本上就是一个函数,不需要任何依赖或其他的什么,实际上他们也仅仅是需要一个任务运行器来确认异步任务何时结束,我们可以把函数定义从 gulp.task
中独立出来,而不仅仅作为一个简单回调函数传给 gulp.task
。举个例子,这个代码之前我们在“依赖陷阱”那个章节的结论:
gulp.task('styles', function() {...}); gulp.task('scripts', function() {...}); gulp.task('clean', function() {...}); gulp.task('default', gulp.series('clean', gulp.parallel('scripts', 'styles')));
我把它变成:
// 只需要在`series` 和 `parallel` 中间引用函数名就能组成一个新任务 gulp.task('default', gulp.series(clean, gulp.parallel(scripts, styles))); // 把单个任务变成一个函数 function styles() {...} function scripts() {...} function clean() {...}
这里有几点要注意的地方:
1.由于js是有函数定义提升,函数的定义可以放在你定义default任务之后,不像之前说的,如果你要用一些小任务组成一个新任务的时候,你就必须要先定义那些小任务。这样就使得你可以在一开始就定义好实际要运行的任务,这样别人阅读起来就更方便一些,以免别人还要在翻阅了一堆其他任务代码后,才能发现藏在最后的实际要运行的那些。
2. styles
, scripts
, 和 clean
现在都相当于“私有”任务,他们无法通过gulp命令行来运行。
3.这样就没有那么多匿名函数了。
4.也没有那么多被引号包裹住的“任务”名了,这样意味着你可以通过你的代码编辑器/IDE帮你检查拼写错误,而不用在运行Gulp的时候才能发现错误。
5.即使把“任务”函数放在多个文件中定义,也能方便的把它们引用到同一个文件中,然后再通过 gulp.task
把它们变成实际可用的任务。
6.这些任务都是可以独立测试的(如果你要测试)而不需要gulp。
当然第2点也是可以修改的,如果你希望它们是可以被gulp命令行所执行的:
gulp.task(styles);
这样你就能新建了一个可以运行在命令行的“styles”任务。注意你可从来没有在代码中定义过它的名字。gulp.task可以很智能的把函数名转成任务名。当然,匿名函数是不行的:Gulp会抛出一个错误当你想要把匿名函数指定成一个任务,却没有给它起一个新名字。
如果你想给函数起个别名,你可以在函数的 displayName
属性中指定它:
function styles(){...} styles.displayName = "pseudoStyles"; gulp.task(styles);
现在任务名将会从“styles”变成“pseudoStyles”。你也可以通过指定description属性来给你的任务添加描述。你可以通过gulp –tasks命令来查看这些描述:
function styles(){...} styles.displayName = "pseudoStyles"; styles.description = "Does something with the stylesheets." gulp.task(styles);
$ gulp --tasks [12:00:00] Tasks for ~/project/gulpfile.js [12:00:00] └── pseudoStyles Does something with the stylesheets.
你甚至可以给你其他已经注册的任务添加描述,比如 default
。首先你要运行 gulp.task('taskName')
来取人这个任务已经被定义过了,然后才给它添加描述:
gulp.task('default', gulp.series(clean, gulp.parallel(scripts, styles))); // Use gulp.task to retrieve the task var defaultTask = gulp.task('default'); // give it a description defaultTask.description = "Does Default Stuff";
我们也可以简化它,取消中间值:
gulp.task('default', gulp.series(clean, gulp.parallel(scripts, styles))); gulp.task('default').description = "Does Default Stuff";
对那些不熟悉你的项目的人来说,这些描述是相当有用的。所以我建议在任何情况下都要添加它们:有时它比注释更有用。最后,这是我推荐的Gulp4的最佳实践:
gulp.task('default', gulp.series(clean, gulp.parallel(scripts, styles))); gulp.task('default').description = "This is the default task and it does certain things"; function styles() {...} function scripts() {...} function clean() {...}
如果你运行 gulp --tasks
,你将会看到:
$ gulp --tasks [12:00:00] Tasks for ~/localhost/gulp4test/gulpfile.js [12:00:00] └─┬ default This is the default task and it does certain things [12:00:00] └─┬ <series> [12:00:00] ├── clean [12:00:00] └─┬ <parallel> [12:00:00] ├── scripts [12:00:00] └── styles
你会发现这里不仅有你添加的描述,你还能看到完整的运行队列树的样子。我也很乐意听到你有其他的最佳实践,不过最好先跟你的团队讨论一下。
不管怎么样,我还是很高兴看到Gulp4有很多有用的改进,但是它们也给迁移带来了不少痛苦。我希望这份指南能帮助你顺利迁移到Gulp4当它正式发布后(可能过几天……也可能……)。上帝保佑~