前端工程化之自动化构建–Grunt与Gulp的使用
1、自动化构建简介
自动化构建是前端工程化中一个非常重要的组成部分。自动化实际上指的是通过机器代替手工完成一些工作。而构建可以理解为转换,就是把一个东西转换为另外的一些东西。总的来说,自动化构建就是把开发阶段写出来的源代码自动化地去转换成生产环境中可以运行的代码或程序。一般我们会把这样的一个转换过程称之为自动化构建工作流,他的作用就是让我们尽可能去脱离运行环境兼容带来的问题而在开发阶段使用一些提高效率的语法、规范以及标准。
最典型的应用场景就是我们在开发网页应用时可以使用ECMAScript的最新标准来提高编码效率以及质量,利用Sass去增强CSS的可编程性,利用模板引擎去抽象页面中重复的HTML。这些用法在浏览器中是没有办法直接被支持的,在这种情况下自动化构建工具就可以派上用场了。我们通过自动化构建的方式将这些不被支持的代码特性转换为能够直接运行的代码,这样我们就可以尽情在开发中通过这些方式去提高编码效率了。
2、自动化构建初体验
接下来我们通过一个案例来体会一下自动化构建的便捷之处。我们在编写CSS时若想使用Sass来增强CSS的编程性,就需要在开发时添加一个构建的环节,这样我们在开发的环节就可以通过sass编写样式,再通过工具将Sass构建为css。另外,注意Sass最早的扩展名是sass,不过因语法规范难以接受,后来就新出了一个语法规范,用scss作为扩展名了(注意下文可能会出现sass和scss混合使用的情况,大家知道都是在讲sass文件就行)。
相对于CSS,Sass的编程能力显然更强一些,但Sass并不能在浏览器环境中直接去使用,所以我们要在开发阶段通过一个工具去把Sass转换为CSS。这里我们可以使用Sass官方提供的一个sass模块,进入命令行,通过执行yarn add sass --dev
我们可以把该模块作为一个开发依赖来安装。安装完成后,在node_modules文件夹下会有.bin目录。在该目录下就会有Sass的命令文件,我们在命令行中就可以执行.\node_modules\.bin\sass
通过路径找到该命令并可以查看该命令的一些用法。接着我们就可以执行.\node_modules\.bin\sass <输入的scss文件所在路径> <输出的css文件所在路径>
,执行完成后sass模块就可以自动帮我们把scss文件转换为css文件了,并且还会帮我们添加Source map文件,让我们在调试阶段可以定位到源代码所在的位置。但这样也是比较麻烦的,我们每次都需要重复地输入复杂的命令,而且在别人接收我们的项目之后也不知道要如何运行构建的任务。所以说我们还要做一些额外的事情去解决这些在项目开发阶段重复执行的命令。
而接下来要介绍的NPM Scripts就是用来解决这个问题的。我们可以在NPM Scripts中定义一些与项目开发过程有关的脚本命令且让这些命令与项目一起去维护,这样就便于我们在开发过程中进行使用。所以这里最好的方式就是使用NPM Scripts去包装我们的命令。
而NPM Scripts具体的实现方式就是在项目根目录下的package.json中添加一个scripts字段:
"scripts": {
"build": "sass <输入的scss所在文件路径> <输出的css文件所在路径>"
}
该字段中是一个对象,键就是script的名称,值就是我们要执行的命令。需要注意的是,scripts中可以自动发现node_modules里面的命令,所以说我们不需要写完整的路径,直接使用命令的名称就行了。完成后我们就可以通过运行yarn build
或npm run build
启动build命令。
另外NPM Scripts也是实现自动化构建的最简单方式。接下来就介绍如何用他来实现自动化构建。首先,我们可以执行yarn add broswer-sync --dev
来为项目安装一个browser-sync模块用于启动一个测试服务器去运行我们的项目。这时我们在scripts中添加一个serve命令,命令中通过browser-sync把当前目录运行起来:
"scripts": {
"build": "sass <输入的scss所在文件路径> <输出的css文件所在路径>",
"serve": "browser-sync ."
}
回到命令行,我们执行yarn serve
运行一下serve命令,此时browser-sync会自动启动一个web服务器并帮我们唤起浏览器运行项目网页。但如果在browser-sync工作之前我们并没有生成我们的样式,此时browser-sync工作的时候我们页面就没有样式文件,我们需要在启动serve命令之前让build任务去工作,所以我们可以借助NPM Scripts的钩子机制去定义一个preserve,他会在serve命令执行前去执行:
"scripts": {
"build": "sass <输入的scss所在文件路径> <输出的css文件所在路径>",
"preserve": "yarn build",
"serve": "browser-sync ."
}
这时若我们再去执行serve,就会先去执行build命令,build完成之后才会再执行serve。而此时我们就可以完成在启动web服务之前自动去构建sass文件。并且我们还可以为sass命令后添加一个–watch参数,有了这个参数后,sass在工作时就会监听文件的变化,一旦代码中的scss文件发生改变,就会自动被编译。
"scripts": {
"build": "sass <输入的scss所在文件路径> <输出的css文件所在路径> --watch",
"preserve": "yarn build",
"serve": "browser-sync ."
}
此时再回到命令行重新运行这项命令,我们会发现sass命令在工作时命令行会阻塞等待文件的变化,这样就导致了后面的browser-sync并没有办法去直接工作,这种情况下我们就需要同时去执行多个任务。这里我们可以借助npm-run-all这个模块去实现。首先我们需要执行yarn add npm-run-all --dev
去 这个模块。有了这个模块之后,我们就可以在scripts中添加一个新命令“start”,在这个命令中我们通过npm-run-all
里的npm-p
命令同时执行build和serve命令:
"scripts": {
"build": "sass <输入的scss所在文件路径> <输出的css文件所在路径> --watch",
"serve": "browser-sync .",
"start": "run-p build serve"
}
回到命令行中,我们再运行yarn start
命令,这时我们就会发现build任务与browser任务被同时执行了。这时若我们打开scss文件去修改里面的内容,我们会发现css文件里的内容也会跟着改变,我们的–watch已经生效了。
最后,我们还可以给browser-sync这个命令去添加一个--files \"css/*.css\"
这样的参数:
"scripts": {
"build": "sass <输入的scss所在文件路径> <输出的css文件所在路径> --watch",
"serve": "browser-sync . --files \"css/*.css\"",
"start": "run-p build serve"
}
这个参数可以让browser-sync在启动后去监听项目下所有./css/*.css
文件的变化,一旦当文件发生变化后,browser-sync会将这些文件的内容自动同步到浏览器中,从而更新浏览器中的界面,让我们可以即时查看到最新的界面效果,避免了我们修改完代码后手动刷新浏览器这样重复的动作。
如此,我们就借助了NPM Scripts完成了一个简单的自动化构建的工作流。具体的工作流程就是在启动后同时运行了build和serve两个命令,并且build还自动监听scss文件的变化,自动编译scss。browser-sync启动外部服务,当文件发生变化后则自动刷新浏览器。
3、常用的自动化构建工具
NPM Scripts确实能解决一部分自动化构建任务,但对于相对复杂的构建过程,NPM Scripts就会显得非常吃力。这时我们就需要一个更加专业的构建工具,在这里先大致介绍一些市面上几个常用的构建化工具,让我们有一个整体的认识,后续再做深入探究。目前市面上开发者使用最多的一些工具主要就是Gulp、Grunt以及FIS,不过可能会有人疑问说webpack哪里去了,其实严格来说webpack只是一个模块打包工具,所以并不在我们这里的讨论范围之内。这些工具都可以帮我们去解决那些重复且无聊的工作,从而实现自动化。并且在用法上,他们也大致相同,都是先通过一些简单的代码去组织插件的使用,然后我们就可以使用这些工具去帮我们执行各种重复的工作了。
Grunt应该算是最早的前端构建系统了,他的插件生态非常完善,用官方的话讲就是Grunt的插件几乎可以帮我们去完成几乎任何我们想要完成的事情。但由于他的工作过程是基于临时文件去实现的,所以他的构建速度会比较慢。例如我们使用他去帮我们完成sass文件的构建,一般我们会先对sass文件做编译操作再去自动添加一些私有属性的前缀,最后再去压缩代码。在这样一个过程中,Grunt每一步都会有磁盘读写操作,比如sass文件在编译完成之后Grunt就会将结果写入到一个临时的文件,然后下一个插件再去读取这个临时文件进行下一步,这样一来我们处理的环节越多,文件读写的次数也就越多。而在一些超大型项目中,项目文件会非常多,所以我们的构建速度也会非常的慢。
而Gulp则很好地解决了Grunt中构建速度慢的问题,因为Gulp是基于内存去实现的,也即他对文件处理的环节都是在内存中完成的,相对于磁盘读写,速度自然就快了很多。另外Gulp还默认支持同时去执行多个任务,这对他效率的提升也有很大帮助。而且Gulp的使用方式相对于Grunt更加直观易懂,插件生态也同样非常完善,所以他后来居上,更受欢迎,应该算是目前最流行的前端构建系统了。
最后来看FIS,FIS是百度的前端团队推出的一款构建系统,最早只在他们团队内部使用,后来开源后就在国内快速流行。相对于前面两个构建系统微内核的特点,FIS更像是一个捆绑套餐,他把我们在项目中那些典型的需求尽可能都集成在内部了。例如我们在FIS中可以很轻松去处理资源加载,模块化开发,代码部署,甚至是性能优化。正是因为这种大而全,所以FIS在国内很多项目中就流行了。
总体而言,如果是初学者的话,FIS可能更适合一些。但如果需要灵活多变的话,Gulp、Grunt无疑是更好的选择。“新手一般是需要规则的,而老鸟则一般渴望自由。”也正是因为这个原因,现在这些小而美的框架或工具才会得以流行。
4、Grunt的基本使用
接下来我们在一个空项目中来看一下Grunt的具体用法。首先在一个空项目中想要使用Grunt的话就需要先执行yarn init
去init一个package.json。有了这个文件后,我们还需要执行yarn add grunt
去添加一下grunt模块。安装完成后我们还需要在项目的根目录下添加一个gruntfile.js文件,这个文件时grunt的入口文件,用于去定义一些需要grunt去自动执行的任务。我们需要在这个文件中导出一个函数,这个函数接受一个叫grunt的形式参数,而这个grunt是一个对象,对象中就是grunt提供的一些API,我们可以借助这些API去块速创建一个构建任务。
module.exports = grunt => {
// 在函数中我们借助grunt的registerTask方法去注册一个任务。
// 这个方法的第一个参数是任务的名字,第二个参数是任务函数,也即任务发生时自动去执行的函数。
grunt.registerTask('task1', () => {
console.log('hi grunt');
})
}
回到命令行中,执行yarn grunt task1
,yarn会自动帮我们找到node_modules下提供的命令,task1
就是我们刚才注册的任务的名字,这样的话grunt就会自动执行task1这个任务了。当然,我们不仅仅可以添加一个任务,还可以去添加更多的任务。而若在添加任务时,第二个参数是字符串的话,这个字符串就会成为这个任务的描述,他会出现在grunt的帮助信息中,我们可以在命令行中通过执行yarn grunt --help
得到帮助信息。在这个帮助信息中会有一个Available tasks
,而在这个tasks中的任务描述就是我们自定义的任务描述了。
module.exports = grunt => {
grunt.registerTask('task1', () => {
console.log('hi grunt');
})
// 添加第二个任务并添加任务描述
grunt.registerTask('task2', 'this is task2', () => {
console.log('other task')
})
}
同样,我们可以通过执行yarn grunt task2
去运行这个任务。除此之外,如果我们在注册任务时我们的任务名称叫做default的话,这个任务就会成为grunt的默认任务。我们在运行这个任务的时候就不需要再指定任务的名称,grunt会自动调用default。一般我们会用default去映射一些其他的任务,具体做法就是在registerTask方法中的第二个参数传入一个数组,这个数组中我们可以去指定一些任务的名字,这时我们执行default时,grunt就会依次执行数组中的这些任务。
module.exports = grunt => {
grunt.registerTask('task1', () => {
console.log('hi grunt');
})
grunt.registerTask('task2', 'this is task2', () => {
console.log('other task');
})
// 通过执行yarn grunt,grunt就会依次执行数组中的这些任务
grunt.registerTask('default', ['task1', 'task2']);
}
回到命令行中,执行yarn grunt
去运行默认任务,此时我们会发现grunt会先执行task1然后再去执行task2,就相当于把task1和task2串联到了一起。
最后我们再去尝试一下grunt中对异步任务的支持。我们在之前的任务中,通过setTimeout去模拟一下异步任务操作。
module.exports = grunt => {
grunt.registerTask('task1', () => {
console.log('hi grunt');
})
grunt.registerTask('task2', 'this is task2', () => {
console.log('other task');
})
grunt.registerTask('default', ['task1', 'task2']);
// 通过setTimeout去模拟一下异步任务来尝试一下grunt中对异步任务的支持
grunt.registerTask('async-task', () => {
setTimeout(() => {
console.log('async task working');
}, 1000)
})
}
回到命令行,执行yarn grun async-task
去运行一下这个任务,之后我们会发现,console.log并没有执行,这是grunt中的一个特点,即grunt的代码默认只支持同步模式。如果需要异步操作的话,就必须要使用this的async方法得到一个回调函数,在异步操作完成后去调用这个回调函数来标识一下这个任务已经被完成。而如果我们要在函数中使用this的话,这个函数就不能是箭头函数了,需要使用普通函数,在这个函数中,我们使用this.async()
来得到一个回调函数并且在setTimeout结束之后还需要去调用一下done回调函数标识一下我们的任务已经完成。此时grunt就知道这是一个异步任务从而等待done的执行,直到done被执行,grunt才会结束这个任务。
module.exports = grunt => {
grunt.registerTask('task1', () => {
console.log('hi grunt');
})
grunt.registerTask('task2', 'this is task2', () => {
console.log('other task');
})
grunt.registerTask('default', ['task1', 'task2']);
// 使用this的async方法得到一个回调函数,并且在setTimeout结束之后去调用一下done回调函数
grunt.registerTask('async-task', function() {
const done = this.async();
setTimeout(() => {
console.log('async task working');
}, 1000)
})
}
回到命令行,执行yarn grun async-task
去运行一下这个任务,就会发现这个任务已经能被正常运行了。
5、Grunt标记任务失败
如果我们在构建任务的逻辑代码中发生错误,例如我们需要的文件找不到了,此时就可以把该任务标记为一个失败的任务,具体的实现方式可以通过在函数体中return false去实现。
module.exports = grunt => {
grunt.registerTask('bad', () => {
console.log('bad working');
// 通过在函数体中return false去标记一个失败的任务
return false;
})
}
此时回到命令行终端,执行yarn grunt bad
与运行一下该任务,我们会发现终端中会提示我们bad这个任务执行失败了。如果一个任务是在任务列表中的话,这个任务的失败会导致后续所有的任务不再被执行。
module.exports = grunt => {
grunt.registerTask('bad', () => {
console.log('bad working');
return false;
})
grunt.registerTask('task1', () => {
console.log('hi task1');
})
grunt.registerTask('task2', () => {
console.log('hi task2');
})
// 如果一个任务是在任务列表中的话,这个任务的失败会导致后续所有的任务不再被执行
grunt.registerTask('default', ['task1', 'bad', 'task2']);
}
正常情况下的话,运行default会依次执行task1,bad,task2。但这里第二个bad执行失败了,task2就不会被执行。回到终端,我们通过执行yarn grunt
去运行一下这个任务,之后我们会发现,task1和bad任务都执行了,但bad任务执行失败了,所以task2就不会被执行了。并且此时命令行中也会给出一个提示,如果指定--force
参数的话会通过强制方式去执行所有任务。我们执行yarn grunt --force
来尝试一下,有了刚刚的force之后,bad任务即使运行失败了,后续的bar任务也会正常去执行。
但如果我们的任务是异步任务的话,异步任务中我们就没法通过return false去标记任务的失败。此时我们只需要给异步的回调函数指定一个false的实参就可以标记这个任务为失败了。
module.exports = grunt => {
grunt.registerTask('bad', () => {
console.log('bad working');
return false;
})
grunt.registerTask('task1', () => {
console.log('hi task1');
})
grunt.registerTask('task2', () => {
console.log('hi task2');
})
grunt.registerTask('default', ['task1', 'bad', 'task2'])
grunt.registerTask('bad-async', function() {
const done = this.async();
setTimeout(() => {
console.log('bad async');
// 只需要给异步的回调函数指定一个false的实参就可以标记这个任务为失败了
done(false);
}, 1000)
})
}
回到终端,执行yarn grunt bad-async
去运行一下这个任务,这时我们就会发现bad-async任务也是执行失败的。
6、Grunt的配置方法
除了registerTask方法之外,Grunt还提供了一个用于添加配置选项的API,initConfig,例如我们在使用config为我们压缩文件时,就可以通过这种方式去配置我们需要压缩的文件路径。
module.exports = grunt => {
// initConfig接收一个对象形式的参数,对象的属性名一般与我们的任务名称保持一致,
// 属性值可以是任意类型数据。
grunt.initConfig({
task1: 'hi task1'
})
// 有了这个配置属性后,我们就可以在一个任务中使用这个配置属性。
grunt.registerTask('foo', () => {
// 在任务中我们可以通过grunt提供的config方法去获取配置。
// config方法接收一个字符串参数,即我们在config中所指定的属性的名字。
console.log(grunt.config('task1'))
})
}
回到终端,执行yarn grunt task1
,就可以看到控制台中打印出了hi task1
。
除此之外,若我们的initConfig中的属性值是一个对象的话,还有一种高级的方法。
module.exports = grunt => {
grunt.initConfig({
task1: {
task2: 'hi task2'
}
})
grunt.registerTask('foo', () => {
// 可以在config中用"."的方式去拿到对象对应的属性值
console.log(grunt.config('task1.task2'))
})
}
当然,其实在使用时我们一般不会使用”.”的方式,因为可以直接通过config把整个task1对象拿到,然后在对象上用”.”的方式拿到对应的属性。
7、Grunt多目标任务
除了普通的形式以外,Grunt中还支持一种叫做多目标模式的任务,可以理解为子任务的概念。这种形式的任务在后续具体通过Grunt去实现各种构建任务时非常有用。
module.exports = grunt => {
// 多目标模式的任务需要通过RegisterMultiTask去定义。
// 这个方法同样接收两个参数,第一个是任务的名字,第二个是一个函数
grunt.registerMultiTask('build', function() {
// 在函数中仍然是任务执行过程中所需要做的一些事情
console.log('build task');
})
}
回到命令行终端,执行yarn grunt build
去运行一下这个任务,运行之后会报出一个错误,提示我们没有为build任务去设置一些targets。因为我们在设置多目标任务时,需要为多目标任务配置不同的目标,配置的方式就是通过grunt的initConfig方法。
module.exports = grunt => {
grunt.initConfig({
// 在config对象中我们需要去指定一个和我们任务名称同名的一个属性,在此即为build
build: {
//并且对应的属性值必须要是一个对象,对象中每一个属性的名字就是我们的目标任务名称
css: '1',
js; '2',
}
})
grunt.registerMultiTask('build', function() {
console.log('build task');
})
}
回到命令行终端再次执行yarn grunt build
后,我们会发现build会运行两个子任务,不过在grunt中就叫多目标
,也即build中有css及js两个目标。我们在运行build后会同时执行这两个目标,也相当于以两个子任务的形式去运行。如果需要运行指定的目标的话,可以通过yarn grunt build:<目标名称,这里为js或css>
去指定运行对应的目标。另外在我们的任务函数中,可以通过this.target去拿到当前执行的目标名称,并且还可以通过this.data去拿到当前目标所对应的数据。
module.exports = grunt => {
grunt.initConfig({
build: {
css: '1',
js; '2',
}
})
grunt.registerMultiTask('build', function() {
// 通过this.target去拿到当前执行的目标名称,通过this.data去拿到当前目标所对应的数据
console.log(`target: ${this.target}, data: ${this.data}`);
})
}
回到命令行终端执行yarn grunt build:css
后,我们拿到的target就是css,对应的data就是1,这里的1也就是我们在initConfig中指定的1。需要注意的是,我们在build中指定的每一个属性的键都会成为一个目标,但除了options以外,在options中指定的信息会作为任务的配置选项出现。
module.exports = grunt => {
grunt.initConfig({
build: {
// options中指定的信息会作为任务的配置选项出现
options: {
foo: 'bar',
},
css: '1',
js; '2',
}
})
grunt.registerMultiTask('build', function() {
console.log(`target: ${this.target}, data: ${this.data}`);
})
}
此时我们再去执行yarn grunt build
后,我们会发现并没有出现一个target叫options,因为options会作为任务的配置选项出现。我们在任务的执行过程中就可以通过this.options
去拿到对应的配置选项,options是一个方法,可以拿到当前任务中的所有配置选项对象。
module.exports = grunt => {
grunt.initConfig({
build: {
options: {
foo: 'bar',
},
css: '1',
js; '2',
}
})
grunt.registerMultiTask('build', function() {
console.log(`target: ${this.target}, data: ${this.data}`);
// options是一个方法,可以拿到当前任务中的所有配置选项对象
console.log(this.options());
})
}
回到命令行终端中再次执行yarn grunt build
后,命令行中就会显示所有的相关任务配置选项。除了在任务中可以添加配置选项外,如果目标配置也是一个对象的话,我们在目标属性中也可以添加options,在添加options时,会覆盖对象中已存在的options。
module.exports = grunt => {
grunt.initConfig({
build: {
options: {
foo: 'bar',
},
css: {
options: {
foo: 'far',
}
},
js; '2',
}
})
grunt.registerMultiTask('build', function() {
console.log(`target: ${this.target}, data: ${this.data}`);
console.log(this.options());
})
}
回到命令行终端,执行yarn grunt build
后,我们会发现在执行css这个target时options中的foo是far,而在执行js这个target时options中的foo是bar,因为在js中并没有去覆盖任务中的options,而css中则覆盖了。
8、Grunt插件的使用
插件的机制是Grunt的核心,因为很多的构建任务都是通用的,如我们在项目中要压缩代码,而别人的项目中也同样需要。所以在社区中就出现了很多预设的插件,而这些插件的内部都封装了通用的构建任务,一般情况下我们的构建过程都是由这些通用的构建任务组成的。使用插件的过程也非常简单,基本上就是先通过npm去安装这个插件,再到gruntfile.js中去载入这个插件提供的任务,最后根据插件的文档去完成相关的配置选项。
我们接下来通过一个用来自动清除项目开发过程中产生的临时文件的叫grunt-contrib-clean的插件来熟悉一下插件的使用。首先我们进入命令行终端执行yarn add grunt-contrib-clean
去安装这个插件,然后在gruntfile.js中通过grunt.loadNpmTasks方法去加载插件中提供的一些任务。另外,在绝大多数情况下,grunt的插件命名规范都是类似于grunt-contirb-<任务名称>
,所以在这里的clean插件提供的任务名称就叫clean。
// gruntfile.js
module.exports = grunt => {
//在gruntfile.js中通过grunt.loadNpmTasks方法去加载插件中提供的一些任务
grunt.loadNpmTasks('grunt-contrib-clean');
}
回到命令行终端执行yarn grunt clean
去运行一下这个任务,运行的过程中,控制台会报错,并提示我们clean任务并没有配置对应的目标。在错误信息中我们可以发现其实clean就是我们之前介绍的多目标任务,我们需要通过initConfig的方式去配置不同的目标。
// gruntfile.js
module.exports = grunt => {
// 通过grunt.initConfig的方式去为clean任务添加一个目标
grunt.initConfig({
// 配置对象中键对应的目标,值为对应目标需要清除的文件路径
temp: 'temp/app.js'
})
grunt.loadNpmTasks('grunt-contrib-clean');
}
再次回到命令行终端执行yarn grunt clean
后,我们会发现temp下的app.js就会被删除。并且除了指定具体的路径之外,我们还可以在路径中使用通配符的方式去通配一些文件类型,例如想要删除temp目录下的所有.txt文件就可以设置temp/app.js
为temp/*.txt
,在执行命令后temp下的所有.txt文件就都会被删除。除了temp/*.txt
这种方式外,我们还可以使用temp/**
这种通配符方式来删除temp目录下的所有子目录以及子目录下的文件。
总结一下,我们使用Grunt插件的第一步就是找到对应的插件并安装到npm模块中。第二步就是在gruntfile.js中通过 grunt.loadNpmTasks
方法去把插件中提供的一些任务加载进来。然后第三步就是在initConfig中添加一些配置选项,之后我们的插件就可以正常工作了。
9、Grunt常用插件及总结
接下来再介绍几个在Grunt中常用的插件,首先就是grunt-sass。需要注意的是grunt官方也提供了sass模块,但那个模块需要本机安装sass环境,使用起来就不太方便。而我们在这里使用的grunt-sass是一个npm的模块,在项目内部会通过npm形式去依赖sass,使用起来的话就不需要对机器有任何的环境要求。另外因grunt-sass还需要sass的支持,所以还需要安装sass官方提供的npm模块,可以通过执行yarn add grunt-sass sass --dev
命令安装grunt-sass以及sass模块到我们的项目开发依赖中。
安装了模块之后,在gruntfile.js中可以通过grunt.loadNpmTasks去载入grunt-sass,又因grunt-sass是一个多目标任务所以我们也需要通过initConfig去为sass这个任务去配置一些目标。
module.exports = grunt => {
grunt.initConfig({
sass: {
// main目标中需要指定sass的输入文件路径以及对应输出的css文件路径,
// 用files属性去指定
main: {
// files是一个对象,键是需要输出的css路径,值是sass文件的路径
files: {
'dist/css/main.css': 'src/scss/main.scss'
}
}
}
});
grunt.loadNpmTasks('grunt-sass')
}
之后回到命令行终端执行yarn grunt sass
去运行一下这个任务,运行后控制台会输出一个错误,提示我们没有传入一个implementation option,这个implementation option其实是用来指定我们在grunt sass中使用哪个模块去处理sass编译。
所以我们还得回到代码中为sass这个任务添加一个options。
const sass = require('sass')
module.exports = grunt => {
grunt.initConfig({
sass: {
// 在options中要去添加一个implementation,注意还需要在顶部引入sass模块
options: {
implementation: sass,
// 还可以添加一个sourceMap选项,在编译后会自动生成对应的sourceMap文件
sourceMap: true,
},
main: {
files: {
'dist/css/main.css': 'src/scss/main.scss'
}
}
}
});
grunt.loadNpmTasks('grunt-sass')
}
有了相关的配置后再回到命令行终端执行yarn grunt sass
去运行sass这个任务,此时我们就会发现在项目的根目录下多了一个dist目录,里面有一个css文件夹,再里面就是我们编译后的css文件与对应的sourceMap文件了。以上就是sass插件在grunt中的基本使用。除此之外grunt-sass还提供更多的选项,我们可以通过grunt-sass官方仓库的文档中(传送门)找到相关信息。
除sass之外,我们还经常在编译中遇到的需求就是编译es6的语法,而对于es6的语法编译器我们使用最多的就是babel。在grunt中我们要去使用babel的话也可去使用一个grunt-babel的插件,我们可以执行yarn add grunt-babel @babel/core @babel/preset-env --dev
命令去在开发依赖中安装grunt-babel与babel的核心模块@babel/core以及babel的预设@babel/preset-env。
有了这三个模块后我们就可以在gruntfile.js中去使用babel里提供的一些任务了,这时回到gruntfile.js,我们会发现又需要通过loadNpmTasks去加载grunt-babel中提供的一些任务了。随着我们的gruntfile.js越来越复杂,loadNpmTasks中的操作也会越来越多,不过其实社区中也有模块去减少loadNpmTasks的使用。具体使用我们可以先执行yarn add load-grunt-tasks --dev
去安装一个叫load-grunt-tasks的模块到开发依赖中。安装完这个模块后我们可以对其先进行导入const loadGruntTasks = require('load-grunt-tasks')
,导入后我们就可以直接使用loadGruntTasks(grunt)
去自动加载所有的 grunt 插件中的任务了。
const sass = require('sass')
// 导入loadGruntTasks
const loadGruntTasks = require('load-grunt-tasks')
module.exports = grunt => {
grunt.initConfig({
sass: {
options: {
implementation: sass,
sourceMap: true,
},
main: {
files: {
'dist/css/main.css': 'src/scss/main.scss'
}
}
}
});
// 导入loadGruntTasks后直接使用loadGruntTasks(grunt)去自动加载所有的 grunt 插件中的任务
loadGruntTasks('grunt-sass')
}
因loadGruntTasks中加载了Grunt-babel里提供的一些任务,所以我们再回到grunt-babel中,去为grunt-babel提供一些目标配置。
const sass = require('sass')
const loadGruntTasks = require('load-grunt-tasks')
module.exports = grunt => {
grunt.initConfig({
sass: {
options: {
implementation: sass,
sourceMap: true,
},
main: {
files: {
'dist/css/main.css': 'src/scss/main.scss'
}
},
},
babel: {
// 对于babel任务同样需要配置选项,具体就是需要配置babel转换时的preset
options: {
// babel支持转换最新ECMAScript的特性,而preset就是需要转换特性的包。
// 这里使用的preset叫env,会根据最新的ES特性去做对应的转换。
presets: ['@babel/preset-env'],
// babel中也同样支持sourceMap
sourceMap: true,
},
main: {
// 同样在Babel的main目标的files属性里指定编译文件的输入与输出
files: {
// 键中是输出文件,对应的值为输入文件
'dist/js/app.js': 'src/js/app.js'
}
}
}
});
loadGruntTasks('grunt-sass')
}
回到命令行中执行yarn grunt babel
后,我们会发现在dist目录下的js文件夹中就会出现编译后的js文件及其对应的sourceMap文件了。
还有一个我们在使用构建系统经常遇到的需求就是当文件修改完了之后需要自动去编译,这时在grunt中我们就需要另外一个叫grunt-contrib-watch的插件。通过执行yarn add grunt-contrib-watch
,我们把grunt-contrib-watch模块添加到开发依赖中,安装完成后,loadGruntTasks会自动把watch这个任务也加载进来,我们就不需要再使用loadNpmTasks了。接下来我们就可以直接为watch添加配置选项就了。
const sass = require('sass')
const loadGruntTasks = require('load-grunt-tasks')
module.exports = grunt => {
grunt.initConfig({
sass: {
options: {
implementation: sass,
sourceMap: true,
},
main: {
files: {
'dist/css/main.css': 'src/scss/main.scss'
}
},
},
babel: {
options: {
presets: ['@babel/preset-env'],
sourceMap: true,
},
main: {
files: {
'dist/js/app.js': 'src/js/app.js'
}
}
},
watch: {
// 在watch中我们需要去添加不同的目标
// 用js目标用来专门监视js的变化
js: {
//这里的files只需要监视特定的源文件就可以了,所以对应的值就为一个数组
files: ['dist/js/*.js'],
//还需要添加一个tasks,指定当文件改变后需要执行的任务
tasks: ['babel']
},
// 用css目标用来监视sass文件的变化
css: {
//注意sass最早的扩展名是sass,不过因语法规范难以接受,
//后来就新出了一个语法规范,用scss作为扩展名了。
files: ['src/scss/*.scss'],
tasks: ['sass']
}
}
});
loadGruntTasks('grunt-sass')
}
回到命令行终端执行yarn grunt watch
运行一下watch这个任务,注意watch任务启动后并不会直接去执行babel和sass对应的一些任务,他只是会去监视文件,一旦当文件发生变化后,watch才会去执行对应的任务。又因watch刚开始并不会去执行babel和sass,所以我们一般会给watch做一个映射。
const sass = require('sass')
const loadGruntTasks = require('load-grunt-tasks')
module.exports = grunt => {
grunt.initConfig({
sass: {
options: {
implementation: sass,
sourceMap: true,
},
main: {
files: {
'dist/css/main.css': 'src/scss/main.scss'
}
},
},
babel: {
options: {
presets: ['@babel/preset-env'],
sourceMap: true,
},
main: {
files: {
'dist/js/app.js': 'src/js/app.js'
}
}
},
watch: {
js: {
files: ['dist/js/*.js'],
tasks: ['babel']
},
css: {
files: ['src/scss/*.scss'],
tasks: ['sass']
}
}
});
loadGruntTasks('grunt-sass');
// 用grunt.registerTasks使运行对应任务时先运行sass、babel,最后再运行watch
grunt.registerTasks('default', ['sass', 'babel', 'watch']);
}
回到命令行终端执行yarn grunt
后,我们就会发现命令行中提示先执行sass与babel任务然后再启动watch监听。
以上就是在使用grunt中最常用的一些小插件,除此之外其实grunt中还提供了其他很多的插件,但我们在此就不做过多介绍了,因为grunt其实基本上已经算是退出历史舞台了。我们介绍grunt的最主要原因也是因为他应该算是构建工具的鼻祖并且在之后介绍gulp时也能起到抛砖引玉的作用。
10、Gulp的基本使用
Gulp作为当下最流行的前端构建系统,其核心特点就是高效易用,使用Gulp的过程非常简单,大体过程就是先在项目中安装一个叫做gulp的开发依赖,然后在项目的根目录也即package.json所在目录中添加一个用于编写Gulp自动执行任务的gulpfile.js文件。完成之后我们就可以在命令行中使用Gulp提供的cli去运行这个任务了。
在空项目文件夹中执行yarn init --yes
初始化项目的package.json后,再执行yarn add gulp --dev
把gulp作为一个开发依赖来安装。安装gulp的同时,还会同时安装一个gulp-cli的模块,即此时我们在node_modules中会出现一个gulp命令,有了这个命令后我们就可以在后续通过这个命令去进行一些构建任务了。此时我们再到项目根目录中创建一个gulpfile.js文件,在这个文件中,我们可以去定义一些需要gulp执行的构建任务,即这个文件是gulp的入口文件。因这个文件是运行在nodejs的环境中,所以我们可以在这个文件中使用commonjs规范,而这个文件中定义构建任务的方式就是通过导出函数成员的方式去定义。
// gulpfile.js
// 通过exports.foo导出一个叫foo的成员,值为一个函数
exports.foo = () => {
// 在函数体中我们通过console.log去提示一下函数的执行
console.log('foo task working!');
}
回到命令行终端,通过gulp提供的cli去运行这个任务,即执行yarn gulp foo
,其中foo参数为指定foo这个任务。执行后,我们会发现命令行中的确显示有运行过foo任务,但同时还会报出一个错误,大体是说我们的foo任务没有执行完成,并问我们是否忘了标识任务的结束。出现这个错误是因为在最新的gulp中,取消了同步代码模式,而约定每一个任务都必须是异步的任务,当我们的任务执行完成后,我们就需要通过调用回调函数或其他的方式标记该任务已经完成。所以我们为了解决这个问题需要做的就是手动去调用一个回调函数,而这个回调函数我们可以在代码中通过foo的形式参数得到。
// gulpfile.js
// 接收一个叫done的参数,done为一个函数
exports.foo = done => {
console.log('foo task working!');
done(); //任务完成后调用done来标识任务完成
}
此时再回到命令行终端重新执行yarn gulp foo
命令,我们会发现foo这个任务已经能够正常启动与结束了。上面就是在gulp中定义一个任务的操作方式,而如果我们的任务名称是default的话,该任务就会作为gulp的默认任务出现。
// gulpfile.js
exports.foo = done => {
console.log('foo task working!');
done();
}
// 如果我们的任务名称是default的话,该任务就会作为gulp的默认任务出现
export.default = done => {
console.log('default task working!');
done();
}
因为default会作为gulp的默认任务出现,所以我们在运行他的时候就不需要指定任务名称参数了。直接执行yarn gulp
就可以运行default任务了。
除此之外,大家还需要注意的一点是在gulp4.0以前,我们注册gulp的任务是需要通过gulp模块里的一个方法去实现的。
// gulpfile.js
exports.foo = done => {
console.log('foo task working!');
done();
}
export.default = done => {
console.log('default task working!');
done();
}
// 在gulp4.0以前,我们注册gulp的任务是需要通过gulp模块里的一个方法去实现的
const gulp = require('gulp');
// 用gulp.task去注册一个任务,具体过程也和上面类似
gulp.task('bar', done => {
console.log('bar task working!');
done();
})
回到命令行终端后再尝试执行yarn gulp bar
去运行一下bar任务,此时我们会发现bar任务也可以正常去工作,这是因为在gulp4.0后的版本中依旧保留了这个API。虽然说我们现在还可以直接使用这个API,但这种方式已经不被推荐了,这里更推荐大家的使用方式就是导出函数成员的方式来定义gulp任务。
11、Gulp的组合任务
除了创建普通的任务以外,Gulp的最新版本还提供了series和parallel这两个用来创建组合任务的API,有了这两个API后我们就可以很轻松地创建并行任务和串行任务。
// gulpfile.js
const { series, parallel } = require('gulp')
// 可以把未被导出的函数理解成私有任务,我们并不能用gulp直接运行这些任务。
// 但可以通过gulp提供的series与parallel这两个API把这些任务进行组合。
const task1 = done => {
setTimeout(() => {
console.log('task1 working!')
done()
}, 1000)
}
const task2 = done => {
setTimeout(() => {
console.log('task2 working!')
done()
}, 1000)
}
const task3 = done => {
setTimeout(() => {
console.log('task3 working!')
done()
}, 1000)
}
// 用series组合任务,每个参数都是一个任务,让多个任务按照顺序依次执行
exports.foo = series(task1, task2, task3)
// 用parallel组合任务,每个参数都是一个任务,可以让多个任务同时执行
exports.bar = parallel(task1, task2, task3)
回到命令行终端中执行yarn gulp foo
后可以发现task1、task2、task3会串行执行,而执行yarn gulp bar
后这些任务则会并行执行。而创建串行任务以及创建并行任务在我们的实际构建工作流时非常有用,例如我们去编译CSS与JS任务时,他们是互不干扰的,这两个任务我们就可以通过并行的方式去执行,从而提高一些构建效率。又比如我们在部署项目时,需要先编译任务,这时我们就需要通过series串行模式去执行任务。
12、Gulp的异步任务
我们在之前曾提到过Gulp中的任务都是异步任务,也就是我们在JS中经常提到的异步函数。我们应该知道,在调用异步函数时是没有办法直接明确这个调用是否完成的,一般都是在函数的内部通过回调或事件的方式去通知外部这个事件已经完成。而我们在异步任务中同样面临如何去通知gulp我们的完成情况这个问题,针对这个问题,gulp中有很多解决方法,下面我们就来了解一下几个最常用的方式。
// gulpfile.js
// 第一种是通过回调的方式去通知gulp任务的完成情况
// 在任务的函数中接收一个回调函数形参
exports.callback = done => {
console.log('callback task!');
//在任务完成后调用一下这个回调函数,通知gulp我们的任务完成了
done();
}
回到命令行终端中执行yarn gulp callback
去运行一下这个任务,正常执行。这个回调函数与node中的回调函数是同样的标准,都是一种“错误优先”的回调函数,也即当我们想在执行过程中报出一个错误去阻止剩下的任务执行的时候,我们可以通过给回调函数的第一个参数指定一个错误对象即可。
// 第一种是通过回调的方式去通知gulp任务的完成情况
exports.callback = done => {
console.log('callback task!');
done();
}
exports.callback_error = done => {
console.log('callback task!');
// 可以通过给回调函数的第一个参数指定一个错误对象来在执行过程中报出一个错误去阻止剩下的任务执行
done(new Error('task failed!'));
}
回到命令行中执行yarn gulp callback_error
,之后命令行中会报出一个错误并且如果我们是多任务同时执行的话,后续的任务也不会再去工作了。
有了回调函数,我们自然会联想到ES6中提供的一个叫做Promise的方案。Promise相对于回调来说是一个相对比较好的替代方案,因为他避免了我们代码中回调嵌套过深的问题,而在gulp中同样支持Promise的方式。
//第二种,通过promise的方式去通知gulp任务的完成情况
exports.promise = () => {
console.log('promise task!');
// 在任务的执行函数中return一个Promise对象,
// 一旦当返回的promise是resolve的,就意味着任务结束了。
// 注意在resolve中不需要返回任何值,在gulp中会忽略掉这些值
return Promise.resolve();
}
回到命令行中执行yarn gulp promise
命令,可以看到这个任务是能正常开始与结束的。当然,使用Promise自然也会涉及到Promise的reject,也即promise失败,一旦return的是一个reject的promise的话,gulp则会认为这是一个失败的任务,同样会结束后续所有任务的执行。
//第二种,通过promise的方式去通知gulp任务的完成情况
exports.promise = () => {
console.log('promise task!');
return Promise.resolve();
}
exports.promise_error = () => {
console.log('promise task!');
// 一旦return的是一个reject的promise的话,gulp则会认为这是一个失败的任务
return Promise.reject(new Error('task failed!'));
}
回到命令行中执行yarn gulp promise_error
后,则会看到任务运行失败的提示。用到了Promise后,我们自然会想到ES2018中提到的async和await,async与await实际上是Promise的一种语法糖,可以让promise的代码更易于理解。如果我们的node环境是8以上版本的话则可以使用这个方式。
//第二种,通过promise的方式去通知gulp任务的完成情况
exports.promise = () => {
console.log('promise task!');
return Promise.resolve();
}
exports.promise_error = () => {
console.log('promise task!');
return Promise.reject(new Error('task failed!'));
}
// 定义一个单独的promise函数用于下面的async任务调用
const timeout = time => {
return new Promise(resolve => {
setTimeout(resolve, time);
})
}
// 将我们的任务函数定义为异步函数,在函数中await一个异步的函数
exports.async = async () => {
await timeout(1000);
console.log('async task!')
}
回到命令行中执行yarn gulp async
后则会看到async任务的执行。
以上都是我们在JavaScript中处理异步的常见方式,这些方式在gulp中也都被支持。除了这些方式以外,gulp还支持另外几种方式,其中通过stream的方式最为常见,又因我们的构建系统一般都是在处理文件,所以这种方式也是最常用到的一种。
// 第三种,通过stream的方式去通知gulp任务的完成情况
// 需要在任务函数中返回一个stream对象
// 例如我们可以通过fs中提供的createReadStream的方法创建一个读取文件的文件流
const fs = require('fs');
exports.stream = () => {
//这里尝试读取一下package.json,这时readSteam就是一个文件流对象
const readStream = fs.createReadStream('package.json');
//我们还需要去创建一个写入文件的文件流,这里我们写入到temp.txt文件里
const writeStream = fs.createWriteStream('temp.txt');
//这时我们可以把readStream通过pipe的方式导到writeStream中
//可以理解为把水流从一个池子里倒到另一个池子里,这里起到文件复制的作用
readStream.pipe(weiteStream);
//最后再把stream给return出去
return readStream;
}
之后再回到命令行执行yarn gulp stream
运行stream任务,我们会发现这个任务也可以正常开始并结束,而这个结束的时机其实就是readStream结束(end)
的时候。因为stream中都有一个事件即end事件,一旦读取的文件流完成之后就会触发end事件,从而gulp就知道了这个任务已经完成了。我们也可以通过下面的代码去模拟一下gulp中做的事情。
// 第三种,通过stream的方式去通知gulp任务的完成情况
const fs = require('fs');
exports.stream_sim = () => {
const readStream = fs.createReadStream('package.json');
const writeStream = fs.createWriteStream('temp.txt');
readStream.pipe(weiteStream);
// gulp中接收到stream之后只是为他注册了一个end事件,在end事件中结束了任务的执行
readStream.on('end', () => {
// 我们通过end事件调用一下done函数去模拟一下gulp中结束这个任务的操作。
done();
})
return readStream;
}
我们回到命令行中执行yarn gulp stream_sim
去运行这个任务,之后我们会发现这个任务也可以正常结束,这也表明其实gulp当中只是注册了一个事件去监听这个任务的结束而已。
以上,就是我们在gulp中经常会用到的一些处理异步流程的操作。
13、Gulp构建过程核心工作原理
在了解了Gulp中如何定义任务之后,我们还需要了解在这些任务中具体要做的工作,也即所谓的构建过程。构建过程大多数情况下都是将文件读取出来进行一些转换,最后写入到另外一个位置,我们可以想象一下在没有构建系统的情况下,我们也都是人工按照这个过程去做的。例如我们压缩一个css文件,就需要把代码复制出来,到一个压缩工具中进行压缩,最后再将压缩的结果粘贴到一个新文件中,这是一个手动的过程。而通过代码的方式去解决也是类似的,下面我们就来通过最原始的底层node的文件流API去模拟实现一下这样一个过程。
相关文件下载:github传送门
// gulpfile.js
const fs = require('fs');
exports.default = () => {
//导入fs模块后再去创建一个文件的读取流,通过fs的createReadStream方法去创建,参数为需要读取的文件路径
const read = fs.createReadStream('normalize.css');
//创建文件写入流,通过fs的createWriteStream方法创建,参数为写入的文件路径
const write = fs.createWriteStream('normalize.min.css');
//有了文件读取流及写入流后我们要把读取的文件流导入到写入流中
read.pipe(write);
//最后把stream给return出去,这样gulp就可以根据流的状态去判定这个任务是否完成
return read;
}
回到命令行中执行yarn gulp
去运行default任务,完成之后我们就可以在normalize.min.css中看到与normalize.css相同的内容,也即文件已经被复制了。但我们要做的是把文件内容读取出来,经过转换之后再去写入文件,所以还需要导入stream中的transform类。有了transform类后,我们就可以通过这个类去创建一个文件转换流对象。
// gulpfile.js
const fs = require('fs');
// 导入stream中的transform类
const { Transform } = require('stream');
exports.default = () => {
const read = fs.createReadStream('normalize.css');
const write = fs.createWriteStream('normalize.min.css');
// 通过transform类去new一个文件转换流对象
const transform = new Transform({
//在transform中需要指定一个transform属性,是转换流的核心转换过程
transform: (trunk, encoding, callback) => {
//可以通过函数中的trunk拿到文件读取流中读取到的文件内容(Buffer)
//因为trunk中保存的是字节数组,所以要再用toString方法把trunk转换为字符串
const input = chunk.toString();
//之后在input中就是对应的文本内容了,我们可以用replace把空白字符以及注释都先替换掉
const output = input.replace(/\s+/, '').replace(/\/*.+?\*\//g, '');
//最后我们再在callback中将output返回出去,而output就会作为转换完的结果接着往后导出
//另外还需要注意callback是错误优先的函数,第一个参数应该传入错误对象,如果没有错误可以传null
callback(null, output);
}
})
//在pipe到write流之前先pipe到transform流里去进行转换
read.pipe(transform)
.pipe(write);
return read;
}
回到命令行中执行yarn gulp
去重新运行这个任务,之后我们会发现在normalize.min.css中就是一个被转换压缩后的结果了。上面就是gulp中常规的构建任务的核心工作过程,这个过程中有三个核心的概念,分别是读取流、转换流、写入流,对应文件的输入、加工、输出。
gulp的官方定义就是The streaming build system
,即基于流的构建系统,至于在gulp构建过程中为什么选择使用文件流的方式,是因为gulp希望实现一个构建管道的概念,这样的话我们在后面去做一些扩展插件的时候就可以有一个很统一的方式。而这些将会在我们接触到插件的使用之后产生明确的体会。
14、Gulp文件操作
gulp中为我们提供了专门用于去创建读取流和写入流的API,相比于底层node的API,gulp的API更强大,也更容易使用,而对于文件加工的转化流,绝大多数情况下我们都是通过独立的插件来提供。我们在实际通过gulp去创建构建任务时的流程,一般就是先通过src方法创建一个读取流,然后再借助于插件提供的转换流来实现文件加工,最后再通过gulp提供的dest方法去创建一个写入流从而写入到目标文件。
相关文件下载:github传送门
// 通过require去载入gulp模块中提供的src与dest方法
const { src, dest } = require('gulp')
exports.default = () => {
// 在默认的任务中通过src方法创建一个文件的读取流,
// 再通过pipe的方式导出到dest所创建的写入流中,
// 其中dest的参数只需要指定写入的目标目录即可,
// 最后将创建的读取流return出去,让gulp控制任务的完成
return src('src/normalize.css')
.pipe(dest('dist'))
}
回到命令行执行yarn gulp
去运行一下default任务,之后我们会发现,dist目录下会多出来一个normalize.css文件,也意味着我们文件的读取流和写入流是可以正常工作的。就像我们之前提过的,相比与原始的API,gulp模块所提供的API会更加强大一些,因为我们可以使用通配符的方式去匹配批量的文件。
const { src, dest } = require('gulp')
exports.default = () => {
// 将上面的normalize改为*号,去通配src目录下所有的css文件
return src('src/*.css')
// dest是指destination(目标),而dist是指distribution(分发,发布)
.pipe(dest('dist'))
}
回到命令行,重新执行yarn gulp
去运行default任务,之后我们会发现除了normalize.css文件,src下的其他css文件也会被复制到dist目录。而对于构建环节最重要的文件转换,比如在这里我们要完成文件的压缩转换的话,就可以去安装一下gulp-clean-css这个插件,这个插件就提供了压缩css代码的转换流。我们先通过yarn add gulp-clean-css --dev
去安装一下这个插件,安装完成后就可以回到代码中通过require的方式去载入这个插件。有了这个插件后我们就可以在pipe到dest之前先去pipe到cleanCss提供的转换流中,这样的话就会对读取流先进行转换最后再被写入到写入流中。
const { src, dest } = require('gulp')
// 在代码中通过require的方式去载入gulp-clean-css插件
const cleanCss = require('gulp-clean-css')
exports.default = () => {
return src('src/*.css')
// 在pipe到dest之前先去pipe到cleanCss提供的转换流中
.pipe(cleanCss())
.pipe(dest('dist'))
}
回到命令行,重新执行yarn gulp
去运行default任务,之后我们就可以看到dist目录下的所有css文件都是压缩过后的css文件了。当然,如果我们还需要执行多个转换的话,可以继续在中间添加额外的pipe操作,例如我们再执行yarn add gulp-rename --dev
去添加一个叫gulp-rename的插件来指定pipe到dest后重命名文件的扩展名。
const { src, dest } = require('gulp')
const cleanCss = require('gulp-clean-css')
// 在代码中通过require的方式去载入gulp-rename插件
const rename = require('gulp-rename')
exports.default = () => {
return src('src/*.css')
.pipe(cleanCss())
// pipe到rename的转换流中,设置extname参数去指定重命名的扩展名为.min.css
.pipe(rename({ extname: '.min.css' }))
.pipe(dest('dist'))
}
回到命令行,重新执行yarn gulp
去运行default任务,之后我们会发现dist目录下会出现对应的.min.css文件了。以上这种通过src去pipe到一些插件转换流再去pipe到写入流的过程就是我们使用gulp的常规过程了。