一篇文章带你深入当下最流行的前端构建系统 Gulp
Gulp基本介绍
Gulp是当下最流行的前端自动化构建系统系统系统系统,特点高效、易用
Gulp基本使用
- 安装依赖
yarn add gulp --dev
-
在根目录中添加gulpfile.js
-
在gulpfile.js中添加构建任务
构建任务即在gulpfile.js中导出函数成员
exports.foo=done=>{
console.log('foo task working')
// 需要调用done函数来标记任务结束
done()
}
执行foo任务:
yarn gulp foo
Gulp的default任务
当任务名是default的时候,执行此任务不需要指定任务名,执行yarn gulp
就默认执行的是default任务
exports.default=done=>{
console.log('defaulttask working')
// 需要调用done函数来标记任务结束
done()
}
gulp 的task方法
gulp的task方法是4.0版本以前的一个注册任务的方法,现在依然保留,使用此方法需要引入gulp作为依赖,现在已经不推荐使用了
const gulp=require('gulp')
gulp.task('bar',done=>{
console.log('bar working')
done()
})
Gulp的组合任务:series和parallel的使用
series组合串行任务,任务执行有前后顺序
parallel组合并行任务,任务执行没有顺序
const {series,parallel} =require('gulp')
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);
}
exports.seriesTask=series(task1,task3,task2)
exports.parallelTask=parallel(task1,task3,task2)
执行seriesTask:
可以看见任务是一个一个顺序执行的,总耗时为3.01s
执行parallelTask:
并行任务同时执行,总耗时1.01s
实际工作中,比如编译js和css,它们是互不干扰的,可以使用parallel,而像部署任务这种,需要先编译,再打包,然后再发布,这样的任务则需要使用series
Gulp的异步任务的三种方式
1、 通过回调的方式来标记任务完成,
const task1=done=>{
setTimeout(() => {
console.log('task1 working')
// 调用回调来标记任务完成
done()
}, 1000);
}
当想要标记任务失败时,可以在回调中传入一个
const task1=done=>{
setTimeout(() => {
console.log('task1 working')
// 调用回调来标记任务完成
done(new Error('task failed!'))
}, 1000);
}
2、通过Promise来处理异步任务
exports.promiseTask=()=>{
console.log('promise task working..')
// 这里并不需要传入什么,因为gulp会忽略掉这个值
return Promise.resolve()
}
要标记任务失败的话就返回Promise.reject
exports.promiseTask_error=()=>{
console.log('promise task working..')
return Promise.reject(new Error('task failed!'))
}
3、使用async/await(node8以上版本)
const asyncTask=time=>{
return new Promise(resolve=>{
setTimeout(resolve,time)
})
}
exports.asyncTask=async ()=>{
await asyncTask(1000)
console.log('asyncTask working..')
}
stream文件流
const fs=require('fs')
// 读取package.json的内容并写入到temp.txt文件中
exports.copyfile=()=>{
const readStream=fs.createReadStream('package.json')
const writeStream=fs.createWriteStream('temp.txt')
// 把读取流通过管道导入写入流
readStream.pipe(writeStream)
// 返回流,gulp会根据流的状态来判断任务是否完成
return readStream
}
Gulp构建过程核心工作原理
首先为看一个css文件的转换、压缩的构建过程
const fs=require('fs');
const { Transform } = require('stream');
exports.copyfile=()=>{
// 文件读取流
const readStream=fs.createReadStream('package.json')
// 文件写入流
const writeStream=fs.createWriteStream('temp.txt')
// 文件转换流
const transform=new Transform({
transform:(chunk,encoding,callback)=>{
// 核心转换过程实现
// chunk=> 读取流中读取到的内容(Buffer,字节数组)
const input=chunk.toString()
// 替换文件中的空白字符和注释
const output=input.replace(/\s+/g,'').replace(/\/\*.?\*\//g,'')
// callback 是一个错误优先的函数,第一个参数是错误信息,没有则传入null
callback(null,output)
}
})
// 把读取流通过管道导入写入流
readStream
.pipe(transform)
.pipe(writeStream)
return readStream
}
从上面代码表示的过程中包含三个概念,分别是读取流、转换流、写入流
通过读取流把需要转换的文件读取出来,然后通过转换流进行转换,最后使用写入流来写入到指定的文件
Gulp的官方定义是The streaming build system,基于流的构建系统
至于在gulp构建过程中为什么使用文件流的方式,是因为gulp希望实现构建管道的概念,这样的话在后续制作扩展插件的时候可以有一种很统一的方式
Gulp的文件操作API
前面的例子中使用fs来读取和写入文件,其实gulp有提供文件读取方法src,和文件写入方法dest,另外一般文件内容的转换是通过gulp的插件来完成,看下面例子:
const {src, dest} =require('gulp')
const cleanCss =require('gulp-clean-css')
const rename=require('gulp-rename')
exports.default=()=>{
// 读取流
return src('src/normalize.css')
// 压缩css代码
.pipe(cleanCss())
// 文件后缀重命名
.pipe(rename({extname:'.min.css'}))
// 写入流
.pipe(dest('dist'))
}
上面的例子使用到了gulp的src来读取css文件
并通过gulp-clean-css插件来压缩css文件
然后通过gulp-rename插件来重命名文件,为文件添加min后缀
最后通过gulp的dest方法写入到dist文件夹中
插件需要安装
yarn add gulp-clean-css gulp-rename --dev
Gulp样式编译
以sass为例,需要先安装gulp-sass插件
yarn add gulp-sass --dev
const {src, dest} =require('gulp')
const sass=require('gulp-sass')
exports.style=()=>{
// 指定base可以保持文件目录结构
return src('src/styles/*.scss',{base:
'src'})
// 使用sass来转换scss文件,并设置格式为完全展开,因为默认的会使每个样式结尾的“}”都根随代码结尾,而不是换行
.pipe(sass({outputStyle:'expanded'}))
.pipe(dest('dist'))
}
Gulp脚本编译
编译脚本中的es6+语法,需要使用babel
yarn add gulp-babel @babel/core @babel/preset-env --dev
const {src, dest} =require('gulp')
const sass=require('gulp-babel')
exports.scripts=()=>{
return src('src/scripts/*.js',{base:'src'})
// 这里需要添加presets,不然转换不生效
.pipe(babel({presets:['@babel/preset-env']}))
.pipe(dest('dist'))
}
Gulp页面模板编译
在页面模板中使用swig模板引擎
yarn add gulp-swig --dev
// 模板中需要的数据
const data={
name:'myweb',
description:'hello gulp'
}
const {src, dest} =require('gulp')
const swig=require('gulp-swig')
exports.html=()=>{
return src('src/tempaltes/*.html',{base:'src'})
// 在swig插件中传入数据
.pipe(swig({data}))
.pipe(dest('dist'))
}
模板文件:
编译后的文件:
图片和字体文件的转换
图片的转换需要使用imagemin插件,imagemin插件需要使用到github上的一些c++的二进制资源,安装可能不成功,可以使用cnpm来安装可能会好一点
cnpm install gulp-imgagemin --dev
const imagemin=require('gulp-imagemin')
exports.image=()=>{
return src('src/images/**',{base:'src'})
.pipe(imagemin())
.pipe(dest('dist'))
}
字体文件的处理和图片一样都可以用imagemin插件来完成
exports.font=()=>{
return src('src/fonts/**',{base:'src'})
.pipe(imagemin())
.pipe(dest('dist'))
}
其它文件和文件的清除
把public的文件直接拷贝到dist目录中
exports.extra=()=>{
return src('public/**',{base:'public'})
.pipe(dest('dist'))
}
清除文件任务需要使用del插件:
yarn add del --dev
组合任务
前面已经讲过,gulp可以通过series和parallel来组合串行和并行任务,那么上面的编译样式、脚本、模板html、字体和图片的任务是各不相关的,可以使用parallel来进行组合,提高编译效率
exports.compile=parallel(this.style,this.scripts,this.html,this.image,this.font)
对于pulic目录的拷贝,其实也可以放到compile中,但是因为compile只是针对src目录,对于extra可以放到build任务中,build同时包含compile,这样显得更清晰一点,还有clean任务需要优先执行完成
exports.build=series(this.clean,parallel(this.compile,this.extra))
自动加载插件
可以使用gulp-load-plugins插件来自动加载插件,而不再需要重复的手动引入 每一个插件了,需要安装此插件:
yarn add gulp-load-plugins --dev
引入插件:
const plugins =loadPlugins()
需要注意的是自动加载插件会把所有的插件都归属为 plugins 的成员属性,所有使用时插件需要加上"plugins."前缀比如前面的imagemin需要改为如下所示:
// const imagemin=require('gulp-imagemin') // 使用loadPlugins之后 ,这句不需要了
exports.image=()=>{
return src('src/images/**',{base:'src'})
.pipe(plugins.imagemin())
.pipe(dest('dist'))
}
启用热更新开发服务器
安装browser-sync模块
yarn add browser-sync --dev
exports.serve=()=>{
bs.init({
notify:false,// 关闭browser-sync的提示
// open:false,// 是否自动访问
// files:监听dist目录中的文件的变动
files:'dist/**',
server:{
// 启动目录
baseDir:'dist'
}
})
}
监视变化及构建优化
上面的serve任务只是能够监听dist目录的文件变化,但我们需要的是在开发时的src目录的变动,还需要对serve添加watch选项来监听文件的变动
const { watch } = require('browser-sync')
exports.serve=()=>{
//watch第一个参数是监听的路径,第二个参数是指定任务
watch('src/styles/*.scss',this.style)
watch('src/scripts/*.js',this.scripts)
watch('src/*.html',this.html)
// 对于图片和字体,压缩前后并不会有显示上的变化,压缩是无损的,并不需要对其进行监听
// 还有public目录是静态目录,也不需要监听
// 若你真的需要监听,可以像下面这样写,因为这几种文件需要执行的任务都是一样的,可以写到一个数组中,以减少任务执行次数
// watch([
// 'src/images/**',
// 'src/fonts/**',
// 'public/**'
// ],bs.reload)
bs.init({
notify:false,
// 指定端口
prot:2080,
//open:false,
// 表示监听dist目录中的文件的变动
files:'dist/**',
server:{
// 这里会按数组由左往右的顺序找文件,即优先找dist目录
baseDir:['dist','src','public'],
}
})
}
另外,对于开发环境和生产环境执行的任务也是不一样的,生产环境需要首先清除dist目录,然后进行编译,同时还要对图片和字体文件进行压缩,对静态文件进行拷贝。
开发环境则不需要清除dist,图片和字体也不需要压缩,因为browser-sync会根据配置先在dist目录中找文件,没有则在src中找,还没有再到public目录中找,开发环境还需要启动热更新服务器,所以组合任务更改如下:
// 编译任务
exports.compile=parallel(this.style,this.scripts,this.html)
// 生产执行任务
exports.build=series(this.clean,parallel(this.compile,this.image,this.font,this.extra))
// 开发执行任务
exports.dev=series(this.compile,this.serve)
gulp-useref
当项目文件中有对类似在node_modules中的文件的引入时,比如对/node_modules/bootstrap/dist/css/bootstrap.css的引入,这样的文件在开发时,可以通过在serve中配置路由映射来进行处理:
bs.init({
notify:false,
prot:2080,
//open:false,
// 表示监听dist目录中的文件的变动
files:'dist/**',
server:{
// 这里会按数组由左往右的顺序找文件,即优先找dist目录
baseDir:['dist','src','public'],
routes:{
'/node_modules':'node_modules'
}
}
})
但这样并不能解决在生产环境时的情况,因为对应的文件并没有拷贝到dist目录
这里可以使用useref插件来获取引用的文件并拷贝到指定路径,useref可以处理的引用标签外添加的构建注释,如下图所示,另外如果构建中包含多个引入,则会把这些引入打包到同一个文件中
<!-- build:css styles/vender.css -->
<link rel="stylesheet" href="/node_modules/bootstrap/dist/css/bootstrap.css">
<!-- endbuild -->
<!-- build:css styles/main.css -->
<link rel="stylesheet" href="styles/main.css">
<!-- endbuild -->
useref处理过后的引入:
分别压缩html、css、js
上面的useref已经把引入的资源获取并打包进了dist目录,但是还存在问题就是这些文件不一定是被压缩的,现在来对html、css、js分别进行不同的压缩工作
需要分别安装对应插件:gulp-htmlmin、gulp-clean-css、gulp-uglify
这时候对三种不同类型的文件进行操作,需要对文件类型进行区分,需要使用插件gulp-if
另外上面的usered输出到release目录中,而其它文件依然在dist目录中,这样是不符合我们预期的,我们需要把全部的文件都放到dist中,那么我们可以把html、css、js这些需要语法编译的内容先通过compile输出到temp目录中,再通过useref把这些文件从temp经处理再输出到dist中
style、html、scirpts三个任务需要进行一些调整,即dest的输出路径改为temp
exports.style=()=>{
return src('src/styles/*.scss',{base:
'src'})
// 使用sass来转换scss文件,并设置格式为完全展开,因为默认的会使每个样式结尾的“}”都根随代码结尾,而不是换行
.pipe(sass({outputStyle:'expanded'}))
.pipe(dest('temp'))
}
exports.scripts=()=>{
return src('src/scripts/*.js',{base:'src'})
// 这里需要添加presets,不然转换不生效
.pipe(babel({presets:['@babel/preset-env']}))
.pipe(dest('temp'))
}
const data={
name:'myweb',
description:'hello gulp'
}
const swig=require('gulp-swig')
exports.html=()=>{
return src('src/*.html',{base:'src'})
.pipe(swig({data}))
.pipe(dest('temp'))
}
serve任务的baseDir也要调整:
exports.serve=()=>{
//watch第一个参数是监听的路径,第二个参数是指定任务
watch('src/styles/*.scss',this.style)
watch('src/scripts/*.js',this.scripts)
watch('src/*.html',this.html)
// 对于图片和字体,压缩前后并不会有显示上的变化,并不需要对其进行监听
// 还有public目录是静态目录,也不需要监听
// 若你真的需要监听,可以像下面这样写,因为这几种文件需要执行的任务都是一样的,可以写到一个数组中,以减少任务执行次数
// watch([
// 'src/images/**',
// 'src/fonts/**',
// 'public/**'
// ],bs.reload)
bs.init({
notify:false,
prot:2080,
//open:false,
// 表示监听dist目录中的文件的变动
files:'dist/**',
server:{
// 这里会按数组由左往右的顺序找文件,即优先找dist目录
baseDir:['temp','src','public'],
routes:{
'/node_modules':'node_modules'
}
}
})
}
yarn add gulp-if --dev
useref 从temp中读取文件并经转换后输出到dist目录中
const loadPlugins=require('gulp-load-plugins')
const plugins =loadPlugins()
exports.useref=()=>{
return src('temp/*.html',{base:'temp'})
.pipe(plugins.useref({searchPath:['temp','.']}))
// 分别对html、css、js进行压缩
.pipe(plugins.if(/\.js$/,plugins.uglify({
//mangle: false,//是否改变变量名
})))
.pipe(plugins.if(/\.css$/,plugins.cleanCss()))
.pipe(plugins.if(/\.html$/,plugins.htmlmin({
collapseWhitespace:true,
minifyCSS:true,
minifyJS:true
})))
.pipe(dest('dist'))
}
最后还需要调整一下组合任务,把useref添加到组合任务,useref只在build的时候需要使用,所以这里只需要再调整build任务:
exports.build=series(this.clean,parallel(series(this.compile,this.useref) ,this.image,this.font,this.extra))
到此,gulpfile.js的代码总体展示如下:
const {series,parallel, src, dest} =require('gulp')
const browserSync=require('browser-sync')
const bs=browserSync.create()
const loadPlugins=require('gulp-load-plugins')
const plugins =loadPlugins()
const data={
name:'myweb',
description:'hello gulp'
}
const del=require('del')
const { watch } = require('browser-sync')
const style=()=>{
return src('src/styles/*.scss',{base:
'src'})
// 使用sass来转换scss文件,并设置格式为完全展开,因为默认的会使每个样式结尾的“}”都根随代码结尾,而不是换行
.pipe(plugins.sass({outputStyle:'expanded'}))
.pipe(dest('temp'))
}
const scripts=()=>{
return src('src/scripts/*.js',{base:'src'})
// 这里需要添加presets,不然转换不生效
.pipe(plugins.babel({presets:['@babel/preset-env']}))
.pipe(dest('temp'))
}
const html=()=>{
return src('src/*.html',{base:'src'})
.pipe(plugins.swig({data}))
.pipe(dest('temp'))
}
const image=()=>{
return src('src/images/**',{base:'src'})
.pipe(plugins.imagemin())
.pipe(dest('dist'))
}
const font=()=>{
return src('src/fonts/**',{base:'src'})
.pipe(plugins.imagemin())
.pipe(dest('dist'))
}
const extra=()=>{
return src('public/**',{base:'public'})
.pipe(dest('dist'))
}
const clean=()=>{
return del(['dist','temp'])
}
const serve=()=>{
//watch第一个参数是监听的路径,第二个参数是指定任务
watch('src/styles/*.scss',style)
watch('src/scripts/*.js',scripts)
watch('src/*.html',html)
// 对于图片和字体,压缩前后并不会有显示上的变化,并不需要对其进行监听
// 还有public目录是静态目录,也不需要监听
// 若你真的需要监听,可以像下面这样写,因为这几种文件需要执行的任务都是一样的,可以写到一个数组中,以减少任务执行次数
// watch([
// 'src/images/**',
// 'src/fonts/**',
// 'public/**'
// ],bs.reload)
bs.init({
notify:false,
prot:2080,
//open:false,
// 表示监听dist目录中的文件的变动
files:'dist/**',
server:{
// 这里会按数组由左往右的顺序找文件,即优先找dist目录
baseDir:['temp','src','public'],
routes:{
'/node_modules':'node_modules'
}
}
})
}
const useref=()=>{
return src('temp/*.html',{base:'temp'})
.pipe(plugins.useref({searchPath:['temp','.']}))
// 分别对html、css、js进行压缩
.pipe(plugins.if(/\.js$/,plugins.uglify({
mangle: true,
})))
.pipe(plugins.if(/\.css$/,plugins.cleanCss()))
.pipe(plugins.if(/\.html$/,plugins.htmlmin({
collapseWhitespace:true,
minifyCSS:true,
minifyJS:true
})))
.pipe(dest('dist'))
}
// 编译任务
const compile=parallel(style,scripts,html)
// 打包执行任务
const build=series(clean,parallel(series(compile,useref) ,image,font,extra))
// 开发执行任务
const dev=series(compile,serve)
module.exports={
clean,
build,
dev
}
上面的gulpfile.js中的内容进行了一点额外的处理,就是只导出了几个任务,而其它的任务作为了私有成员,因为平时使用时,只会用到这几个任务,
接下来我们可以把这几个任务添加到package.json的scripts中,这样应该不会有人还不知道这几个任务是干什么用的吧。
"scripts": {
"clean":"gulp clean",
"build":"gulp build",
"dev":"gulp dev"
},
这样我们可以直接使用yarn clean
、yarn build
、yarn dev
来使用