概述
Gulp
作为当下前端最流行的构建系统之一,其核心特点就是高效、易用。它的使用方法非常简单,大体上就是先在项目中安装上Gulp
的开发依赖,然后再项目根目录创建一个gulpfile.js的文件,用于去编写让gulp
去自动执行的构建任务,最后在命令行内使用gulp
提供的cli
去运行这些任务。
使用方法
基本使用
先创建一个空的项目文件夹并使用yarn init --yes
或者npm init -y
去初始化一下package.json
文件,这里及后面都会使用yarn
做演示:
将gulp
作为开发依赖安装进入项目:
安装gulp
的时候会自动安装gulp-cli
,有了这个脚手架工具就可以在后续使用它来运行构建任务。
在项目根目录下创建一个gulpfile.js
文件,在这里可以去编写用于自动构建的任务:
在gulpfile.js
里定义构建任务是使用导出函数成员的方式来定义的:
如此,gulp
中就定义了一个名为foo
的任务,可以通过yarn
来运行gulp
以此来执行这个任务:
但是这个时候会报一个错误,说“foo
任务没有完成,是否忘记标识任务的结束”。这是因为最新的gulp
中取消了同步代码模式,约定每一个任务都是异步任务。当任务执行结束之后需要调用回调函数或者使用特定的代码来标识该任务已经完成。
在当前任务中,可以给函数写上一个名为done
的形参,在任务结束后调用这个参数,标识任务执行完成:
这样foo
任务便能正常的结束。
如果任务名为
default
,它会作为gulp
的默认任务出现,可以直接使用yarn gulp
来执行该任务
在gulp 4.0
之前,是需要在gulpfile.js
中引入gulp
,并通过其下面的task
方法来注册任务:
const gulp = require('gulp')
gulp.task('bar', done => {
console.log('bar')
done()
})
组合任务
在gulp
中,它提供了series
和parallel
两个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)
运行结果:
可以发现
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)
执行结果为:
这里可以看到任务是并行执行的。
异步任务
在上面的方式中,都是在执行结束后通过调用回调函数来标识该任务执行完成的,在异步任务中主要就是该如何通知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'))
}
执行一下这个任务:
可以看到这里就报了一个cb err
的错误。而且如果这是多个任务同时执行的话,后续的任务也不会再执行。
Promise
promise
作为回调函数的解决方案,在gulp
中也是支持的。
exports.promise = () => {
console.log('promise task')
// return Promise.resolve()
return Promise.reject()
}
在这里使用promise
时需要注意的是:当返回的promise
是resolved
状态时就会表示这个任务已经成功执行完成,如果返回的promise
是rejected
状态,则表示这个任务出现了错误。
而且在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')
}
执行的结果为:
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
}
执行结果为:
同时文件夹中也会新建一个pack.txt
文件:
Gulp构建过程
构建过程大多数都是将文件读取出来,然后经过特定的转换再写入指定的位置。这里先在空项目下创建一个css
文件,里面可以随意写一点样式:
再用刚刚的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
的工作过程有三个核心的概念:读取流、转换流以及写入流:
可以通过读取流将目标文件读取出来,再将其导入转换流将其转换为我们想要的结果,最后再通过写入流将其写入指定的位置。这样一个流程就完成了我们在构建过程的所需要的工作。
文件操作API + 插件使用
相比于底层node
的API,gulp
中的文件读取API更强大也更易于使用,而中间过程的转换流,绝大多数情况下都是通过相应的插件来完成。因此,通过gulp
去创建构建任务的流程一般就是:
-
通过
src
方法去创建读取流 -
借助插件提供的转换流实现文件加工
-
通过
dest
方法去创建一个写入流
先上一个示例:
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
这个插件。
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自动化构建案例
先来上一下项目的基本结构
先通过yarn init --yes
初始化一下package.json
文件,再安装一下gulp
:yarn 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
文件夹,里面包含了所有的样式文件。
但是文件并没有按照原有结构输出,所以得再加一句:
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,
}
当文件名由
_
开头时,将不能被转换。而且默认情况下,选择器后面的括号会被折到上一行:这样可以通过给
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
文件已经被转译了:
模板编译
为了让HTML
文件中一些可以复用的地方被抽象出来,可以使用模板引擎,如swig
先在项目中安装一下swig
:yarn add gulp-swig --dev
,再在项目中使用一下:
const swig = require('gulp-swig')
const page = () => {
return src('./src/**/*.html', { base: 'src' })
.pipe(swig())
.pipe(dest('dist'))
}
执行一下任务之会发现之前的模板标记都已经被替换成了html
结构:
还有一些插值表达式之中的数据可以通过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
就可以并行执行style
、script
和page
这三个任务。
图片和字体文件
对于图片文件,可以通过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'))
}
执行以下这个任务会发现这个插件会将图片进行压缩:
还可以通过imagemin
对svg
格式的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-sync
:yarn 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
: