自动化构建工具

自动化构建

自动化构建简介

一切重复工作本应自动化

  • 自动化构建(自动化构建工作流):把开发阶段代码自动转换为生产环境运行的代码/程序;
  • 作用:脱离运行环境兼容带来的问题,使用提高效率的语法、规范和标准
  • 自动化构建工具,构建转换那些不被支持的【特性】(sass、ECMAScript next、模板引擎)

自动化构建初体验

NPM Scripts 可以在 NPM Scripts 中定义一些与项目开发过程有关的脚本命令便于后期开发使用

  • 使用过程
    • 在命令行执行yarn init -y生成 package.json 文件
    • 在 package.json 文件添加 scripts 字段,该字段是一个对象,键是 scripts 名称,值是需要执行的命令;scripts 可以自动发现 node_modules 里面的命令,所以不用写完整的路径直接使用命令的名称即可
    • 使用 npm/yarn 启动 scripts
    • --watch: scss在工作是会监听文件的变化,scss 文件发生改变时会自动编译
1
2
3
"scripts": {
"build": "sass scss/main.scss css/style.css --watch",
}
  • NPM Scripts 实现自动化构建工作流的最简单方式

使用 browser-sync 模块可以开启测试服务器运行项目,在 scripts 中添加命令"serve": "browser-sync ."运行项目

--files "css/*.css" 参数可以让 browser-sync 在启动过后监听项目下文件的变化,browser-sync 会将文件内容自动同步到浏览器,更新浏览器界面

1
2
3
4
"scripts": {
"build": "sass scss/main.scss css/style.css --watch",
"serve": "browser-sync . --files \"css/*.css\""
},

在启动 serve 前让 build 工作可以借助 NPM Scripts 钩子机制定义一个 preserve,它可以在 serve 执行前去执行

1
2
3
4
5
"scripts": {
"build": "sass scss/main.scss css/style.css --watch",
"preserve": "yarn build",
"serve": "browser-sync . --files \"css/*.css\""
},

可以使用npm-run-all模块同时执行多个任务

1
2
3
4
5
"scripts": {
"build": "sass scss/main.scss css/style.css",
"serve": "browser-sync . --files \"css/*.css\"",
"start": "run-p build serve"
},

常用的自动化构建工具

  • Grunt 最早的构建系统;插件生态完善;工作过程基于临时文件实现所以构建速度较慢,如使用它完成 scss 文件构建,我们会先对 scss 文件进行编译工作,自动添加属性前缀,压缩代码,Grunt 每一步都有磁盘读写操作,处理环节越多,磁盘的读写越多;大型项目中速度会非常慢
  • Gulp 可以解决 Grunt 中构建速度慢的问题,他基于内存实现的,对文件的读写都是基于内存完成的,相对与磁盘读写就快了;支持同时执行多个任务效率高;使用方式相对于 Grunt 更直观易懂;插件生态完善
  • FIS 微内核;更像是一种捆绑套餐,把项目中典型的需求尽可能集成在内部

Grunt

Grunt 的基本使用

  • 项目命令行输入 yarn init -y 生成 package.json 文件
  • 安装 grunt yarn add grunt
  • 项目根目录创建 gruntfile.js 文件,该文件是 Grunt 入口文件,用于定义一些需要 Grunt 自动执行的任务,需要导出一个函数,此函数接收一个 grunt 的形参,内部提供一些创建任务时可以用到的 API
  • 最后使用 yanr grunt taskName 执行任务

使用 grunt.registerTask() 方法注册一个任务

  • 第一个参数为任务名称,如果任务名称为 default,该任务将作为默认任务,运行时不用指定任务名,Grunt 将自动调用 default
  • 第二个参数
    • 第二个参数是字符串时,该字符串将是任务的描述,命令行输入 yarn grunt --help 可以看到该信息;
    • 一般会使用 default 任务映射一些其他的任务,其他的任务将作为第二个参数,参数是数组,元素为任务名,当执行 default 任务时 Grunt 会依次执行数组中的任务;
    • 当第二个参数为函数时,将制定任务函数,任务发生时自动执行函数
  • 异步任务:需要使用 this.async() 方法得到一个回调函数,在异步回调函数完成后调用这个函数标识任务已经完成
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
// Grunt 的入口文件
// 用于定义一些需要 Grunt 自动执行的任务
// 需要导出一个函数
// 此函数接收一个 grunt 的对象类型的形参
// grunt 对象中提供一些创建任务时会用到的 API

module.exports = (grunt) => {
grunt.registerTask("foo", "a sample task", () => {
console.log("hello grunt");
});

grunt.registerTask("bar", () => {
console.log("other task");
});

// // default 是默认任务名称
// // 通过 grunt 执行时可以省略
// grunt.registerTask('default', () => {
// console.log('default task')
// })

// 第二个参数可以指定此任务的映射任务,
// 这样执行 default 就相当于执行对应的任务
// 这里映射的任务会按顺序依次执行,不会同步执行
grunt.registerTask("default", ["foo", "bar"]);

// 也可以在任务函数中执行其他任务
grunt.registerTask("run-other", () => {
// foo 和 bar 会在当前任务执行完成过后自动依次执行
grunt.task.run("foo", "bar");
console.log("current task runing~");
});

// 默认 grunt 采用同步模式编码
// 如果需要异步可以使用 this.async() 方法创建回调函数
// grunt.registerTask('async-task', () => {
// setTimeout(() => {
// console.log('async task working~')
// }, 1000)
// })

// 由于函数体中需要使用 this,所以这里不能使用箭头函数
grunt.registerTask("async-task", function () {
const done = this.async();
setTimeout(() => {
console.log("async task working~");
done();
}, 1000);
});
};

Grunt 标记任务失败

在 grunt.registerTask() 任务函数 return false,在命令行执行该任务时将会提示执行失败,在任务列表中会导致后面的任务无法执行,在执行任务时添加 –force 命令,将会强制执行所有的任务 yarn grunt --force,在异步任务中无法通过 return false 来标记失败,要给异步的回调函数指定 false 实参就可以标记失败了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
module.exports = (grunt) => {
grunt.registerTask("foo", () => {
console.log("hello grunt~");
});

grunt.registerTask("bar", "任务描述", () => {
console.log("other task~");
});

grunt.registerTask("bad-async", function () {
const done = this.async();
setTimeout(() => {
console.log("async task working~");
done(false); // 出入实参false标识异步任务失败
}, 1000);
});

grunt.registerTask("bad", () => {
console.log("bad working~");
return false;
});

grunt.registerTask("default", ["foo", "bad", "bar"]);
};

Grunt 的配置方法

  • 通过 grunt.initConfig() 方法为任务添加一些配置项
  • grunt.initConfig() 参数为一个对象,键一般对应任务的名,值可以是任意类型的数据
  • 在 grunt.registerTask() 中可以通过 grunt.config() 获取配置,如果配置中的属性值是对象的话,config 可以使用点的方式定位对象中的属性值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
module.exports = (grunt) => {
// grunt.initConfig() 用于为任务添加一些配置选项
grunt.initConfig({
// 键一般对应任务的名称
// 值可以是任意类型的数据
foo: {
bar: "baz",
},
});

grunt.registerTask("foo", () => {
// 任务中可以使用 grunt.config() 获取配置
console.log(grunt.config("foo"));
// 如果属性值是对象的话,config 中可以使用点的方式定位对象中属性的值
console.log(grunt.config("foo.bar"));
});
};

Grunt 多目标任务(子任务)

  • grunt.registerMultiTask()
    • 通过 grunt.registerMultiTask() 定义多目标
    • 多目标模式,可以让任务根据配置形成多个子任务
    • grunt.registerMultiTask() 第一个参数是任务的名字,第二个参数是任务执行过程要做的事情
    • 多任务,要使用 grunt.initConfig()为任务配置目标
    • 当运行任务时会去执行目标(子任务),如果要运行指定的目标可以使用“:目标名”,yarn grunt build:css
    • 可以通过 this.target 拿到当前执行的目标名称,通过 this.data 拿到目标的配置数据
    • 可以通过 this.options()方法拿到配置选项
  • grunt.initConfig()
    • 参数为对象,键名与任务名相同(registerMultiTask()方法的第一个参数),值必须为对象,对象中每个属性名就是目标名称
    • 除了 options 键,其他都会作为目标,options 指定的信息会作为这个任务的配置选项
    • 目标当中也可以添加 options,运行目标是可以获取相应的 options(目标 options 覆盖任务 options),如果目标没指定而任务指定了则可以获取任务的 options
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
module.exports = (grunt) => {
grunt.initConfig({
build: {
// ‘build’名与
options: {
foo: "bar",
},
css: {
options: {
foo: "baz",
},
},
js: "2",
},
});
grunt.registerMultiTask("build", function () {
console.log(this.options());
console.log(`task: ${this.target}, data:${this.data}`);
});
};

Grunt 插件的使用

  • 通过 npm 安装
  • 在 gruntfile.js 文件中通过 grunt.loadNpmTasks() 方法把插件中提供的任务加载进来,根据插件文档配置
  • 在 grunt.initConfig() 方法中为任务添加配置选项
1
2
3
4
5
6
7
8
9
10
11
12
13
// 删除文件的任务

module.exports = (grunt) => {
grunt.initConfig({
clean: {
// temp:'temp/app.js' // 需要清除的文件,可以使用通配符"*",“**”表示找到该目录下的文件以及子目录的文件(即该目录下的所有文件)
temp: "temp/*.txt",
},
});

// 通过 loadNpmTasks 加载插件中提供的任务
grunt.loadNpmTasks("grunt-contrib-clean");
};

Grunt 常用插件及总结

  • grunt-sass
    • 安装 yarn grunt-sass sass --dev
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const sass = require("sass");
module.exports = (grunt) => {
grunt.initConfig({
sass: {
options: {
implementation: sass,
},
main: {
// 目标main(随便写)
files: {
"dist/css/main.css": "src/scss/main.scss", // 键是输出文件,值是输入文件
},
},
},
});
// 通过 loadNpmTasks 加载插件中提供的任务
grunt.loadNpmTasks("grunt-sass");
};
  • load-grunt-tasks
    自动加载所有的 grunt 插件
    • 安装 yarn add load-grunt-tasks --dev
1
2
3
4
5
const loadGruntTasks = require("load-grunt-tasks");
module.exports = (grunt) => {
// grunt.loadNpmTasks('grunt-sass')
loadGruntTasks(grunt); // 自动加载所有的 grunt 插件中的任务
};
  • babel
    • 安装 yarn add grunt-babel @babel/core @babel/preset-env --dev
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
const sass = require("sass");
const loadGruntTasks = require("load-grunt-tasks");

module.exports = (grunt) => {
grunt.initConfig({
sass: {
options: {
implementation: sass,
},
main: {
// 目标main(随便写)
files: {
"dist/css/main.css": "src/scss/main.scss", // 键是输出文件,值是输入文件
},
},
},
babel: {
options: {
sourceMap: true,
presets: ["@babel/preset-env"],
},
main: {
files: {
"dist/js/app.js": "src/js/app.js",
},
},
},
});

// grunt.loadNpmTasks('grunt-sass')
loadGruntTasks(grunt); // 自动加载所有的 grunt 插件中的任务
};
  • grunt-contrib-watch 自动编译
    • 安装 yarn add grunt-contrib-watch --dev
    • 执行 yarn grunt watch,当监听的文件发生变化的时候会执行相应的任务,但是刚开始的时候不会执行,可以使用 grunt.registerTask() 方法,执行yarn grunt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
const sass = require("sass");
const loadGruntTasks = require("load-grunt-tasks");

module.exports = (grunt) => {
grunt.initConfig({
sass: {
options: {
implementation: sass,
},
main: {
// 目标main(随便写)
files: {
"dist/css/main.css": "src/scss/main.scss", // 键是输出文件,值是输入文件
},
},
},
babel: {
options: {
sourceMap: true,
presets: ["@babel/preset-env"],
},
main: {
files: {
"dist/js/app.js": "src/js/app.js",
},
},
},
watch: {
js: {
files: ["src/js/*.js"], // 监听的文件
tasks: ["babel"], // 当文件发生改变要执行什么任务
},
css: {
files: ["src/scss/*.scss"], // 监听的文件
tasks: ["sass"], // 当文件发生改变要执行什么任务
},
},
});

// grunt.loadNpmTasks('grunt-sass')
loadGruntTasks(grunt); // 自动加载所有的 grunt 插件中的任务
grunt.registerTask("default", ["sass", "babel", "watch"]);
};

Gulp

Gulp 的基本使用

  • 执行 yarn init -y
  • 安装 yarn add gulp --dev
  • 在根目录新建 gulpfile.js,此文件作为 gulp 入口文件
  • 导出函数,导出的函数都会作为 gulp 任务
  • gulp 的任务函数都是异步的,可以通过调用回调函数标识任务完成
  • default 是默认任务,在运行是可以省略任务名参数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// gulp入口文件

// 导出的函数都会作为 gulp 任务
exports.foo = (done) => {
// gulp 的任务函数都是异步的
// 可以通过调用回调函数标识任务完成
console.log("foo task working~");
done(); // 标识任务执行完成
};

// default 是默认任务
// 在运行是可以省略任务名参数
exports.default = (done) => {
console.log("default task working~");
done();
};

Gulp 的组合任务

series(),parallel() 可以创建并行任务和串行任务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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); // // 让多个任务按照顺序依次执行
exports.bar = parallel(task1, task2, task3); // // 让多个任务同时执行

Gulp 的异步任务

  • 回调函数
    • gulp 的任务为异步,异步函数无法知道是否执行完成,可以通过回调解决(done),任务执行完后调用回调函数通知 gulp 任务执行完成
    • 回调函数叫做一种错误优先的回调函数,在执行过程中报错,阻止剩下任务执行可以通过给回调函数的第一个参数指定一个错误对象
1
2
3
4
exports.callback_error = (done) => {
console.log("callback task~");
done(new Error("task failed!"));
};
  • promise
    • 使用 promise 通知任务执行完成
    • 任务中返回一个 Promise 对象,如果 resolve 意味着任务结束了,不需要返回任何的值,如果 reject,gulp 会认为是一个失败任务结束后续任务的执行
1
2
3
4
5
6
7
8
exports.promise = () => {
console.log("promise task~");
return Promise.resolve();
};
exports.promise = () => {
console.log("promise task~");
return Promise.reject(new Error("task failed~"));
};
  • async await
1
2
3
4
5
6
7
8
9
const timeout = (time) => {
return new Promise((resolve) => {
setTimeout(resolve, time);
});
};
exports.async = async () => {
await timeout(1000);
console.log("async task~");
};
  • stream
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// exports.stream = () => {
// const readStream = fs.createReadStream('package.json')
// const writeStream = fs.createWriteStream('temp.txt')
// readStream.pipe(writeStream)
// return readStream
// }
exports.stream = (done) => {
const readStream = fs.createReadStream("package.json");
const writeStream = fs.createWriteStream("temp.txt");
readStream.pipe(writeStream);
readStream.on("end", () => {
done();
});
};

Gulp 构建过程核心工作原理

  • 工作过程:输入(读取流) => 加工(转换流) => 输出(写入流)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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 input = chunk.toString();
// 替换空格、注释(压缩代码)
const output = input.replace(/\s+/g, "").replace(/\/\*.+?\*\//g, "");
// 把内容传出
callback(null, output); // 错误优先的回调函数
},
});

// 把读取出来的文件流导入写入文件流
read
.pipe(transform) // 转换
.pipe(write); // 写入
return read;
};

Gulp 文件操作 API

  • src:读取流,方法参数为文件路径,可以使用通配符匹配所以的文件
  • dest:写入流
1
2
3
4
5
6
7
8
9
10
11
const { src, dest } = require("gulp");
const cleanCss = require("gulp-clean-css"); // 压缩代码转换流模块
const rename = require("gulp-rename"); // 重命名扩展名模块

exports.default = () => {
// gulp读取流 和 写入流,可以使用统配符
return src("src/*.css")
.pipe(cleanCss())
.pipe(rename({ extname: ".min.css" })) // 指定重命名的扩展名
.pipe(dest("dist"));
};

Gulp 案例 - 样式编译

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const { src, dest } = require("gulp");

// 该依赖会把下划线文件名的文件认为是主文件中依赖的文件,所以不会吧下划线文件名的文件转换
const sass = require("gulp-sass");

const style = () => {
// base 基准路径,会把src后面的路径保留下来,否则不会按照原本路径输出
return src("src/assets/styles/*.scss", { base: "src" })
.pipe(sass({ outputStyle: "expanded" })) // outputStyle编译后“}”位于后面
.pipe(dest("dist"));
};

module.exports = {
style,
};

Gulp 案例 - 脚本编译

安装 babel 依赖 yarn add babel @babel/core @babel/preset-env --dev

1
2
3
4
5
6
7
8
9
10
11
12
13
const { src, dest } = require("gulp");
const babel = require("gulp-babel");

const script = () => {
// base 基准路径,会把src后面的路径保留下来,否则不会按照原本路径输出
return src("src/assets/scripts/*.js", { base: "src" })
.pipe(babel({ presets: ["@babel/preset-env"] })) // 如果不添加presets转换无效
.pipe(dest("dist"));
};

module.exports = {
script,
};

Gulp 案例 - 页面模板编译

安装 swig 模板引擎转换插件 yarn add gulp-swig --dev

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
const swig = require("gulp-swig");

const data = {
menus: [
{
name: "Home",
icon: "aperture",
link: "index.html",
},
{
name: "Features",
link: "features.html",
},
{
name: "About",
link: "about.html",
},
{
name: "Contact",
link: "#",
children: [
{
name: "Twitter",
link: "https://twitter.com/w_zce",
},
{
name: "About",
link: "https://weibo.com/zceme",
},
{
name: "divider",
},
{
name: "About",
link: "https://github.com/zce",
},
],
},
],
pkg: require("./package.json"),
date: new Date(),
};
const page = () => {
// 如果存在子目录:'src/**/*.html'
return (
src("src/*.html", { base: "src" })
// data 把数据传入到模板中,渲染对应的数据
// cache 防止模板缓存导致页面不能及时更新
.pipe(swig({ data, defaults: { cache: false } }))
.pipe(dest("dist"))
);
};
module.exports = {
page,
};

Gulp 案例 - 图片和字体文件转换

安装图片压缩转换插件 yarn add gulp-imagemin --dev

1
2
3
4
5
6
7
8
9
10
11
12
13
const imagemin = require("gulp-imagemin");

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"));
};

Gulp 案例 - 其他文件及文件清除

打包是自动删除 dist 文件目录,安装插件 yarn add del --dev,这个模块不是 gulp 的插件,只是可以在 gulp 中使用

1
2
3
4
5
6
7
8
9
10
const del = require("del");

const extra = () => {
// 转换其他文件
return src("public/**", { base: "public" }).pipe(dest("dist"));
};

const clean = () => {
return del(["dist"]);
};

Gulp 案例 - 自动加载插件

安装插件 yarn add gulp-load-plugins --dev

1
2
3
4
5
6
7
8
9
10
const loadPlugins = require("gulp-load-plugins");
// 自动加载所有的插件
const plugins = loadPlugins(); // 把‘gulp-’去掉,如果后面有多个‘-’会变为驼峰命名

const style = () => {
// base 基准路径,会把src后面的路径保留下来,否则不会按照原本路径输出
return src("src/assets/styles/*.scss", { base: "src" })
.pipe(plugins.sass({ outputStyle: "expanded" })) // 使用plugins.sass
.pipe(dest("dist"));
};

Gulp 案例 - 开发服务器

热更新开发服务器
安装模块 yarn add browser-sync --dev

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const browserSync = require("browser-sync");
// 创建服务器
const bs = browserSync.create();
// 服务任务
// 此时dist文件目录的样式指向node_modules,需要修改配置
const serve = () => {
bs.init({
notify: false, // 关闭notify提示
port: 8080, // 端口
// open: false, // 是否自动打开浏览器
files: "dist/**", // browserSync服务监听的路径通配符,文件发生改变自动刷新浏览器
server: {
baseDir: "dist",
routes: {
// 优先于baseDir,请求发生后会先看routes是否有对应的配置,如果有就走routes配置,否则就走baseDir对应的文件
"/node_modules": "node_modules", // 键是请求的前缀
},
},
});
};

Gulp 案例 - 监视变化以及构建优化

使用 gulp 提供的 watch()方法,第一个参数是监听的文件,第二个参数是执行任务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
const { watch } = require("gulp");
// 服务任务
// 此时dist文件目录的样式指向node_modules,使用路由映射
const serve = () => {
watch("src/assets/styles/*.scss", style);
watch("src/assets/scripts/*.js", script);
watch("src/*.html", page);

// 开发阶段对图片、字体不构建,提高构建效率
// watch('src/assets/images/**', image)
// watch('src/assets/fonts/**', font)
// watch('public/**', extra)

// 文件变化后自动更新浏览器,浏览器重新发起请求
watch(
["src/assets/images/**", "src/assets/fonts/**", "public/**"],
bs.reload
);

bs.init({
notify: false, // 关闭notify提示
port: 8080, // 端口
// open: false, // 是否自动打开浏览器
files: "dist/**", // browserSync服务监听的路径通配符,文件发生改变自动刷新浏览器
server: {
baseDir: ["dist", "src", "public"], // 如果资源在dist找不到就到src找,还找不到就到public找
routes: {
// 优先于baseDir,请求发生后会先看routes是否有对应的配置,如果有就走routes配置,否则就走baseDir对应的文件
"/node_modules": "node_modules", // 键是请求的前缀
},
},
});
};

Gulp 案例 - useref 文件引用处理

安装 yarn add gulp-useref --dev

把 HTML 文件中的构建注释去除、把构建注释中的内容合并到一个文件(比如上边提到的引用了 node_modules 目录中的文件)

1
2
3
4
5
6
7
8
const useref = () => {
return (
src("dist/*.html", { base: "dist" })
// 去除HTML文件构建注释、把构建注释的内容合并到一个文件中
.pipe(plugins.useref({ searchPath: ["dist", "."] }))
.pipe(dest("dist"))
);
};

Gulp 案例 - 文件压缩

对 html、js、css 压缩

安装插件 yarn add gulp-htmlmin gulp-uglify gulp-clean-css --dev

判断读取流的文件使用对应的压缩插件,安装 yarn add gulp-if --dev

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const useref = () => {
return (
src("dist/*.html", { base: "dist" })
// 去除HTML文件构建注释、把构建注释的内容合并到一个文件中
.pipe(plugins.useref({ searchPath: ["dist", "."] }))
.pipe(plugins.if(/\.js$/, plugins.uglify())) // 压缩js
.pipe(plugins.if(/\.css$/, plugins.cleanCss())) // 压缩css
.pipe(
plugins.if(
/\.html$/,
plugins.htmlmin({
collapseWhitespace: true,
minifyCSS: true, // 把html文件中的style压缩
minifyJS: true, // 把script中的js压缩
})
)
) // 压缩html
.pipe(dest("release"))
); // 写入到另外一个文件,防止读、写同一个文件引发错乱
};

Gulp 案例 - 重新规划构建过程

因为 useref 任务导致项目目录结构发生改变,所以构建时先把 html、css、js 文件放到一个临时目录中,开启的服务读取文件也是读取临时目录中的文件;在 useref 任务中把临时目录中的 html、css、js 取出压缩放进最终目录(dist)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
/ 转换样式
const style = () => {
// base 基准路径,会把src后面的路径保留下来,否则不会按照原本路径输出
return src('src/assets/styles/*.scss', { base: 'src' })
.pipe(plugins.sass({ outputStyle: 'expanded' })) // outputStyle编译后“}”位于后面
.pipe(dest('temp'))
.page(bs.reload({ stream: true })) // 把信息以流的方法推到浏览器
}

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

// 转换模板
const page = () => {
// 如果存在子目录:'src/**/*.html'
return src('src/*.html', { base: 'src' })
// data 把数据传入到模板中,渲染对应的数据
// cache 防止模板缓存导致页面不能及时更新
.pipe(plugins.swig({ data, defaults: { cache: false } }))
.pipe(dest('temp'))
.page(bs.reload({ stream: true }))
}

const useref = () => {
return src('temp/*.html', { base: 'temp' })
// 去除HTML文件构建注释、把构建注释的内容合并到一个文件中
.pipe(plugins.useref({ searchPath: ['temp', '.'] }))
.pipe(plugins.if(/\.js$/, plugins.uglify())) // 压缩js
.pipe(plugins.if(/\.css$/, plugins.cleanCss())) // 压缩css
.pipe(plugins.if(/\.html$/, plugins.htmlmin({
collapseWhitespace: true,
minifyCSS: true, // 把html文件中的style压缩
minifyJS: true // 把script中的js压缩
}))) // 压缩html
.pipe(dest('dist')) // 写入到另外一个文件,防止读、写同一个文件引发错乱
}

完整构建

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
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(); // 把‘gulp-’去掉,如果后面有多个‘-’会变为驼峰命名

// 创建服务器
const bs = browserSync.create();

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"]);
};

const style = () => {
// base 基准路径,会把src后面的路径保留下来,否则不会按照原本路径输出
return src("src/assets/styles/*.scss", { base: "src" })
.pipe(plugins.sass({ outputStyle: "expanded" })) // outputStyle编译后“}”位于后面
.pipe(dest("temp"))
.pipe(bs.reload({ stream: true })); // 把信息以流的方法推到浏览器
};

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 }));
};

const page = () => {
// 如果存在子目录:'src/**/*.html'
return (
src("src/*.html", { base: "src" })
// data 把数据传入到模板中,渲染对应的数据
// cache 防止模板缓存导致页面不能及时更新
.pipe(plugins.swig({ data, defaults: { cache: false } })) // 防止模板缓存导致页面不能及时更新
.pipe(dest("temp"))
.pipe(bs.reload({ stream: true }))
);
};

// 压缩图片
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 extra = () => {
// 转换其他文件
return src("public/**", { base: "public" }).pipe(dest("dist"));
};

// 服务任务
// 此时dist文件目录的样式指向node_modules,需要修改配置
const serve = () => {
watch("src/assets/styles/*.scss", style);
watch("src/assets/scripts/*.js", script);
watch("src/*.html", page);

// 开发阶段对图片、字体不构建,提高构建效率
// watch('src/assets/images/**', image)
// watch('src/assets/fonts/**', font)
// watch('public/**', extra)

// 文件变化后自动更新浏览器,浏览器重新发起请求
watch(
["src/assets/images/**", "src/assets/fonts/**", "public/**"],
bs.reload
);

bs.init({
notify: false, // 关闭notify提示
port: 8080, // 端口
// open: false, // 是否自动打开浏览器
files: "dist/**", // browserSync服务监听的路径通配符,文件发生改变自动刷新浏览器
server: {
baseDir: ["temp", "src", "public"], // 如果资源在dist找不到就到src找,还找不到就到public找
routes: {
// 优先于baseDir,请求发生后会先看routes是否有对应的配置,如果有就走routes配置,否则就走baseDir对应的文件
"/node_modules": "node_modules", // 键是请求的前缀
},
},
});
};

const useref = () => {
return (
src("temp/*.html", { base: "temp" })
// 去除HTML文件构建注释、把构建注释的内容合并到一个文件中
.pipe(plugins.useref({ searchPath: ["temp", "."] }))
.pipe(plugins.if(/\.js$/, plugins.uglify())) // 压缩js
.pipe(plugins.if(/\.css$/, plugins.cleanCss())) // 压缩css
.pipe(
plugins.if(
/\.html$/,
plugins.htmlmin({
collapseWhitespace: true,
minifyCSS: true, // 把html文件中的style压缩
minifyJS: true, // 把script中的js压缩
})
)
) // 压缩html
.pipe(dest("dist"))
); // 写入到另外一个文件,防止读、写同一个文件引发错乱
};

// 并行执行任务
const compile = parallel(style, script, page);

// 串行执行任务,上线前执行的任务
const build = series(
clean,
parallel(series(compile, useref), image, font, extra)
);

const develop = series(compile, serve);

module.exports = {
clean,
build,
develop,
};

Gulp 案例 - 补充

  • 不需要全部任务都导出,只需要导出部分用到的就行
  • 把任务写入到 package.json 文件中的 scripts 属性
1
2
3
4
5
"scripts": {
"clean": "gulp clean",
"build": "gulp build",
"dev": "gulp develop"
},

封装工作流 - 提取 gulpfile

  • 把 gulp 构建文件封装打包为模块发布到 npm
  • 如果模块还在本地没有发布到 npm 可以使用 yarn link 的方式把构建模块 link 到全局,然后在项目中 yarn link zce-pages (脚手架名) 的方式安装进项目(相当于 yarn add xxx)
  • 如果有不应该提取到 gulp 构建模块的内容,应把它们抽离到项目配置文件(约定文件名),然后 gulp 构建模块读取该配置文件
    • 在 gulp 构建模块中获取抽离的配置,process.cwd()返回当前命令行所在的工作目录
  • 如果报 babel 相关的错误,如@babel/preset-env,因为 gulp 构建模块使用 babel 转换语法,@babel/preset-env 会查找根目录的 node_modules 目录中的@babel/preset-env 模块,所以就报错;可以在 gulp 构建模块 javascript 任务的 babel 修改为 {presets: [require('@babel/preset-env')]}
  • 如果是通过 link 到全局的话,在构建项目时会报 gulp gulp-cli 相关的错误,是因为构建时在 node_modules 找不到 gulp 相关的命令,这时可以先在项目中安装依赖 yarn add gulp gulp-cli --dev
  • 在入口文件 gulpfile.js 导入 gulp 构建模块
1
module.exports = require("zce-pages");

封装工作流 - 抽象路径配置

封装的 gulp 构建模块中任务使用的路径应该可配置,使任务中的文件路径可根据开发者配置
模块中默认路径配置;在开发项目中也可以传入该配置,让封装的 gulp 构建模块灵活

1
2
3
4
5
6
7
8
9
10
11
12
13
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/**'
}
}

封装工作流 - 包装 Gulp CLI

在 gulp 构建模块添加一个 cli,在 cli 里自动传递参数(参数:–gulpfile): yarn gulp build --gulpfile,然后在内部调用 gulp 提供的可执行程序

  • 在模块根目录新建目录 bin,在目录下新建 js 文件作为 cli 的入口,入口文件的文件头需要添加 #!/usr/bin/env node
  • 在 package.json 文件中添加‘bin’字段,值为该入口文件路径
  • 把 gulp 的调用和传入的参数放在该文件中
  • 在 window 系统执行 gulp 构建命令,会去 node_modules 下的.bin 目录下的 gulp.cmd 文件,如下代码;在该文件中根据判断执行当前目录外面的 gulp-cli/bin/gulp.js,该文件就 require ('gulp-cli')(),所以我们只需在入口文件中引入这个 gulp.js: require('gulp/bin/gulp')
1
2
3
4
5
6
7
8
// %~dp0: 当前目录
@IF EXIST "%~dp0\node.exe" (
"%~dp0\node.exe" "%~dp0\..\gulp-cli\bin\gulp.js" %*
) ELSE (
@SETLOCAL
@SET PATHEXT=%PATHEXT:;.JS;=;%
node "%~dp0\..\gulp-cli\bin\gulp.js" %*
)
  • 接下来要指定 gulpfile 和 cmd 路径,命令行传递的产生可以通过process.argv获取,该属性返回一个数组
  • 从中可以看出gulp-cli是通过process.argv拿到参数的,可以在代码运行前 push 需要传递的参数
1
2
3
4
5
6
7
8
9
10
11
12
13
// 入口文件

// 参数cwd:告诉gulp工作目录是命令行所在的目录
process.argv.push("--cwd");
process.argv.push(process.cwd());

// gulpfile:告诉gulp gulpfile的目录
process.argv.push("--gulpfile");
process.argv.push(require.resolve(".."));
// require:载入模块
// resolve:找到模块所对应的路径;这里传递‘..’它会找到package.json中的bin字段对应的文件

require("gulp/bin/gulp");

这时把该构建模块安装在项目,项目中就不在需要 gulpfile.js 文件

封装工作流 - 发布并使用模块

  • 在发布 npm 时会把项目根目录的文件和 package.json 中的 files 字段中的目录发布到 npm 仓库,所以要在 files 字段添加一个 cli 入口目录 bin,然后yarn publish --registry https ://registry.yarnpkg.com 推到 npm 仓库
  • 在新项目中安装发布的模块,如果发布时间和安装时间间距短可能会安装到的是老版本,因为国能的镜像同步 npm 原地址需要时间,可以在淘宝镜像中搜索发布的模块,然后点击 SYNC
  • 在项目中执行 yarn zce-pages build就可以构建项目了,zce-pages 是发布到 npm 的模块名
  • 也可以在项目的 package.json 文件中的 scripts 字段添加命令
1
2
3
4
5
"scripts": {
"clean": "zce-pages clean",
"build": "zce-pages build",
"develop": "zce-pages develop"
}