【学习笔记】Part2·前端工程化实战–开发脚手架及封装自动化构建工作流(一、工程化概述)
【学习笔记】Part2·前端工程化实战–开发脚手架及封装自动化构建工作流(二、脚手架工具)
【学习笔记】Part2·前端工程化实战–开发脚手架及封装自动化构建工作流(三、自动化构建 – 主Grunt)
【学习笔记】Part2·前端工程化实战–开发脚手架及封装自动化构建工作流(三、自动化构建 – 主Gulp)
Gulp 作为当下前端最流行的构建系统,其核心特点就是高效、易用,因为使用 gulp 的过程非常简单,大体的过程就是先在项目中安装 gulp 的开发依赖,然后在项目的根目录,也就是 package.json 所在目录去添加一个 gulpfile 的 js 文件,用于编写我们需要 gulp 自动执行的构建任务,完成之后我们就可以在命令行终端去使用 gulp 模块提供 cli 去运行这些构建任务。
我们去一个空项目中具体看一下:
mkdir gulp-empty
cd gulp-empty
yarn init --yes
yarn add gulp --dev
安装 gulp 同时它会自动安装一个叫 gulp-cli 的模块,也就是说在 node_modules 的 .bin 目录下会出现一个 gulp 的命令,有了这个命令之后我们就可以在后续通过这个命令去运行我们的构建任务。
再创建一个叫 gulpfile 的 js 文件
在这个文件当中我们定义一些需要 gulp 执行的一些构建任务,因为这个文件是运行在 nodejs 的运行环境当中,所以我们可以使用 commonjs 的规范来编写,定义构建任务的方式就是去导出函数成员的方式去定义:
// gulp 入口文件
// 相当于定义了一个叫做 foo 的任务,可以在命令行终端运行这个任务
exports.foo = () => {
console.log('foo task working ~')
}
通过 yarn gulp foo
运行这个任务:
此时命令行终端确实帮我们执行了这个任务,并且 console 也正常打印出来了,但是报了一个错误,大致意思是 foo 任务没执行完成,是否忘记去标记这个任务的结束了?
这是因为在最新的 gulp 中它取消了同步模式,约定每个任务都必须是一个异步的任务,当我们的任务执行过后,我们需要通过回调函数或者其他的方式去标记这个任务已经完成,所以说我们需要手动去调用一个回调函数,这个回调函数我们可以通过 foo 这个函数的形式参数得到:
// gulp 入口文件
// 相当于定义了一个叫做 foo 的任务,可以在命令行终端运行这个任务,接收一个形式参数 done ,就是回调函数,手动调用它标识该任务的结束
exports.foo = done => {
console.log('foo task working ~')
done() // 表示任务完成
}
重新运行 yarn gulp foo
此时这个任务就正常开始正常结束,并且打印了我们的任务体中的消息。
这个就是在 gulp 当中去定义一个任务的操作方式,如果任务名称是 default 的话,它会作为一个默认任务出现,这里可以测试一下:
// gulp 入口文件
// 相当于定义了一个叫做 foo 的任务,可以在命令行终端运行这个任务
exports.foo = done => {
console.log('foo task working ~')
done()
}
exports.default = done => {
console.log('default working ~')
done()
}
有了 default 之后,我们就可以在命令行中运行这个任务,因为此时这个 default 会作为 gulp 的默认任务出现,所以我们在运行的时候可以不加任务名称直接运行 yarn gulp
:
除此之外,还有一个需要注意的就是 gulp 4.0 以前,去注册任务是需要通过 gulp 模块里边的一个方法 gulp.task 去实现,具体就是:
// gulp 入口文件
// 相当于定义了一个叫做 foo 的任务,可以在命令行终端运行这个任务
exports.foo = done => {
console.log('foo task working ~')
done()
}
exports.default = done => {
console.log('default working ~')
done()
}
const gulp = require('gulp') // 4.0 以前
gulp.task('bar', done => { // 4.0 以前
console.log('bar working ~')
done()
})
bar 任务也正常工作,这是因为 4.0 以后也正常保留了 task 方法。
虽然说我们还可以直接使用这个 api 但是这种方式已经不被推荐了,更加推荐的方式就是导出函数成员的方式定义我们的 gulp 任务。
除了创建普通的任务以外,最新的 gulp 还提供了 series 和 parallel 两个用来创建组合任务的两个 API,有了这两个 API 过后就可以很轻松的去创建并行任务和串行任务。
具体来看:
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)
}
我们把这种未被导出的成员函数理解成私有任务,他们各自模拟了一个需要执行1S的任务,这里我们并不能通过 gulp 直接去运行他们,我们可以通过 gulp 提供的 series 和 parallel 这两个API 把他们组合成一个组合任务:
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.foo = series(task1, task2, task3) // series 组合三个函数为串行任务,按照顺序依次执行这些任务
exports.bar = parallel(task1, task2, task3) // parallel 组合三个函数为并行任务,同时执行
命令行运行看一下 yarn gulp foo
yarn gulp bar
:
可以看到 foo 任务执行三个任务是依次执行
可以看到 bar 任务执行三个任务是同时执行
总的来说,创建并行任务,或者串行 任务,它在我们实际创建构建工作流时,非常有用,例如我们编译 css 和 编译 js,他们是互不干扰的,这两个任务我们就可以通过并行的方式去执行,这样就能提高一些构建效率。
再比如我们去构建一些部署任务,部署任务需要先执行编译任务,这时候我们就需要通过 series 串行的模式去执行这样的任务。
正如我们一开始所说的,gulp 的任务都是异步任务,也就是我们 js 中经常说的异步函数。我们都知道去调用一个异步函数的时候是没办法直接知道这个异步函数是否调用完成的。都是在函数内部通过回调或者事件的方式去通知外部这个函数是否完成。
这里在异步任务当中同样面临着如何去通知 gulp 任务完成情况的这样一个问题。针对于这个问题,gulp 中有很多解决方法,我们这里看几个最常用的方式。
第一种自然是回调的方式去解决:
exports.callback = done => {
console.log('callback task ~')
done() // 通知 gulp 这个任务执行完成了
}
我们尝试运行一下这个任务 yarn gulp callback
:
没有问题,这个回调函数与 node 当中的回调函数是同样的标准,都是一种叫做错误优先的回调函数,也就是说当我们想在执行过程当中去报出一个错误,阻止剩下任务执行的时候,这时候我们可以通过给回调函数的第一个参数去指定一个错误对象就可以了。
exports.callback_error = done => {
console.log('callback_error task ~')
done(new Error('task failed !'))
}
运行一下 yarn gulp callback_error
可以看到命令行执行过程当中报出了这样一个错误,而且如果你是多个任务同时执行的话,后续的任务也就不会工作了。
这个就是回调函数错误优先需要注意的点。
通过这个回调函数我们联想到 ES6 当中一个叫做 promise 的方案,promise 是相对于回调来讲一个比较好的替代方案,因为它避免了代码中回调嵌套过深的问题。
在 gulp 中同样支持 promise 的方式,具体使用就是在任务的执行函数中去return 一个 promise 对象,这里直接通过 Promise.resolve() 直接返回一个成功的 promise ,一旦当我们返回的 promise 对象 resolve 了,也就意味着我们这个任务结束了。这里需要注意的是 resolve 的时候不需要返回任何值,因为 gulp 中会忽略掉这个值,这时候我们重新执行:
exports.promise = () => {
console.log('promise task ~')
return Promise.resolve()
}
当然,使用 promise 自然会涉及到 promise 的 reject, 也就是 失败,一旦当你return 的是一个失败的 promise ,我们的 gulp会认为这是一个失败的任务,同样会结束后续所有任务的执行。
exports.promise_error = () => {
console.log('promise_error task ~')
return Promise.reject(new Error('task failed ~'))
}
用到了 promise 自然会想到 ES7 当中提供的 async 和 await ,它实际上是 promise 的语法糖,它可以让我们使用 promise 的代码更加容易理解。
如果说 node 环境是 8 以上的版本,可以使用这种方式,具体就是将任务函数定义成一个异步函数,在函数当中去 await 一个异步任务(其实 await 的就是一个 promise 对象).
const timeout = time => {
return new Promise(resolve => {
setTimeout(resolve, time)
})
}
exports.async = async () => {
await timeout(1000)
console.log('async task ~')
}
运行查看 yarn gulp async
:
实际上内部还是 promise ,这种方式只受限于 node 环境,只要 node 环境支持,都可以使用这种方式。
这几种都是我们在 JavaScript 中去处理异步的几种常见方式,在 gulp 当中都被支持。
除了这些方式以外,gulp 还支持另外几种方式,其中,通过 stream 的方式是最为常见的。
因为我们的构建系统大多都是处理文件,所以这种方式也是最常用的一种,具体就是我们在函数当中需要返回一个 stream 对象:
const fs = require('fs')
exports.stream = () => {
const readStream = fs.createReadStream('package.json') // 文件读取流对象
const writeStream = fs.createWriteStream('temp.txt') // 文件写入流对象
readStream.pipe(writeStream) // readStream 通过 pipi (管道) 导入 writeStream 中,启到一个复制作用
return readStream
}
运行一下 yarn gulp stream
:
可以发现,任务正常开始正常结束。
它结束的时机实际上就是 readStream end 的时候,因为 stream 当中都有一个 end 事件,一旦当这个读取的文件六读取完成过后,会触发 end 事件,从而 gulp 就知道这个任务已经完成了。
我们这里可以使用这个 end 事件模拟一下这个任务:
const fs = require('fs')
exports.stream = done => {
const readStream = fs.createReadStream('package.json') // 文件读取流对象
const writeStream = fs.createWriteStream('temp.txt') // 文件写入流对象
readStream.pipe(writeStream) // readStream 通过 pipi (管道) 导入 writeStream 中,启到一个复制作用
readStream.on('end', ()=>{
done()
})
}
重新执行:
会发现这个任务也会正常结束。这也就意味着其实 gulp 当中只是注册了这么一个事件,去监听这个任务的结束罢了。
这些就是我们在 gulp 当中经常会用到的一些处理异步流程的操作。
在了解了 gulp 当中定义任务的方式过后,接下来看一下这些任务中需要做的具体的工作:构建过程
构建过程大多数情况下都是将文件读取出来然后经过一些转换,然后写入另外一个位置。其实在没有构建过程的时候,我们也是人工按照这样一个过程去做的。
比如我们要压缩一个 css 文件,我们需要把代码复制出来,然后到一个压缩工具当中去压缩一下,最后将压缩过后的结果粘贴到一个新的文件当中。这是一个手动的过程,其实通过代码的方式去解决也是一个类似的过程。接下来我们就回到代码中通过底层 node 的,最原始的 api 去模拟实现以下这样一个过程。
mkdir gulp-build-process
cd gulp-build-process
yarn init --yes
yarn add gulp --dev
# 在编辑器当中打开当前目录
code .
# 在编辑器当中编辑 gulpfile.js 保存之后就会生成对应的 gulpfile.js 文件
code gulpfile.js
随便准备一个 css 文件,比如:normalize.css 查看
const fs = require('fs')
exports.default = () => {
// 文件读取流
const readStream = fs.createReadStream('normalize.css')
// 文件写入流
const writeStream = fs.createWriteStream('normalize.min.css')
// 把读取出来的文件六导入到写入文件流
readStream.pipe(writeStream)
return readStream // 这样的话 gulp 能通过 stream 的状态判断是否完成
}
命令行执行 yarn gulp
:
可以看到,文件已经被复制了。但是这里要做的是把文件内容读取出来经过转换过后再去写入文件,并不是直接写入,所以这里需要导入以下 stream 模块当中的 Transform 类型,有了这个类型之后,就可以通过这个类型去创建一个文件转换流对象。
const fs = require('fs')
const { Transform } = require('stream')
exports.default = () => {
// 文件读取流
const readStream = fs.createReadStream('normalize.css')
// 文件写入流
const writeStream = fs.createWriteStream('normalize.min.css')
// 文件转换流
const transformStream = new Transform({ // output 就会作为转换过后的结果往后导出,在 write 之前
transform: (chunk, encoding, callback)=>{ // 核心转换过程实现
// chunk => 读取流中读取到的内容(Buffer) 可以通过 toString 的方式转换成字符串,因为读取出来的是字节数组
const input = chunk.toString()
const output = input.replace(/\s+/g, '').replace(/\/\*.+?\*\//g, '') // 先替换掉空格,再替换掉注释 /* ... */ 这种
callback(null, output) // 注意,callback 是一个错误优先的函数,第一个参数:如果有错误应该传递错误对象,如果没有错误,直接传递 null
}
})
// 把读取出来的文件六导入到写入文件流
readStream
.pipe(transformStream) // 转换
.pipe(writeStream) // 写入
return readStream // 这样的话 gulp 能通过 stream 的状态判断是否完成
}
执行一下:
再看 normalize.min.css 当中就是一个被转换压缩过后的结果了。
这是 gulp 当中一个常规的构建任务的核心工作过程,这个过程当中有三个核心的概念:
这样一个过程就完成了我们日常构建过程当中所需要的工作。
Gulp 官方定义就是 The streaming build system (基于流的构建系统)
至于在 gulp 当中为什么选择使用文件流的方式,这是因为 gulp 希望实现一个构建管道的概念,这样我们在后续做一些扩展插件的时候,就可以有一个很统一的方式。这些在后续接触到插件的使用过后就会有明确的体会了。
Gulp 中为我们提供了专门去创建文件读取流和写入流的 API,相比于底层 node 的 api,gulp 的 API 更强大也更容易使用,至于负责文件加工的转换流,绝大多数情况下我们都是通过独立的插件来提供,这样的话我们在实际通过 gulp 去创建构建任务的流程,就是先通过 src 方法去创建一个读取流,然后再借助于插件提供的转换流去实现加工,最后再通过 gulp 提供的 dest 方法去创建一个写入流,从而写入到目标文件。
具体来看:
mkdir gulp-files-api
cd gulp-files-api
yarn init --yes
yarn add gulp --dev
随便准备两个 css:
boorstrap.css 查看
normalize.css 查看
const { src, dest } = require('gulp') // 载入 src dest
exports.default = () => {
return src('src/normalize.css') // src 创建读取流, return 出去这个读取流,这样 gulp 就可以控制这个任务是否完成了
.pipe(dest('dist')) // 通过 pipe 方法导出到 dest 创建的写入流当中,dest 方法只需要给一个写入的目标目录就可以了
}
尝试运行:
dist 目录下多出来一个 normalize.css 文件,也就意味着我们这个文件读取流和写入流是可以正常工作的。
正如我们之前所说的,相比于原始 API ,gulp 模块所提供的 API 更为强大一些。因为我们可以在这里使用通配符的方式去匹配批量的文件。
const { src, dest } = require('gulp') // 载入 src dest
exports.default = () => {
return src('src/*.css') // src 创建读取流, return 出去这个读取流,这样 gulp 就可以控制这个任务是否完成了,使用 * 通配符
.pipe(dest('dist')) // 通过 pipe 方法导出到 dest 创建的写入流当中,dest 方法只需要给一个写入的目标目录就可以了
}
重新运行:
除了 normalize 文件 ,其他的 css 文件也会被复制过去。
当然了,构建过程最重要的就是文件的转换,我们这里如果需要去完成文件的压缩转换,我们可以去安装一个叫 gulp-clean-css 这样一个插件,这个插件提供了压缩 css 代码的转换流。
yarn add gulp-clean-css --dev
const { src, dest } = require('gulp') // 载入 src dest
const cleanCss = require('gulp-clean-css')
exports.default = () => {
return src('src/*.css') // src 创建读取流, return 出去这个读取流,这样 gulp 就可以控制这个任务是否完成了,使用 * 通配符
.pipe(cleanCss()) // 在 write 之前先去 pipe 到 cleanCss 提供的转换流当中,这样就会先经过转换,最后再被写入到写入流当中
.pipe(dest('dist')) // 通过 pipe 方法导出到 dest 创建的写入流当中,dest 方法只需要给一个写入的目标目录就可以了
}
重新运行:
此时,normalize.css 和 bootstrap.css 就是压缩过后的样式代码了。
当然,如果在这个过程当中需要执行多个转换的话,那么还可以继续在中间添加额外的 pipe 操作。例如:gulp-rename
yarn add gulp-rename --dev
const { src, dest } = require('gulp') // 载入 src dest
const cleanCss = require('gulp-clean-css')
const rename = require('gulp-rename')
exports.default = () => {
return src('src/*.css') // src 创建读取流, return 出去这个读取流,这样 gulp 就可以控制这个任务是否完成了,使用 * 通配符
.pipe(cleanCss()) // 在 write 之前先去 pipe 到 cleanCss 提供的转换流当中,这样就会先经过转换,最后再被写入到写入流当中
.pipe(rename({ extname: '.min.css' })) // 继续 pipe , 指定 extname 参数
.pipe(dest('dist')) // 通过 pipe 方法导出到 dest 创建的写入流当中,dest 方法只需要给一个写入的目标目录就可以了
}
dist 下方出现了两个 .min.css 文件,这也就意味着我们第二个转换流的插件也正常工作了。
以上这种 通过 src 去 pipe 到插件转换流,再去 pipe 到一个 写入流这样的过程,就是我们使用 gulp 的一个常规过程。
这里通过一个实际案例一起学习一下如何使用 gulp 完成一个网页应用的自动化构建工作流。
我们这里先下载一个提前准备好的基本网页应用的工作结构:【查看下载】
上边这个结构是完成之后的,这里有个空的,可以下载这个:【查看下载】
注意看,这里要做的是演示案例自动化构建工作流操作,所以我们首先把项目里边的东西删除一下:
最终是这么一个东西:
先说一下这个项目的目录对应的功能等东西:
这些就是我们对自动化构建网页的一些诉求,有了这些诉求过后接下来我们就具体来去做。
首先是安装 gulp
yarn add gulp --dev
在项目根目录下新建 gulpfile.js 的文件,所有的构建需求都需要在这个文件中来完成,我们依次来看:
const { src, dest } = require('gulp') // 先导入 gulp 所提供的 API
// 首先先定义私有的任务,后续再通过 module.exports 选择性的导出
// 定义 style 任务
const style = () => {
return src('src/assets/styles/*.scss') // 创建文件读取流
.pipe(dest('dist')) // 写入流
}
module.exports = {
style
}
命令行运行 yarn gulp style
项目根目录下就多出来 dist 目录,里边的文件就是我们的样式目录下方的一些文件,这里需要注意的是我们实际上是希望它可以按照 src 下面的这样一个目录结构去输出,而此时只是按照我们匹配出来的这些文件的名字直接放到了 dist 下面,这个就丢失掉了我们原本的目录结构。
这个问题可以通过 src 去指定一个选项参数,叫做 base ,去指定,就是我们转换的时候基准路径是什么,这里叫 src。
const { src, dest } = require('gulp') // 先导入 gulp 所提供的 API
// 首先先定义私有的任务,后续再通过 module.exports 选择性的导出
// 定义 style 任务
const style = () => {
return src('src/assets/styles/*.scss', { base: 'src' }) // 创建文件读取流, 指定一个 base 基准路径,就可以把 src 下面的路径给保留下来
.pipe(dest('dist')) // 写入流
}
module.exports = {
style
}
重新运行测试:
此时我们 dist 下面就会有 assets/style/样式文件,这样文件拷贝过程就正常了。但是文件还没有经过转换,按照之前的流程,转换的过程我们需要通过插件提供的一些转换流来实现。
yarn add gulp-sass --dev
在安装 gulp-sass 的时候,它内部会去安装 node-sass ,内部会有一些对 c++ 程序集的依赖,会慢一些。安装完成过后我们就可以去使用了。
这里提示一下:基本上每一个插件提供的都是一个函数,函数的调用结果会返回一个文件的转换流,这样呢我们就可以去实现文件的转换过程。
const { src, dest } = require('gulp') // 先导入 gulp 所提供的 API
const sass = require('gulp-sass') // 导入 gulp-sass
// 首先先定义私有的任务,后续再通过 module.exports 选择性的导出
// 定义 style 任务
const style = () => {
return src('src/assets/styles/*.scss', { base: 'src' }) // 创建文件读取流, 指定一个 base 基准路径,就可以把 src 下面的路径给保留下来
.pipe(sass())
.pipe(dest('dist')) // 写入流
}
module.exports = {
style
}
重新运行
可以看到生成了两个 css 文件,但是之前的文件它不会给你清空掉,如果同名它会给覆盖掉,后续我们再加个插件去自动删除这些文件,现在先手动删除一下。
删除之后重新运行一下,这样不会收到干扰。
转换之后的文件已经生成。我们会发现通过 src 匹配读取的时候匹配到 4 个文件,但是输出过后只有两个文件,这是因为 sass 模块在工作的时候,它会认为这种 _***开头的文件都是在主文件当中依赖的文件:
它就不会被转换,会被忽略掉,所以说最终只有这种没有 _ 开头的文件被转换出来。比如这里的 demo.scss 文件,文件里面我们什么都不做,编译之后就会生成对应的 demo.css 文件。
关于 sass 的使用就是这么简单,还有一个点就是在生成的代码当中你可能觉得排版有问题,
这里我们可以通过给 sass 指定一个选项去完成:
const { src, dest } = require('gulp') // 先导入 gulp 所提供的 API
const sass = require('gulp-sass') // 导入 gulp-sass
// 首先先定义私有的任务,后续再通过 module.exports 选择性的导出
// 定义 style 任务
const style = () => {
return src('src/assets/styles/*.scss', { base: 'src' }) // 创建文件读取流, 指定一个 base 基准路径,就可以把 src 下面的路径给保留下来
.pipe(sass({ outputStyle: 'expanded' })) // 给 sass 指定选项
.pipe(dest('dist')) // 写入流
}
module.exports = {
style
}
这是我们样式的编译任务,后边还有脚本编译任务和 html 编译任务。
这里做一下脚本文件 ES6 的编译。再添加一个新的任务,叫 script。同样的如果不用转换插件的话,也只是将文件直接拷贝过去,这里同样需要安装一个转换插件,中间做一下转换:
yarn add gulp-babel --dev
const babel = require('gulp-babel')
// 定义 script 任务
const script = () => {
return src('src/assets/scripts/*.js', { base: 'src' }) // 同样给一个基准路径
.pipe(babel())
.pipe(dest('dist'))
}
module.exports = {
script
}
先尝试运行一下 yarn gulp script
这里报一个错,未找到 @babel/core 这个模块,这里和之前使用 grunt 的时候遇到的问题是一样的,因为这里使用 babel 去转换,而 gulp-babel 这个插件只是去帮你唤醒 @babel/core 这个模块中的转换过程,它没有像 gulp-sass 一样自动安装 node-sass 这个核心模块,所以这里需要手动安装一下:
yarn add @babel/core @babel/preset-env --dev
env 这个模块会把 ES6 的全部的新特性做一个转换。
这里去 babel 选项中添加以下配置:
// 定义 script 任务
const script = () => {
return src('src/assets/scripts/*.js', { base: 'src' }) // 同样给一个基准路径
.pipe(babel({ presets: ['@babel/preset-env'] })) // 添加一下 babel 的配置
.pipe(dest('dist'))
}
module.exports = {
script
}
重新运行:
此时就能看到被转换完成过后的代码了。我们测试一下如果我们忘记了添加 babel 的 presets 配置的话是什么效果:
运行测试:
可以看到生成的文件里边代码和源文件里边的代码几乎是一摸一样的,没做任何的改动,原因是因为 babel 默认只是转换 ECMAScript 的一个平台,我们要知道平台是不做任何事情的,只是提供一个环境,具体去做转换的实际上是 babel 内部的一些插件,而 preset 就是一些插件的集合,比如 preset-env,它实际上就是一些最新的特性的整体打包,我们使用它的话就会把所有特性全部做转换,也可以根据需要去安装对应的 babel 转换插件在这里指定要使用的对应的插件。这样只会转换对应的特性。
而且一般我们对 babel 的特性呢一般我们会单独添加一个叫 .babelrc 的一个配置,那个也是可以的,这里只是写在了代码里边,没什么区别。
这里看一下模板文件的编译:
模板文件也就是 html 文件,在这些 html 当中我们为了可以去让页面当中重用的地方被抽象出来,我们使用了模板引擎,这里使用的模板引擎叫 swig 我们可以单独安装一下 swig 的插件
yarn add gulp-swig --dev
const swig = require('gulp-swig')
// 定义 page 任务
const page = () => {
// 如果不只是在 src 下面有 html 文件的话,可以使用 src/**/*.html 意思是 src 下面任意子目录下的 *.html 文件,这是子目录的通配方式,这里的 base 设置的就没有意义了,因为通配符就在 src 目录下,为了保证统一,所以设置一下 base
return src('src/*.html', { base: 'src' })
.pipe(swig())
.pipe(dest('dist'))
}
module.exports = {
page
}
运行一下这个任务 yarn gulp page
:
这个任务完成过后,三个 html 文件就会被转换到我们的目标目录,而且这里边的模板页和部分页就会正常工作了。
但是有一个点就是在源文件的 html 里边用到了一些数据标记:
这些数据标记实际上就是把我们网页开发过程当中有可能会发生变化的地方,比如网站名字等,提取成一些数据,我们需要在模板引擎工作的时候通过选项去指定,这里有一份提前准备好的数据,可以直接拿走,或者自己模拟几个数据,根据开发的 html 页面来决定数据结构等:【查看】
这里把数据直接拷贝过来直接使用,通过 swig 的 data 参数传递过去,
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()
}
// 定义 page 任务
const page = () => {
// 如果不只是在 src 下面有 html 文件的话,可以使用 src/**/*.html 意思是 src 下面任意子目录下的 *.html 文件,这是子目录的通配方式,这里的 base 设置的就没有意义了,因为通配符就在 src 目录下,为了保证统一,所以设置一下 base
return src('src/*.html', { base: 'src' })
.pipe(swig({ data })) // 数据传递过去
.pipe(dest('dist'))
}
module.exports = {
page
}
然后再次运行 page 任务 yarn gulp page
:
此时再去看转换完之后的结果,你会发现,很多地方的东西都已经被填补上去了。
这样就把我们网页当中经常写死的数据提取出来,在代码当中去配置,也可以单独写个 json 文件或者 js 文件都行。这里只是用一个对象模拟一下。
有了这三个任务之后,src 下面主体需要去转换或者说编译的事情都已经完成了。接下来就创建一个组合任务把三者组合在一起,因为这三个任务一旦运行的话不可能只运行一个,都是一起运行,所以这里单独创建一个 compile 的组合任务。
至于使用 series 还是 parallel ,因为这三个任务相互之间没有任何的牵连,所以我们可以让这三个任务同时开始执行,提高编译运行效率,所以这里要使用 parallel。这里就不需要导出 style page script 三个任务了,直接导出 compile 就行。
const { src, dest, series, parallel } = require('gulp') // 先导入 gulp 所提供的 API
const sass = require('gulp-sass') // 导入 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()
}
// 首先先定义私有的任务,后续再通过 module.exports 选择性的导出
// 定义 style 任务
const style = () => {
return src('src/assets/styles/*.scss', { base: 'src' }) // 创建文件读取流, 指定一个 base 基准路径,就可以把 src 下面的路径给保留下来
.pipe(sass({ outputStyle: 'expanded' })) // 给 sass 指定选项
.pipe(dest('dist')) // 写入流
}
// 定义 script 任务
const script = () => {
return src('src/assets/scripts/*.js', { base: 'src' }) // 同样给一个基准路径
.pipe(babel({ presets: ['@babel/preset-env'] })) // 添加以下 babel 的配置
.pipe(dest('dist'))
}
// 定义 page 任务
const page = () => {
// 如果不只是在 src 下面有 html 文件的话,可以使用 src/**/*.html 意思是 src 下面任意子目录下的 *.html 文件,这是子目录的通配方式,这里的 base 设置的就没有意义了,因为通配符就在 src 目录下,为了保证统一,所以设置一下 base
return src('src/*.html', { base: 'src' })
.pipe(swig({ data }))
.pipe(dest('dist'))
}
const compile = parallel( style, script, page )
module.exports = {
compile
}
先删除一下 dist 整个目录,运行 yarn gulp compile
:
运行 compile :
可以看到,三个任务同时开始工作,并且都正常工作完成,并且生成了对应的文件。这里编译的任务就算是有一个小结了。后续看其他的一些任务
除了样式文件,脚本文件还有页面文件的编译,还需要处理一下字体文件以及图片文件的处理过程。需要单独添加两个任务:
图片需要一个单独的插件去压缩 gulp-imagemin:
yarn add gulp-imagemin --dev
const imagemin = require('gulp-imagemin')
// image 任务
const image = () => {
return src('src/assets/images/**', { base: 'src' }) // ** images 下边的所有文件
.pipe(imagemin())
.pipe(dest('dist'))
}
module.exports = {
image
}
尝试运行一下:
我这里运行报错了,提示缺少了三个插件,imagemin-pluginName(忘了是啥了),发现是安装插件有东西安装失败报错了,因为当时公司项目出问题了,这里就没做记录,只记录一下都做了哪些操作吧,也不知道哪个才是关键点:
npm config get registry
# http://registry.npm.taobao.org/
yarn config get registry
# https://resgistry.npmjs.org
#github
192.30.253.113 github.com
192.30.253.113 github.com
192.30.253.118 gist.github.com
192.30.253.119 gist.github.com
13.229.188.59 github.com
54.169.195.247 api.github.com
140.82.113.25 live.github.com
8.7.198.45 gist.github.com
# 185.199.108.154 github.githubassets.com
# 185.199.109.154 github.githubassets.com
185.199.110.154 github.githubassets.com
# 185.199.111.154 github.githubassets.com
34.196.247.240 collector.githubapp.com
# 52.7.232.208 collector.githubapp.com
52.216.92.163 github-cloud.s3.amazonaws.com
151.101.108.133 raw.githubusercontent.com
151.101.108.133 user-images.githubusercontent.com
151.101.108.133 avatars.githubusercontent.com
151.101.108.133 avatars0.githubusercontent.com
151.101.108.133 avatars1.githubusercontent.com
151.101.108.133 avatars2.githubusercontent.com
151.101.108.133 avatars3.githubusercontent.com
151.101.108.133 avatars4.githubusercontent.com
151.101.108.133 avatars5.githubusercontent.com
151.101.108.133 avatars6.githubusercontent.com
151.101.108.133 avatars7.githubusercontent.com
151.101.108.133 avatars8.githubusercontent.com
151.101.108.133 avatars9.githubusercontent.com
151.101.108.133 avatars10.githubusercontent.com
151.101.108.133 avatars11.githubusercontent.com
151.101.108.133 avatars12.githubusercontent.com
151.101.108.133 avatars13.githubusercontent.com
151.101.108.133 avatars14.githubusercontent.com
151.101.108.133 avatars15.githubusercontent.com
151.101.108.133 avatars16.githubusercontent.com
151.101.108.133 avatars17.githubusercontent.com
151.101.108.133 avatars18.githubusercontent.com
151.101.108.133 avatars19.githubusercontent.com
151.101.108.133 avatars20.githubusercontent.com
npm install --global --production windows-build-tools
yarn add gulp-imagemin --dev
报错,就切换 npm install gulp-imagemin -D
,移除使用 yarn remove gulp-imagemin
和 npm uninstall gulp-imagemin
结果:gulp-imagemin 安装成功,运行 yarn gulp image
成功:
你会发现,imagemin 会自动的压缩这两个图片,而且会告诉你压缩的比例是多少,26+%,压缩比例还是很大的,而且这里是无损压缩。
我们还需要处理一下字体文件,字体文件没什么额外需要处理的东西,就是拷贝过去就好了,但是在字体文件当中也会遇到 SVG ,所以我们同样也可以使用 imagemin 去处理一下字体文件,对于那些不能被压缩的文件,imagemin 不会去处理,所以我们这儿直接用就行。
const imagemin = require('gulp-imagemin')
// font 任务
const font = () => {
return src('src/assets/fonts/**', { base: 'src' }) // ** images 下边的所有文件
.pipe(imagemin())
.pipe(dest('dist'))
}
module.exports = {
font
}
再来运行一下 yarn gulp font
:
只压缩了一张图片,原因是其它格式的文件是不支持压缩的。
这里我们的字体文件同样被拷贝过去了,到这里我们 src 下面所有的文件就都被拷贝过去了。我们可以把 src 下面的操作都放到 compile 当中。这样的话我们的 compile 任务就算是完整了。
const { src, dest, series, parallel } = require('gulp') // 先导入 gulp 所提供的 API
const sass = require('gulp-sass') // 导入 gulp-sass
const babel = require('gulp-babel')
const swig = require('gulp-swig')
const imagemin = require('gulp-imagemin')
// 提前准备好的一些数据
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()
}
// 首先先定义私有的任务,后续再通过 module.exports 选择性的导出
// 定义 style 任务
const style = () => {
return src('src/assets/styles/*.scss', { base: 'src' }) // 创建文件读取流, 指定一个 base 基准路径,就可以把 src 下面的路径给保留下来
.pipe(sass({ outputStyle: 'expanded' })) // 给 sass 指定选项
.pipe(dest('dist')) // 写入流
}
// 定义 script 任务
const script = () => {
return src('src/assets/scripts/*.js', { base: 'src' }) // 同样给一个基准路径
.pipe(babel({ presets: ['@babel/preset-env'] })) // 添加以下 babel 的配置
.pipe(dest('dist'))
}
// 定义 page 任务
const page = () => {
// 如果不只是在 src 下面有 html 文件的话,可以使用 src/**/*.html 意思是 src 下面任意子目录下的 *.html 文件,这是子目录的通配方式,这里的 base 设置的就没有意义了,因为通配符就在 src 目录下,为了保证统一,所以设置一下 base
return src('src/*.html', { base: 'src' })
.pipe(swig({ data }))
.pipe(dest('dist'))
}
// image 任务
const image = () => {
return src('src/assets/images/**', { base: 'src' }) // ** images 下边的所有文件
.pipe(imagemin())
.pipe(dest('dist'))
}
// font 任务
const font = () => {
return src('src/assets/fonts/**', { base: 'src' }) // ** images 下边的所有文件
.pipe(imagemin())
.pipe(dest('dist'))
}
const compile = parallel(style, script, page, image, font)
module.exports = {
compile
}
src 下面的这些文件都处理完了之后,接下来我们去把 public 目录中的文件做一个拷贝,添加一个额外的任务 extra (额外的) :
// 额外的
const extra = () => {
return src('public/**', {base: 'public'})
.pipe(dest('dist'))
}
这里可以把 extra 放到 compile 当中,但是 compile 它的定义就是把 src 下面的文件做一些转换,如果把 extra 放进去后续执行 compile 就会有一些混淆,不太合适,所以这里添加一个新的任务 build
,然后通过 parallel 去执行 compile 和 extra:
// 额外的
const extra = () => {
return src('public/**', {base: 'public'})
.pipe(dest('dist'))
}
const compile = parallel(style, script, page, image, font)
const build = parallel(compile, extra) // 在组合的基础上又组合了一次
module.exports = {
compile,
build
}
这样后续就可以使用 build 去完成所有文件的构建,compile 只完成 src 下面的文件的构建。
我们运行一下 yarn gulp build
:
到这里,站点当中需要构建的文件,基本上就完成了。除此之外,还需要做一些开发体验上的增强,例如集成一个 web 服务器,让我们可以有一个开发测试的服务器。
在做开发服务器之前先做一个小操作,自动的清除 dist 目录下的文件。这里先安装一个模块,注意这个模块不是 gulp 的插件,只不过在 gulp 当中可以使用 yarn add del --dev
或者 npm install del -D
。
为什么说这个模块可以在 gulp 当中使用呢,因为我们之前使用 gulp 定义任务的时候会发现,gulp 的任务并不是说非要通过 src 找文件流,然后最终 pipe 到 dest 当中,我们也可以通过自己写代码去实现这个构建过程,例如这个 del 模块,它就可以自动帮我们删除指定的那些文件,而且它是一个 promise 方法,gulp 的任务是支持这种 promise 方式的。所以我们可以定义一个 clean 任务。
const del = require('del')
const clean = () => {
return del(['dist']) // 返回的是 promise ,也就意味着 clean 任务完成之后可以被标记为完成状态
}
clean 任务肯定要放在 build 之前,所以我们需要给 build 任务重新去包装一下,因为要先删除 dist 下的文件,然后再构建,所以这里需要使用 series 去包装:
const { src, dest, series, parallel } = require('gulp') // 先导入 gulp 所提供的 API
const build = series(clean, parallel(compile, extra)) // 在组合的基础上又组合了一次
module.exports = {
compile,
build
}
再次运行一下:
我们这里先测试一下 clean :
运行 build:
可以看到是先执行 clean 一直到 clean 结束过后才会去执行的其它任务。而其它任务基本上都是同时启动的。
随着我们的构建任务越来越复杂,使用到的插件也越来越多,如果都是通过手动的方式载入插件的话,require 的操作会非常的多,不太利于我们后期去回顾维护这个代码,所以说这儿呢可以通过一个插件去解决这个小问题 gulp-load-plugins
yarn add gulp-load-plugins --dev
# npm install gulp-load-plugins -D
安装成功之后就可以通过 gulp-load-plugins 去自动加载插件了
const loadPlugins = require('gulp-load-plugins') // 得到一个方法
// 通过方法得到一个对象,所有的插件都会成为这个对象下方的一个属性,命名方式就是把 gulp- 去掉,后边的变成小驼峰格式 比如: gulp-pa-bbb 使用的时候就是 plugins.paBbb
const plugins = loadPlugins()
下面修改整个 gulpfile.js 引用的插件:
这里说一个小技巧:
选中使用到的插件变量,右键选择重命名符号 或者英文的 Rename Symbol 去修改,这样下方使用到的地方就都变了。
下方使用到的地方也变了,虽然可能当前行会报错,但是这里修改过后就删掉了,所以不用管,它,这样避免我们手动一个个修改不完全或者失误导致遗漏或错误什么的问题。
同样的,用到的插件都修改一下:
const { src, dest, series, parallel } = require('gulp') // 先导入 gulp 所提供的 API
const del = require('del')
const loadPlugins = require('gulp-load-plugins') // 得到一个方法
const plugins = loadPlugins() // 得到一个对象,所有的插件都会成为这个对象下方的一个属性,命名方式就是把 gulp- 去掉,后边的变成小驼峰格式 比如: gulp-pa-bbb 使用的时候就是 plugins.paBbb
// const sass = require('gulp-sass') // 导入 gulp-sass
// const babel = require('gulp-babel')
// const swig = require('gulp-swig')
// const imagemin = require('gulp-imagemin')
// 提前准备好的一些数据
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 clean = () => {
return del(['dist']) // 返回的是 promise ,也就意味着 clean 任务完成之后可以被标记为完成状态
}
// 首先先定义私有的任务,后续再通过 module.exports 选择性的导出
// 定义 style 任务
const style = () => {
return src('src/assets/styles/*.scss', { base: 'src' }) // 创建文件读取流, 指定一个 base 基准路径,就可以把 src 下面的路径给保留下来
.pipe(plugins.sass({ outputStyle: 'expanded' })) // 给 sass 指定选项
.pipe(dest('dist')) // 写入流
}
// 定义 script 任务
const script = () => {
return src('src/assets/scripts/*.js', { base: 'src' }) // 同样给一个基准路径
.pipe(plugins.babel({ presets: ['@babel/preset-env'] })) // 添加以下 babel 的配置
.pipe(dest('dist'))
}
// 定义 page 任务
const page = () => {
// 如果不只是在 src 下面有 html 文件的话,可以使用 src/**/*.html 意思是 src 下面任意子目录下的 *.html 文件,这是子目录的通配方式,这里的 base 设置的就没有意义了,因为通配符就在 src 目录下,为了保证统一,所以设置一下 base
return src('src/*.html', { base: 'src' })
.pipe(plugins.swig({ data }))
.pipe(dest('dist'))
}
// image 任务
const image = () => {
return src('src/assets/images/**', { base: 'src' }) // ** images 下边的所有文件
.pipe(plugins.imagemin())
.pipe(dest('dist'))
}
// font 任务
const font = () => {
return src('src/assets/fonts/**', { base: 'src' }) // ** images 下边的所有文件
.pipe(plugins.imagemin())
.pipe(dest('dist'))
}
// 额外的
const extra = () => {
return src('public/**', {base: 'public'})
.pipe(dest('dist'))
}
const compile = parallel(style, script, page, image, font)
const build = series(clean, parallel(compile, extra)) // 在组合的基础上又组合了一次
module.exports = {
compile,
build,
clean
}
尝试重新运行一下:
OK,没有问题。这就意味着我们 load-plugins 正常工作。
除了对文件的构建操作以外,我们这里还需要一个开发服务器,用于在开发阶段调试我们的应用。我们可以通过 gulp 去启动或者管理我们的服务器,这样我们就可以在后续配合其它的一些构建任务,去实现在代码修改过后自动编译并且刷新浏览器页面,大大提高在开发阶段的效率,因为它会减少我们在开发阶段的重复操作。
首先先安装一个叫 browser-sync
的一个模块:yarn add browser-sync --dev
或者 npm install browser-sync -D
这个模块会提供给我们一个开发服务器,它相对于使用普通 express 创建的服务器来说呢,功能更强大一些,它支持我们代码修改过后自动热更新到浏览器当中,让我们可以及时看到最新的页面效果。
同样的,它也不是一个 gulp 插件,只不过我们通过 gulp 去管理它而已。
const browserSync = require('browserSync')
const bs = browserSync.create() // 创建一个开发服务器
const serve = () => { // 把服务器放到一个任务当中
bs.init({ // 初始化一下开发服务器
server: { // 添加一些 server 配置
baseDir: 'dist', // 根目录
}
})
}
module.exports = {
compile,
build,
clean,
serve // 导出尝试运行
}
yarn gulp serve
任务会自动唤醒浏览器,并打开对应的链接,看到最终呈现的效果。只不过这个效果差强人意,因为我们在编译过程中并没有去处理 node_modules 下面的模块的拷贝,只是编译拷贝了一下自己写的源代码。
这时候在 dist 下面是没有 node_modules 这些东西的,而 node_modules 是项目根目录下的文件,这里先不考虑,先考虑开发阶段让它正常工作,这里先用另外一个办法解决一下,单独给 browser-sync 加一个特殊的路由,让它对于这种 /node_modules 开头的请求都指向到同一个目录下面去:
const serve = () => { // 把服务器放到一个任务当中
bs.init({ // 初始化一下开发服务器
server: { // 添加一些 server 配置
baseDir: 'dist', // 根目录
routes: { // 这个是优先于 baseDir 的配置,先看在 routes 里面有没有对应的配置,如果有,走这儿,否则走 baseDir 里面的目录
'/node_modules': 'node_modules'
}
}
})
}
重新启动一下:
此时,我们针对于 bootstrap 这些请求就会自动映射到我们项目下的 node_modules 当中,我们的 web 服务器就可以正常工作了。
bs.init 还有一些其它选项,比如 notify,它的作用就是启动之后在浏览器右上角的状态通知提示消息,这个小提示有可能会影响我们页面中调试的一些样式。我们试着关闭一下:
还有 port 端口,open 自动打开浏览器
const serve = () => { // 把服务器放到一个任务当中
bs.init({ // 初始化一下开发服务器
notify: false, // 链接状态
port: 8888, // 修改启动端口
open: false, // 自动打开浏览器
server: { // 添加一些 server 配置
baseDir: 'dist', // 根目录
routes: { // 这个是优先于 baseDir 的配置,先看在 routes 里面有没有对应的配置,如果有,走这儿,否则走 baseDir 里面的目录
'/node_modules': 'node_modules'
}
}
})
}
其它的一些配置:【查看】
现在有了 browser-sync 之后我们要考虑的是在我们修改完代码过后,自动在浏览器里呈现修改过后的效果,当然,现在的情况下修改了代码之后浏览器肯定是没有任何效果的。
修改源代码,经过编译到 dist 目录,browser-sync 监听 dist 目录,有了变化才往浏览器推送最新样式。这里先看 dist 下面的文件发生变化之后让浏览器即时的更新。
const serve = () => { // 把服务器放到一个任务当中
bs.init({ // 初始化一下开发服务器
notify: false, // 链接状态
port: 8888, // 修改启动端口
open: false, // 自动打开浏览器
files: 'dist/**', // 要监听的哪些文件,可以使用通配符
server: { // 添加一些 server 配置
baseDir: 'dist', // 根目录
routes: { // 这个是优先于 baseDir 的配置,先看在 routes 里面有没有对应的配置,如果有,走这儿,否则走 baseDir 里面的目录
'/node_modules': 'node_modules'
}
}
})
}
重新启动一下,试着修改一下 dist 下面的文件试试:
可以看到页面上的内容随着保存代码也跟着更新过来了,就说明此时 dist 下面的文件已经被监听了,一旦文件发生变化之后,就会同步到浏览器当中。对于样式也是一样的,比如:
也就意味着 browser-sync 这个同步没有问题。
下一步就是监视源代码修改过后,自动的去更新。其实就是监视 src 下面文件的变化,一旦变化之后,我们不是直接刷新浏览器,而是先执行构建任务。
这里要考虑的是修改源代码过后如何让它自动的编译,这里需要借助 gulp 提供的另外一个 API 叫做 watch ,这个 api 会自动监视通配符对应的文件,根据文件是否变化决定是否重新执行某些任务。
const { src, dest, series, parallel, watch } = require('gulp') // 先导入 gulp 所提供的 API
// ... 其它任务代码
// ...
const serve = () => { // 把服务器放到一个任务当中
watch('src/assets/styles/*.scss', style) // 在 serve 任务开始的时候先监视一些文件(源代码文件),两个参数,1-通配符,2-对应的执行任务
watch('src/assets/scripts/*.js', script) // 在 serve 任务开始的时候先监视一些文件(源代码文件),两个参数,1-通配符,2-对应的执行任务
watch('src/*.html', page) // 在 serve 任务开始的时候先监视一些文件(源代码文件),两个参数,1-通配符,2-对应的执行任务
watch('src/assets/images/**', image) // 在 serve 任务开始的时候先监视一些文件(源代码文件),两个参数,1-通配符,2-对应的执行任务
watch('src/assets/fonts/**', font) // 在 serve 任务开始的时候先监视一些文件(源代码文件),两个参数,1-通配符,2-对应的执行任务
watch('public/**', extra) // 在 serve 任务开始的时候先监视一些文件(源代码文件),两个参数,1-通配符,2-对应的执行任务
bs.init({ // 初始化一下开发服务器
notify: false, // 链接状态
port: 8888, // 修改启动端口
open: false, // 自动打开浏览器
files: 'dist/**', // 要监听的哪些文件,可以使用通配符
server: { // 添加一些 server 配置
baseDir: 'dist', // 根目录
routes: { // 这个是优先于 baseDir 的配置,先看在 routes 里面有没有对应的配置,如果有,走这儿,否则走 baseDir 里面的目录
'/node_modules': 'node_modules'
}
}
})
}
watch 到源代码变动,自动编译构建对应的文件,导致 dist 下面的文件跟着变动,browser-sync 监视到 dist 下面的文件变动了,就会自动更新我们的浏览器。这就实现了我们修改源代码自动刷新浏览器样式的功能了。我们尝试运行一下:
跑起来了,我们去看一下效果,当然,现在还是有问题的,比如运行之前应该先运行一下 build ,因为我们不能保证运行之前 dist 就已经生成了,这里先不考虑,一会儿再说。
可以发现确实能够生效,这里有一个小点需要注意:
可能会运维 swig 模板引擎缓存机制导致页面不会变化,此时需要额外将 swig 选项中的 cache 设置为 false
【具体查看】
我们再回到代码中看一个更常见的用法,因为 watch 对于 样式文件,html 文件,或者 js 文件的编译实际上是有意义的,而对于 图片 字体 和一些额外文件,在开发阶段没有太大的意义,因为图片包括字体,我们只是对它进行了一个压缩,无损压缩,并不影响页面中的呈现,意味着在开发阶段监视更多的文件,做更多的任务,开销也就更大,而这个开销对开发阶段实际上是没有意义的,只是在发布之前,上线之前,希望通过压缩一下文件的体积,提高页面的运行效率,但是如果开发阶段这么搞,会降低开发阶段的运行效率,所以一般不会这么做。具体做法是启动这个 web 服务器的时候 baseDir 指定两个目录,一个是 dist 目录,一个是 src 目录,启动 src 目录的目的是对于图片也好,字体也好,其它文件也好,不让它们参与这次构建,它们只需要在上线之前参与一次构建就行了。
const serve = () => { // 把服务器放到一个任务当中
watch('src/assets/styles/*.scss', style) // 在 serve 任务开始的时候先监视一些文件(源代码文件),两个参数,1-通配符,2-对应的执行任务
watch('src/assets/scripts/*.js', script) // 在 serve 任务开始的时候先监视一些文件(源代码文件),两个参数,1-通配符,2-对应的执行任务
watch('src/*.html', page) // 在 serve 任务开始的时候先监视一些文件(源代码文件),两个参数,1-通配符,2-对应的执行任务
// watch('src/assets/images/**', image) // 在 serve 任务开始的时候先监视一些文件(源代码文件),两个参数,1-通配符,2-对应的执行任务
// watch('src/assets/fonts/**', font) // 在 serve 任务开始的时候先监视一些文件(源代码文件),两个参数,1-通配符,2-对应的执行任务
// watch('public/**', extra) // 在 serve 任务开始的时候先监视一些文件(源代码文件),两个参数,1-通配符,2-对应的执行任务
bs.init({ // 初始化一下开发服务器
notify: false, // 链接状态
port: 8888, // 修改启动端口
open: false, // 自动打开浏览器
files: 'dist/**', // 要监听的哪些文件,可以使用通配符
server: { // 添加一些 server 配置
// baseDir: 'dist', // 根目录
baseDir: ['dist', 'src', 'public'], // 根目录, 指定为一个数组,当一个请求过来之后,先到数组中第一个目录去找,找不到再去下一个目录去找
routes: { // 这个是优先于 baseDir 的配置,先看在 routes 里面有没有对应的配置,如果有,走这儿,否则走 baseDir 里面的目录
'/node_modules': 'node_modules'
}
}
})
}
重新启动一下这个任务,不过在启动之前先去 clean 一下,把 dist 清空掉
module.exports = {
compile,
build,
clean,
serve
}
这时候再去启动 serve 就会遇到刚才说的那个问题,还没生成 dist 目录
所以这里我们需要单独加一个组合任务把 html css js 做一下编译:
// const compile = parallel(style, script, page)
// 开发时候用到的,只编译一部分需要编译的代码
const compile = parallel(style, script, page) // 图片和字体不需要在开发阶段去编译压缩
// 上线之前用到的,所有文件都编译
const build = series(clean, parallel(compile, image, font, extra)) // 在组合的基础上又组合了一次
// 开发阶段构建任务
const develop = series(compile, serve)
module.exports = {
compile,
build,
clean,
develop // 暴露出去
}
可以看到,样式,js,html 都是有的,没有图片,字体,icon 。
同样,页面运行效果没收到任何影响。原因很简单,当图片的请求请求过来,找 dist 下面没找到,就去 src 下面找去了。如果还找不到就会去找 public 了,例如这里的 favicon.ico ,所以这个不影响我们开发阶段的工作。然后在上线之前去执行一下 build ,因为每个任务对于上线之前都是有价值的。我们这里做一个这样的一个操作只是对于开发过程中做了一个优化。
这样就是关于监视文件以及文件变化过后去刷新浏览器,就完成了。这里还想补充的一点就是对于 src 下边的这些图片,字体还有 public 下边的文件 发生变化之后也想要更新浏览器,这儿可以这么干:
const serve = () => { // 把服务器放到一个任务当中
watch('src/assets/styles/*.scss', style) // 在 serve 任务开始的时候先监视一些文件(源代码文件),两个参数,1-通配符,2-对应的执行任务
watch('src/assets/scripts/*.js', script) // 在 serve 任务开始的时候先监视一些文件(源代码文件),两个参数,1-通配符,2-对应的执行任务
watch('src/*.html', page) // 在 serve 任务开始的时候先监视一些文件(源代码文件),两个参数,1-通配符,2-对应的执行任务
// watch('src/assets/images/**', image) // 在 serve 任务开始的时候先监视一些文件(源代码文件),两个参数,1-通配符,2-对应的执行任务
// watch('src/assets/fonts/**', font) // 在 serve 任务开始的时候先监视一些文件(源代码文件),两个参数,1-通配符,2-对应的执行任务
// watch('public/**', extra) // 在 serve 任务开始的时候先监视一些文件(源代码文件),两个参数,1-通配符,2-对应的执行任务
watch([
'src/assets/images/**',
'src/assets/fonts/**',
'public/**'
], bs.reload) // 同时监听三个目标文件,发生变化去调用一下 browser-sync 提供的 reload 就可以了
bs.init({ // 初始化一下开发服务器
notify: false, // 链接状态
port: 8888, // 修改启动端口
open: false, // 自动打开浏览器
files: 'dist/**', // 要监听的哪些文件,可以使用通配符
server: { // 添加一些 server 配置
// baseDir: 'dist', // 根目录
baseDir: ['dist', 'src', 'public'], // 根目录, 指定为一个数组,当一个请求过来之后,先到数组中第一个目录去找,找不到再去下一个目录去找
routes: { // 这个是优先于 baseDir 的配置,先看在 routes 里面有没有对应的配置,如果有,走这儿,否则走 baseDir 里面的目录
'/node_modules': 'node_modules'
}
}
})
}
这是针对于文件修改过后的自动更新,以及如何优化我们以最小模型的方式去启动我们的开发服务。
当然还有一个就是别人在使用 browser-sync 的时候不使用 files 属性,它们使用的方式都是 reload 。很简单,我们可以在执行任务的时候再 pipe 一下,pipe 到 reload ,而且这个 reload 执行完之后就是一个文件流,把流推送到浏览器,所以我们这里给 reload 指定一个参数叫 bs.reload({stream: true})
,以流的方式往浏览器推,这种方式其实是会更常见一些。
const { src, dest, series, parallel, watch } = require('gulp') // 先导入 gulp 所提供的 API
const del = require('del')
const browserSync = require('browser-sync')
const loadPlugins = require('gulp-load-plugins') // 得到一个方法
const plugins = loadPlugins() // 得到一个对象,所有的插件都会成为这个对象下方的一个属性,命名方式就是把 gulp- 去掉,后边的变成小驼峰格式 比如: gulp-pa-bbb 使用的时候就是 plugins.paBbb
const bs = browserSync.create() // 自动创建一个开发服务器
// const sass = require('gulp-sass') // 导入 gulp-sass
// const babel = require('gulp-babel')
// const swig = require('gulp-swig')
// const imagemin = require('gulp-imagemin')
// 提前准备好的一些数据
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 clean = () => {
return del(['dist']) // 返回的是 promise ,也就意味着 clean 任务完成之后可以被标记为完成状态
}
// 首先先定义私有的任务,后续再通过 module.exports 选择性的导出
// 定义 style 任务
const style = () => {
return src('src/assets/styles/*.scss', { base: 'src' }) // 创建文件读取流, 指定一个 base 基准路径,就可以把 src 下面的路径给保留下来
.pipe(plugins.sass({ outputStyle: 'expanded' })) // 给 sass 指定选项
.pipe(dest('dist')) // 写入流
.pipe(bs.reload({ stream: true }))
}
// 定义 script 任务
const script = () => {
return src('src/assets/scripts/*.js', { base: 'src' }) // 同样给一个基准路径
.pipe(plugins.babel({ presets: ['@babel/preset-env'] })) // 添加以下 babel 的配置
.pipe(dest('dist'))
.pipe(bs.reload({ stream: true }))
}
// 定义 page 任务
const page = () => {
// 如果不只是在 src 下面有 html 文件的话,可以使用 src/**/*.html 意思是 src 下面任意子目录下的 *.html 文件,这是子目录的通配方式,这里的 base 设置的就没有意义了,因为通配符就在 src 目录下,为了保证统一,所以设置一下 base
return src('src/*.html', { base: 'src' })
.pipe(plugins.swig({ data }))
.pipe(dest('dist'))
.pipe(bs.reload({ stream: true }))
}
// image 任务
const image = () => {
return src('src/assets/images/**', { base: 'src' }) // ** images 下边的所有文件
.pipe(plugins.imagemin())
.pipe(dest('dist'))
}
// font 任务
const font = () => {
return src('src/assets/fonts/**', { base: 'src' }) // ** images 下边的所有文件
.pipe(plugins.imagemin())
.pipe(dest('dist'))
}
// 额外的
const extra = () => {
return src('public/**', {base: 'public'})
.pipe(dest('dist'))
}
const serve = () => { // 把服务器放到一个任务当中
watch('src/assets/styles/*.scss', style) // 在 serve 任务开始的时候先监视一些文件(源代码文件),两个参数,1-通配符,2-对应的执行任务
watch('src/assets/scripts/*.js', script) // 在 serve 任务开始的时候先监视一些文件(源代码文件),两个参数,1-通配符,2-对应的执行任务
watch('src/*.html', page) // 在 serve 任务开始的时候先监视一些文件(源代码文件),两个参数,1-通配符,2-对应的执行任务
// watch('src/assets/images/**', image) // 在 serve 任务开始的时候先监视一些文件(源代码文件),两个参数,1-通配符,2-对应的执行任务
// watch('src/assets/fonts/**', font) // 在 serve 任务开始的时候先监视一些文件(源代码文件),两个参数,1-通配符,2-对应的执行任务
// watch('public/**', extra) // 在 serve 任务开始的时候先监视一些文件(源代码文件),两个参数,1-通配符,2-对应的执行任务
watch([
'src/assets/images/**',
'src/assets/fonts/**',
'public/**'
], bs.reload) // 同时监听三个目标文件,发生变化去调用一下 browser-sync 提供的 reload 就可以了
bs.init({ // 初始化一下开发服务器
notify: false, // 链接状态
port: 8888, // 修改启动端口
open: false, // 自动打开浏览器
// files: 'dist/**', // 要监听的哪些文件,可以使用通配符
server: { // 添加一些 server 配置
// baseDir: 'dist', // 根目录
baseDir: ['dist', 'src', 'public'], // 根目录, 指定为一个数组,当一个请求过来之后,先到数组中第一个目录去找,找不到再去下一个目录去找
routes: { // 这个是优先于 baseDir 的配置,先看在 routes 里面有没有对应的配置,如果有,走这儿,否则走 baseDir 里面的目录
'/node_modules': 'node_modules'
}
}
})
}
// const compile = parallel(style, script, page)
// 开发时候用到的,只编译一部分需要编译的代码
const compile = parallel(style, script, page) // 图片和字体不需要在开发阶段去编译压缩
// 上线之前用到的,所有文件都编译
const build = series(clean, parallel(compile, image, font, extra)) // 在组合的基础上又组合了一次
// 开发阶段构建任务
const develop = series(compile, serve)
module.exports = {
compile,
build,
clean,
develop
}
这就是文件修改过后自动编译自动更新的整个的一个过程。
注意:刚才的代码里有个小问题,就是 bs.reload 只能执行一次的问题:
gulp.watch("****",bs.reload)
这个只能刷新一次的原因就是,这里要使用异步函数,或者使用回调,否则 gulp 不知道任务是否已经完成,gulp 不能判断 bs.reload 是否已经完成造成的.
gulp.watch("****").on(‘change’, reload)
这个每次更改都能触发刷新,是因为每次change这个事件触发的时候去调用reload,这个时候并不需要去判断这个reload是否已经完成.
截止到目前,我们绝大多数构建任务都已经完成了。这些任务基本上完成了我们核心想要做的事情,但是对于 dist 生成的文件还有一些小问题。这里运行一下 build 任务:
dist 下面会按照最终上线要生成的文件来生成。我们可以打开看一下:
针对于像 html 里边的一些引用,会有一些在 node_modules 里的一些依赖,这些文件并没有拷贝到我们的 dist 目录,我们部署上线的话肯定会出现问题。会找不到 bootstrap 这些文件,而我们在开发阶段运行没有问题是因为我们在 serve 命令里面去做了映射,这个并不能在线上的环境这样做。所以这里还需要单独去处理一下。
针对于这个问题的处理方式有很多,有一种比如说我们在 html 代码里边就写一个不存在的路径,然后通过构建的方式把这些文件拷贝到我们指定的路径,也可以,但是相对 LOW 一点吧,这里介绍一个更为常见或者说更为强大的一种方式。
借助于一个叫 useref 的插件,它会自动去处理我们 html 中的这种构建注释,有一定规律的构建注释:
意思是说我们会自动的将 开始标签和结束标签中间引入的这些文件,最终打包到一个文件当中,文件的路径就是注释里边的路径。如果说注释中间引入了多个同类型的文件,它可以把这些文件都合并到一块。
除此之外还可以在这个过程当中自动的对文件进行压缩,相对来讲,要比我们使用其它的方式更为完善一点。因为在这个过程当中我们可以把剩下的压缩啊,合并啊,统统都给他完成。这里去使用一下它:
yarn add gulp-useref --dev
# npm install gulp-useref -D
安装成功之后去使用,注意,插件会自动引入进来,使用 plugins.useref 就行:
const useref = () => {
return src('dist/*.html', {base: 'dist'}) // 这里要找的是编译过后的 dist 下的 html 文件,因为找 src 下面没有编译的模板没有意义,这也是为什么最后来说这个的原因
.pipe(plugins.useref({ searchPath: ['dist', '.'] })) // 需要指定转换参数
.pipe(dest('dist'))
}
module.exports = {
useref
}
当然,现在这里边存在一定不合理,一会儿再说,尝试运行一下 yarn gulp useref
:
可以看到,去掉了注释,并且把注释内部的所有引用文件都合并成了一个文件,可以查看源码自行验证一下。
当然了,我们可以在这个读取流的过程当中做一些新的操作,比如压缩。
有了 useref 之后,它就自动的帮我们把对应的那些依赖的文件全部拿过来 了,我们还需要一个额外的操作就是对那些生成的文件进行压缩。三种:html,css,js。
# gulp-htmlmin 压缩 html
# gulp-uglify 压缩 js
# gulp-clean-css 压缩 css
yarn add gulp-htmlmin gulp-uglify gulp-clean-css --dev
# npm install gulp-htmlmin gulp-uglify gulp-clean-css -D
安装过后就可以使用了,但是这里会遇到一个小问题,因为在之前的构建过程当中读取的都是同类型的文件,但是这个时候读取流当中有三种类型的文件,我们需要去分别做不同的操作,这时候需要额外的一个操作,就是判断一下读取的是什么类型的文件就做什么样的操作。需要一个叫 gulp-if 的插件:
yarn add gulp-if --dev
# npm install gulp-if -D
我们就可以尝试使用它了:
const useref = () => {
return src('dist/*.html', {base: 'dist'}) // 这里要找的是编译过后的 dist 下的 html 文件,因为找 src 下面没有编译的模板没有意义,这也是为什么最后来说这个的原因
.pipe(plugins.useref({ searchPath: ['dist', '.'] })) // 需要指定转换参数
// html css js 压缩
.pipe(plugins.if(/\.js$/, plugins.uglify())) // 会根据给定的条件执行对应的转换
.pipe(dest('dist'))
}
尝试使用一下它:
我们看到此时这个文件并没有被压缩,我们可以把 vendor.js 删除运行一下看看:
运行 yarn gulp useref
我们会发现 vendor.js 并没有被生成。
是因为我们第一次去执行 useref 的时候,它已经把 html 里边那些构建注释删除掉了:
再去构建的时候,它里面没有那些构建注释,useref 就不会产生 JS 文件,所以就不会有这种所谓的压缩的一个转换,这块我们不能单纯的执行 useref ,我们要先去执行以下 compile ,然后再执行 useref
执行 useref
再看:
这个压缩比较慢,所以一定是放在上线之前的那个操作,开发过程中使用的话效率太过于低下了。
同样的对于 css html 的压缩也做一下:
const useref = () => {
return src('dist/*.html', {base: 'dist'}) // 这里要找的是编译过后的 dist 下的 html 文件,因为找 src 下面没有编译的模板没有意义,这也是为什么最后来说这个的原因
.pipe(plugins.useref({ searchPath: ['dist', '.'] })) // 需要指定转换参数
// html css js 压缩
.pipe(plugins.if(/\.js$/, plugins.uglify())) // 会根据给定的条件执行对应的转换
.pipe(plugins.if(/\.css$/, plugins.cleanCss())) // 会根据给定的条件执行对应的转换
.pipe(plugins.if(/\.html$/, plugins.htmlmin())) // 会根据给定的条件执行对应的转换
.pipe(dest('dist'))
}
再次先执行 compile ,再执行 useref:
我们再来看一下 css 和 html:
貌似 css 是好的,html 是不正常的,html 没压缩是正常的,因为我们还需要指定一些其它的选项,一会儿再看。这里其实有一个情况就是 有可能 main.css 当中什么都没有,我这里看着正常可能是概率原因,这个地方应该爆露出来我们这样用的一个小问题,因为我们这儿实际上是读取流 src(‘dist’) 下面的这些文件,然后又写入流写入到同样的文件当中 dest(‘dist’),此时其实就产生了一个文件读写的冲突,一边读一边写,没分离开的话很有可能产生写文件写不进去的情况。
我们还需要一个小操作,就是将 dest() 这个结果不要放在 dist 下了,放在 就叫 release 下,这样就不会产生冲突了。重新运行:
const useref = () => {
return src('dist/*.html', {base: 'dist'}) // 这里要找的是编译过后的 dist 下的 html 文件,因为找 src 下面没有编译的模板没有意义,这也是为什么最后来说这个的原因
.pipe(plugins.useref({ searchPath: ['dist', '.'] })) // 需要指定转换参数
// html css js 压缩
.pipe(plugins.if(/\.js$/, plugins.uglify())) // 会根据给定的条件执行对应的转换
.pipe(plugins.if(/\.css$/, plugins.cleanCss())) // 会根据给定的条件执行对应的转换
.pipe(plugins.if(/\.html$/, plugins.htmlmin())) // 会根据给定的条件执行对应的转换
.pipe(dest('release')) // 换个目录
}
html 并没有压缩是因为 html 需要另外的参数,htmlmin 默认只处理属性当中的空白字符,但是针对于其它的比如换行符等默认不帮我们删除。可以指定一个选项:
const useref = () => {
return src('dist/*.html', {base: 'dist'}) // 这里要找的是编译过后的 dist 下的 html 文件,因为找 src 下面没有编译的模板没有意义,这也是为什么最后来说这个的原因
.pipe(plugins.useref({ searchPath: ['dist', '.'] })) // 需要指定转换参数
// html css js 压缩
.pipe(plugins.if(/\.js$/, plugins.uglify())) // 会根据给定的条件执行对应的转换
.pipe(plugins.if(/\.css$/, plugins.cleanCss())) // 会根据给定的条件执行对应的转换
.pipe(plugins.if(/\.html$/, plugins.htmlmin({collapseWhitespace: true}))) // 会根据给定的条件执行对应的转换,html 需要其它选项
.pipe(dest('release')) // 换个目录
}
再次运行 useref,因为已经换了目录,所以这里直接运行 useref 任务就行:
可以看到,还有一个问题就是内部的样式没有压缩,我们可以指定其它的选项压缩:
const useref = () => {
return src('dist/*.html', { base: 'dist' }) // 这里要找的是编译过后的 dist 下的 html 文件,因为找 src 下面没有编译的模板没有意义,这也是为什么最后来说这个的原因
.pipe(plugins.useref({ searchPath: ['dist', '.'] })) // 需要指定转换参数
// html css js 压缩
.pipe(plugins.if(/\.js$/, plugins.uglify())) // 会根据给定的条件执行对应的转换
.pipe(plugins.if(/\.css$/, plugins.cleanCss())) // 会根据给定的条件执行对应的转换
.pipe(plugins.if(/\.html$/, plugins.htmlmin({
collapseWhitespace: true,
minifyCSS: true,
minifyJS: true,
}))) // 会根据给定的条件执行对应的转换
.pipe(dest('release')) // 换个目录
}
再次执行:
整个就都被压缩到一行当中了,还有一些其它的参数,比如删除掉注释,删除掉空属性等等,具体参考这儿:【查看】
到这里我们的 useref 就可以了,但是好像我们的构建结构被打破了,下面继续看
这里的 useref 打破了我们的目录结构,我们看一下,之前约定的打包上线的目录是 dist ,源码在 src,但是因为这里从 dist 读取,还往 dist 写,一边读一边写的话会存在文件冲突,所以我们不得已用了一个 release 目录。其实这时候正常上线的应该是 release 这个目录的文件,而 release 当中又没有图片字体等那些文件,所以这里需要重新规整一下。
其实在 useref 之前,生成的那些文件算是中间产物,就是说我们将 src 下面的文件去编译,后续再通过 useref 再做一个转换,这个转换才是最终要上线的代码,所以说我们直接将 src 编译之后的 style script html 文件放 dist 是不合理的,应该放一个临时目录当中,然后在 useref 的时候通过这个临时目录把文件拿出来做一个转换操作,最后再放入 dist 目录会更加合理一些。
这里修改一下:主要是修改 dist 为一个临时目录 temp ,生成的最终文件那个 release 改成 dist,修改 script style page 三个任务的目录,并且现在 useref 也需要放入 build 组合任务当中,先 clean,再执行其它的,由于 useref 需要在 compile 之后,所以这里用 series 再次包装组合一下
const { src, dest, series, parallel, watch } = require('gulp') // 先导入 gulp 所提供的 API
const del = require('del')
const browserSync = require('browser-sync')
const loadPlugins = require('gulp-load-plugins') // 得到一个方法
const plugins = loadPlugins() // 得到一个对象,所有的插件都会成为这个对象下方的一个属性,命名方式就是把 gulp- 去掉,后边的变成小驼峰格式 比如: gulp-pa-bbb 使用的时候就是 plugins.paBbb
const bs = browserSync.create() // 自动创建一个开发服务器
// const sass = require('gulp-sass') // 导入 gulp-sass
// const babel = require('gulp-babel')
// const swig = require('gulp-swig')
// const imagemin = require('gulp-imagemin')
// 提前准备好的一些数据
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 clean = () => {
return del(['dist', 'temp']) // 返回的是 promise ,也就意味着 clean 任务完成之后可以被标记为完成状态
}
// 首先先定义私有的任务,后续再通过 module.exports 选择性的导出
// 定义 style 任务
const style = () => {
return src('src/assets/styles/*.scss', { base: 'src' }) // 创建文件读取流, 指定一个 base 基准路径,就可以把 src 下面的路径给保留下来
.pipe(plugins.sass({ outputStyle: 'expanded' })) // 给 sass 指定选项
.pipe(dest('temp')) // 写入流
.pipe(bs.reload({ stream: true }))
}
// 定义 script 任务
const script = () => {
return src('src/assets/scripts/*.js', { base: 'src' }) // 同样给一个基准路径
.pipe(plugins.babel({ presets: ['@babel/preset-env'] })) // 添加以下 babel 的配置
.pipe(dest('temp'))
.pipe(bs.reload({ stream: true }))
}
// 定义 page 任务
const page = () => {
// 如果不只是在 src 下面有 html 文件的话,可以使用 src/**/*.html 意思是 src 下面任意子目录下的 *.html 文件,这是子目录的通配方式,这里的 base 设置的就没有意义了,因为通配符就在 src 目录下,为了保证统一,所以设置一下 base
return src('src/*.html', { base: 'src' })
.pipe(plugins.swig({ data }))
.pipe(dest('temp'))
.pipe(bs.reload({ stream: true }))
}
// image 任务
const image = () => {
return src('src/assets/images/**', { base: 'src' }) // ** images 下边的所有文件
.pipe(plugins.imagemin())
.pipe(dest('dist'))
}
// font 任务
const font = () => {
return src('src/assets/fonts/**', { base: 'src' }) // ** images 下边的所有文件
.pipe(plugins.imagemin())
.pipe(dest('dist'))
}
// 额外的
const extra = () => {
return src('public/**', { base: 'public' })
.pipe(dest('dist'))
}
const reload = () => {
bs.reload()
}
const serve = () => { // 把服务器放到一个任务当中
watch('src/assets/styles/*.scss', style) // 在 serve 任务开始的时候先监视一些文件(源代码文件),两个参数,1-通配符,2-对应的执行任务
watch('src/assets/scripts/*.js', script) // 在 serve 任务开始的时候先监视一些文件(源代码文件),两个参数,1-通配符,2-对应的执行任务
watch('src/*.html', page) // 在 serve 任务开始的时候先监视一些文件(源代码文件),两个参数,1-通配符,2-对应的执行任务
// watch('src/assets/images/**', image) // 在 serve 任务开始的时候先监视一些文件(源代码文件),两个参数,1-通配符,2-对应的执行任务
// watch('src/assets/fonts/**', font) // 在 serve 任务开始的时候先监视一些文件(源代码文件),两个参数,1-通配符,2-对应的执行任务
// watch('public/**', extra) // 在 serve 任务开始的时候先监视一些文件(源代码文件),两个参数,1-通配符,2-对应的执行任务
watch([
'src/assets/images/**',
'src/assets/fonts/**',
'public/**'
]).on('change', reload) // 同时监听三个目标文件,发生变化去调用一下 browser-sync 提供的 reload 就可以了
bs.init({ // 初始化一下开发服务器
notify: false, // 链接状态
port: 8888, // 修改启动端口
open: false, // 自动打开浏览器
// files: 'dist/**', // 要监听的哪些文件,可以使用通配符
server: { // 添加一些 server 配置
// baseDir: 'dist', // 根目录
baseDir: ['temp', 'src', 'public'], // 根目录, 指定为一个数组,当一个请求过来之后,先到数组中第一个目录去找,找不到再去下一个目录去找
routes: { // 这个是优先于 baseDir 的配置,先看在 routes 里面有没有对应的配置,如果有,走这儿,否则走 baseDir 里面的目录
'/node_modules': 'node_modules'
}
}
})
}
const useref = () => {
return src('temp/*.html', { base: 'temp' }) // 这里要找的是编译过后的 dist 下的 html 文件,因为找 src 下面没有编译的模板没有意义,这也是为什么最后来说这个的原因
.pipe(plugins.useref({ searchPath: ['temp', '.'] })) // 需要指定转换参数
// html css js 压缩
.pipe(plugins.if(/\.js$/, plugins.uglify())) // 会根据给定的条件执行对应的转换
.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, script, page)
// 开发时候用到的,只编译一部分需要编译的代码
const compile = parallel(style, script, page) // 图片和字体不需要在开发阶段去编译压缩
// 上线之前用到的,所有文件都编译
const build = series(clean, parallel(series(compile, useref), image, font, extra)) // 在组合的基础上又组合了一次
// 开发阶段构建任务
const develop = series(compile, serve)
module.exports = {
compile,
build,
clean,
develop,
useref
}
测试一下:
OK,开发版本正常。再看一下打包:
打包正常,运行一下打包之后的代码:
OK,打包之后运行起来也正常。
到这里,完整的构建过程就都结束了。当然,这其中的插件有一些其它的选项自行查看。
接下来解决两个小问题:
module.exports = {
build,
clean,
develop
}
"scripts": {
"clean": "gulp clean",
"build": "gulp build",
"dev": "gulp develop"
},
这里考虑 gulpfile 的复用问题,因为如果涉及到开发多个同类型的项目,那我们这个自动化的构建工作流应该是一样的,这时候就涉及到在多个项目当中重复去使用这些构建任务,这些构建任务绝大多数情况下它们都是相同的,所以就面临一个复用相同的 gulpfipe 的问题。
针对于这个问题,我们可以通过代码段的方式,我们把这个 gulpfile 作为一个代码段保存起来,在不同的项目当中去使用,但是这种方式也有一个弊端,就是 gulpfile 散落在各个项目当中,一旦 gulpfile 有一些问题需要修复或者升级的时候,就要去对每个项目做相同的操作,这也不利于我们整体的维护,所以这里重点看怎样提取一个可复用的自动化构建工作流。
通过创建一个新得模块,去包装一下 gulp ,然后把这个自动化构建工作流给包装进去。
具体来说就是 因为 gulp 就是一个自动化平台,不负责给你提供任何的构建任务,构建任务需要通过自己的 gulpfile 去定义,现在我们有了 gulpfile 也有了 gulp,我们把二者结合到一起,结合到一起之后,以后在同类型的项目中就使用同一个模块去提供自动化构建工作流就好了,这就是我们的一个办法。
具体的做法就是 先去创建一个 npm 的模块,然后把这个模块发布到 npm 仓库上,最后在项目当中去使用这个模块就可以了。这里先做一些准备工作。
首先,先去 github 上创建一个仓库,可以把代码托管到上面
然后本地创建空目录作为要创建的模块,我们可以使用传统的方式自己初始化 package.json 等一系列文件,但是我们之前已经结识过脚手架,对于相同类型的项目,我们一般都是使用脚手架去做的。这里使用一个 zce 老师自己创建的一个个人的脚手架去创建这个项目,专门是这些例子使用的脚手架
【npm地址】【源码】
yarn global add zce-cli
# npm install zce-cli -g
直接 zce init nm
nm 是这个脚手架对应模块的名称,意思是 node_modules
这里什么都不用选,一会儿需要的时候会手动装进去
创建成功了,初始化仓库:
git init
git remote add origin 仓库地址
git status
git add .
git commit -m "feat: initial commit"
git push origin master # 第一次链接自己仓库需要用户名密码
推送上去了
对于这些警告不用管,因为在 windows 上默认的换行符是 \r\n,所有的源代码托管仓库都是以 \n 的方式去存储代码,所以说在 windows 上使用有这么一个问题,不用管它。
这里介绍一下这个项目的基本结构
这个结构就是我们脚手架默认的一个约定,根目录就是一些特定工具的配置文件,比如
.editorconfig 编辑器对应的配置文件
.gitignore git 提交忽略文件
CHANGELOG.md 项目变更日志
LICENE 开源许可证,一种法律许可,确定作者和用户的权利和限制
package.json 和 README.md 就不用说了
再就是 lib/index.js 当前目录下的一个入口文件,也就是说我们后续要实现的代码都放这里,这是一个约定,就是刚才 zce-cli 生成的 node_modules 提供的一个约定,就按照这样一个约定的方式去创建我们的那个模块,我们去把之前创建的构建工作流的实现以及 gulp 结合到一起,形成一个新的模块,然后在后续使用同类型项目的时候提高效率。
这里看一下如何去实现:
# -a 的参数是在同一个窗口打开多个项目的意思,append
code . -a
先看原本的编辑器是这样的
输入指令
可以看到,同一个编辑器打开了多个项目
这里要做的事情就是把在 zce-gulp-demo 中创建的自动化构建工作流提取到 zce-pages 当中,在这个项目当中我们去封装好这样的工作流,把一系列需要解决的问题都解决掉,最后我们就可以 在多个项目中去使用这个工作流了。
首先,第一件事儿很简单,我们需要在 zce-pages 模块中去包装 gulp 和 gulpfile 当中 提供的工作流的任务的定制,最简单的就是先把 gulpfile 整体挪过去,作为 zce-pages 项目的入口文件:
入口有了之后,这里考虑一个问题,对于这个项目来讲,它里面提供了一些构建任务,这些构建任务是依赖一些模块的,我们肯定是需要这些模块作为 zce-pages 模块的依赖去安装,这样后续在别的项目使用到这个模块的时候就会自动安装这些依赖,所以我们需要把之前的依赖都拿过来,
放到我们项目当中,注意,对于原项目当中的 dependencies 肯定是不需要的,因为不同的项目生产依赖是不需要的,但是对于自动化构建任务的开发依赖都是相同的,所以我们拿到新模块当中,作为 dependencies 出现。
这里说一下为什么不放到 devDependencies 里,因为你安装一个模块,比如这里你去安装 zce-pages ,它会自动帮你安装 dependencies 里面的依赖,并不会去安装开发依赖(devDependencies),开发依赖指的是开发这个模块所需要用到的依赖,最终工作环节,只会有 dependencies 里面的依赖,所以我们这里需要的是 dependencies ;
安装一下所有的依赖:
yarn
# npm install
因为之前是一个个装的,这里是一次性全装的,时间稍微长一点
安装成功之后,我们去看一下入口文件,对于这个文件肯定是有一些需要修改的地方,但是都是通过使用的时候发生的一些问题根据问题去修改,可以更好的理解。
到这里第一步就算完成了。
回到 zce-gulp-demo 中,把 gulpfile 工作流给删除掉,取而代之的是新的模块提供的自动化构建工作流。
正常流程肯定是要把 zce-pages 发布到 npm 当中,然后项目里边去安装这个模块,但是现在是开发阶段,模块还没有完成,我们需要本地调试,最简单的方式就是通过 link 的方式把这个模块 link 到当前项目的 node_modules 当中。
# zce-pages 终端目录下
yarn link
yarn link "zce-pages"
这时候我们项目里边其实就缺 gulpfile 里边的内容了,里边的内容在 zce-pages 的 lib/index 里边,我们项目里边的 gulpfile 可以直接用 zce-pages:module.exports = require('zce-pages')
这样入口文件 gulpfile 就 OK了,但是刚才项目里边的生产依赖也被删除了,所以这里还需要安装一下 yarn 或者 npm install
:
安装成功之后按道理来讲已经能跑起来了,因为 zce-pages 模块提供了三个任务了,我们尝试着通过 clean build develop 执行一下:
找不到 gulp 命令,还需要本地安装一下 gulp ,这里暂时先在本地安装一下 gulp,yarn add gulp
,这个问题其实在真正发布出去之后就不存在了,因为发布出去之后再安装 zce-pages 的时候会自动的去安装 gulp,就不会有这个问题了,再次尝试 yarn build
:
这个错是之前定制 gulpfile 的时候用到了 package.json 报错信息也说了在 index.js 的 56 行 10 列
在这个地方用到了,当时我们的想法是定义一个 data 的数据成员,然后在这个数据成员里边把模块的信息包括进去,最后在模板编译的时候去使用这个数据信息,但是现在把 gulpfile 提取出来了,那这个 package.json 位置就不一样了,就不成立了。另外一方面是 这个 data 实际上是项目才知道的,对于封装的模块是不知道的。可以想一下,如果有三个项目都依赖这个模块,这三个模块里边的数据可能是不一样的,所以说在模块里边写死实际上是不合适的,这时候我们有一种约定大于配置的方式去解决。
在项目中抽象一个配置文件,在 gulpfile 当中去读取这个配置文件,这实际上才是合理的。
现在要做的是把我们公共模块当中那些不应该被提取的东西全部抽出来,第一个就是 data,上面也说了,用约定大于配置的方式解决,也就是在项目中抽象一个配置文件,在 gulpfile 当中去读取这个配置文件,这实际上才是合理的。
比如这里在项目中新建一个配置文件叫 pages.config.js
这也是很多程序的自动化构建工作流或者说成熟的库的配置文件的实现,大部分都是这种情况,例如 vue-cli,在工作的时候就会读取项目中的 vue.config.js 文件,道理是一样的。
我们在这个文件里抽象一下那些不应该在公共模块里边出现的东西:
这时候这段代码出现的是当前项目目录下面,它去找当前目录下的 package.json 是没问题的。
一来是能运行过去,二来是它本应该属于当前项目。
在模块当中去动态 require 一下项目中的 pages.config.js。
process.cwd() 类似于命令行输入 pwd 指令返回当前命令行所在工作目录一样
再去载入一下配置文件:
此时,config 里面就应该有 data,后面的 data 换成 config.data
改正完成了,尝试运行一下 build:
可以看到又报了一个错叫 @babel/preset-env
在使用 babel 的时候使用了 presets。
之前在 gulpfile 里定义的 babel 的转换,去使用 babel 的时候是没有问题,babel 会通过定义的 presets 去找对应的 preset 模块,然后通过 preset 去转换我们的 js 代码,这时候找的规则是项目下的 node_modules 去找一个叫 @babel/preset-env 的模块,但是此时并没有一个这样的模块,这个模块会被之前提取的公共模块包装进去,这时候在项目里去使用自然而然就出了问题。
解决这个问题的方式就是把 presets 的方式做一个修改,因为 presets 最简单的方式是传递一个字符串,babel 工作的时候会自动去 node_modules 下边去找,还有一种方式是直接去载入一个 presets 对象,这时候可以通过 require 的方式去载入。
因为 require 是先到当前文件所在目录依次往上找,lib 下面没有,再往上 zce-pages 下边有个 node_modules 里边有,这时候就可以找到这个模块,这时候这个模块就可以正常工作了。
再次运行 build
这里有个现象就是子任务的名字没有出现,因为 gulp 在工作的时候是根据 gulpfile 推断出来这些任务的名字,这里是把任务全部包装到了 zce-pages 这个模块,他在工作的时候就不知道里边任务的名字了,唯一知道的就是你启动的这个名字,所以说就不打印那些名字了。如果真的想做,可以通过解构的方式解构出来那些任务,然后单独导出那些任务,这样 gulpfile 就可以推断出来那些任务了,但是没什么太大意义,所以这里就不做了。
这里看一下我们提取出来的模块能不能满足我们的要求:
结构也对,文件内容也对,内容也读取出来了,转换也对,这就意味着我们提取的 gulpfile 是OK 的了
到这里我们的自动化构建模块就已经完成了,但是这里有的地方还可以进行一下深度化的包装,具体就是对于在代码里写死的一些路径,这些路径在我们使用者或者说在使用的项目中来说,实际上就可以看作是一个约定,约定固然好,但是有的时候提供可以配置这种能力也很重要。
因为在我们的项目当中,如果项目要求 src 这个目录不叫 src,必须叫一个别的目录的时候,这时候可以通过配置的方式去覆盖,这样可能更加灵活一些。
这里在 lib/index.js 里先加一些默认配置,后续就可以通过 pages.config.js 去覆盖了。
const path = require('path')
const { src, dest, series, parallel, watch } = require('gulp') // 先导入 gulp 所提供的 API
const del = require('del')
const browserSync = require('browser-sync')
const loadPlugins = require('gulp-load-plugins') // 得到一个方法
const plugins = loadPlugins() // 得到一个对象,所有的插件都会成为这个对象下方的一个属性,命名方式就是把 gulp- 去掉,后边的变成小驼峰格式 比如: gulp-pa-bbb 使用的时候就是 plugins.paBbb
const bs = browserSync.create() // 自动创建一个开发服务器
const cwd = process.cwd() // 返回当前命令行所在工作目录
let config = { // 为甚么用 let ,因为读 pages.config.js 的时候不一定有这个文件,程序是肯定不能让报错的,可以有一些默认的配置出现
// 默认配置
build: {
src: 'src',
dist: 'dist',
temp: 'temp',
public: 'public',
paths: {
styles: 'assets/styles/*.scss',
scripts: 'assets/scripts/*.js',
pages: '*.html',
images: 'assets/images/**',
fonts: 'assets/fonts/**'
}
}
}
try { // 尝试读取
let loadConfig = require(path.join(cwd, 'pages.config.js'))
config = Object.assign({}, config, loadConfig)
} catch (err) { } // 错误实际上是不需要处理的,有默认的配置选项
const clean = () => {
return del([config.build.dist, config.build.temp]) // 返回的是 promise ,也就意味着 clean 任务完成之后可以被标记为完成状态
}
// 首先先定义私有的任务,后续再通过 module.exports 选择性的导出
// 定义 style 任务
const style = () => {
// 这里直接 config.build.paths.styles 的话会有问题,因为原来是在 src 下面的 assets ... 可以在 base 后面添加一个 cwd 意思是当前任务工作路径,也就是在 src 下面工作的,这时候找的就是 src 下面的 assets ...
return src(config.build.paths.styles, { base: config.build.src, cwd: config.build.src }) // 创建文件读取流, 指定一个 base 基准路径,就可以把 src 下面的路径给保留下来
.pipe(plugins.sass({ outputStyle: 'expanded' })) // 给 sass 指定选项
.pipe(dest(config.build.temp)) // 写入流
.pipe(bs.reload({ stream: true }))
}
// 定义 script 任务
const script = () => {
return src(config.build.paths.scripts, { base: config.build.src, cwd: config.build.src }) // 同样给一个基准路径
.pipe(plugins.babel({ presets: [require('@babel/preset-env')] })) // 添加以下 babel 的配置
.pipe(dest(config.build.temp))
.pipe(bs.reload({ stream: true }))
}
// 定义 page 任务
const page = () => {
// 如果不只是在 src 下面有 html 文件的话,可以使用 src/**/*.html 意思是 src 下面任意子目录下的 *.html 文件,这是子目录的通配方式,这里的 base 设置的就没有意义了,因为通配符就在 src 目录下,为了保证统一,所以设置一下 base
return src(config.build.paths.pages, { base: config.build.src, cwd: config.build.src })
.pipe(plugins.swig({ data: config.data }))
.pipe(dest(config.build.temp))
.pipe(bs.reload({ stream: true }))
}
// image 任务
const image = () => {
return src(config.build.paths.images, { base: config.build.src, cwd: config.build.src }) // ** images 下边的所有文件
.pipe(plugins.imagemin())
.pipe(dest(config.build.dist))
}
// font 任务
const font = () => {
return src(config.build.paths.fonts, { base: config.build.src, cwd: config.build.src }) // ** images 下边的所有文件
.pipe(plugins.imagemin())
.pipe(dest(config.build.dist))
}
// 额外的
const extra = () => {
// 这里直接通配 public 下面所有的文件就好了
return src('**', { base: config.build.public, cwd: config.build.public })
.pipe(dest(config.build.dist))
}
const reload = () => {
bs.reload()
}
const serve = () => { // 把服务器放到一个任务当中
// watch 的 cwd 同样也可以通过第二个参数的方式传递进去
watch(config.build.paths.styles, { cwd: config.build.src }, style) // 在 serve 任务开始的时候先监视一些文件(源代码文件),两个参数,1-通配符,2-对应的执行任务
watch(config.build.paths.scripts, { cwd: config.build.src }, script) // 在 serve 任务开始的时候先监视一些文件(源代码文件),两个参数,1-通配符,2-对应的执行任务
watch(config.build.paths.pages, { cwd: config.build.src }, page) // 在 serve 任务开始的时候先监视一些文件(源代码文件),两个参数,1-通配符,2-对应的执行任务
// watch('src/assets/images/**', image) // 在 serve 任务开始的时候先监视一些文件(源代码文件),两个参数,1-通配符,2-对应的执行任务
// watch('src/assets/fonts/**', font) // 在 serve 任务开始的时候先监视一些文件(源代码文件),两个参数,1-通配符,2-对应的执行任务
// watch('public/**', extra) // 在 serve 任务开始的时候先监视一些文件(源代码文件),两个参数,1-通配符,2-对应的执行任务
watch([
config.build.paths.images,
config.build.paths.fonts
], { cwd: config.build.src }).on('change', reload) // 同时监听三个目标文件,发生变化去调用一下 browser-sync 提供的 reload 就可以了
// 因为 public 的 cwd 是 public 目录,所以这里单独抽取出来
watch("**", { cwd: config.build.public }).on('change', reload) // 同时监听三个目标文件,发生变化去调用一下 browser-sync 提供的 reload 就可以了
bs.init({ // 初始化一下开发服务器
notify: false, // 链接状态
port: 8888, // 修改启动端口
open: false, // 自动打开浏览器
// files: 'dist/**', // 要监听的哪些文件,可以使用通配符
server: { // 添加一些 server 配置
// baseDir: 'dist', // 根目录
baseDir: [config.build.temp, config.build.src, config.build.public], // 根目录, 指定为一个数组,当一个请求过来之后,先到数组中第一个目录去找,找不到再去下一个目录去找
routes: { // 这个是优先于 baseDir 的配置,先看在 routes 里面有没有对应的配置,如果有,走这儿,否则走 baseDir 里面的目录
'/node_modules': 'node_modules'
}
}
})
}
const useref = () => {
return src(config.build.paths.pages, { base: config.build.temp, cwd: config.build.temp }) // 这里要找的是编译过后的 dist 下的 html 文件,因为找 src 下面没有编译的模板没有意义,这也是为什么最后来说这个的原因
.pipe(plugins.useref({ searchPath: [config.build.temp, '.'] })) // 需要指定转换参数
// html css js 压缩
.pipe(plugins.if(/\.js$/, plugins.uglify())) // 会根据给定的条件执行对应的转换
.pipe(plugins.if(/\.css$/, plugins.cleanCss())) // 会根据给定的条件执行对应的转换
.pipe(plugins.if(/\.html$/, plugins.htmlmin({
collapseWhitespace: true,
minifyCSS: true,
minifyJS: true,
}))) // 会根据给定的条件执行对应的转换
.pipe(dest(config.build.dist)) // 换个目录
}
// const compile = parallel(style, script, page)
// 开发时候用到的,只编译一部分需要编译的代码
const compile = parallel(style, script, page) // 图片和字体不需要在开发阶段去编译压缩
// 上线之前用到的,所有文件都编译
const build = series(clean, parallel(series(compile, useref), image, font, extra)) // 在组合的基础上又组合了一次
// 开发阶段构建任务
const develop = series(compile, serve)
module.exports = {
build,
clean,
develop
}
看起来我们的代码确实复杂了一些,但是对于后续的使用会灵活很多。修改完之后在项目里重新运行一下 build 看能否正常工作:
OK 运行正常,并且生成的结果也是正常的,测试一下 dev :
可以看到项目正常启动,测试一下在 pages.config.js 里也添加一个 build 选项测试一下看能否覆盖:
修改一下配置并保存重新测试:
可以看到配置是生效的。
这样对于我们这个模块需要完全抽象出来的东西就已经完成了。
对于开发者来讲,刚开始需要技能,后来需要想法,想法建立在技能之上,当技能能满足想法的时候,想法越多越好,想法越多,尝试的可能性也就越多,获得的东西也就越多。
这里可以自行把工作流整理一下,比如把 config 单独抽取出来等等。
至此,zce-pages 自动化构建工作流模块就完成了。但是我们这边还可以做更多的操作,让我们在使用它的时候更加方便一些。这里来看:
我们在项目里想使用它的时候,我们需要把它安装到我们的项目当中,之后在项目中添加配置文件,配置文件是必要的,再然后需要在项目里添加 gulpfile.js 去把我们 zce-pages 这个模块的任务导出去,然后再通过 gulp 去运行它,其实这个 gulpfile 对于我们当前项目来讲的话就是把模块提供的任务导出去,这个其实显得有些冗余,没什么太大意义,我们希望在项目根目录下没有 gulpfile.js 也可以正常工作。
这里先删除 gulpfile.js 这个文件,直接运行 yarn gulp 这个指令:
找不到 gulpfile 这个文件,它就没办法正常工作。
但是 gulp 的 cli 提供的命令行可以让我们指定这个参数,我们可以在命令行尝试手动指定一下:
yarn gulp --gulpfile ./node_modules/zce-pages/lib/index.js
报了一个没有 default 任务的错误,但是不报没找到 gulpfile 的错误了。我们尝试运行一下 build 任务:
可以看到可以正常工作。
但是有一个小小的问题是:它的工作目录已经变到了 lib 目录下面,因为你的 gulpfile 在 lib 目录,也就是 lib/index.js 所在目录。他会认为你的工作目录也在 lib 目录,这里就不会把你项目所在根目录当作当前工作目录了。要想指定的话可以再指定一个 --cwd 的参数
yarn gulp build --gulpfile ./node_modules/zce-pages/lib/index.js --cwd .
这时候就可以正常去使用这个工作流了。只不过这个任务执行过程需要传参数就比较复杂了。
这里就可以想一下如果在 zce-pages 里也提供一个 cli ,这个 cli 里面自动传递这些参数,然后在内部去调 gulp cli 提供的可执行程序,这样我们在外界使用的话就不用去使用 gulp 了。就相当于将 gulp 完全包装到我们的 zce-pages 当中。这里来操作一下:
先在 zce-pages 下面添加 cli 的程序,一般项目的模块代码放在 lib 下面,对于 cli 的代码一般放在 bin 目录。
这个文件会作为 cli 的执行入口,既然作为 cli 的入口,就必须要出现在 package.json 的 bin 字段当中:
"bin":"bin/zce-pages.js",
文件名字叫什么无所谓,一般会叫的跟 cli 命令保持一致。如果想指定命令的话可以把 bin 字段指定为一个对象,比如:
"bin":{
"zp":"bin/zce-pages.js"
},
这时候最终生成 cli 命令就叫做 zp。这里就不用这种方式了。
#!/usr/bin/env node
当然,在 mac 里边需要修改该文件的读写权限为 755 。
这里先随便打印个东西测试一下:
回到项目中使用这个 zce-pages 的 cli 了,需要重新 link 一下,因为只有重新 Link 了才能把这个 cli 注册到全局。
yarn unlink
yarn link
link 完之后我们就可以在命令行里使用 zce-pages 了:
它就会执行我们 cli 入口对应的代码了:
看到这里可以想到我们只需要把对 gulp-cli 的调用以及传递的参数放到这个入口就可以了,我们需要知道这里怎么使用的,借鉴一下 gulp-cli 是怎么工作的 node_modules/.bin/gulp.cmd:
可以看到它实际上是按照命令行的语法去写的一段代码,只需要看懂就可以了。
"%~dp0\node.exe" -- 当前目录(%~dp0)下的node.exe
# 这儿不用管,就是配置一下环境变量,让我们可执行文件名字加上了 .js 的扩展名
@SETLOCAL
@SET PATHEXT=%PATHEXT:;.JS;=;%
# 使用 node 执行一下当前目录(.bin)的上一级目录(node_modules)下gulp下的bin下的gulp.js文件,
node "%~dp0\..\gulp\bin\gulp.js" %*
这个实际上是载入了一下 gulp-cli 这个模块的文件并立即执行,那么我们想在 zce-pages/bin/zce-pages.js 里执行这个文件的话只需要 载入 gulp\bin\gulp.js 就行了:
载入它的话它会自动载入 gulp-cli。至于命令行的参数如何传递一会儿再看,这里先看一下它能不能工作:
可以看到,gulp-cli 工作了,只是找不到 gulpfile 文件。
这里我们要知道一点,命令行传递的参数是用 process.argv 可以接收到,我们测试一下:
可以看到 argv 前两个是固定的,后边是我们传递的参数,也就是说它是通过 process.argv 拿到所有参数的。
我们可以在代码运行之前先往这个 argv 里边 push,
#!/usr/bin/env node
process.argv.push('--cwd')
process.argv.push(process.cwd())
process.argv.push('--gulpfile')
process.argv.push(require.resolve('..')) // require 是载入这个模块,resolve 是找到这个模块的相对路径 传递的参数都是相同的,都是相对路径,这里可以传 ../lib/index.js 但是对于这个模块,package.json 的 main 字段指定的入口文件就是 lib/index.js 所以这里直接传递 .. 就行了
console.log(process.argv)
// require('gulp/bin/gulp')
进入项目所在目录,测试一下看看:
可以看到是没问题的。打开注释我们去执行一下任务试试:
再测试一下 clean 任务:
OK,都正常工作。
这时候就不要求项目的根目录下有 gulpfile.js 文件了,而且如果把 zce-pages 作为全局安装的话甚至都不需要项目里安装这个依赖。这样后续使用的时候就更加方便了。相当于完全把 gulp 包装到了封装 的模块当中,使用的时候完全不需要安装 gulp gulp-cli 那些东西了。
这里把封装好的模块发布到 npm 上,然后在一个新的项目里使用一下它提供的 cli 以及自动化构建工作流。
发布之前做一下小小的改动:
在这个模块当中,通过 npm 去publish 的时候它默认会把项目根目录下的文件和 package.json 当中的 files 字段对应的目录发布到对应的 npm 仓库当中。
我们创建的时候默认只有一个 lib 目录,现在又新增了一个 bin 目录,这里添加一下:
然后我们去命令行当中去 publish 一下,发布之前先通过 git 提交一下:
这里 publish 的时候可能会 publish 不上去,因为在国内使用的是 淘宝 镜像 源,可以修改一下配置文件,如果不想修改配置文件,可以携带 --registry 临时参数:
没注意看,以为用的地址不对,仔细一看才发现是发布的模块名被注册了,那当然了,zce老师早都发布了,这里修改一下:
测试一下:
OK 正常工作,再次发布:
发布成功,yarn 的镜像源和 npm 的镜像源是同步的,这里就可以使用它了。
测试一下:
mkdir zgp-pages-demo
cd zgp-pages-demo
code .
这个项目的目录结构跟我们的 zce-gulp-demo 有相同的结构,这里把 public src pages.config.js 都拷贝过去:
还需要初始化一个 package.json yarn init --yes
以及安装咱们发布的 zgp-pages 模块
# 可以安装为全局依赖,建议装成局部
yarn add zgp-pages --dev
# npm install zgp-pages -D
之所以建议安装局部依赖,是因为当你的项目放到别人的机器上,别人的机器可不一定会安装咱们的 zgp-pages 模块。
这里在国内操作的时候有可能会出现一个问题,国内大家一般使用的是 淘宝镜像源,淘宝镜像源同步 npm 镜像源的包会有一定时间差,发布之后如果当时就安装可能会出现找不到这个包的情况,需要等一等,如果是发布升级包的操作可能安装的还是老版本的,这时候可以手动去 npm.taobao.org/package/包名 这个网址手动同步
额 貌似地址换了,叫 https://developer.aliyun.com/mirror/npm/package/zgp-pages 最后边换成自己发布的包名就行
安装成功之后在 node_modules/.bin 下边就应该有一个 zgp-pages.cmd 的文件,让我们可以在命令行执行 zgp-pages 这个指令,这里测试一下:
yarn zgp-pages build
可以执行。我们往 package.json 里添加以下 scripts :
"scripts": {
"clean": "zgp-pages clean",
"build": "zgp-pages build",
"dev": "zgp-pages develop"
},
测试一下 dev 指令:
样式不对,可以看到是使用的运行环境框架的依赖没有安装,之前拷贝东西有个东西忽略掉了:
运行环境的依赖也需要安装一下:
安装成功之后再次运行测试:
OK,成功了,说明我们这个自动化构建工作流模块是正常可以使用的。
这里梳理一下上面封装的构建过程:… 还是从上边自行查看吧,这里不总结了。
zce 老师也发布了一另外一个包,就是把数据往外拿了一下,想借鉴的可以自行过去【查看】
FIS 是百度的前端团队推出的一款构建系统,最早只是在它们团队内部使用,后来开源之后在国内确实流行了一段时间,只是现在用的人越来越少。… 其它的有一些负面信息就不说了。
但是这里之所以来说一下它,是因为 FIS 完全属于另外一种类型的构建系统,相比于 gulp 和 grunt ,FIS 的特点是高度集成,它把前端日常开发过程当中常见的构建任务和调试任务都集成在了内部,开发者就可以通过简单的配置文件的方式去配置构建过程需要完成的工作。
也就是说在 FIS 当中不需要像 gulp 或者 grunt 一样去定义任务。
FIS 中有一些内置任务,这些内置任务会根据开发者的配置自动完成整个构建过程,除此之外,FIS 还内置了一款 webserver 可以非常方便的调试我们的构建结果。这一系列的东西在 gulp 或者 grunt 当中都是需要通过一些插件实现的。
简单了解一下它的使用:
yarn global add fis3
随便打开一个简单的网页应用,看一下:
这里执行一个叫 release 的任务,它是默认任务
它会将所有被构建的文件自动构建到一个临时的目录当中
可以指定参数
添加配置文件
先尝试对 scss 文件做一个编译:
这里说一个点:
为什么自调用函数的时候会去传递 window 和 document ,一来是性能问题,因为这样一来,代码当中实际上使用的是局部变量的 window 和 document ,不会沿着原型链一层层往上找,另外一方面就是压缩过程当中使用这种方式使用的越多,压缩的比例会越大:
相对于使用原始的 window 和 document ,字符数肯定会少很多。
这里不做过多的介绍了,感兴趣的自己去官方查看好了。
【学习笔记】Part2·前端工程化实战–开发脚手架及封装自动化构建工作流(一、工程化概述)
【学习笔记】Part2·前端工程化实战–开发脚手架及封装自动化构建工作流(二、脚手架工具)
【学习笔记】Part2·前端工程化实战–开发脚手架及封装自动化构建工作流(三、自动化构建 – 主Grunt)
【学习笔记】Part2·前端工程化实战–开发脚手架及封装自动化构建工作流(三、自动化构建 – 主Gulp)