自动化构建
自动化构建
什么是自动化构建
- 自动化就是让电脑帮你干活
- 不管是代码压缩还是 less 转换,通过手动方式进行工作量巨大(例如手动压缩2000行代码,估计程序员就疯了)。
- 构建就是转换,将开发代码转换成生产环境能够运行的代码
- 自动化构建是指将手动构建任务,进行排列组合,然后通过命令(或工具)自动执行的过程。
- 实现自动化构建最简单的方式是 npm scripts (npm 脚本)。
为什么需要自动化构建
- 代码需要编译( CSS3,ES6+ ), 保证浏览器的兼容性
- 代码需要压缩( CSS,JS,HTML,图片等 )。节省带宽,提高加载速度
- 代码需要做格式化校验,统一代码风格。
NPM Scripts
定义一些与项目有关的脚本命令
多人项目开发时,别人也可以直接使用你定义的脚本
是实现自动化构建工作流的最简方式
简单例子
- 将 scss 语法转换成 css 语法
初始化一个新项目,并安装 sass 开发依赖
yarn init yarn add sass -D
直接使用 sass 命令转换
./node_modules/.bin/sass scss/index.scss style/index.css
将以上脚本命令写入
npm scripts
中{ "name": "sass-tanfer", "version": "1.0.0", "main": "index.js", "license": "MIT", "devDependencies": { "sass": "^1.54.9" }, "scripts": { "build": "sass scss/index.scss style/index.css" } }
终端中输入以下代码可以达到同样的效果
## 等同于./node_modules/.bin/sass scss/index.scss style/index.css yarn build
npm scripts任务的执行方式
有两种执行方式:串行和并行
串行执行:任务之间有明确的先后顺序,必须先执行前一个任务,然后才能执行下一个任务
- 相当于 4X100 接力,拿到上一个队员的接力棒后,下一个队员才能继续跑
并行执行:任务之间没有明确的先后顺序,同时执行,可以提高效率
- 相当于 100 米短跑,8个人同时起跑
具体代码
- 并行用
&
,串行用&&
{
"scripts": {
// 并行
"parallel": "node task1.js & node task2.js & node task3.js",
// 串行
"series": "node task1.js && node task2.js && node task3.js"
}
}
windows 下兼容性问题处理
- & 符号在 Windows 操作系统下不起作用。 此时,我们可以借助插件 npm-run-all,在 Windows 下实现并行
## 先在项目中安装
npm i npm-run-all -D
## 并行执行:其中 p 是 parallel(并行)的意思
npm-run-all -p 脚本1 脚本2 脚本3
## 或简写为
run-p 脚本1 脚本2 脚本3
## 串行执行:其中 s 是 series(串行)的意思
npm-run-all -s 脚本1 脚本2 脚本3
## 或简写为
run-s 脚本1 脚本2 脚本3
示例
{
"name": "sass-tanfer",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"devDependencies": {
"browser-sync": "^2.27.10",
"npm-run-all": "^4.1.5",
"sass": "^1.54.9"
},
"scripts": {
// --watch 监听 scss 文件的变化,自动编译
"build": "sass scss/index.scss style/index.css --watch",
// --files 监听 css 文件的变化,通知浏览器刷新页面
"serve": "browser-sync . --files \"**/*.css\"",
// 并行执行
"start": "run-p build serve"
}
}
常用的自动化构建工具
npm scripts 只适用于简单的工作流程,想要创建更复杂的工作流需要更加专业的构建工具:比如:grunt、gulp、fis
Grunt 是最早的自动化构建工具之一,生态系统比较完善。但是工作过程是基于临时文件来实现的,每一步都有磁盘读写操作,构建速度相对较慢,目前已经退出历史舞台。
Gulp 是基于内存来实现的,想对于磁盘读写会快很多。并且支持同时执行多个任务,构建效率大大提高。使用方式相对于 grunt 更加操作易懂,生态系统也比较完善,是当下最流行的自动化构建工具。
fis 是百度团队开发的一款构建工具,封装度较高,灵活度较差。
Grunt
初体验
初始化一个新项目,并安装 grunt 开发依赖
## 项目初始化 yarn init ## 安装开发依赖 yarn add grunt -D
在根目录下新建 gruntfile.js 文件
module.exports = (grunt) => { grunt.registerTask('test',() => { console.log('GodX------>log hello grunt!'); }) }
终端输入以下命令(也可以全局安装 grunt-cli 运行任务)
yarn grunt test ## 全局安装 grunt-cli npm i grunt-cli -g ## 全局安装后,直接使用 grunt 命令 grunt test
不出意外的话,控制台会打印以下结果
initConfig配置选项
- grunt.initConfig() 用于为任务添加一些配置选项
- 键一般对应任务的名称
- 值可以是任意类型的数据
- 任务中可以使用 grunt.config() 获取配置
- 如果属性值是对象的话,config 中可以使用点的方式定位对象中属性的值
module.exports = grunt => {
grunt.initConfig({
foo: {
bar: 'baz'
}
})
grunt.registerTask('foo', () => {
// 任务中可以使用 grunt.config() 获取配置
console.log(grunt.config('foo'))
// 如果属性值是对象的话,config 中可以使用点的方式定位对象中属性的值
console.log(grunt.config('foo.bar'))
})
}

默认任务
- 第二个参数可以指定此任务的映射任务,
- 这样执行 default 就相当于执行对应的任务
- 这里映射的任务会从左往右顺序依次执行,不会同步执行
module.exports = (grunt) => {
grunt.registerTask('default', ['foo', 'bar'])
}
任务的第二个参数
任务的第二个参数为字符串时,会被 grunt 视为任务描述,可以通过 grunt –help 查看,没有默认是 custom task
module.exports = (grunt) => {
grunt.registerTask('test',() => {
console.log('GodX------>log hello grunt!');
})
grunt.registerTask('test2',() => {
console.log('GodX------>log test2');
})
grunt.registerTask('default','xxxxxx',['test', 'test2']);
}

任务中执行其他任务
- test2和 test 会在当前任务执行完成过后自动依次执行
module.exports = (grunt) => {
grunt.registerTask('test',() => {
console.log('GodX------>log hello grunt!');
})
grunt.registerTask('test2',() => {
console.log('GodX------>log test2');
})
grunt.registerTask('other-task',() => {
grunt.task.run('test2','test');
console.log('GodX------>logother',);
})
}

异步任务的编写方式
- grunt 默认采用同步模式编码
- 如果需要异步可以使用 this.async() 方法创建回调函数,告诉 grunt 异步任务已结束
module.exports = (grunt) => {
grunt.registerTask("async-task", function () {
const done = this.async();
setTimeout(() => {
console.log("GodX------>logasync-task");
done();
}, 1000);
});
};
标记任务失败
同步任务中使用 return false 来标记任务失败,后续的任务将不会被执行
module.exports = grunt => { grunt.registerTask('bad', () => { console.log('GodX------>logbad'); return false; }) grunt.registerTask('a', () => { console.log('GodX------>loga'); return false; }) grunt.registerTask('b', () => { console.log('GodX------>logb'); return false; }) grunt.registerTask('default', ['a','bad','b']) }
image-20220917094255001 如果想要后续命令依然被执行,可以使用 –force 参数强制执行
module.exports = grunt => { grunt.registerTask('bad', () => { console.log('GodX------>logbad'); return false; }) grunt.registerTask('a', () => { console.log('GodX------>loga'); return false; }) grunt.registerTask('b', () => { console.log('GodX------>logb'); return false; }) grunt.registerTask('default', ['a','bad','b']) }
image-20220917094223435 异步任务在回调中传入
false
标记任务失败module.exports = (grunt) => { grunt.registerTask("async-task", function () { const done = this.async(); setTimeout(() => { console.log("GodX------>logasync-task"); done(false); }, 1000); }); };
image-20220917094520253
多目标任务
多目标模式,可以让任务根据配置形成多个子任务
module.exports = grunt => {
grunt.initConfig({
build: {
foo: 100,
bar: '456'
}
})
grunt.registerMultiTask('build', function () {
console.log(`task: build, target: ${this.target}, data: ${this.data}`)
})
}

可以使用 :指定特定的子任务

通过 target 、data 属性和 options 方法获取任务相关信息
子任务中配置会覆盖 intConfig 中的配置
module.exports = grunt => {
grunt.initConfig({
build: {
options: {
msg: 'task options'
},
foo: {
options: {
msg: 'foo target options'
},
},
bar: '456'
}
})
grunt.registerMultiTask('build', function () {
console.log(`任务名称:${this.target};任务数据:${this.data};任务配置:${this.options().msg}`)
})
}

插件的使用
这里使用 clean 插件来示例
安装对应依赖
yarn add grunt-contrib-clean
根据文档编写
gruntfile.js
文件module.exports = (grunt) => { grunt.initConfig({ clean: { temp: ["temp2/*.txt","temp"] } }); grunt.loadNpmTasks("grunt-contrib-clean"); };
运行任务使用插件
grunt clean
Grunt 常用插件总结
grunt-sass
将 sass 语法 编译成 css
安装依赖
yarn add dart-sass grunt-sass -D
编写 gruntfile 文件
const sass = require("dart-sass"); module.exports = (grunt) => { grunt.initConfig({ sass: { // sass 是任务名称,为了匹配任务配置 options: { implementation: sass, // 需要借助的依赖 sourceMap: true // 是否显示sourceMap文件 }, dist: { files: { "dist/style/index.css": "src/index.scss" // 输出路径:需要处理的文件 } } } }); grunt.loadNpmTasks("grunt-sass"); // 加载 npm 任务 };
运行以下命令查看效果
grunt sass
grunt-babel
Babel 是一个工具链,主要用于在旧的浏览器或环境中将 ECMAScript 2015+ 代码转换为向后兼容版本的 JavaScript 代码
- 安装依赖
yarn add @babel/core @babel/preset-env grunt-babel -D
2. 编写 gruntfile 文件
```js
const sass = require("dart-sass");
const loadGruntTasks = require("load-grunt-tasks");
module.exports = (grunt) => {
grunt.initConfig({
sass: {
options: {
implementation: sass,
sourceMap: true
},
dist: {
files: {
"dist/style/index.css": "src/index.scss"
}
}
},
babel: {
options: {
sourceMap: true,
presets: ["@babel/preset-env"]
},
main: {
files: {
"dist/js/app.js": "src/js/app.js"
}
}
}
});
grunt.loadNpmTasks("grunt-sass");
grunt.loadNpmTasks("grunt-babel");
};
- 运行以下命令查看效果
grunt babel
**load-grunt-tasks**
*该插件会自动帮你注册任务,无需一个一个的手动注册*
1. 安装依赖
```bash
yarn add load-grunt-tasks -D
- 编写
gruntfile
文件const sass = require("dart-sass"); const loadGruntTasks = require("load-grunt-tasks"); module.exports = (grunt) => { grunt.initConfig({ sass: { options: { implementation: sass, sourceMap: true }, dist: { files: { "dist/style/index.css": "src/index.scss" } } }, babel: { options: { sourceMap: true, presets: ["@babel/preset-env"] }, main: { files: { "dist/js/app.js": "src/js/app.js" } } } }); // grunt.loadNpmTasks("grunt-sass"); // grunt.loadNpmTasks("grunt-babel"); // 自动加载所有的 grunt 插件中的任务 loadGruntTasks(grunt); };
grunt-contrib-watch
该插件会帮你监听文件的变化,自动执行相应的任务
- 安装依赖
yarn add grunt-contrib-watch -D
- 编写 gruntfile 文件
const sass = require("dart-sass");
const loadGruntTasks = require("load-grunt-tasks");
module.exports = (grunt) => {
grunt.initConfig({
sass: {
options: {
implementation: sass,
sourceMap: true
},
dist: {
files: {
"dist/style/index.css": "src/index.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/index.scss",
tasks: ["sass"]
}
}
});
// grunt.loadNpmTasks("grunt-sass");
// grunt.loadNpmTasks("grunt-babel");
loadGruntTasks(grunt);
grunt.registerTask("default", ["sass", "babel", "watch"]);
};
Gulp
当下最流行的模块构建系统
构建过程核心工作原理(原生nodejs)
const fs = require("fs");
const { Transform } = require("stream");
exports.default = () => {
// 文件读取流
const readStream = fs.createReadStream("normalize.css");
// 文件转换流
const tranformStream = new Transform({
// 核心转换过程
transform: (chunk, encoding, callback) => {
// 获取文件原始内容
const input = chunk.toString();
// 通过正则匹配去除空格和注释
const output = input.replace(/\s+/g, "").replace(/\/\*.+?\*\//g, "");
callback(null, output);
}
});
// 文件写入流
const writeStream = fs.createWriteStream("normalize.min.css");
return readStream.pipe(tranformStream).pipe(writeStream);
};
gulp 的基本使用
gulp 帮我们封装了 nodejs 的文件操作,而且增加了通配符匹配功能,同时提供了转换流的插件,使得文件操作更加强大、简单
简单示例
// 获取写入流和输出流方法
const { src, dest } = require("gulp");
// 引入 css 插件
const cleanCSS = require("gulp-clean-css");
// 引入文件重命名 插件
const rename = require("gulp-rename");
exports.default = () => {
return src("src/*.css")
.pipe(cleanCSS())
.pipe(rename({ extname: ".min.css" }))
.pipe(dest("dist"));
};
样式编译
- gulp-sass 目前不在默认下载node-sass,需要手动指定
- src 中的 base 选项指定基础路径,这会保留原始的目录结构
const { src, dest } = require("gulp");
const sass = require("gulp-sass")(require("dart-sass"));
const style = () => {
return src("src/assets/styles/*.scss", { base: "src" }).pipe(sass()).pipe(dest("dist"));
};
module.exports = {
style
};
脚本编译
const { src, dest } = require("gulp");
const sass = require("gulp-sass")(require("dart-sass"));
const babel = require('gulp-babel');
const style = () => {
return src("src/assets/styles/*.scss", { base: "src" }).pipe(sass()).pipe(dest("dist"));
};
const script = () => {
return src("src/assets/scripts/*.js", { base: "src" })
.pipe(
babel({
// babel 插件,将 es6 的一些最新特性转换
presets: ["@babel/preset-env"]
})
)
.pipe(dest("dist"));
};
module.exports = {
style,
script
};
页面模版编译
- 这里用的是 swip 模版语法,所以使用 gulp-swip 插件编译
const { src, dest } = require("gulp");
const sass = require("gulp-sass")(require("dart-sass"));
const babel = require("gulp-babel");
const swig = require("gulp-swig");
// 插值数据
const data = {
menus: [
{
name: "Home",
icon: "aperture",
link: "index.html"
},
{
name: "Features",
link: "features.html"
},
{
name: "About",
link: "about.html"
},
{
name: "Contact",
link: "#",
children: [
{
name: "Twitter",
link: "https://twitter.com/w_zce"
},
{
name: "About",
link: "https://weibo.com/zceme"
},
{
name: "divider"
},
{
name: "About",
link: "https://github.com/zce"
}
]
}
],
pkg: require("./package.json"),
date: new Date()
};
const style = () => {
return src("src/assets/styles/*.scss", { base: "src" }).pipe(sass()).pipe(dest("dist"));
};
const script = () => {
return src("src/assets/scripts/*.js", { base: "src" })
.pipe(
babel({
// babel 插件,将 es6 的一些最新特性转换
presets: ["@babel/preset-env"]
})
)
.pipe(dest("dist"));
};
const page = () => {
return src("src/**/*.html").pipe(swig({data})).pipe(dest("dist"));
};
module.exports = {
style,
script,
page
};
组合任务
将多个任务组合在一起
const { src, dest, parallel } = require("gulp");
const sass = require("gulp-sass")(require("dart-sass"));
const babel = require("gulp-babel");
const swig = require("gulp-swig");
// 插值数据
const data = {
menus: [
{
name: "Home",
icon: "aperture",
link: "index.html"
},
{
name: "Features",
link: "features.html"
},
{
name: "About",
link: "about.html"
},
{
name: "Contact",
link: "#",
children: [
{
name: "Twitter",
link: "https://twitter.com/w_zce"
},
{
name: "About",
link: "https://weibo.com/zceme"
},
{
name: "divider"
},
{
name: "About",
link: "https://github.com/zce"
}
]
}
],
pkg: require("./package.json"),
date: new Date()
};
const style = () => {
return src("src/assets/styles/*.scss", { base: "src" }).pipe(sass()).pipe(dest("dist"));
};
const script = () => {
return src("src/assets/scripts/*.js", { base: "src" })
.pipe(
babel({
// babel 插件,将 es6 的一些最新特性转换
presets: ["@babel/preset-env"]
})
)
.pipe(dest("dist"));
};
const page = () => {
return src("src/**/*.html").pipe(swig({ data })).pipe(dest("dist"));
};
// 组合任务
const compile = parallel(style, script, page);
module.exports = {
compile
};
图片和字体文件的转换
- 利用
gulp-imagemin
插件压缩图片和字体文件
注意
目前 gulp-imagemin
最新版本已经不支持 commonjs 语法,可以安装低版本使用 require 导入,例如 6.1.0
版本
const { src, dest, parallel } = require("gulp");
const sass = require("gulp-sass")(require("dart-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 style = () => {
return src("src/assets/styles/*.scss", { base: "src" }).pipe(sass()).pipe(dest("dist"));
};
const script = () => {
return src("src/assets/scripts/*.js", { base: "src" })
.pipe(
babel({
// babel 插件,将 es6 的一些最新特性转换
presets: ["@babel/preset-env"]
})
)
.pipe(dest("dist"));
};
const page = () => {
return src("src/**/*.html").pipe(swig({ data })).pipe(dest("dist"));
};
const image = () => {
return src("src/assets/images/**", { base: "src" }).pipe(imagemin()).pipe(dest("dist"));
};
const font = () => {
return src("src/assets/fonts/**", { base: "src" }).pipe(imagemin()).pipe(dest("dist"));
};
// 组合任务
const compile = parallel(style, script, page, image, font);
module.exports = {
compile
};
其他文件及文件清除
- 一些目录不需要编译,原样输出即可。比如 public、static
- 打包前,删除 dist 目录
const { src, dest, parallel, series } = require("gulp");
const del = require("del");
const sass = require("gulp-sass")(require("dart-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"]);
};
const style = () => {
return src("src/assets/styles/*.scss", { base: "src" }).pipe(sass()).pipe(dest("dist"));
};
const script = () => {
return src("src/assets/scripts/*.js", { base: "src" })
.pipe(
babel({
// babel 插件,将 es6 的一些最新特性转换
presets: ["@babel/preset-env"]
})
)
.pipe(dest("dist"));
};
const page = () => {
return src("src/**/*.html").pipe(swig({ data })).pipe(dest("dist"));
};
const image = () => {
return src("src/assets/images/**", { base: "src" }).pipe(imagemin()).pipe(dest("dist"));
};
const font = () => {
return src("src/assets/fonts/**", { base: "src" }).pipe(imagemin()).pipe(dest("dist"));
};
const 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
};
自动加载插件
使用gulp-load-plugins
插件自动加载所有 gulp 插件
const { src, dest, parallel, series } = require("gulp");
const del = require("del");
const loadPlugins = require("gulp-load-plugins");
const sass = require("gulp-sass")(require("dart-sass"));
const plugins = loadPlugins();
// 插值数据
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"]);
};
const style = () => {
return src("src/assets/styles/*.scss", { base: "src" }).pipe(sass()).pipe(dest("dist"));
};
const script = () => {
return src("src/assets/scripts/*.js", { base: "src" })
.pipe(
plugins.babel({
// babel 插件,将 es6 的一些最新特性转换
presets: ["@babel/preset-env"]
})
)
.pipe(dest("dist"));
};
const page = () => {
return src("src/**/*.html").pipe(plugins.swig({ data })).pipe(dest("dist"));
};
const image = () => {
return src("src/assets/images/**", { base: "src" }).pipe(plugins.imagemin()).pipe(dest("dist"));
};
const font = () => {
return src("src/assets/fonts/**", { base: "src" }).pipe(plugins.imagemin()).pipe(dest("dist"));
};
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
};
热更新开发服务器
- 修改代码,自动编译,提高开发效率
- 通过
browser-sync
插件将 dist 目录托管到文件服务器上
const { src, dest, parallel, series } = require("gulp");
const del = require("del");
const loadPlugins = require("gulp-load-plugins");
const sass = require("gulp-sass")(require("dart-sass"));
const plugins = loadPlugins();
const browserSync = require("browser-sync");
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"]);
};
const style = () => {
return src("src/assets/styles/*.scss", { base: "src" }).pipe(sass()).pipe(dest("dist"));
};
const script = () => {
return src("src/assets/scripts/*.js", { base: "src" })
.pipe(
plugins.babel({
// babel 插件,将 es6 的一些最新特性转换
presets: ["@babel/preset-env"]
})
)
.pipe(dest("dist"));
};
const page = () => {
return src("src/**/*.html").pipe(plugins.swig({ data })).pipe(dest("dist"));
};
const image = () => {
return src("src/assets/images/**", { base: "src" }).pipe(plugins.imagemin()).pipe(dest("dist"));
};
const font = () => {
return src("src/assets/fonts/**", { base: "src" }).pipe(plugins.imagemin()).pipe(dest("dist"));
};
const extra = () => {
return src("public/**", { base: "public" }).pipe(dest("dist"));
};
const server = () => {
bs.init({
port: 8090,
open: true,
notify: false, // 关闭右上角的提示
files: "dist/**",
server: {
baseDir: "dist", // 文件服务器基础路径
routes: { // 自配置优先于 baseDir 配置
"/node_modules": "./node_modules"
}
}
});
};
// 组合任务
const compile = parallel(style, script, page, image, font);
const build = series(clean, parallel(compile, extra));
module.exports = {
compile,
build,
server
};
监视变化已及构建过程优化
- 修改不同的文件,执行相对应的任务进行构建编译,然后通知浏览器刷新
- 开发环境下,不用编译图片、字体和public 目录下的文件,可以根据适当的配置优化
- 新增图片、字体等,需要重载服务器
- 可以用通过 pipe 推流的方式取代 browser-sync files配置监听
const { src, dest, parallel, series, watch } = require("gulp");
const del = require("del");
const loadPlugins = require("gulp-load-plugins");
const sass = require("gulp-sass")(require("dart-sass"));
const plugins = loadPlugins();
const browserSync = require("browser-sync");
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"]);
};
const style = () => {
return src("src/assets/styles/*.scss", { base: "src" })
.pipe(sass())
.pipe(dest("dist"))
.pipe(bs.reload({ stream: true }));
};
const script = () => {
return src("src/assets/scripts/*.js", { base: "src" })
.pipe(
plugins.babel({
// babel 插件,将 es6 的一些最新特性转换
presets: ["@babel/preset-env"]
})
)
.pipe(dest("dist"))
.pipe(bs.reload({ stream: true }));
};
const page = () => {
return src("src/**/*.html")
.pipe(plugins.swig({ data, defaults: { cache: false } })) //! 防止模板缓存导致页面不能及时更新
.pipe(dest("dist"))
.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"));
};
const server = () => {
watch("src/assets/styles/*.scss", style);
watch("src/assets/scripts/*.js", script);
watch("src/**/*.html", page);
//todo 新增文件时重载服务器
watch(["src/assets/images/**", "src/assets/fonts/**", "public/**"], bs.reload);
// todo 服务器初始化
bs.init({
port: 8090,
open: true,
notify: false, // 关闭右上角的提示
// files: "dist/**",
server: {
baseDir: "dist", // 文件服务器基础路径
routes: {
// 自配置优先于 baseDir 配置
"/node_modules": "./node_modules"
}
}
});
};
// 组合任务
const compile = parallel(style, script, page, image, font);
const build = series(clean, parallel(compile, extra));
module.exports = {
compile,
build,
server
};
Useref 文件引用处理
gulp-useref
这是一款可以将HTML引用的多个CSS和JS合并起来,减小依赖的文件个数,从而减少浏览器发起的请求次数。gulp-useref
根据注释将HTML中需要合并压缩的区块找出来,对区块内的所有文件进行合并。
基本使用
安装
gulp-useref
插件yarn add gulp-useref -D
在 html 文件中添加注释,例如:
<!-- build:js assets/scripts/vendor.js --> <script src="/node_modules/jquery/dist/jquery.js"></script> <script src="/node_modules/popper.js/dist/umd/popper.js"></script> <script src="/node_modules/bootstrap/dist/js/bootstrap.js"></script> <!-- endbuild --> <!-- build:js assets/scripts/main.js --> <script src="assets/scripts/main.js"></script> <!-- endbuild -->
编写
gulpfile
文件const useref = () => { return src("dist/**/*.html", { base: "dist" }) // 搜索的文件目录 .pipe(plugins.useref({ searchPath: ["dist", "."] })) .pipe(dest("dist")); };
注意
注意:gulp-useref
只负责合并,不负责压缩!如果需要做其他操作,可以配合gulp-if
插件使用
gulp-if 条件压缩
根据不同类型的文件流,采取不同的插件压缩
安装依赖
yarn add gulp-htmlmin gulp-clean-css gulp-uglify gulp-if -D
编写
gulpfile
文件const useref = () => { return src("dist/**/*.html", { base: "dist" }) .pipe(plugins.useref({ searchPath: ["dist", "."] })) .pipe(plugins.if(/\.js$/,plugins.uglify())) .pipe(plugins.if(/\.css$/,plugins.cleanCss())) .pipe(plugins.if(/\.html$/,plugins.htmlmin({ collapseWhitespace: true, minifyCSS: true, minifyJS: true }))) // 临时目录 .pipe(dest("temp")); };
注意
如果一个任务中对同一个目录下的文件同时读写,可能会导致文件的写入失败。所以推荐将文件流输出到一个临时的目录下
相关信息
gulp-if
插件的第一个参数为正则表达式,用来匹配对应文件流;第二个参数为执行插件的方法
重新规划构建过程
useref
打破了原本的目录结构(因将文件输出到了一个临时目录下)- 解决方案:将一些需要编译的任务的输出目录改为临时目录,然后将useref 的输出目录改为最终输出的目录(dist)
const { src, dest, parallel, series, watch } = require("gulp");
const del = require("del");
const loadPlugins = require("gulp-load-plugins");
const sass = require("gulp-sass")(require("dart-sass"));
const plugins = loadPlugins();
const browserSync = require("browser-sync");
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 = () => {
return src("src/assets/styles/*.scss", { base: "src" })
.pipe(sass())
.pipe(dest("temp"))
.pipe(bs.reload({ stream: true }));
};
const script = () => {
return src("src/assets/scripts/*.js", { base: "src" })
.pipe(
plugins.babel({
// babel 插件,将 es6 的一些最新特性转换
presets: ["@babel/preset-env"]
})
)
.pipe(dest("temp"))
.pipe(bs.reload({ stream: true }));
};
const page = () => {
return src("src/**/*.html")
.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"));
};
const server = () => {
watch("src/assets/styles/*.scss", style);
watch("src/assets/scripts/*.js", script);
watch("src/**/*.html", page);
//todo 新增文件时重载服务器
watch(["src/assets/images/**", "src/assets/fonts/**", "public/**"], bs.reload);
// todo 服务器初始化
bs.init({
port: 8090,
open: true,
notify: false, // 关闭右上角的提示
// files: "dist/**",
server: {
baseDir: "temp", // 文件服务器基础路径
routes: {
// 自配置优先于 baseDir 配置
"/node_modules": "node_modules"
}
}
});
};
const useref = () => {
return src("temp/**/*.html", { base: "temp" })
.pipe(plugins.useref({ searchPath: ["temp", "."] }))
.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, image, font);
const build = series(clean, parallel(series(compile, useref), extra));
const develop = series(compile, server);
module.exports = {
clean,
build,
develop,
};
将 gulp 任务放置到 npm scripts 中
{
"scripts": {
"clean": "gulp clean",
"build": "gulp build",
"dev": "gulp develop"
}
}
封装自动化构建工作流
- 同类型的项目构建大体都差不多,可以使用同一套构建方案,比如 webpack 、gulp 等等
- 可以将之前配置好的代码保存起来,后面有项目用的时候在 copy 过来,但是如果我们用这种方法运用到多个项目中的时候,如果构建代码出了 bug ,我们需要一个个修改每个项目中的构建代码,不利于后期的维护。所以这种方法我们是不推荐使用的
- 接下来我们来封装一个自动化构建的工作流来解决这个问题化化
准备工作
申请一个 git 地址
初始化一个模块项目,这里我们使用导师创建的脚手架来创建:
安装
npm i caz -g
初始化一个模版
caz nm my-project
设置仓库的远程地址
初始化git
git init
设置远程地址
git remote add origin https://github.com/GodX-18/zx-pages.git
提交本地修改至远程仓库
git add . git commit -m "init" git push -u origin master
提取 Gulpfile 到模块
- 将原我们写好的
gulpfile.js
代码拷贝到模块的入口文件lib/index.js
中 - 拷贝原项目的开发依赖到模块项目的生产依赖中,并安装所有的依赖包
相关信息
之所以将开发依赖安装到模块的生产依赖中,是因为当我们安装一个依赖时,会自动的帮我们去安装生产依赖中的依赖包,开发中的依赖并不会下载
- 接下来通过
yarn link
将我们写好的模块暴露到本地的全局环境下
相关信息
通常开发一个模块时,为了方便,会暂时利用 yarn link
的方式进行本地调试
在原本项目中打开终端,通过
yarn link
使用该模块yarn link "zx-pages"
image-20221007102007752 现在可以将原本项目的
node_modules目录、package.json中的开发依赖和gulpfile.js
文件删除,然后重新安装一下生产依赖,接着在根目录下新建一个新的gulpfile.js
文件,写入以下代码:module.exports = require("zx-pages")
接着运行一下命令测试
yarn build
我们会发现控制台报出一下错误
image-20221007102223731 这是因为在
node_modules/.bin
目录下找不到对应的可执行文件导致的注意
使用
yarn link
暴露的模块,.bin目录不会出现在当前项目的node_modules
的一级目录下。依赖包发布到 npm 平台上后不会出现此问题,因为安装依赖时会自动下载所有的生产依赖。此时,我们可以暂时在当前项目下安装
gulp
依赖解决此问题yarn add gulp -D
现在我们再次运行
yarn build
将不会报刚刚的错误,但是又产生了一个新的问题image-20221007103953157 这是因为我们的
gulpfile.js
中有一些字段我们是写死的,比如模版数据中的 pkg 字段我们使用的相对路径pkg: require('./package.json')
用户在使用我们的依赖时,肯定是找不到该文件的,所以我们需要将类似这种不能写死的字段使用变量暴露出去,让用户使用配置文件自己去配置,当然用户不配置也是可以的(约定大于配置),例如
vue-cli
的vue.config.js
文件一样,这样不仅仅可以提高用的体验,也让工作流更加的灵活现在,让我们改造一下我们依赖项目中的
lib/index.js
中的代码const { src, dest, parallel, series, watch } = require('gulp') const del = require('del') const browserSync = require('browser-sync') const loadPlugins = require('gulp-load-plugins') const plugins = loadPlugins() const bs = browserSync.create() const cwd = process.cwd() let config = { // default config 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 { const loadConfig = require(`${cwd}/pages.config.js`) config = Object.assign({}, config, loadConfig) } catch (e) {} const clean = () => { return del([config.build.dist, config.build.temp]) } const style = () => { return src(config.build.paths.styles, { base: config.build.src, cwd: config.build.src }) .pipe(plugins.sass({ outputStyle: 'expanded' })) .pipe(dest(config.build.temp)) .pipe(bs.reload({ stream: true })) } const script = () => { return src(config.build.paths.scripts, { base: config.build.src, cwd: config.build.src }) .pipe(plugins.babel({ presets: [require('@babel/preset-env')] })) .pipe(dest(config.build.temp)) .pipe(bs.reload({ stream: true })) } const page = () => { return src(config.build.paths.pages, { base: config.build.src, cwd: config.build.src }) .pipe(plugins.swig({ data: config.data, defaults: { cache: false } })) .pipe(dest(config.build.temp)) .pipe(bs.reload({ stream: true })) } const image = () => { return src(config.build.paths.images, { base: config.build.src, cwd: config.build.src }) .pipe(plugins.imagemin()) .pipe(dest(config.build.dist)) } const font = () => { return src(config.build.paths.fonts, { base: config.build.src, cwd: config.build.src }) .pipe(plugins.imagemin()) .pipe(dest(config.build.dist)) } const extra = () => { return src('**', { base: config.build.public, cwd: config.build.public }) .pipe(dest(config.build.dist)) } const serve = () => { watch(config.build.paths.styles, { cwd: config.build.src }, style) watch(config.build.paths.scripts, { cwd: config.build.src }, script) watch(config.build.paths.pages, { cwd: config.build.src }, page) // watch('src/assets/images/**', image) // watch('src/assets/fonts/**', font) // watch('public/**', extra) watch([ config.build.paths.images, config.build.paths.fonts ], { cwd: config.build.src }, bs.reload) watch('**', { cwd: config.build.public }, bs.reload) bs.init({ notify: false, port: 2080, // open: false, // files: 'dist/**', server: { baseDir: [config.build.temp, config.build.dist, config.build.public], routes: { '/node_modules': 'node_modules' } } }) } const useref = () => { return src(config.build.paths.pages, { base: config.build.temp, cwd: config.build.temp }) .pipe(plugins.useref({ searchPath: [config.build.temp, '.'] })) // html js css .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 build = series( clean, parallel( series(compile, useref), image, font, extra ) ) const develop = series(compile, serve) module.exports = { clean, build, develop }
相关信息
Gulp 的
src、watch等
api 提供了一个cwd
选项用来指定查找的目录在项目中新建一个约定好的配置文件
pages.config.js
文件module.exports = { build: { src: 'src', dist: 'release', temp: '.tmp', public: 'public', paths: { styles: 'assets/styles/*.scss', scripts: 'assets/scripts/*.js', pages: '*.html', images: 'assets/images/**', fonts: 'assets/fonts/**' } }, 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() } }
到此一个基本的自动化工作流已经完成
包装 Gulp CLI
为什么要包装
让用户的使用体验更好。我们使用当前的自动化工作流时,每次都需要在根目录下新建一个gulpfile.js
文件来导出gulp任务,其实这一步是可以集成到我们的自动化工作流中的,接下来让我们来具体操作一下。
首先将我们项目中的
gulpfile.js
文件删除接着我们在运行以下命令,我们发现命令依旧生效
gulp build --gulpfile ./node_modules/zx-pages/lib/index.js --cwd .
相关信息
- 可以利用命令行参数的形式来替代在根目录下新建
gulpfile.js
的方式 - cwd用来指定当前的工作目录
- 可以利用命令行参数的形式来替代在根目录下新建
此时我们有了思路:在依赖项目中定义一个脚本文件,用来运行以上脚本代码
首先,我们在依赖项目中新建一个
bin
目录并新建一个zx-pages
文件(文件名自定义,一般和依赖包名字相同),并写入以下代码#!/usr/bin/env node console.log('GodX------>logzx-pages');
接着,我们在
package.json
中新增以下代码,用来指定脚本文件的入口"bin":"bin/zx-pages.js",
现在我们重新
yarn link
以下依赖包,将脚本文件注册到全局模块下# 断开连结 yarn unlink # 重新连接 yarn link
现在我们在项目中运行以下命令进行测试
zx-pages
注意
如果是 Linux 或者 macOS 系统下还需要修改脚本文件的读写权限为 755
具体就是通过chmod 755 cli.js
实现修改如果一切顺利的话,控制台会打印以下信息
image-20221007162831718 继续编写脚本文件
#!/usr/bin/env node process.argv.push('--cwd'); process.argv.push(process.cwd()); process.argv.push('--gulpfile'); process.argv.push(require.resolve('..')); require("gulp/bin/gulp")
相关信息
process.argv
打印的是命令行参数相关的信息,我们通过修改process.argv
数组的方式等价于在命令行中手动传入参数相关信息
require.resolve
如果是相对路径的话,会先定位到相对路径的位置,然后根据package.json
中的main
字段去找到匹配文件的绝对路径至此
gulp cli
包装完成,可以运行以下代码进行测试zx-pages build
发布并使用模块
将本地代码同步至远程仓库
利用
yarn publish
发布发布完成后即可使用我们写好的模块
# 安装 yarn add zx-pages -D # 使用,如果全局安装了`gulp-cli`可以省略 yarn 前缀 yarn zx-pages develop/clean/builld
相关信息
国内发布可能会存在一定的延时,可以进入https://npmmirror.com/
,搜索你的包名,点击sync
进行手动同步

FIS
fis是百度团队开发的一款前端构建工具,其中内置了很多构建任务,只需要简单的配置即可使用。但是目前,官网差不多已经停止迭代,有五百多个 issues还没有解决,所以不推荐大家使用,稍作了解即可
基本使用
具体使用方式详见 fis官网
安装
yarn add fis3 -D
编译
# 指定文件输出
fis3 release -d output
相关信息
Fis 首要解决的问题就是资源定位,开发时使用相对定位,打包后自动将路径转换成绝对路径