Glup简介
- Gulp基本使用
- Gulp的组合任务
- Gulp异步任务(异步任务的三种方式)
- Gulp构建过程核心原理
- Gulp文件操作API+插件的使用
- Gulp自动化构建案例
Gulp基本使用
1.安装g依赖
$ yarn add gulp -D
- 根目录下新建gulpfile.js文件,定义构建任务的方式就是通过导出函数成员的方式定义
注意
2.1 :foo任务中的代码正常执行了,但控制台会抛出一个错误=>foo任务执行没有完成,问我们是否忘记去标识这个任务的结束.这是因为在最新的gulp当中,它取消了同步任务模式,约定我们的每个任务必须是异步任务,当我们的任务执行完成过后,我们需要通过调用回调函数去标识这个任务已经完成
2.2 这个回调函数我们可以通过foo函数的形参拿到
- gulps的默认任务 default成员
Gulp的组合任务
- 除了创建普通的任务外,Gulp还可以创建组合任务(并行任务个串行任务),没有被导出的成员函数称为私有任务,
串行任务 series(任务1,任务2,…)
依次执行任务一,任务二…
并行任务 parallel(任务1,任务2,…)
Gulp异步任务(异步任务的三种方式)
回调函数 done参数
- done这个回调函数和node中的回调函数是同样的标准,都是一种错误优先的回调函数,
也就是说当我们想在执行过程中抛出错误去阻止剩余任务执行的时候,我们可以给回调函数的第一个参数去指定一个错误对象即可
promise 避免回调嵌套太深
- 在执行任务中return 一个promsie对象
- return Promise.resolve() 任务成功
- return Promise.reject() 任务失败,后续任务不会执行reject的参数会作为失败的原因
async…await
- 实际上是Promise的语法糖,内部还是Promise
源码
exports.callback = done => {
console.log("callback task~")
done()
}
exports.callback_error = done => {
console.log("callback task~")
done(new Error("tash failed!"))
}
// 返回成功的promise代表这个任务成功
exports.promise = () => {
console.log("promise task~")
// 一旦执行到Promise.resolve()则代表这个任务结束,返回一个成功的promise对象
// Promise.resolve()不需要返回任何值,即Promise.resolve()函数中没有必要加参数,gulp会忽略这个返回值
return Promise.resolve()
}
// 返回失败的promsise代表这个任务失败
exports.promise_error = () => {
console.log("promise task~")
return Promise.reject()
}
const timeout = time => {
return new Promise(resolve => {
setTimeout(resolve, time);
})
}
exports.async = async () => {
await timeout(1000)
console.log("async task~")
}
其他的几种方式
stream
- 因为我们的构建大多数情况下都是在处理文件,所以这种方式也是最常用的一种
- 实现方式:在任务函数中返回一个stream对象
- 任务结束的时机是readStream读取结束的时候(readStream的end事件触发的时候),从而使gulp知道这个任务完成了
- 读取流.pipe(写入流),实现数据写入 pipe(派普)
gulp中只是在内部将任务中返回的stream对象注册了一个end事件,去监听这个事件而已
代码
const fs = require("fs")
// exports.stream = () => {
// // 通过node模块创建一个读取文件的文件流
// const readStream = fs.createReadStream("package.json")
// // 写入文件的文件流
// const writeStream = fs.createWriteStream("temp.txt")
// // 将readStream通过pipe的方式导入writeStream
// readStream.pipe(writeStream)
// return readStream
// }
// 模拟
exports.stream = done => {
const readStream = fs.createReadStream("package.json")
const writeStream = fs.createWriteStream("temp.txt")
readStream.pipe(writeStream)
// 没有返回readStream对象,直接注册end事件
readStream.on("end", () => {
done()
})
}
Gulp构建过程核心原理
- 构建过程大多数情况下都是将文件读取出来进行一些转换,最后写入另外一个位置
- 正常的读写操作
- 对读取的文件流在写入之前进行转换(压缩 => 转换流)
callback导出的是经过转换之后的文件流
代码
const fs = require("fs")
const {
Transform
} = require("stream")
exports.default = () => {
// 文件读取流
const read = fs.createReadStream("normalize.css")
// 文件写入流
const write = fs.createWriteStream("normalize.min.css")
// 文件转换流
const transform = new Transform({
transform: (chunk, encoding, callback) => {
// 核心转换过程实现
// chunk => 读取流中读取到的内容(Buffer),将空白字符去掉(压缩)
const ouput = chunk.toString().replace(/\s+/g, "")
callback(null, ouput) //错误优先的回调
}
})
// 先转换后写入
read.pipe(transform).pipe(write)
return read
}
Gulp文件操作API+插件的使用
gulp文件操作API src(读取流) dest(写入流)
- Gulp中为我们提供了专门用于创建文件读取流和写入流的API,相比于底层Node的API,GuIlp的API更加强大,更容易使用
- 负责文件加工的转换流绝大多数情况下,我们都是通过独立的插件来完成
- src(源文件路径)函数创建读取流,dest(输出文件路径)函数创建写入流,这两个函数都输Gulp模块提供
文件的转换 (插件)
gulp-clean-css 插件 压缩css代码
- 安装插件
yarn add gulp-clean-css -D
- 导入插件
const cleanCss = require("gulp-clean-css")
- 每个gulp插件都是一个任务函数
- 如果需要在转换过程中执行多个转换,可以继续在中间添加额外的pipe()操作,例如我们通过gulp-raname的插件
gulp-rename 插件 对输出文件的扩展名进行重命名
- 安装插件
$ yarn add gulp-rename -D
- 导入插件
const rename = require("gulp-rename")
- 配置选项extname
Gulp自动化构建案例
- 拉取实例案例代码
代码仓库
样式编译 => 转换流插件:gulp-sass
$ git clone https://github.com/zce/zce-gulp-demo.git
$ yarn add gulp -D
- css直接在dist目录下输出,丢失了css的目录结构,我们是希望src下面的css路径能在dist目录下能完整体现=>可以为src函数设置第二个参数:选项配置,配置输出的基准路径
- 安装插件
$ yarn add gulp-sass -D
- 导入插件
const sass = require("gulp-sass")
- 在安装gulp-sass插件的时候,内部会先安装node-sass,而node-sass依赖一些c++的二进制的包,这些二进制的包需要通过国外的网站下载,经常提示下载失败
- 解决方案:先下载node-sass再安装gulp-sass
$ npm i node-sass --sass_binary_site=https://npm.taobao.org/mirrors/node-sass/
$ yarn add gulp-sass
- 基本上每个插件提供的都是一个函数,这个函数的调用结果会返回一个文件转换流
脚本编译 => 转换流插件:gulp-babel
- 原始编译
- 安装插件
$ yarn add gulp-babel -D
$ yarn add @babel/core @babel/preset-env -D // 转换的工作实际上@babel/preset-env来完成
- 导入插件
页面模板编译 = > 转换流插件:gulp-swig
- 安装插件
$ yarn add gulp-swig -D
- 导入插件
在模板中预设号的变量,因为没有传入模板数据上下文而变成null(空值)
- 往gulp-swig插件中传入模板数据上下文
创建组合任务=>将样式/脚本/页面统一构建
- style/script/paged三个任务之间没有依赖,可同时执行=>parallel()函数有gulp提供
图片和字体文件的编译 => gulp-imagemin
- 安装插件
$ yarn add gulp-imagemin -D
- 导入插件
- gulp-imagemin插件可以帮助我们压缩图片,同时字体文件也是通过该插件进行压缩
- 改进:将所有的任务放在compile中
const compile = parallel(style, script, page,image,font)
module.exports = {
compile,
}
其他文件及文件清除
public文件处理
- src下面的任务已经全部处理完成后,我们再把public下面的文件做一个拷贝(不经过转换流处理)
- 添加一个额外的拷贝任务extra,这个任务可以放在compile任务中,但是compile的功能是吧src下的文件做转换,所以我么可以添加一个新的任务build同时执行compile和extra(组合的基础上再组合一次)
dist文件清除 => del插件(不是gulp的插件)
- del插件不是gulp的插件,但在gulp中可以使用
- 安装插件
$ yarn add del -D
- 导出插件
自动加载插件 => gulp-load-plugins插件
- 随着构建任务越来越复杂,使用到的插件也会越来越多,如果我们都是通过手动的方式载入插件的话,require的操作会非常多,不利于我们后期去回顾代码
- 安装插件
$ yarn add gulp-load-plugins -D
- 通过gulp-load-plugins插件提供的api自动加载全部的插件(plugin)
注意点
- gulp-load-plugins插件只能自动导出gulp的插件,所以上面的del插件是不能通过此方法自动导入,还是需要手动导入del插件
- 格式
2.1 gulp-sas s=> plugins.sass
2.3 gulp-sass-env=> plugins.sassEnv 需要转换成驼峰格式
热更新开发服务器 browser-sync 插件
启动服务器
- 开发阶段调试应用
- browser-sync 插件不是gulp的插件,需要手动导入
- 安装插件
$ yarn add browser-sync
- 导入插件
const browserSync = require("browser-sync")
- 创建服务器
const bs = browserSync.create()
- 创建启动服务的任务
const serve = () => {
// 初始化这个服务器的相关配置
bs.init({
//server:网站根目录,就是web服务器帮我们把那个目录当成根目录
server: {
baseDir: "dist" //dist目录就是网站根目录
}
})
}
- 执行启动服务器的任务
$ yarn gulp serve
- 效果
- 看到的效果差强人意,因为我们在代码的编译过程中,我们并没对/node_modules下模块的拷贝,我们只是将自己写的src和public目录下的文件进行了编译
- 改进:在给browser-sync单独加一个特殊的路由routes,让对于/node_module这种开头的网页请求指定到同一目录下
- 则对于网页中bootstrap库的样式请求就会自动映射到我们项目下的node_modules中
- 效果
- 服务器其它常用选项配置
模块热更新
dist目录热跟新
- 服务器配置选项=> files:“监听文件的路径”
- files选项配置只是监听dist目录下文件发生变化,浏览器自动更新,并不是src下的源码
- src目录下文件修改如何触发热更新:src下文件修改后自动更新,监视src下面文件变化,一旦文件变化后,重新完成构建任务(build),构建完成后,dist目录下文件发生变化,也就触发了files选项配置,浏览器同步更新
src目录热更新 gulp 提供的api => watch
- watch会自动监视一个文件路径符,然后根据这些文件的变化决定是否去执行某一个任务
- wtach()接收两个参数 参数一:通配符 参数二:任务函数
- 导入watch插件
- 在serve任务中配置watch选项(其实在哪里配置都可以,只是watch监听后的结果要在浏览器中看到结果,和服务器放在一起方便维护)
构建优化(减少构建的次数)
前提知识:
- watch() 对于css/js/html文件的编译的是有意义的,但是对于字体/图片和额外文件(public下文件)的编译在开发阶段没什么意义
- 因为图片和字体文件,我们只是对它们进行了压缩,并不影响在页面中的呈现,在开发阶段监视更多的文件(字体/图片/public下文件),开销也会越大,而这个开销在开发阶段是没有意义的,并且降低开发阶段构建的效率 =>develop环境
- 我们只是希望在项目上线前通过压缩减小上线项目的体积,从而提高网站运行的效率=>build(生产)环境
优化步骤
- 在开发(develop)阶段,字体/图片/public下文件不参与构建
- 执行构建命令
$ gulp develop
- 虽然字体/图片/public下文件在开发阶段并不参与构建,但是这并不影响他们的使用,因为
baseDir: ["dist", "src", "public"],
- src下的字体/图片文件及public下的文件发生变化,我们也希望同时更新下浏览器,可以通过一个watch(),此时我们watch的第二个参数不是构建任务,因为我们在开发阶段,以上的文件是不参与构建的
- watch() 第二个是browser-sync创建的服务器实例的一个方法bs.reload
useref文件引用处理 (uesref:使用依赖)
- 执行build命令时,dis下会以最终上线的状态来呈现所有生成的文件
$ yarn gulp build
- 发现在dist/index.html中存在node_modules下文件的依赖,但这些依赖并没有拷贝到dist目录下,我们将dist目录上线肯定会出现问题,它会找不到这些bootstrap文件
- 在开发阶段运行没有问题的原因是在serve命令中做了路由(routes)的映射,并不能在线上环境这么操作,
- 解决方式=>useref插件会自动处理html中的构建注释
- 构建注释结构
1.1 开始 build:css dist目录下的文件地址
1.2 结束 endbuild
- 安装插件
$ yarn add useref
- 创建uesref任务
- 代码
const useref = () => {
// 对dist目录中的html文件进行处理D
return src("dist/*.html", {
base: "dist"
})
.pipe(plugins.useref({
searchPath: ["dist", "."] // 在哪些文件中找这些依赖文件"."项目根目录包含了包含了node_modules目录
}))
.pipe(dest("dist"))
}
- 执行命令(前提:build之后产生了dist目录)
$ yarn gulp useref
注意点: " . "代表根目录,那为什么前面还需要加上dist呢?
原因: 插件uesref的searchPath参数可以是一个数组,使用较多的情况是在dist下寻找htm文件,所以dist写在前面,而在没有dist文件的情况下就要向根目录下面寻找,所以写在后面,如果只写’’ . "的情况,那么打包后的dist文件中的html就不会处理
文件压缩
- 有了useref之后,它会自动的帮我们把对应的依赖文件全部打包过来了,但我们还需要对这些文件进行一些压缩的处
- 分别压缩HTML,CSS,javascript
- html是通过src文件读取流创建出来的,css和js是useref工作过程中创建出来的
- 因为有三种文件需要处理,需要一个gulp-if插件来判断文件的类型来做不同的处理
$ yarn add gulp-if -D
$ yarn add gulp-uglify -D // 压缩js
$ yarn add gulp-cleanCss-D //压缩css
$ yarn add gulp-=htmlmin-D //压缩html
- 但这个时候还有一个问题
重新规划构建过程
背景
- useref任务中的读取流和写入流是同一个文件,会产生冲突问题
- 在useref之前,我们所有生成的文件实际上是一个中间产物,我们需要创建一个临时目录temp,当我们执行useref时是对临时目录temp的处理
代码
const {
src,
dest,
parallel,
series,
watch
} = require("gulp")
const del = require('del')
const browserSync = require("browser-sync")
const bs = browserSync.create()
const loadPlugins = require("gulp-load-plugins")
const {
pipe
} = require("stdout-stream")
const plugins = loadPlugins()
const data = {
pkg: {
name: 'szy',
homepage: "123"
}
}
const clean = () => {
// 删除临时的目录
return del(["dist", "temp"])
}
const style = () => {
// 放在temp目录中
return src("src/assets/styles/*.scss", {
base: 'src'
})
.pipe(plugins.sass())
.pipe(dest("temp"))
}
const script = () => {
// 放在临时目录中
return src("src/assets/scripts/*.js", {
base: "src"
})
.pipe(plugins.babel({
presets: ["@babel/preset-env"]
}))
.pipe(dest("temp"))
}
const page = () => {
// 放在临时目录中
return src("src/*.html", {
base: "src"
})
.pipe(plugins.swig({
data
}))
.pipe(dest("temp"))
}
const image = () => {
// 这个过程只会在build的时候做,在开发阶段我们始终引用的是src下的图片等
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 extra = () => {
return src("public/**", {
base: "public"
}).pipe(dest("dist/"))
}
const serve = () => {
watch("src/assets/styles/*.scss", style),
watch("src/assets/scripts/*.js", script),
watch("src/*.html", page),
watch(["src/assets/images/**", "src/assets/fonts/**", "public/**"], bs.reload)
bs.init({
files: "temp/**",
server: {
baseDir: ["temp", "src", "public"], //放在临时目录中
routes: {
'/node_modules': "node_modules"
}
}
})
}
const useref = () => {
// 临时目录中取文件
return src("temp/*.html", {
base: "temp"
})
.pipe(plugins.useref({
searchPath: ["temp", "."]
}))
// 此处有html,css,js代码
.pipe(plugins.if(/\.js$/, plugins.uglify()))
.pipe(plugins.if(/\.css$/, plugins.cleanCss()))
.pipe(plugins.if(/\.html$/, plugins.htmlmin()))
.pipe(dest("dist"))
}
const compile = parallel(style, script, page)
// useref只会在最终上线的项目中使用(build操作中)
// compile和useref有依赖关系(串行任务), compile先执行,生成了temp目录,useref再对temp目录压缩生成dist目录
// const build = series(clean, parallel(compile, image, font, extra))
const build = series(clean, parallel(series(compile, useref), image, font, extra))
const develop = series(clean, compile, serve)
module.exports = {
build,
develop,
}