一切重复工作本应自动化。
自动化构建就是把我们开发阶段写出来的源代码自动化的转化成生产环境中可以运行的代码或者程序。这种转换的过程称为自动化构建工作流。作用是尽可能脱离环境兼容带来的问题,并在开发阶段去使用提高效率的语法、规范和标准
典型应用,开发网页时,可以使用
- ECMAScript Next
- Sass
- 模板引擎
这些用法大都不被浏览器直接支持,通过自动化构建的方式构建转化那些不被支持的[特性]。
自动化构建初体验
NPM Scripts
实现自动化构建工作流的最简方式。
- 在使用Sass写样式时,需要把Sass转换成CSS才能被浏览器识别。
通常会在package.json中添加一个script对象,使用build字段自动将Sass转换成CSS。
{
“script”: {
"build":sass scss/main.scss css/style.css
}
}
script可以自动发现node_modules里面的命令,所以不需要写完整的路径,直接写命令的名称就可以了。然后通过npm或yran运行script下面的命名名称,npm用yun启动,yarn可以省略run nom run build 或 yarn build。
- 安装browser-sync模块,用于启动服务器去运行我们的项目,在script中添加serve命令
我们需要在serve之前执行build,这样浏览器才能正常显示。{
“script”: {
"build":sass scss/main.scss css/style.css,
“serve”: "browser-sync"
}
}
preserve是一个钩子,保证在执行serve之前,先去执行build,对样式先处理,然后执行serve。就可以完成在启动web之前,自动去构建我们的项目文件。{
“script”: {
"build":sass scss/main.scss css/style.css,
"preserve":"yarn build",
“serve”: "browser-sync"
}
}
我们可以添加 —watch 字段去监听sass文件的变化自动编译。
{
“script”: {
"build":sass scss/main.scss css/style.css --watch",
"preserve":"yarn build",
“serve”: "browser-sync"
}
}
此时sass命令在工作时,命令行代码会堵塞,去等待文件的变化,导致了后面serve无法工作,这种情况就需要同时执行多个任务,我们可以借助于npm-run-all这个模块去实现。
{
“script”: {
"build":sass scss/main.scss css/style.css --watch",
“serve”: "browser-sync ."
“start”:"run-p build serve"
}
}
运行npm run start命令,build和serve就会被同时执行。
我们可以给browser-sync这个命令添加—file \”css/*.css\”这个参数,这个参数可以在browser-sync启动之后,去监听files文件的变化,一旦文件发生变化之后,browser-sync会自动将这些内容同步到浏览器。
{
“script”: {
"build":sass scss/main.scss css/style.css --watch",
“serve”: "browser-sync . --file \"css/*.css\"",
“start”:"run-p build serve"
}
}
避免了修改完代码后,再去手动刷新浏览器的重复操作。
常用的自动化构建工具
这些工具用法上大体相同,都是先通过一些简单的代码去组织一些插件的使用,然后就可以用这些工具帮你执行各种各样重复的工作了。
Grunt
**
最早的前端构建系统,插件生态非常的完善,Grunt的插件几乎可以帮你去完成自动化的完成任何你想要做的事情
缺点:它的工作过程是基于临时文件去实现的,所以说构建速度相对较慢。每一个步都会有磁盘读写操作,处理环节越多,文件读写次数就越多。
Gulp
**
其核心特点就是高效、易用。它很好的解决了Grunt构建速度慢的问题,它是基于内存实现的,它的文件读写都是在内存中实现的,相对于磁盘读写,速度自然是快了很多,默认支持同时执行多个任务,效率自然也就大大提高,使用方式相对于Grunt更加直观易懂,插件生态也同样非常完善,目前市面上最流行的前端构建系统。
FIS
FIS是百度的前端团队推出的构建系统,FIS相对于前两种微内核的特点,它更像是一种捆绑套餐,它把我们的需求都尽可能的集成在内部了,例如资源加载、模块化开发、代码部署、甚至是性能优化。正式因为FIS的大而全,所以在国内流行。
初学者可能更适合FIS,老手可能更适合Grunt,gulp 新手需要规则,而老手渴望自由
Grunt
基本使用
- 安装grunt yarn add grunt
- 添加gruntfile.js文件 ```javascript // Grunt的入口文件 //用于定义一些需要 Grunt 自动执行的任务 //需要导出一个函数 //此函数接受一个grunt的形参,内部提供一些创建任务时可以用到的API
module.exports = grunt => { //注册任务 //第一个参数是任务名字 //第二个参数接收一个回调函数,是指定任务的执行内容 //执行命令yarn grunt foo grunt.registerTask(‘foo’, () => { console.log(‘helllo grunt~’) })
//如果第二个参数是字符串,则是任务描述,会出现grunt的帮助信息中
//我们可以通过 yarn grunt --help得到grunt的帮助信息
//同样可以通过 yarn grunt bar 执行该任务
grunt.registerTask('bar','任务描述',() => {
console.log('other task~')
})
//如果任务名称是'default',则为默认任务
//grunt在运行时不指定任务名称,则执行默认任务
//执行命令是 yarn grunt
// grunt.registerTask('default', () => {
// console.log('default task')
// })
//一般用default映射其他任务,第二个参数传入一个数组
//数组中指定任务的名称,grunt执行默认任务,则会依次执行数组中的任务
grunt.registerTask('default',['foo','bar'])
//grunt默认支持同步模式
//执行 yarn grunt async-task , 以下任务不会被执行
// grunt.registerTask('async-task', () => {
// setTimeout(() => {
// console.log('async task working')
// }, 1000)
// })
//异步任务, done()表示结束
//如果需要异步操作,则需要通过this.async()得到一个回调函数
//在你的异步操作完成过后,去调用这个回调函数
//标记这个任务已被完成
//直到done()被执行,grunt才会结束这个任务的执行
grunt.registerTask('async-task', function () {
const done = this.async ()
setTimeout(() => {
console.log('async task working..')
done()
}, 1000)
})
<a name="Qi4mD"></a>
### <br />
<a name="odEqZ"></a>
### Grunt标记任务失败
如果在构建任务的代码中发现错误,那么此时我们就可以将这个任务标记为一个失败的任务,通过在函数体当中return false实现。
```javascript
//失败任务
//通过return false 标志这个任务执行失败
//如果是在任务列表中,这个任务的失败会导致后续所有任务不再被执行
//可以通过--force参数强制执行所有的任务,执行命令是 yarn grunt default --force
grunt.registerTask('bad', () => {
console.log('bad working...')
return false
})
//异步失败任务,done(false)表示任务失败
grunt.registerTask('bad-asybc-task', function () {
const done = this.async()
setTimeout(() => {
console.log('bad async task working..')
done(false)
}, 1000)
})
Grunt的配置方法
Grunt提供了一个用于添加配置选项的API initConfig() 接收对象形式的参数
//grunt配置选项
module.exports = grunt => {
grunt.initConfig({
// 对象的属性名一般与任务名保持一致。
// foo: 'bar'
foo: {
bar: 123
}
})
grunt.registerTask('foo', () => {
// console.log(grunt.config('foo)) // bar
// grunt的config支持通过foo.bar的形式获取属性值
// 也可以通过获取foo对象,然后获取属性值
console.log(grunt.config('foo.bar')) // 123
})
}
Grunt多目标任务
可以理解成子任务,通过regosterMultiTask()方法去定义
module.exports = grunt => {
grunt.initConfig({
// 与任务名称同名
build: {
options: { // 是配置选项,不会作为任务
foo: 'bar'
},
// 每一个对象属性都是一个任务
css: {
options: { // 会覆盖上层的options
foo: 'baz'
}
},
// 每一个对象属性都是一个任务
js: '2'
}
})
// 多目标任务,可以让任务根据配置形参多个子任务,registerMultiTask方法
// 第一个参数是任务名,第二个参数是任务的回调函数
// 多目标任务需要为不同的任务配置不同的目标
// 配置方法需要通过initConfig()方法配置
grunt.registerMultiTask('build', function () {
// 拿到任务的配置选项
console.log(this.options())
// 拿到对应的目标和数据
console.log(`build task: ${this.target}, data: ${this.data}`)
})
}
输出结果:
Running "build:css" (build) task
{ foo: 'baz' }
build task: css, data: [object Object]
Running "build:js" (build) task
{ foo: 'bar' }
build task: js, data: 2
Grunt插件的使用
插件机制是grunt的核心,因为很多构建任务都是通用的,社区当中也出现了很多通用的插件,这些插件中封装了很多通用的任务,一般情况下我们的构建过程都是通过构建任务组成的。先去npm中安装需要的插件,再去gruntfie中使用grunt.loadNpmTasks方法加载插件的任务,最后根据插件的文档完成相关的配置选项。
例如clean插件的使用,安装 yarn add grunt-contrib-clean
,用来清除临时文件。
module.exports = grunt => {
// 多目标任务需要通过initConfig配置目标
grunt.initConfig({
clean: {
temp: 'temp/**' // ** 表示temp下的子目录以及子目录下的文件
}
})
grunt.loadNpmTasks('grunt-contrib-clean')
执行:yarn grunt clean
,就会删除temp文件夹
常用插件的使用
**
- grunt-sass
- grunt-babel
grunt-watch
const sass = require('sass')
const loadGruntTasks = require('load-grunt-tasks')
module.exports = grunt => {
grunt.initConfig({
sass: {
options: {
sourceMap: true,
implementation: sass, // implementation指定在grunt-sass中使用哪个模块对sass进行编译,我们使用npm中的sass
},
main: {
files: {
'dist/css/main.css': 'src/scss/main.scss'
}
}
},
babel: {
options: {
presets: ['@babel/preset-env'],
sourceMap: true
},
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的过程:安装gulp开发依赖,在项目的根目录下创建gulpfile.js文件,用于编写我们需要gulp自动执行的构建任务,然后在命名行运行构建的任务。
// gulp 的入口文件
//通过导出函数成员的方式定义gulp任务
//在最新的gulp当中,取消了同步代码模式,约定我们每一个任务都必须是一个异步的任务
//当我们的任务执行完之后,我们需要通过回调函数或者其他方式标记这个任务以及完成
exports.foo = done => {
console.log('foo task working~')
done() // 标识任务完成
}
//default 默认任务,执行时不需要指定任务名
exports.default = done => {
console.log('default task working~')
done()
}
//gulp4.0之前,我们注册gulp任务需要通过gulp模块里面的方法实现
//这种方式已经不被推荐
const gulp = require('gulp')
gulp.task('bar', done => {
console.log('bar working~')
done()
})
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 ) // 依次执行task1, task2, task3
exports.bar = parallel(task1, task2, task3 ) // 同步执行task1, task2, task3
gulp异步任务
**
调用异步函数时,是没有办法明确这个调用是否完成的。都是在函数内部,通过回调去通知外部 函数执行完成。异步任务中同样有着如何通知gulp我们的完成情况这样一个问题,针对这个问题 gulp 提供了很多方法。
const fs = require('fs')
//通过callback方式解决
exports.callback = done => {
console.log('callback task~')
done()
}
//这种回调函数是一种错误优先的回调函数
//也就是说可以通过报出错误,阻止后续任务执行
exports.callback_error = done => {
console.log('callback task~')
done(new Error('task failed!'))
}
//promise方式
exports.promise = () => {
console.log('promise task~')
return Promise.resolve()
}
//传出错误信息时,同样会阻止后续的执行
exports.promise_error = () => {
console.log('promise task~')
return Promise.reject(new Error('task failed'))
}
//async\await方式
const timeout = time => {
return new Promise(resolve => {
setTimeout(resolve, time)
})
}
exports.async = async () => {
await timeout(1000)
console.log('async task~')
}
//通过stream方式,最常用
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构建过程核心工作原理
**
构建过程
- 输入
- 读取文件
- 读取流
- 加工
- 压缩文件
- 转换流
- 输出
- 写入文件
- 写入流 ```javascript const fs = require(‘fs’) //通过callback方式解决 exports.callback = done => { console.log(‘callback task~’) done() } //这种回调函数是一种错误优先的回调函数 //也就是说可以通过报出错误,阻止后续任务执行 exports.callback_error = done => { console.log(‘callback task~’) done(new Error(‘task failed!’)) }
//promise方式 exports.promise = () => { console.log(‘promise task~’) return Promise.resolve() } //传出错误信息时,同样会阻止后续的执行 exports.promise_error = () => { console.log(‘promise task~’) return Promise.reject(new Error(‘task failed’)) }
//async\await方式 const timeout = time => { return new Promise(resolve => { setTimeout(resolve, time) }) }
exports.async = async () => { await timeout(1000) console.log(‘async task~’) }
//通过stream方式,最常用 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() }) }
<a name="9045df47"></a>
### gulp文件操作API
```javascript
src 读取流 插件 转换流 dest 写入流
const { src, dest } = require('gulp')
const cleanCss = require('gulp-clean-css') // 压缩css文件插件
const rename = require('gulp-rename') // 重命名插件
exports.default = () => {
return src('src/*.css')
.pipe(cleanCss())
.pipe(rename({ extname: '.min.css' }))
.pipe(dest('dist'))
}
gulp自动化构建案例
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 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()
}
// 其他文件及文件清除
// 插件 del
const clean = () => {
return del(['dist', 'temp'])
}
// 样式编译
// 插件 gulp-sass
// sass模块工作时,_ 下划线开头的样式文件都是我们在主文件中依赖的文件,不会被转化
const style = () => {
return src('src/assets/styles/*.scss', { base: 'src' })
.pipe(plugins.sass({ outputStyle: 'expanded' }))
.pipe(dest('temp'))
.pipe(bs.reload({ stream: true }))
}
// 脚本编译
// 插件 gulp-babel
// 同时还需要安装 @babel/core @babel/preset-env
// 在文件流中需要传递babel({ presets: ['@babel/preset-env'] })
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 }))
}
// 页面模板编译
// swig模板引擎转换插件 gulp-swig
const page = () => {
return src('src/*.html', { base: 'src' })
.pipe(plugins.swig({ data, defaults: { cache: false } })) // 防止模板缓存导致页面不能及时更新
.pipe(dest('temp'))
.pipe(bs.reload({ stream: true }))
}
// 图片和字体转换
// 插件 gulp-imagemin
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'))
}
// 热更新开发服务器
// 插件 browser-sync
// 这个插件并不是gulp,只不过我们需要gulp管理
const serve = () => {
// watch监视路径文件通配符
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, // 关闭小提示
port: 2080, // 启动端口
// open: false, // 取消自动打开浏览器
// files: 'dist/**', // 服务启动后,自动监听的目录下文件的改动,同步浏览器
server: {
baseDir: ['temp', 'src', 'public'],
routes: {
'/node_modules': 'node_modules'
}
}
})
}
// usere文件引用处理
// 插件 gulp-useref
const useref = () => {
return src('temp/*.html', { base: 'temp' })
.pipe(plugins.useref({ searchPath: ['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('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
}
构建的任务中,比如图片压缩,压缩这类任务在开发阶段可以不去构建,以此优化构建效率。
可能会因为swig模板引擎缓存的机制导致页面不会变化,此时需要额外将swig选项中的cache设置为false。
依赖文件:
"devDependencies": {
"@babel/core": "^7.10.2",
"@babel/preset-env": "^7.10.2",
"browser-sync": "^2.26.7",
"del": "^5.1.0",
"gulp": "^4.0.2",
"gulp-babel": "^8.0.0",
"gulp-clean-css": "^4.3.0",
"gulp-htmlmin": "^5.0.1",
"gulp-if": "^3.0.0",
"gulp-imagemin": "^7.1.0",
"gulp-load-plugins": "^2.0.3",
"gulp-sass": "^4.1.0",
"gulp-swig": "^0.9.1",
"gulp-uglify": "^3.0.2",
"gulp-useref": "^4.0.1"
},
封装自动化构建工作流
- 准备
- 新建一个项目
- 安装zce-cli
- 提取gulofile
- 将gulp-demo中的gulpfile.js 文件内容复制到gxw-pages项目下的lib/index.js(入口文件)中
- 将gulp-demo中的package.json中安装的依赖复制到gxw-pages的package.json的dependencies
- 删除gulp-demo项目中的依赖、清空gulpfile.js
- gxw-pages项目通过yarn link 链接到本地全局
- 在gulp-demo项目中通过
yarn link "gxw-pages"
链接到本项目 - gulp-demo项目中gulpfile.js添加代码
module.exports = require('gxw-pages')
- gulp-demo项目中安装一下依赖(原本项目依赖)
- 安装一下gulp-cl、gulp
- 运行脚本
yarn gulp clean
- 解决模块中的问题
- gulp-demo项目中创建page.config.js文件(目的是抽离出一些配置信息)
- 在gxw-pages项目中lib/index.js加载配置文件
``javascript const cwd = process.cwd();// 返回当前命令行工作目录 let config = { // default config } try { const loadConfig = require(
${cwd}/pages.config.js`) config = Object.assign({},config,loadConfig) } catch (error) {
}
- 将gxw-pages项目中lib/index.js用到的相关配置改成加载过来的数据
```javascript
const page = () => {
return src('src/*.html', { base: 'src' })
.pipe(swig({ data: config.data }))
.pipe(dest('dist'))
}
抽象路径配置
- 把写死的路径改成可配置的
let config = {
// default config
build:{
src:'src',
dist:'dist',
temp:'temp',
public:'public',
paths:{
styles:'assets/style/*.scss',
scripts:'assets/scripts/*.js',
pages:'*.html',
images:'assets/images/**',
fonts:'assets/font/**'
}
}
}
const style = () => {
return src(config.build.paths.styles, { base: config.build.src,cwd:config.build.src})
.pipe(sass({ outputStyle: 'expanded' }))
.pipe(dest('dist'))
}
- 把写死的路径改成可配置的
包装gulp cli
- gulp-demo项目中gulpfile.js删除
yarn gulp build --gulpfile .\node_modules\gxw-pages\lib\index.js
- yarn gulp build —gulpfile .\node_modules\gxw-pages\lib\index.js —cwd . (指定当前目录为工作目录)
- 上面的方法传参太多
- 解决:在gxw-pages项目中提供一个cli
require(‘gulp/bin/gulp’)
- gxw-pages clean·
- 发布使用 [gwx-pages](https://github.com/demong89/gxw-pages.git)
- package.json文件files增加
```javascript
"files": [
"lib",
"bin"
],
- npm publish (要先登录)
- npm i gwx-pages 在其他项目中使用
FIS
基本使用
- 安装fis3
- yarn fis3 release (默认构建任务)
- yarn fis3 release -d dist (指定输出目录)
- 配置文件fis-conf.js
编译与压缩
- yarn fis3 inspect 查看编译过程 ```javascript // 安装 fis-parser-node-sass fis.match(‘*/.scss’,{ rExt:’.css’,// 修改扩展名 parser:fis.plugin(‘node-sass’), optimizer:fis.plugin(‘clean-css’)//压缩 })
// 安装 fis.match(‘*/.js’,{ parser:fis.plugin(‘babel-6.x’), optimizer:fis.plugin(‘uglify-js’) }) ```