一篇文章带你深入当下最流行的前端构建系统 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 cleanyarn buildyarn dev来使用

原文地址:https://www.cnblogs.com/MissSage/p/14899477.html