说起前端自动化构建,相信做过前端的小伙伴们都不会陌生,可能第一感觉就会想到webpack。但是,其实webpack本质意义上应该是一个强大的模块打包器,以入口文件为起点,结合文件间各种引用关系,将各种复杂的文件最终打包成一个或多个浏览器可识别的文件。所以说,webpack更大意义上是一个模块打包器,而非自动化构建工具。今天我们来介绍的是一款强大的自动化构建工具gulp
什么是自动化构建
自动化构建简单理解就是将源代码转化为生产环境代码的过程。
它的出现,省去了我们很大一部分人工的重复性工作,一定程度的提升了我们的开发效率
常用的自动化构建工具
- Grunt
- Gulp
- FIS
区别
- grunt 和 gulp本身更像一个构建平台,而实际完成构建需要借助各种插件来实现各个具体的构建任务。故gurnt和gulp之间其实是可以相互转化得,即能用grunt完成得事情,用gulp也能完成,能用gulp完成的事情,用grunt同样能完成。
- grunt 任务的构建是基于临时文件完成的,也就是说,grunt去解析一个文件时,会先读取这个文件,然后经过插件处理后,先写入到一个临时文件中,然后另一个插件做下一步处理时,会去读取这个临时文件中的内容,然后经过插件处理后,再写入到另一个临时文件中,直到全部处理完成,再写入到目标文件中(生产代码)。故可以看出,grunt的每一步任务的构建,都会伴随磁盘的读写。故其构建速度会比较慢。故现在用的人也少了
- gulp 任务的构建是基于内存完成的,也就是说,gulp解析一个文件是以文件流的形式,先读取文件的文件流,写入到内存中,然后经过中间各种插件处理,最终才写入到目标文件中(生产代码)。故gulp一个任务的构建过程,只有第一步和最后一步是设计到磁盘读写的,其他中间环节都是在内存中完成,故其构建速度会非常快。故gulp应该是当前最主流的自动化构建工具
- FIS 百度团队推出的自动化构建工具,大而全,集成了很多功能,更容易上手。但现在没怎么维护了,用的人也非常少了
初识Gulp
gulp工作原理
以上图片是对gulp工作原理很好的一个解读。gulp主要工作原理就是将文件读取出来,然后中间经过一系列的处理,最终转换成我们生产环境所需要的内容,然后写入到目标文件中。
而这个过程中最重要的就是gulp的管道pipe(),gulp就是利用pipe()来实现一个流程到下一个流程的过渡。详情请看代码
const fs = require('fs')
const stream = (done) => {
const readStream = fs.createReadStream('package.json') // 读取流,读取文件
const writeStream = fs.createWriteStream('temp.txt') // 写入流,写入文件
const transform = new Transform({
transform: (chunk, encoding, callback) => {
// 这里可以对读取的流进行各种转换操作,具体如何转换我就不写了
}
}) // 转换流
return readStream // 读取
.pipe(transform) // 转换
.pipe(writeStream) // 写入
// return 读取流 实际会调用readStream的end事件,告知结束任务
}
module.exports = {
stream
}
如上,gulp核心工作原理就是这样,通过pipe这样一个管道将上一步处理完的东西传递给下一步进行处理。全部处理完成后,最终写入目标文件
gulp需要有一个gulpfile.js文件,实现这些构建任务的代码一般就写在这个gulpfile.js文件中,如以上代码就是写在gulpfile.js中的
但是,以上代码我们是通过node.js原生实现的,实际读取文件,写入文件以及中间对文件进行各种处理,gulp都给我们提供了各种插件以及方法,我们都可以直接安装或者直接使用
gulp常用Api
const {
src, dest, parallel, series, watch } = require('gulp')
- src:创建读取流,可直接src(‘源文件路径’) 来读取文件流
- dest:创建写入流,可直接dest(‘目标文件路径’) 来将文件流写入目标文件中
- parallel:创建一个并行的构建任务,可并行执行多个构建任务 parallel(‘任务1’,‘任务2’,‘任务3’,…)
- series:创建一个串行的构建任务,会按照顺序依次执行多个任务 series(‘任务1’,‘任务2’,‘任务3’,…)
- watch:对文件进行监视,当文件发生变化时,可执行相关任务
watch(‘src/assets/styles/*.scss’, 执行某个任务)
从0到1实现一个完整的自动化工作流
下面我们利用一个例子来从0到1实现一个完整的自动化工作流
首先,我们得准备一份开发时得源代码
代码目录大家可以通过脚手架去生成
目录介绍
1、public下存放不需要经过转换得静态资源
2、src下存放项目源文件
3、assets下存放其他资源文件,如,样式文件,脚本文件,图片,字体等
下面,我们要利用gulp来实现一个自动化构建工作流,将这些文件都能够自动转化为生产环境可用得资源文件
目标
1、将html文件转化为html文件,存放到dist下,并且处理html中得一些模板解析,以及资源文件得引入问题(如html文件中引入了css,js 等)。并对html文件进行压缩处理
2、将scss文件转化为浏览器可识别得css文件,并压缩
3、将js文件转化为js文件,并处理js代码中一些浏览器无法识别得语法转化为可识别得。如ES6.ES7转ES5
4、将图片进行压缩
5、将字体进行压缩
6、实现一个开发服务器,实现边开发,边构建
7、相关优化
8、封装自动化工作流,将我们完成得gulpfile.js 封装成一个公用模块,便于后续其他类似项目可以直接按照这个模块就可立即使用
开始实现
准备工作
按照gulp,并引入相关api
yarn add gulp --dev
在项目根目录下创建gulpfile.js文件,在文件中引入gulp相关方法
const {
src, dest, parallel, series } = require('gulp')
1、创建相关得构建任务,并测试
创建样式编译任务
// 定义样式编译任务
const sass = require('gulp-sass') // 编译scss文件得
const scss = () => {
return src('./src/assets/styles/main.scss', {
base: 'src'}) // 读取文件
.pipe(sass()) // sass编译处理
.pipe(dest('./dist')) // 写入到dist文件夹下
}
// 导出相关任务
module.exports = {
scss
}
以上src方法中第二个参数 是为了指定基础路径。如果不指定,打包后则会丢失路径,直接将打包后的css文件放在dist目录下。
如果指定了,就会将指定的目录后面的目录都保留下来,即 assets/styles/main.css
运行yarn gulp scss 运行构建任务
其他构建任务也都一样创建
思路:
先建立不同类型文件的编译构建任务,将需要编译的各个任务进行编译构建,并一个个进行测试,确保构建没问题
当然,编译不同文件需要用到不同的插件。故同时需要安装相应的插件,并引入相关插件(引入的代码我就不贴了)
- 编译scss 需要gulp-scss插件 (任务scss)
- 编译脚本 需要gulp-babel插件,同时需要安装@babel/core,gulp-babel的作用主要就是去调用@babel/core插件,
同时为了能够转换ES6及以上新特性代码,还需要安装@babel/preset-env插件,用于转换新特性 (任务script) - 编译html 需要gulp-swig插件,用于传入模板所需要的数据 (任务html)
- 编译image图片以及font字体文件,需要 gulp-imagemin插件,用于对图片和字体进行压缩 (任务image和font)
- 建立其他不需要编译的文件的构建任务,不需要编译的就直接拷贝到目标路径中 (任务copy)
附上以上6个任务代码
// html模板中需要的数据
const data = {
menus: [
{
name: 'Home',
icon: 'aperture',
link: 'index.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 scss = () => {
return src('./src/assets/styles/*.scss', {
base: 'src' })
.pipe(sass())
.pipe(dest('./dist'))
}
// 定义脚本编译任务
const script = () => {
return src('./src/assets/scripts/*.js', {
base: 'src'})
.pipe(babel({
presets: ['@babel/preset-env'] })) // 指定babel去解析ECMAScript新特性代码
.pipe(dest('./dist'))
}
// 定义html模板编译任务
const html = () => {
return src('./src/**/*.html', {
base: 'src' })
.pipe(swig({
data })) // 指定html模板中的数据
.pipe(dest('./dist'))
}
// 定义图片编译任务
const image = () => {
return src('./src/assets/images/**', {
base: 'src' })
.pipe(imagemin())
.pipe(dest('./dist'))
}
// 定义字体编译任务
const font = () => {
return src('./src/assets/fonts/**', {
base: 'src' })
.pipe(imagemin())
.pipe(dest('./dist'))
}
// 定义其他不需要经过编译的任务
const copy = () => {
return src('./public/**', {
base: 'public' })
.pipe(dest('./dist'))
}
module.exports = {
scss, script, html, image, font, copy }
然后运行yarn gulp 任务名 来运行构建任务进行测试
这里说明下,html任务中传入的data,因为html源文件中用到了模板引擎,里面用到了相关数据,故我们解析时,需要传入相关的数据
2、合并任务
因以上6个任务在构建过程中户不影响,故可以进行并行构建,故此时,我们可以利用gulp提供的parallel方法来新建一个并行任务
但在建立任务之前,我们可以把任务进行分类,前面5个为都需要进行编译的任务,我们可以先合并为一个compile任务。然后再用这个compile任务
和copy任务并行合并为一个新的任务build
// 因以上任务都是需要编译的任务,且工作过程互相不受影响,故可以并行执行,故将以上5个任务合并成一个并行任务
const compile = parallel(scss, script, html, image, font)
// 将需要编译的任务和不需要进行编译的任务合并为一个构建任务
const build = parallel(compile, copy)
下面我们测试一下
运行 yarn gulp build
可以看到,相关任务,就都被打包了
3、任务初步优化
1、 每次构建时,都会把构建后的文件写入到dist目录下,那么我们是不是要在每次写入dist之前,将dist目前清空一下会比较好啊,可以防止多余无用代码的出现
怎么做:新增del模块,可以用于帮我们删除指定目录下的文件(yarn add del --dev)
const del = require('del')
// 定义清除目录下的文件任务
const clean = () => {
return del(['dist'])
}
此时,我们需要将新增的这个clean任务加入到构建流程中,此时,我们要想,我们是不是希望在其他任务将文件写入dist之前去清除dist目录下的文件啊
那么,此时,clean任务是不是就得在其他构建任务之前去执行啊。所以此时,我们需要将原来得build任务,串行加上一个clean任务
// 合并构建任务
const build = series(clean, parallel(compile, copy))
2、我们之前安装了很多gulp插件(gulp-开头得插件),每次我们新安装一个,就得引入一次,如果以后插件多了,是不是就会有很多插件得引用啊,此时我们可以借助gulp得另一个插件来解决这个问题gulp-load-plugins, 此插件会帮我们加载gulp下得所有插件,故我们只需要引入这个插件后,就可以直接通过这个插件,拿到gulp下得所有插件,下面,我们来修改一下代码,前面插件得引入,我们就不需要了
const loadPlugins = require('gulp-load-plugins')
const plugins = loadPlugins()
// 定义样式编译任务
const scss = () => {
return src('./src/assets/styles/*.scss', {
base: 'src' })
.pipe(plugins.sass())
.pipe(dest('./dist'))
}
// 定义脚本编译任务
const script = () => {
return src('./src/assets/scripts/*.js', {
base: 'src'})
.pipe(plugins.babel({
presets: ['@babel/preset-env'] }))
.pipe(dest('./dist'))
}
// 定义html模板编译任务
const html = () => {
return src('./src/**/*.html', {
base: 'src' })
.pipe(plugins.swig({
data }))
.pipe(dest('./dist'))
}
// 定义图片编译任务
const image = () => {
return src('./src/assets/images/**', {
base: 'src' })
.pipe(plugins.imagemin())
.pipe(dest('./dist'))
}
// 定义字体编译任务
const font = () => {
return src('./src/assets/fonts/**', {
base: 'src' })
.pipe(plugins.imagemin())
.pipe(dest('./dist'))
}
4、起一个开发服务器
下面,我们开始起一个开发服务器,完成开发时边开发边构建的功能
起一个开发服务器需要用到插件browser-sync
安装browser-sync插件 yarn add browser-sync
引入插,并创建一个开发服务器
const browserSync = require('browser-sync')
// 创建一个开发服务器
const bs = browserSync.create()
const serve = () => {
bs.init({
notify: false, // 关闭页面打开时browser-sync的页面提示
port: 2080, // 设置端口
server: {
baseDir: 'dist', // 设置开发服务器的根目录,会取此目录下的文件运行
routes: {
'/node_modules': 'node_modules' // 解决dist后的文件直接引入node_modules下文件的问题
}
}
})
}
上面说一下routes选项
主要是指定打包后,html文件中直接引入的node_modules下的包文件的问题,告知开发服务器直接去根目录下的node_modules文件夹下面找对应的文件
此时,我们开发服务器已经起了一个了,并告知了服务器去取dist下的文件作为运行文件。但是此时,还会有问题,那就是,如果dist下的文件发生了变化后,我们的开发服务器是无法得知的,此时我们需要配置一个files属性,来对dist下的文件进行监视。
const serve = () => {
bs.init({
notify: false, // 关闭页面打开时browser-sync的页面提示
port: 2080, // 设置端口
files: 'dist/**', // 监听dist下所有文件
server: {
baseDir: 'dist', // 设置开发服务器的根目录,会取此目录下的文件运行
routes: {
'/node_modules': 'node_modules' // 解决dist后的文件直接引入node_modules下文件的问题
}
}
})
}
此时,我们已经可以监听dist下的文件了。
5、开发服务器优化
虽然我们现在能对dist下的文件进行监视了,但是,依然是无法实现开发过程中,页面能即时响应的目的的。因为我们开发过程中修改的是源代码,而不是dist下的代码。那如何实现呢。继续往下看
5.1 监听构建前的源文件,保证开发过程中能够实现修改代码后,页面立刻得到相应
实现方式:利用gulp自带的watch模块对src下的源文件进行监听,源文件发生变化时,重新执行对应的构建任务,那么会重新构建,构建后,dist下的文件就会发生变化,serve通过files属性就能监听到
const serve = () => {
// watch监听相关源文件
watch('src/assets/styles/*.scss', scss)
watch('src/assets/scripts/*.js', script)
watch('src/*.html', html)
watch('src/assets/images/**', image)
watch('src/assets/fonts/**', font)
watch('public/**', copy)
bs.init({
notify: false,
port: 2080,
files: 'dist/**',
server: {
baseDir: 'dist',
routes: {
'/node_modules': 'node_modules'
}
}
})
}
5.2 进一步优化,上面我们已经实现了开发过程中,修改文件页面能即时响应。但是我们上面6个watch监听了6类文件,每类文件发生变化后,我们都重新执行了对应的构建任务。
我们试想,在开发过程中,我们只需要当文件发生变化时,页面能即时响应就行了,像html,scss,js等文件,需要编译成浏览器可识别的文件我们才能看到页面发生变化,故每次这类文件发生变化时,我们都去启动对应的任务重新构建一次这无可厚非。但是,像图片,字体以及不需要编译的静态文件。我们只需要看到变化就行了,有必要调用对应构建任务吗,像图片,字体,都是对它们进行了压缩,但我们实际开发阶段,这个完全没必要。
故,我们对这类开发阶段不需要处理的文件做个特殊处理。
5.2.1 我们在监听图片,字体,和public下的静态文件时,不再启动对应的构建任务,而是直接调用browserSync的reload()方法去重新加载页面
那么此时,我们开发服务器要拿到这些文件是不是就不能在dist下拿了啊,因为我们没有重新构建,故dist下不会有改变后的文件。
此时,我们修改baseDir的根目录为一个数组[‘dist’, ‘src’, ‘public’]。那么,服务器会优先去dist下找文件,如果找不到,会依次去src和public目录下寻找。像图片,字体,以及相关静态文件,开发服务器是不是就会去src和public下去加载啊
const serve = () => {
// watch监听相关源文件
watch('src/assets/styles/*.scss', scss)
watch('src/assets/scripts/*.js', script)
watch('src/**/*.html', html)
// watch('src/assets/images/**', image)
// watch('src/assets/fonts/**', font)
// watch('public/**', copy)
watch(
[
'src/assets/images/**',
'src/assets/fonts/**',
'public/**'
],
bs.reload
)
bs.init({
notify: false,
port: 2080,
files: 'dist/**',
server: {
baseDir: ['dist', 'src', 'public'],
routes: {
'/node_modules': 'node_modules'
}
}
})
}
5.3 有一个容易忽略的问题,我们上面serve服务器是以dist下的文件为跟目录,也就是服务器启动,会默认去取dist目录下的文件,如果找不到,就会去取src和public下的文件。那如果重来没有执行过build命令,那么dist下是不是空的啊,这么一来,像样式文件,js文件,html文件,他都会取src下面找,那找到的文件能运行吗,是不是不能啊。所以,我们需要新建一个develop任务,此任务在启动serve前,先执行一次compile任务。
// 因以上任务都是需要编译的任务,且工作过程互相不受影响,故可以并行执行,故将以上5个任务合并成一个并行任务
const compile = parallel(scss, script, html)
// 合并构建任务
const build = series(clean, parallel(compile, copy, image, font))
// 开发构建任务
const develop = series(compile, serve)
我们新建了一个develop任务,让起串行先执行compile和serve
同时,我们修改了一下compile任务,将image和font任务放入到build中了,这样我们develop中便不需要执行这两个任务了
5.4 上面我们说过,serve服务器是通过files属性去监听dist目录下的文件变化来实现即时更新的。可是像上面的图片,字体以及静态文件,我们好像并没有用到这个files属性,也实现了浏览器的实时更新吧。那我们其他文件,是不是也可以这样呢。对的,也可以这样,具体用法,见下面代码
// 定义样式编译任务
const scss = () => {
return src('./src/assets/styles/*.scss', {
base: 'src' })
.pipe(plugins.sass({
outputStyle: 'expanded' }))
.pipe(dest('./dist'))
.pipe(bs.reload({
stream: true })) // 构建任务每次执行后,都reload一次
}
// 定义脚本编译任务
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 })) // 构建任务每次执行后,都reload一次
}
// 定义html模板编译任务
const html = () => {
return src('./src/**/*.html', {
base: 'src' })
.pipe(plugins.swig({
data }))
.pipe(dest('./dist'))
.pipe(bs.reload({
stream: true })) // 构建任务每次执行后,都reload一次
}
以上添加了一个stream:true,意思是重新加载不需要进行读写操作,而是直接以流的方式往浏览器中推
好了,开发服务器的优化就到这了
下面,我们继续来优化生产环境的构建build
6、build的构建任务的优化
6.1 上面我们说过,serve服务器中配置了一个routes,是因为构建后的html文件引入了一些外部的资源文件,我们去处理那些资源文件了。
但是,build环境中,这些文件可能就找不到了,因为dist下没有node_modules文件夹,那么我们构建的时候该如何去处理这种构建后的资源引用问题呢
首先,我们可以看下构建后的html
可以看出,这种资源文件,构建后,会生成对应的build注释,标识了后续可将两个注释中间的部分合并成为一个新的文件(vendor.css)。那么如何处理这种情况呢。
gulp提供了一种叫useref的插件来处理这种情况,他会将注释中间引用的资源合并成为一个新的资源文件
安装 gulp-useref (yarn add gulp-useref --dev)
新建任务用此插件去处理这种情况
const useref = () => {
return src('dist/*html', {
base: 'dist' }) // 读取的是构建后的文件,故是dist下
.pipe(plugins.useref({
searchPath: ['dist', '.']})) // 请求的资源路径去哪找
.pipe(dest('dist'))
}
上面的searchPath 是指定构建时,请求的资源文件去什么地方找,如上图中的main.css,我们可以直接在dist下找,如果找不到,那么我们去当前根目录下找,故配置了第二个 ‘.’ 这个 . 就代表当前根目录 。比如上面的bootstrap.css 就会去根目录下找,找到后,直接将引入的这个css打包进dist下,并合并成vendor.css 。
这个合并,可能你们不大理解,看下图 ,你们就理解了
这个注释中间引入了3个文件,那么都会被打包成vendor.js一个文件。同时会将注释删除
此时,其实还会有点问题,大家可以看到读取文件是从dist下去读取,写入文件又是写入到dist下面,这其实会产生冲突,从同一个地方又读又写,是不是有问题啊。
此时,我们可以通过一个中间文件来进行一个过度。如何过度,请看6.2
6.2 我们可以在构建的时候,可以先让他构建到一个中间目录中,比如temp,然后useref再去temp中去读文件,读取后,再通过useref插件进行处理,然后再写入到dist中。那么我们原来的构建任务的写入路径就都要改了。但是这个只针对html,style,js 因为useref是处理引入的html以及js,css等资源路径的
// 定义样式编译任务
const scss = () => {
return src('./src/assets/styles/*.scss', {
base: 'src' })
.pipe(plugins.sass({
outputStyle: 'expanded' }))
.pipe(dest('./temp')) // 改成temp
.pipe(bs.reload({
stream: true })) // 构建任务每次执行后,都reload一次
}
// 定义脚本编译任务
const script = () => {
return src('./src/assets/scripts/*.js', {
base: 'src'})
.pipe(plugins.babel({
presets: ['@babel/preset-env'] }))
.pipe(dest('./temp')) // 改成temp
.pipe(bs.reload({
stream: true })) // 构建任务每次执行后,都reload一次
}
// 定义html模板编译任务
const html = () => {
return src('./src/**/*.html', {
base: 'src' })
.pipe(plugins.swig({
data }))
.pipe(dest('./temp')) // 改成temp
.pipe(bs.reload({
stream: true })) // 构建任务每次执行后,都reload一次
}
const useref = () => {
return src('temp/*html', {
base: 'temp' }) // 改成从temp下去读取文件流
.pipe(plugins.useref({
searchPath: ['dist', '.']})) // 改成从temp下去读取文件流
.pipe(dest('dist')) // 写入到dist
}
// 定义清除目录下的文件任务
const clean = () => {
return del(['dist', 'temp']) // 添加清除temp
}
然后修改构建流程,将useref放到compile之后再执行,同时,我们构建完以后,是不是还要将temp目录给清除啊,因为他只是个临时目录
// 清除temp
const cleanTemp = () => {
return del('temp')
}
// 合并构建任务
const build = series(clean, parallel(series(compile, useref, cleanTemp), copy, image, font))
6.3 文件压缩
前面我们利用useref构建的html,css,js等是不是还没有给他进行压缩处理啊,我们build任务一般是打包线上代码,那么这些文件肯定都是要进行压缩的。那么如何压缩呢
当然是针对不同的文件利用不同的插件进行压缩了
html 使用插件gulp-htmlmin yarn add gulp-htmlmin --dev
js 使用插件gulp-uglify yarn add gulp-uglify --dev
css 使用插件cleanCss yarn add gulp-clean-css --dev
同时,我们知道useref任务中是一个读取流可能读取到不同类型的文件(html或css或js),因此,我们还需要一个gulp-if插件来做判断
const useref = () => {
return src('temp/*html', {
base: 'temp' }) // 读取的是构建后的文件,故是dist下
.pipe(plugins.useref({
searchPath: ['dist', '.']})) // 请求的资源路径去哪找
.pipe(plugins.if(/\.js$/, plugins.uglify())) // 压缩脚本文件
.pipe(plugins.if(/\.css$/, plugins.cleanCss())) // 压缩样式文件
.pipe(plugins.if(/\.html$/, plugins.htmlmin({
collapseWhitespace: true, // 压缩html
minifyCss: true, // 压缩html文件中的内嵌样式
minifyJs: true // 压缩html文件中内嵌的js
})))
.pipe(dest('dist'))
}
6.4 导出相关指令
上面我们一般都是只暴露了develop 和 build两个任务,但一般还有个clean任务,我们也是比较常用的,我们将这个任务也单独导出
// 导出相关任务
module.exports = {
clean,
build,
develop
}
导出后,我们可以在package.json文件中去配置相关指令,以便我们更方便去执行我们的命令
"scripts": {
"clean": "gulp clean",
"build": "gulp build",
"develop": "gulp develop"
}
此时,我们可以直接通过yarn build去进行项目构建了
整个构建流程基本已经完成了。
下面我们来附上gulpfile.js完整代码
// 实现这个项目的构建任务
// 引入相关依赖
const {
src, dest, parallel, series, watch } = require('gulp')
const del = require('del')
const browserSync = require('browser-sync')
const loadPlugins = require('gulp-load-plugins')
const plugins = loadPlugins()
// 创建一个开发服务器
const bs = browserSync.create()
// const sass = require('gulp-sass')
// const babel = require('gulp-babel')
// const swig = require('gulp-swig')
// const imagemin = require('gulp-imagemin')
// 定义html模板需要得数据
const data = {
menus: [
{
name: 'Home',
icon: 'aperture',
link: 'index.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 scss = () => {
return src('./src/assets/styles/*.scss', {
base: 'src' })
.pipe(plugins.sass({
outputStyle: 'expanded' }))
.pipe(dest('./temp'))
.pipe(bs.reload({
stream: true })) // 构建任务每次执行后,都reload一次
}
// 定义脚本编译任务
const script = () => {
return src('./src/assets/scripts/*.js', {
base: 'src'})
.pipe(plugins.babel({
presets: ['@babel/preset-env'] }))
.pipe(dest('./temp'))
.pipe(bs.reload({
stream: true })) // 构建任务每次执行后,都reload一次
}
// 定义html模板编译任务
const html = () => {
return src('./src/**/*.html', {
base: 'src' })
.pipe(plugins.swig({
data }))
.pipe(dest('./temp'))
.pipe(bs.reload({
stream: true })) // 构建任务每次执行后,都reload一次
}
// 定义图片编译任务
const image = () => {
return src('./src/assets/images/**', {
base: 'src' })
.pipe(plugins.imagemin())
.pipe(dest('./dist'))
}
// 定义字体编译任务
const font = () => {
return src('./src/assets/fonts/**', {
base: 'src' })
.pipe(plugins.imagemin())
.pipe(dest('./dist'))
}
// 定义其他不需要经过编译的任务
const copy = () => {
return src('./public/**', {
base: 'public' })
.pipe(dest('./dist'))
}
// 定义清除目录下的文件任务
const clean = () => {
return del(['dist', 'temp'])
}
// 清除temp
const cleanTemp = () => {
return del('temp')
}
// 初始化开发服务器
const serve = () => {
// watch监听相关源文件
watch('src/assets/styles/*.scss', scss)
watch('src/assets/scripts/*.js', script)
watch('src/**/*.html', html)
// watch('src/assets/images/**', image)
// watch('src/assets/fonts/**', font)
// watch('public/**', copy)
watch(
[
'src/assets/images/**',
'src/assets/fonts/**',
'public/**'
],
bs.reload
)
bs.init({
notify: false,
port: 2080,
// files: 'dist/**',
server: {
baseDir: ['dist', 'src', 'public'],
routes: {
'/node_modules': 'node_modules'
}
}
})
}
const useref = () => {
return src('temp/*html', {
base: 'temp' }) // 读取的是构建后的文件,故是dist下
.pipe(plugins.useref({
searchPath: ['dist', '.']})) // 请求的资源路径去哪找
.pipe(plugins.if(/\.js$/, plugins.uglify()))
.pipe(plugins.if(/\.css$/, plugins.cleanCss()))
.pipe(plugins.if(/\.html$/, plugins.htmlmin({
collapseWhitespace: true, // 压缩html
minifyCss: true, // 压缩html文件中的内嵌样式
minifyJs: true // 压缩html文件中内嵌的js
})))
.pipe(dest('dist'))
}
// 因以上任务都是需要编译的任务,且工作过程互相不受影响,故可以并行执行,故将以上5个任务合并成一个并行任务
const compile = parallel(scss, script, html)
// 合并构建任务
const build = series(clean, parallel(series(compile, useref, cleanTemp), copy, image, font))
// 开发构建任务
const develop = series(compile, serve)
// 导出相关任务
module.exports = {
clean,
build,
develop
}
但是,此时,我们发现没有,我们写了这么多,只是用于处理了这一个项目的构建任务,但是我们肯定是希望我们所写的这些东西,能够作为和当前项目结构相似的一类项目的自动化构建工具。那么,最好的办法是不是将这个gulpfile.js封装成一个模块,然后发布到npm上面去啊。
那么以后人家需要使用的时候,是不是可以直接通过按照这个模块,就立马可以进行项目构建了啊。下面我们就来封装一下这个工作流
自动化构建工作流封装
1、首先,我们需要新建一个node_modules包。包名我们定为cgp-build
我这里使用了一个脚手架工具(caz)生成node_modules包的一些基础目录
我们先全局安装这个脚手架
yarn global add caz
运行caz nm cgp-build生成我们的包的基本目录
这个包中,lib下的index.js就是我们这个包的入口文件(一般包的入口文件都是lib下的index.js文件,而cli指令文件的入口文件一般是bin下的cli.js或者index.js)
也就是说,我们原来写在gulpfile.js中的代码,现在要放到lib/index.js中来,这里当别人执行这个包时,才会执行到这些具体的构建代码
2、将gulpfile.js中的代码拷贝到index.js中来
此时,gulpfile.js这个依赖了很多插件,所以这些插件都会被作为我们封装得这个包得生产依赖。故我们需要把之前那个打包项目中得package.json中devDependencies都拷贝到我们这个包目录中得package.json文件中的dependencies中
那么此时,后面有项目安装了我们这个cgp-build的时,就会自动安装这个包所依赖的这些插件。
3、提取项目中的数据
此时,还有问题,我们往上去看gulpfile.js中的代码,发现在解析html时,是不是传入了一个data数据啊。而data我们是直接定义在gulpfile.js中的。但是我们都知道,这个data数据,是不是项目的数据啊,不同的项目可能这个数据就不一样了,可能有的项目html文件中还没有这种模板数据。所以说,这个data,是不是应该提到项目中去啊。那么提到哪呢。
我们知道,很多项目中 是不是都有config.js文件啊,比如vue的vue.config.js。那么我们是不是也可以定义一个config文件啊,比如就叫page.config.js。那么用我们这个cgp-build进行自动化构建的项目都需要创建一个page.config.js文件,那我们是不是可以把这个data放到config文件中,当作配置数据传入啊。
而此时,我们lib/index.js文件中,我们就可以通过引入这个config.js文件中的配置,然后在构建的时候再使用这个配置数据
那么: 如何拿到项目目录下的config.js文件呢
我们分析下:我们这个包,最终是会被安装在项目目录的node_modules文件夹下的
那么我们这个包中的lib/index 相当于是在项目目录(我们假设项目目录是page-demo)下的node_models/cgp-build/lib/index.js 。
那么我们不是拿到了项目的根目录,就能拿到项目中的page.config.js文件啊。
node,js提供了一个全局api process.cwd() 可以获取到当前项目根目录
// 获取根目录
const cwd = process.cwd()
let config = {
} // 定义配置文件,这里面可能会有些默认配置
try {
const loadConfig = require(`${
cwd}/page.config.js`) // 获取项目目录中的配置文件
config = Object.assign({
}, config, loadConfig) // 合并config和loadConfig
} catch (err) {
throw err
}
// 定义html模板编译任务
const html = () => {
return src('./src/**/*.html', {
base: 'src' })
.pipe(plugins.swig({
data: config.data })) // 修改为config中的data
.pipe(dest('./temp'))
.pipe(bs.reload({
stream: true })) // 构建任务每次执行后,都reload一次
}
那么,page.config.js的配置文件,格式如下
现在,数据问题解决了,但是很多文件的路径我们是不是还是写死的啊
像这种路径,不同项目,是不是可能不一样啊,所以我们这样写死,也是不合理的,也应该抽象到page.config.js的配置中去
4、抽象路径
我们先在/lib/index.js中写入一份默认配置,当项目中配置了相关配置后,会覆盖index.js中的默认配置
// 获取根目录
const cwd = process.cwd()
let config = {
build: {
src: 'src',
dist: 'dist',
temp: 'temp',
public: 'public',
paths: {
styles: 'assets/styles/*.scss',
scripts: 'assets/styles/*.js',
pages: '*.html',
images: 'assets/images/**',
fonts: 'assets/fonts/**'
}
}
} // 定义配置文件,这里面可能会有些默认配置
然后将index.js中的路径都用config变量去代替
// 定义样式编译任务
const scss = () => {
return src(config.build.paths.styles, {
base: config.build.src, cwd: config.build.src })
.pipe(plugins.sass({
outputStyle: 'expanded' }))
.pipe(dest(config.build.temp))
.pipe(bs.reload({
stream: true })) // 构建任务每次执行后,都reload一次
}
// 定义脚本编译任务
const script = () => {
return src(config.build.paths.scripts, {
base: config.build.src, cwd: config.build.src })
.pipe(plugins.babel({
presets: ['@babel/preset-env'] }))
.pipe(dest(config.build.temp))
.pipe(bs.reload({
stream: true })) // 构建任务每次执行后,都reload一次
}
// 定义html模板编译任务
const html = () => {
return src(config.build.paths.pages, {
base: config.build.src, cwd: config.build.src })
.pipe(plugins.swig({
data: config.data })) // 修改为config中的data
.pipe(dest(config.build.temp))
.pipe(bs.reload({
stream: true })) // 构建任务每次执行后,都reload一次
}
// 定义图片编译任务
const image = () => {
return src(config.build.paths.images, {
base: config.build.src, cwd: config.build.src })
.pipe(plugins.imagemin())
.pipe(dest(config.build.dist))
}
// 定义字体编译任务
const font = () => {
return src(config.build.paths.fonts, {
base: config.build.src, cwd: config.build.src })
.pipe(plugins.imagemin())
.pipe(dest(config.build.dist))
}
// 定义其他不需要经过编译的任务
const copy = () => {
return src('**', {
base: config.build.public, cwd: config.build.public })
.pipe(dest(config.build.dist))
}
// 定义清除目录下的文件任务
const clean = () => {
return del([config.build.dist, config.build.temp])
}
// 清除temp
const cleanTemp = () => {
return del(config.build.temp)
}
// 初始化开发服务器
const serve = () => {
// watch监听相关源文件
watch(config.build.paths.styles, {
cwd: config.build.src}, scss)
watch(config.build.paths.scripts, {
cwd: config.build.src}, script)
watch(config.build.paths.pages, {
cwd: config.build.src}, html)
// watch('src/assets/images/**', image)
// watch('src/assets/fonts/**', font)
// watch('public/**', copy)
watch(
[
config.build.paths.images,
config.build.paths.images,
`${
config.build.public}/**`
],
bs.reload
)
bs.init({
notify: false,
port: 2080,
// files: 'dist/**',
server: {
baseDir: [config.build.dist, config.build.src, config.build.public],
routes: {
'/node_modules': 'node_modules'
}
}
})
}
const useref = () => {
return src(config.build.paths.pages, {
base: config.build.temp, cwd: config.build.temp }) // 读取的是构建后的文件,故是dist下
.pipe(plugins.useref({
searchPath: [config.build.temp, '.']})) // 请求的资源路径去哪找
.pipe(plugins.if(/\.js$/, plugins.uglify()))
.pipe(plugins.if(/\.css$/, plugins.cleanCss()))
.pipe(plugins.if(/\.html$/, plugins.htmlmin({
collapseWhitespace: true, // 压缩html
minifyCss: true, // 压缩html文件中的内嵌样式
minifyJs: true // 压缩html文件中内嵌的js
})))
.pipe(dest(config.build.dist))
}
我们在上面很多地方加了个cwd选项,是因为我们抽象出来的路径,去掉了src,所以我们需要通过cwd去指定去哪个目录下找这个路径
5、包装gulp-cli
下面我们要包装一下我们自己的cli命令,为什么要包装呢,因为gulp构建时,默认是找gulpfile.js文件的,而我们现在是放在/bin/index.js中,对于项目而言,这个文件在/node_modules/cgp-build/lib/index.js中,所以在项目中运行yarn gulp build是会报错的,报错,找不到gulpfile.js文件
此时,我们需要手动去指定gulpfile.js文件为哪个文件
yarn gulp build --gulpfile ./node_modules/cgp-build/lib/index.js --cwd .
–cwd . 的意思是以当前项目目录作为根目录,因为gulp会默认以gulpfile.js文件所在目录为根目录,所以我们需要特别指定一下根目录
那么这么弄,是不是很繁琐啊,每次我需要执行下构建任务时,都要输入这么一大串。此时,我们就可以自定义这个包文件自己的cli指令,将这些 --gulpfile --cwd等参数都集成到指令中去
如何定义cli
在包文件目录下新建bin文件夹,并在bin中新建cli.js。然后在package.json文件中添加bin字段
cli.js文件需要加个文件头 #!/usr/bin/env node(cli入口文件都需要的)
这里我解释一下:一般包的cli指令文件都是在包目录下的bin目录下,比如webpack,当你运行webpack main.js命令去打包main.js时,也是会先去找node_modules/webpack/bin/*.js文件的
那么,此时,我们cli.js 文件中需要写什么呢,我们分析下
大家想啊,我们本质是要去执行gulp build --gulpfile …这种命令,
只是我们先去执行了我们自己的cli命令 cgp-build,那cgp-build执行后,去找了/bin/cli.js文件后,我们是不是只需要在这里去执行gulp的构建命令就可以了啊,那执行gulp的构建命令本质上是不是去执行gulp/bin/**.js文件啊。所以此时,我们只需要在我们的cli.js文件中去运行gulp/bin/gulp.js文件就行了
这样一来,当我们执行cgp-build build时,实际上就会执行gulp build命令
但这样还不够啊,我们前面是不是说了啊,我们需要携带参数去查找gulpfile.js文件以及指定根目录啊。此时,我们可以借助全局方法process.argv 这个可以拿到的其实就是参数列表,是个数组,如:–gulpfile /node_modules/cgp-build/lib/index.js 数组中就是[’–gulpfile’, ‘/node_modules/cgp-build/lib/index.js’]
那么,我们可以通过push方法往参数中添加参数
此时,整个cli的封装就完成了。
npm提交
npm提交我们上篇文章已经说过了,这里就随便提一下了,
1、将包上次至开源库,如github
2、npm publish 或者 yarn publish上传至npm库中
提交完后,我们测试下
先在本地准备一个项目目录gulp-demo,里面放入我们之前那个项目
然后安装我们提交至npm的包 cgp-build
yarn add cgp-build --dev
然后运行yarn cgp-build build 或者 cgp-build build
可以看出,是没有问题的,正常打包成功
好了,自动化构建就写到这了。喜欢请点个赞,谢谢
转载:https://blog.csdn.net/weixin_42707287/article/details/115598630