自动化构建介绍
自动化构建在前端有着重要的地位。把重复的工作使用机器来代替,从源代码到生产环境可运行的程序,这一整个流程。使得项目整个构建效率,发布流程大大提高。
常用的自动化构建工具
我们看一下几种:
-
优势
- 生态完善,有大量的插件可以使用;
不足
- 基于临时文件实现,构建速度较慢;
- 需要配置
-
优势
- 基于内存实现,构建速度比grunt快;
- 支持多个任务同时执行;
- 生态完善
- 使用灵活、易懂
不足
- 需要配置
-
- 集成较多功能,开箱即用
Grunt
什么是Grunt
-
Grunt是一个基于任务的JavaScript工程命令行构建工具。
-
Grunt有大量现成的插件封装了常见的任务,也能管理任务之间的依赖关系,自动化地执行依赖的任务,每个任务的具体执行代码和依赖关系写在配置文件Gruntfile.js里。
Grunt使用
Grunt基本使用
- 创建npm项目
npm init -y
- 安装grunt
npm install grunt --save-dev
- 创建gruntfile.js配置文件
grunt任务创建
// 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)
})
}
任务执行命令:
npx grunt <task-name>
grunt多任务使用
module.exports = grunt => {
// 多目标模式,可以让任务根据配置形成多个子任务
// grunt.initConfig({
// build: {
// foo: 100,
// bar: '456'
// }
// })
// grunt.registerMultiTask('build', function () {
// console.log(`task: build, target: ${this.target}, data: ${this.data}`)
// })
grunt.initConfig({
build: {
options: {
msg: 'task options'
},
foo: {
options: {
msg: 'foo target options'
}
},
bar: '456'
}
})
grunt.registerMultiTask('build', function () {
console.log(this.options())
})
}
grunt标记任务失败状态
module.exports = grunt => {
// 任务函数执行过程中如果返回 false
// 则意味着此任务执行失败
grunt.registerTask('bad', () => {
console.log('bad working~')
return false
})
grunt.registerTask('foo', () => {
console.log('foo working~')
})
grunt.registerTask('bar', () => {
console.log('bar working~')
})
// 如果一个任务列表中的某个任务执行失败
// 则后续任务默认不会运行
// 除非 grunt 运行时指定 --force 参数强制执行
grunt.registerTask('default', ['foo', 'bad', 'bar'])
// 异步函数中标记当前任务执行失败的方式是为回调函数指定一个 false 的实参
grunt.registerTask('bad-async', function () {
const done = this.async()
setTimeout(() => {
console.log('async task working~')
done(false)
}, 1000)
})
}
Grunt配置方法
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插件使用
- 安装对应插件
- 编写插件任务
const sass = require("sass");
const loadGruntTasks = require("load-grunt-tasks");
module.exports = (grunt) => {
grunt.initConfig({
clean: {// clean插件任务配置项
temp: 'temp/**'
},
sass: {
options: {
sourceMap: true,
implementation: sass,
},
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 插件中加载任务。此插件必须通过npm安装到本地,并且是参照 Gruntfile 文件的相对路径
// grunt-contrib-clean 清除文件
// grunt.loadNpmTasks("grunt-contrib-clean");
// grunt.loadNpmTasks('grunt-sass')
loadGruntTasks(grunt); // 自动加载所有的 grunt 插件中的任务
// 加载插件之前,先默认加载下,之后可实现保存监听
grunt.registerTask("default", ["sass", "babel", "watch"]);
};
Gulp
gulpjs是一个前端构建工具,与gruntjs相比,gulpjs无需写一大堆繁杂的配置参数,API也非常简单,学习起来很容易,而且gulpjs使用的是nodejs中stream来读取和操作数据,其速度更快。
快速上手
创建项目目录并进入
npx mkdirp my-project
cd my-project
项目目录下创建 package.json 文件
npm init
上述命令将指引你设置项目名、版本、描述信息等。
安装 gulp,作为开发时依赖项
npm install --save-dev gulp
创建 gulpfile 文件
利用任何文本编辑器在项目大的根目录下创建一个名为 gulpfile.js 的文件,并在文件中输入以下内容:
// gulp默认执行的任务
function defaultTask(cb) {
// place code for your default task here
cb();
}
exports.default = defaultTask
Gulpfile 详解
gulpfile 是项目目录下名为 gulpfile.js
(或者首字母大写 Gulpfile.js
,就像 Makefile 一样命名)的文件,在运行 gulp
命令时会被自动加载。在这个文件中,你经常会看到类似 src()
、dest()
、series()
或 parallel()
函数之类的 gulp API,除此之外,纯 JavaScript 代码或 Node 模块也会被使用。任何导出(export)的函数都将注册到 gulp 的任务(task)系统中。
Gulpfile 转译
你可以使用需要转译的编程语言来书写 gulpfile 文件,例如 TypeScript 或 Babel,通过修改 gulpfile.js
文件的扩展名来表明所用的编程语言并安装对应的转译模块。
- 对于 TypeScript,重命名为
gulpfile.ts
并安装 ts-node 模块。 - 对于 Babel,重命名为
gulpfile.babel.js
并安装 @babel/register 模块。
针对此功能的高级知识和已支持的扩展名的完整列表,请参考 gulpfile 转译 文档。
Gulpfile 分割
大部分用户起初是将所有业务逻辑都写到一个 gulpfile 文件中。随着文件的变大,可以将此文件重构为数个独立的文件。
每个任务(task)可以被分割为独立的文件,然后导入(import)到 gulpfile 文件中并组合。这不仅使事情变得井然有序,而且可以对每个任务(task)进行单独测试,或者根据条件改变组合。
Node 的模块解析功能允许你将 gulpfile.js
' 文件替换为同样命名为 gulpfile.js
的目录,该目录中包含了一个名为 index.js
的文件,该文件被当作 gulpfile.js
使用。并且,该目录中还可以包含各个独立的任务(task)模块。
创建任务(task)
每个 gulp 任务(task)都是一个异步的 JavaScript 函数,此函数是一个可以接收 callback 作为参数的函数,或者是一个返回 stream、promise、event emitter、child process 或 observable 类型值的函数。
// gulp入口文件
// default 是默认任务
// 在运行是可以省略任务名参数
function defaultTask(done) {
// place code for your default task here
done();
}
// gulp 的任务函数都是异步的
// 可以通过调用回调函数标识任务完成
exports.foo = (done) => {
console.log("foo task working~");
done(); // 标识任务执行完成
};
// v4.0 之前需要通过 gulp.task() 方法注册任务
const gulp = require('gulp')
gulp.task('bar', done => {
console.log('bar task working~')
done()
})
// 导出的函数都会作为 gulp 任务
exports.default = defaultTask;
执行gulp:
// 执行gulp的默认任务
npx gulp
// 执行gulp的对应任务
npx gulp <task-name>
创建组合任务(task)
Gulp 提供了两个强大的组合方法: series()
和 parallel()
,允许将多个独立的任务组合为一个更大的操作。这两个方法都可以接受任意数目的任务(task)函数或已经组合的操作。series()
和 parallel()
可以互相嵌套至任意深度。
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)
异步任务(task)
Node 库以多种方式处理异步功能。最常见的模式是 error-first callbacks,但是你还可能会遇到 streams、promises、event emitters、child processes, 或 observables。gulp 任务(task)规范化了所有这些类型的异步功能。
使用 callback
如果任务(task)不返回任何内容,则必须使用 callback 来指示任务已完成。在如下示例中,callback 将作为唯一一个名为 done()
的参数传递给你的任务(task)。
exports.callback = done => {
console.log('callback task')
done()
}
如需通过 callback 把任务(task)中的错误告知 gulp,请将 Error
作为 callback 的唯一参数。
exports.callback_error = done => {
console.log('callback task')
done(new Error('task failed'))
}
然而,你通常会将此 callback 函数传递给另一个 API ,而不是自己调用它。
const fs = require('fs');
function passingCallback(cb) {
fs.access('gulpfile.js', cb);
}
exports.default = passingCallback;
返回 promise
exports.promise = () => {
console.log('promise task')
return Promise.resolve()
}
exports.promise_error = () => {
console.log('promise task')
return Promise.reject(new Error('task failed'))
}
const timeout = time => {
return new Promise(resolve => {
setTimeout(resolve, time)
})
}
使用 async/await
还可以将任务(task)定义为一个 async
函数,它将利用 promise 对你的任务(task)进行包装。这将允许你使用 await
处理 promise,并使用其他同步代码。
const timeout = time => {
return new Promise(resolve => {
setTimeout(resolve, time)
})
}
exports.async = async () => {
await timeout(1000)
console.log('async task')
}
处理文件
gulp 暴露了 src()
和 dest()
方法用于处理计算机上存放的文件。
src()
接受 glob 参数,并从文件系统中读取文件然后生成一个 Node 流(stream)。它将所有匹配的文件读取到内存中并通过流(stream)进行处理。
由 src()
产生的流(stream)应当从任务(task)中返回并发出异步完成的信号,就如 创建任务(task) 文档中所述。
const { src, dest } = require('gulp')
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'))
}
流(stream)所提供的主要的 API 是 .pipe()
方法,用于连接转换流(Transform streams)或可写流(Writable streams)。
dest()
接受一个输出目录作为参数,并且它还会产生一个 Node 流(stream),通常作为终止流(terminator stream)。当它接收到通过管道(pipeline)传输的文件时,它会将文件内容及文件属性写入到指定的目录中。gulp 还提供了 symlink()
方法,其操作方式类似 dest()
,但是创建的是链接而不是文件( 详情请参阅 symlink()
)。
大多数情况下,利用 .pipe()
方法将插件放置在 src()
和 dest()
之间,并转换流(stream)中的文件。
Gulp 构建过程核心工作原理示例
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({
// 核心转换过程
transform: (chunk, encoding, callback) => {
const input = chunk.toString()
const output = input.replace(/\s+/g, '').replace(/\/\*.+?\*\//g, '')
callback(null, output)
}
})
return readStream
.pipe(transformStream) // 转换
.pipe(writeStream) // 写入
}