Gulp的基本使用及使用案例

431 阅读13分钟

概述

Gulp作为当下前端最流行的构建系统之一,其核心特点就是高效、易用。它的使用方法非常简单,大体上就是先在项目中安装上Gulp的开发依赖,然后再项目根目录创建一个gulpfile.js的文件,用于去编写让gulp去自动执行的构建任务,最后在命令行内使用gulp提供的cli去运行这些任务。

使用方法

基本使用

先创建一个空的项目文件夹并使用yarn init --yes或者npm init -y去初始化一下package.json文件,这里及后面都会使用yarn做演示:

image.png

gulp作为开发依赖安装进入项目:

image.png

安装gulp的时候会自动安装gulp-cli,有了这个脚手架工具就可以在后续使用它来运行构建任务。 在项目根目录下创建一个gulpfile.js文件,在这里可以去编写用于自动构建的任务:

image.png

gulpfile.js里定义构建任务是使用导出函数成员的方式来定义的:

image.png

如此,gulp中就定义了一个名为foo的任务,可以通过yarn来运行gulp以此来执行这个任务:

image.png

但是这个时候会报一个错误,说“foo任务没有完成,是否忘记标识任务的结束”。这是因为最新的gulp中取消了同步代码模式,约定每一个任务都是异步任务。当任务执行结束之后需要调用回调函数或者使用特定的代码来标识该任务已经完成。

在当前任务中,可以给函数写上一个名为done的形参,在任务结束后调用这个参数,标识任务执行完成:

image.png

image.png

这样foo任务便能正常的结束。

如果任务名为default,它会作为gulp的默认任务出现,可以直接使用yarn gulp来执行该任务

image.png

image.png

gulp 4.0之前,是需要在gulpfile.js中引入gulp,并通过其下面的task方法来注册任务:

const gulp = require('gulp')

gulp.task('bar', done => {
    console.log('bar')
    done()
})

组合任务

gulp中,它提供了seriesparallel两个api来为我们创建串行任务和并行任务。

使用series

const { series } = require('gulp')


const task1 = done => {
    setTimeout(() => {
        console.log('task1 done')
        done()
    }, 3000)
}

const task2 = done => {
    setTimeout(() => {
        console.log('task2 done')
        done()
    }, 2000)
}

const task3 = done => {
    setTimeout(() => {
        console.log('task3 done')
        done()
    }, 1000)
}

exports.foo = series(task1, task2, task3)

运行结果:

image.png 可以发现series是将任务一个接着一个执行的。

使用parallel

const { parallel } = require('gulp')


const task1 = done => {
    setTimeout(() => {
        console.log('task1 done')
        done()
    }, 3000)
}

const task2 = done => {
    setTimeout(() => {
        console.log('task2 done')
        done()
    }, 2000)
}

const task3 = done => {
    setTimeout(() => {
        console.log('task3 done')
        done()
    }, 1000)
}

exports.foo = parallel(task1, task2, task3)

执行结果为:

image.png

这里可以看到任务是并行执行的。

异步任务

在上面的方式中,都是在执行结束后通过调用回调函数来标识该任务执行完成的,在异步任务中主要就是该如何通知gulp该任务何时完成。对于这个问题gulp中有很多解决方法:

回调函数

exports.callback = done => {
    console.log('cb')
    done()
}

与之前一样,通过调用参数中的done函数来通知gulp任务结束。而且与node中的回调函数一样,这也是一个错误优先的回调函数。也就是说如果想在任务执行时报一个错误阻止任务往下执行的时候,可以向done()中传入一个错误对象:

exports.callback_err = done => {
    console.log('cb')
    done(new Error('cb err'))
}

执行一下这个任务:

image.png

可以看到这里就报了一个cb err的错误。而且如果这是多个任务同时执行的话,后续的任务也不会再执行。

Promise

promise作为回调函数的解决方案,在gulp中也是支持的。

exports.promise = () => {
    console.log('promise task')
    // return Promise.resolve()
    return Promise.reject()
}

在这里使用promise时需要注意的是:当返回的promiseresolved状态时就会表示这个任务已经成功执行完成,如果返回的promiserejected状态,则表示这个任务出现了错误。

而且在return Promise.resolve()这个语句中是不需要在resolve方法内传入值,因为gulp会忽略这个值。

Async/Await

node版本不低于8.0的时候,还可以使用async await这样的语法糖来处理异步任务:

const timeout = time => {
    return new Promise(resolve => setTimeout(resolve, time))
}

exports.async = async () => {
    await timeout(1000)
    console.log('async-await task')
}

执行的结果为:

image.png

Stream

在构建工具中文件的读写也是也是一种常见的操作,同时这种操作也是异步的。在gulp的任务中,也可以返回一个stream对象:

const fs = require('fs')

exports.stream = () => {
    const read = fs.createReadStream('package.json')
    const write = fs.createWriteStream('pack.txt')
    read.pipe(write)
    return read
}

执行结果为:

image.png

同时文件夹中也会新建一个pack.txt文件:

image.png

Gulp构建过程

构建过程大多数都是将文件读取出来,然后经过特定的转换再写入指定的位置。这里先在空项目下创建一个css文件,里面可以随意写一点样式:

image.png

再用刚刚的fs模块创建读取流和写入流,

const fs = require('fs')

exports.default = () => {
    // 文件读取流
    const read = fs.createReadStream('init.css')

    // 文件写入流
    const write = fs.createWriteStream('init.min.css')

    // 将读取出来的文件导入写入流
    read.pipe(write)

    return read
}

执行这个任务后可以看见项目中又多了一个init.min.css文件。但是这样只是做到了简单的复制操作,所以还需要引入转换流:

const fs = require('fs')

const { Transform } = require('stream')

exports.default = () => {
    // 文件读取流
    const read = fs.createReadStream('init.css')

    // 文件写入流
    const write = fs.createWriteStream('init.min.css')

    // 文件转换流
    const transform = new Transform({
        // 这个transform属性为核心转换过程
        transform(chunk, encoding, callback) {
            // chunk为读取流中读取到的内容
            const input = chunk.toString()
            const output = input.replace(/\s+/g, '').replace(/\/\*.+?\*\//g, '')
            
            callback(null, output)
        }
    })

    // 将读取出来的文件先导入转换流再导入写入流
    read
        .pipe(transform)
        .pipe(write)

    return read
}

callback中第一个参数为错误对象,如果没有错误就传入null

执行gulp任务后就可以看见一个被压缩过的css文件了。

由上可见,gulp的工作过程有三个核心的概念:读取流转换流以及写入流

image.png

可以通过读取流将目标文件读取出来,再将其导入转换流将其转换为我们想要的结果,最后再通过写入流将其写入指定的位置。这样一个流程就完成了我们在构建过程的所需要的工作。

文件操作API + 插件使用

相比于底层node的API,gulp中的文件读取API更强大也更易于使用,而中间过程的转换流,绝大多数情况下都是通过相应的插件来完成。因此,通过gulp去创建构建任务的流程一般就是:

  • 通过src方法去创建读取流

  • 借助插件提供的转换流实现文件加工

  • 通过dest方法去创建一个写入流

先上一个示例:

image.png

const { src, dest } = require('gulp')

exports.default = () => {
    return src('./src/init.css')
        .pipe(dest('dist'))
}

执行完任务后会看见根目录下会多出一个dist文件夹,文件夹中有一个init.css文件。也就是说这个任务的读取流和写入流是可以正常工作的。

src方法中,还可以通过通配符的方式去匹配批量的文件:

src('./src/*.css')

这样将会读取src文件夹下的所有css文件。 如果想要完成css的压缩,可以安装gulp-clean-css这个插件。

image.png

const { src, dest } = require('gulp')
const cleanCss = require('gulp-clean-css')

exports.default = () => {
    return src('./src/init.css')
        .pipe(cleanCss())
        .pipe(dest('dist'))
}

这样,dist文件夹下都会是被压缩过的代码了。

而且如果还要再使用其他的转换流插件,还可以在中间添加更多的pipe操作。

Gulp自动化构建案例

先来上一下项目的基本结构

image.png

先通过yarn init --yes初始化一下package.json文件,再安装一下gulpyarn add gulp --dev

样式编译

先将src/assets/styles目录下的样式文件给编译成普通的css文件:

const { src, dest } = require('gulp')

// 定义成私有方法
const style = () => {
    // 读取styles文件夹下所有的scss文件
    return src('./src/assets/styles/*.scss')
        .pipe(dest('dist'))
}

// 通过modules.exports按需导出
module.exports = {
    style,
}

执行yarn gulp style任务后会发现根目录下多了一个dist文件夹,里面包含了所有的样式文件。

image.png

但是文件并没有按照原有结构输出,所以得再加一句:

const { src, dest } = require('gulp')

const style = () => {
    return src('./src/assets/styles/*.scss', { base: 'src' })
        .pipe(dest('dist'))
}

module.exports = {
    style,
}

这样就会保留原有结构了。再装一个gulp-sass插件来完成文件的转换:yarn add gulp-sass --dev。在任务中使用该转换流:

const { src, dest } = require('gulp')
const sass = require('gulp-sass')

const style = () => {
    return src('./src/assets/styles/*.scss', { base: 'src' })
        .pipe(sass())
        .pipe(dest('dist'))
}

module.exports = {
    style,
}

当文件名由_开头时,将不能被转换。而且默认情况下,选择器后面的括号会被折到上一行: image.png 这样可以通过给sass指定一个选项去修改这样的现象:

sass({ outputStyle: 'expanded' })

脚本编译

先装一个处理js文件的转换流插件:yarn add gulp-babel @babel/core @babel/preset-env --dev,这个babel可以将一些高级的语法转换成es5的,以满足一些浏览器的兼容问题。

读取和写入的代码也是如样式编译一样:

const { src, dest } = require('gulp')
const script = () => {
    return src('./src/assets/scripts/*.js', { base: 'src' })
        .pipe(babel({ presets: ['@babel/preset-env'] }))
        .pipe(dest('dist'))
}

module.exports = {
    script,
}

执行一下任务,可以发现js文件已经被转译了:

image.png

模板编译

为了让HTML文件中一些可以复用的地方被抽象出来,可以使用模板引擎,如swig

image.png

先在项目中安装一下swigyarn add gulp-swig --dev,再在项目中使用一下:

const swig = require('gulp-swig')

const page = () => {
    return src('./src/**/*.html', { base: 'src' })
        .pipe(swig())
        .pipe(dest('dist'))
}

执行一下任务之会发现之前的模板标记都已经被替换成了html结构:

image.png

还有一些插值表达式之中的数据可以通过swig方法中配置项的data属性传入:

swig({ data: data })

这个时候我们就可以通过parallel方法将以上三个任务给并行处理一下,整体结果就是:

const { src, dest, parallel } = require('gulp')
const sass = require('gulp-sass')
const babel = require('gulp-babel')
const swig = require('gulp-swig')

const data = {
    menus: [
        {
            name: 'Home',
            icon: 'aperture',
            link: 'index.html'
        },
        {
            name: 'Features',
            link: 'features.html'
        },
        {
            name: 'About',
            link: 'about.html'
        },
        {
            name: 'Contact',
            link: '#',
            children: [
                {
                    name: 'Twitter',
                    link: 'https://twitter.com/w_zce'
                },
                {
                    name: 'About',
                    link: 'https://weibo.com/zceme'
                },
                {
                    name: 'divider'
                },
                {
                    name: 'About',
                    link: 'https://github.com/zce'
                }
            ]
        }
    ],
    pkg: require('./package.json'),
    date: new Date()
}

const page = () => {
    return src('./src/**/*.html', { base: 'src' })
        .pipe(swig({ data }))
        .pipe(dest('dist'))
}

const style = () => {
    return src('./src/assets/styles/*.scss', { base: 'src' })
        .pipe(sass({ outputStyle: 'expanded' }))
        .pipe(dest('dist'))
}

const script = () => {
    return src('./src/assets/scripts/*.js', { base: 'src' })
        .pipe(babel({ presets: ['@babel/preset-env'] }))
        .pipe(dest('dist'))
}

const compile = parallel(style, script, page)

module.exports = {
    compile
}

这样处理后只需要执行yarn gulp compile就可以并行执行stylescriptpage这三个任务。

图片和字体文件

对于图片文件,可以通过gulp-imagemin这个插件来完成转换:yarn add gulp-imagemin --dev,再在gulpfile.js中创建一个任务:

const imagemin = require('gulp-imagemin')

const image = () => {
    return src('./src/assets/images/**', { base: 'src' })
        .pipe(imagemin())
        .pipe(dest('dist'))
}

执行以下这个任务会发现这个插件会将图片进行压缩:

image.png

还可以通过imageminsvg格式的font文件进行处理:

const font = () => {
    return src('./src/assets/fonts/**', { base: 'src' })
        .pipe(imagemin())
        .pipe(dest('dist'))
}

最后将这两个任务也丢入parallel方法:

其他文件及文件清除

对于其他文件,只需要直接读取、写入这两个操作就行了:

const extra = () => {
    return src('./public/**', { base: 'src' })
        .pipe(dest('dist'))
}

对于文件的清除,可以使用del这个插件:yarn add del --dev

const del = require('del')

const clean = () => {
    // del方法内传入的是要清除的目录
    return del('dist')
}

再用series方法将clean方法和其他方法串行处理一下,整体代码为:

const { src, dest, parallel, series } = require('gulp')
const sass = require('gulp-sass')
const babel = require('gulp-babel')
const swig = require('gulp-swig')
const imagemin = require('gulp-imagemin')
const del = require('del')

const clean = () => {
    return del('dist')
}

const font = () => {
    return src('./src/assets/fonts/**', { base: 'src' })
        .pipe(imagemin())
        .pipe(dest('dist'))
}


const image = () => {
    return src('./src/assets/images/**', { base: 'src' })
        .pipe(imagemin())
        .pipe(dest('dist'))
}

const data = {
    menus: [
        {
            name: 'Home',
            icon: 'aperture',
            link: 'index.html'
        },
        {
            name: 'Features',
            link: 'features.html'
        },
        {
            name: 'About',
            link: 'about.html'
        },
        {
            name: 'Contact',
            link: '#',
            children: [
                {
                    name: 'Twitter',
                    link: 'https://twitter.com/w_zce'
                },
                {
                    name: 'About',
                    link: 'https://weibo.com/zceme'
                },
                {
                    name: 'divider'
                },
                {
                    name: 'About',
                    link: 'https://github.com/zce'
                }
            ]
        }
    ],
    pkg: require('./package.json'),
    date: new Date()
}

const page = () => {
    return src('./src/**/*.html', { base: 'src' })
        .pipe(swig({ data }))
        .pipe(dest('dist'))
}

const style = () => {
    return src('./src/assets/styles/*.scss', { base: 'src' })
        .pipe(sass({ outputStyle: 'expanded' }))
        .pipe(dest('dist'))
}

const script = () => {
    return src('./src/assets/scripts/*.js', { base: 'src' })
        .pipe(babel({ presets: ['@babel/preset-env'] }))
        .pipe(dest('dist'))
}

const extra = () => {
    return src('./public/**', { base: 'src' })
        .pipe(dest('dist'))
}

const compile = parallel(style, script, page, image, font)

const build = series(clean, parallel(compile, extra))


module.exports = {
    build,
}

这样,只需要执行yarn gulp build就可以让整个构建启动起来。

自动加载插件

在越来越多的构建任务后,引入的插件也久越来越多,所以需要有一个统一管理这些插件的插件,可以使用gulp-load-plugins来完成这个需求:yarn add gulp-load-plugins --dev

gulpfile.js中的内容处理一下:

const { src, dest, parallel, series } = require('gulp')
const del = require('del')
const browserSync = require('browser-sync')

const plugins = loadPlugins()

const clean = () => {
    return del('dist')
}

const font = () => {
    return src('./src/assets/fonts/**', { base: 'src' })
        .pipe(plugins.imagemin())
        .pipe(dest('dist'))
}


const image = () => {
    return src('./src/assets/images/**', { base: 'src' })
        .pipe(plugins.imagemin())
        .pipe(dest('dist'))
}

const data = {
    menus: [
        {
            name: 'Home',
            icon: 'aperture',
            link: 'index.html'
        },
        {
            name: 'Features',
            link: 'features.html'
        },
        {
            name: 'About',
            link: 'about.html'
        },
        {
            name: 'Contact',
            link: '#',
            children: [
                {
                    name: 'Twitter',
                    link: 'https://twitter.com/w_zce'
                },
                {
                    name: 'About',
                    link: 'https://weibo.com/zceme'
                },
                {
                    name: 'divider'
                },
                {
                    name: 'About',
                    link: 'https://github.com/zce'
                }
            ]
        }
    ],
    pkg: require('./package.json'),
    date: new Date()
}

const page = () => {
    return src('./src/**/*.html', { base: 'src' })
        .pipe(plugins.swig({ data }))
        .pipe(dest('dist'))
}

const style = () => {
    return src('./src/assets/styles/*.scss', { base: 'src' })
        .pipe(plugins.sass({ outputStyle: 'expanded' }))
        .pipe(dest('dist'))
}

const script = () => {
    return src('./src/assets/scripts/*.js', { base: 'src' })
        .pipe(plugins.babel({ presets: ['@babel/preset-env'] }))
        .pipe(dest('dist'))
}

const extra = () => {
    return src('./public/**', { base: 'src' })
        .pipe(dest('dist'))
}

const compile = parallel(style, script, page, image, font)

const build = series(clean, parallel(compile, extra))


module.exports = {
    build,
}

热更新开发服务器

在之前的任务里,每次修改完src目录下的源码时还需要再次启动构建任务才能对源码进行刷新。所以需要一个模块来实现热更新功能。这里使用browser-syncyarn add browser-sync

在项目里使用一下:

const browserSync = require('browser-sync')

const bs = browserSync.create()

const serve = () => {
    bs.init({
        server: {
            // 指定目录下文件发送变化后会重新渲染浏览器
            files: 'dist/**',
            // 指定要打开的项目目录
            baseDir: 'dist'
        }
    })
}

module.exports = {
    build,
    serve,
}

这时来执行一下serve这个任务,就会发现项目已经自动在浏览器打开了,同时在dist目录内的文件发生变化时还能再重新渲染。只不过项目中对于node_modules内的依赖以及项目源码发生变化后无法及时得到更新的问题还没有解决。

监视文件变化

对于监视文件变化,可以通过gulp中提供的watch方法来解决。

const { src, dest, parallel, series, watch } = require('gulp')
const del = require('del')
const browserSync = require('browser-sync')
const browserSync = require('browser-sync')

const plugins = loadPlugins()

const bs = browserSync.create()

const clean = () => {
    return del('dist')
}

const font = () => {
    return src('./src/assets/fonts/**', { base: 'src' })
        .pipe(plugins.imagemin())
        .pipe(dest('dist'))
}


const image = () => {
    return src('./src/assets/images/**', { base: 'src' })
        .pipe(plugins.imagemin())
        .pipe(dest('dist'))
}

const data = {
    menus: [
        {
            name: 'Home',
            icon: 'aperture',
            link: 'index.html'
        },
        {
            name: 'Features',
            link: 'features.html'
        },
        {
            name: 'About',
            link: 'about.html'
        },
        {
            name: 'Contact',
            link: '#',
            children: [
                {
                    name: 'Twitter',
                    link: 'https://twitter.com/w_zce'
                },
                {
                    name: 'About',
                    link: 'https://weibo.com/zceme'
                },
                {
                    name: 'divider'
                },
                {
                    name: 'About',
                    link: 'https://github.com/zce'
                }
            ]
        }
    ],
    pkg: require('./package.json'),
    date: new Date()
}

const page = () => {
    return src('./src/**/*.html', { base: 'src' })
        .pipe(plugins.swig({
            data,
            cache: false
        }))
        .pipe(dest('dist'))
        .pipe(bs.reload({ stream: true }))
}

const style = () => {
    return src('./src/assets/styles/*.scss', { base: 'src' })
        .pipe(plugins.sass({ outputStyle: 'expanded' }))
        .pipe(dest('dist'))
        .pipe(bs.reload({ stream: true }))
}

const script = () => {
    return src('./src/assets/scripts/*.js', { base: 'src' })
        .pipe(plugins.babel({ presets: ['@babel/preset-env'] }))
        .pipe(dest('dist'))
        .pipe(bs.reload({ stream: true }))
}

const extra = () => {
    return src('./public/**', { base: 'src' })
        .pipe(dest('dist'))
}

const compile = parallel(style, script, page, image, font)

const build = series(clean, parallel(compile, extra))

const serve = () => {
    // watch('./src/assets/fonts/**', font)
    watch('./src/assets/scripts/*.js', script)
    watch('./src/assets/styles/*.scss', style)
    watch('./src/**/*.html', page)
    // watch('./src/assets/images/**', image)
    // watch('./public/**', extra)
    watch([
        './src/assets/fonts/**',
        './src/assets/images/**',
        './public/**'
    ], bs.reload)
    bs.init({
        files: 'dist/**',
        server: {
            baseDir: ['dist', 'src', 'public'],
            routes: {
                '/node_modules': 'node_modules'
            }
        }
    })
}


module.exports = {
    build,
    serve,
}

useref

针对引用node_modules的处理方法,可以使用gulp-useref来解决:yarn add gulp-useref

const useref = () => {
    return src('dist/*.html', { base: 'dist' })
        .pipe(plugins.useref({ searchPath: ['dist', '.'] }))
        .pipe(dest('dist'))
}

经过测试,dist目录下index.html中的引用也会被打包进入dist

image.png