在前端快速发展的今天,如果不能时刻保持学习就会很快被淘汰。分享一下前端工程化中有关于自动化构建的相关知识,文章有点长,希望对大家有所帮助。每天进步一点点。
一、自动化构建简介
自动化构建就是把我们开发阶段写出来的源代码,自动化的转换成生产环境中可以运行的代码或者程序,这个自动化转换的过程称为自动化构建工作流。
它的作用是尽量脱离运行环境兼容带来的问题,在开发阶段使用一些提高效率的语法,规范和标准,例如,ES6+、Sass、模板引擎等。
二、自动化构建初体验
这里引入 进阶__前端漫漫路 写的 自动化构建 的一片文章
三、Gulp 快速上手
1、Gulp 的基本使用
// 先使用 npm init -y 初始化package.json
// 然后在项目中安装 gulp 的开发依赖
// 在项目根目录添加 .gulpfile 文件,作为 gulp 的入口文件
// .gulpfile 文件用于编写需要 gulp 执行的构建任务
// 编写完成后就可以在命令行终端,使用 yarn gulp 任务名 运行这些构建任务
// 推荐使用导出函数成员的方式,导出构建任务
exports.foo = done => {
console.log(123)
done() // 标识任务完成
}
exports.default = done => {
console.log('default')
done()
}
// gulp 4.0 以后仍保留了 gulp.task 创建任务(但不推荐使用)
const gulp = require('gulp')
gulp.task('bar', done => {
console.log('4.0 bar')
done()
})
2、Gulp 的组合任务
// 载入两个 构建组合任务的 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)
}
// 使用 series 构建 串行 的组合任务
// series 是一个函数,接收多个构建任务,并按顺序执行
exports.foo = series(task1, task2, task3)
// 使用 parallel 构建 并行 的组合任务
// parallel 是一个函数,接收多个构建任务,同时执行所有任务
exports.bar = parallel(task1, task2, task3)
3、Gulp 的异步任务
/ Gulp 异步任务的三种方式
// 1、回调函数
exports.callback = done => {
console.log('task working')
done()
}
// 错误优先,如果有多个任务,报错后的任务不再执行
exports.callback_err = done => {
console.log('task working')
done(new Error('task error'))
}
// 2、支持 promise
exports.promise = () => {
console.log('task promise')
return Promise.resolve()
}
// promise 任务失败 [同样失败后的任务不再执行]
exports.promise_err = () => {
console.log('task promise')
return Promise.reject(new Error('task promise error'))
}
// async/await [node需要版本8以上]
const timeout = time => {
return new Promise(resolve => {
setTimeout(resolve, time)
})
}
exports.async = async () => {
await timeout(1000)
console.log('task async await')
}
// 3、stream
const fs = require('fs')
// exports.stream = () => {
// const readStream = fs.createReadStream('package.json')
// const writeStream = fs.createWriteStream('temp.txt')
// readStream.pipe(writeStream)
// return readStream // readStream 在 end 的时候完成
// }
// 模拟 gulp 做的事情[只是注册了一个事件去监听任务结束]
exports.stream = done => {
const readStream = fs.createReadStream('package.json')
const writeStream = fs.createWriteStream('temp.txt')
readStream.pipe(writeStream)
readStream.on('end', () => {
done()
})
}
4、Gulp 构建过程核心工作原理
// Gulp 官方定义,The straming build system[基于流的构建系统]
// 核心原理就是:
// 1、通过读取流把我们需要转换的文件读取出来
// 2、经过转换流的转换逻辑,得到我们想要的结果
// 3、再通过写入流把结果写入到指定的写入位置
const fs = require('fs')
const { Transform } = require('stream')
exports.default = () => {
// 文件读取流
const read = fs.createReadStream('package.json')
// 转换流
const transform = new Transform({
transform: (chunk, encoding, callback) => {
// 核心转换过程
// chunk => 读取流中的读取到的内容 (Buffer)
const input = chunk.toString()
const output = input.replace(/\s+/g, '')
callback(null, output) // 错误优先,将错误值为 null
}
})
// 文件写入流
const write = fs.createWriteStream('temp.txt')
// 把读取出来的文件导入写入文件流
read
.pipe(transform) // 转换
.pipe(write) // 写入
return read
}
5、Gulp 文件操作 API
// Gulp 中 专门用于读取写入文件的 API src 和 dest
// 负责文件加工的转换流一般通过独立的插件提供
// Gulp 工作流程:
// 1、通过src方法创建读取流
// 2、通过插件提供的转换流实现文件加工
// 3、通过dest方法创建写入流,写入到目标文件
const { src, dest } = require('gulp')
// 完成文件压缩转化,需要安装 gulp-clean-css
const cleanCss = require('gulp-clean-css')
// 对文件重命名,需要安装 gulp-rename
const rename = require('gulp-rename')
exports.default = () => {
return src('src/*.css') // 读取
.pipe(cleanCss()) // 文件压缩转化
.pipe(rename({ extname: '.min.css' })) // 文件重命名转化
.pipe(dest('dist')) // 写入
}
四、Gulp 自动化构建案例
将准备好的资料拷贝到项目中,并安装好基础的依赖。
1、样式编译
gulp-sass 编译 scss 文件
// 导入 读取写入文件的 API src 和 dest
const { src, dest } = require('gulp')
// 需要安装 gulp-sass 插件
const sass = require('gulp-sass')
// 样式编译任务
const style = () => {
// { base: 'src' } => 转换时候的基准路径
// 转换的时候就会将基准路径后面的路径保留下来
return src('src/assets/styles/*.scss', { base: 'src' })
// sass 在工作的时候,认为 _ 开头的样式文件是主文件依赖的文件
// 所以 sass 直接忽略掉这些 _ 开头的样式文件
// { outputStyle: 'expanded' } => 完全展开的格式生成
.pipe(sass({ outputStyle: 'expanded' }))
.pipe(dest('dist'))
}
// 导出任务成员
module.exports = {
style
}
2、脚本编译
gulp-babel 编译 JS
// 需要安装 gulp-babel 插件
// 同时需要安装 @babel/core @babel/preset-env
const babel = require('gulp-babel')
// 脚本编译任务
const script = () => {
return src('src/assets/scripts/*.js', { base: 'src' })
// babel 只是提供一个环境, presets 是 babel 插件的集合
// 不配置 { presets: ['@babel/preset-env'] } ,转换就不会生效
.pipe(babel({ presets: ['@babel/preset-env'] }))
.pipe(dest('dist'))
}
// 导出任务成员
module.exports = {
style,
script
}
3、页面模板编译
gulp-swig 处理 HTML 模板文件
// 读取 src, 写入 dest, 组合任务 parallel
const { src, dest, parallel } = require('gulp')
// 需要安装 gulp-swig 插件
const swig = require('gulp-swig')
// 模拟模板页面中需要的动态数据
const data = {
menus: [
{
name: 'Home',
icon: 'aperture',
link: 'index.html'
},
{
name: 'About',
link: 'about.html'
},
{
name: 'Contact',
link: '#',
children: [
{
name: 'CSDN',
link: 'https://blog.csdn.net/gongye2019'
},
{
name: 'zhihu',
link: 'https://www.zhihu.com/people/gong-ye-18-46'
},
{
name: 'Gitee',
link: 'https://gitee.com/gongyexj'
},
{
name: 'Github',
link: 'https://github.com/gyxj'
}
]
}
],
pkg: require('./package.json'),
date: new Date()
}
// 页面模板编译任务
const page = () => {
return src('src/*.html', { base: 'src' })
.pipe(swig({ data }))
.pipe(dest('dist'))
}
// 组合多个任务同时执行
const compile = parallel(style, script, page)
// 导出任务成员
module.exports = {
compile
}
4、图片和文字文件转换
gulp-imagemin 处理图片、 字体
// 需要安装 gulp-imagemin 插件
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'))
}
// 组合多个任务同时执行
const compile = parallel(style, script, page, image, font)
// 导出任务成员
module.exports = {
compile
}
5、其他文件及文件清除
del 插件 清除文件
// 需要安装 del 插件,它是一个promise方法
const del = require('del')
// 读取 src, 写入 dest, 组合任务[并行] parallel, 组合任务[串行] series
const { src, dest, parallel, series } = require('gulp')
// 清除文件的方法
const clean = () => {
return del(['dist'])
}
// 额外的文件,直接拷贝
const extra = () => {
return src('public/**', { base: 'public' })
.pipe(dest('dist'))
}
// 通过 series 先执行 clean 任务,在同时执行其他任务
const build = series(clean, parallel(compile, extra))
// 导出任务成员
module.exports = {
compile,
build
}
6、自动加载插件
gulp-load-plugins 自动加载插件
// 需要安装 gulp-load-plugins 插件
const loadPlugins = require('gulp-load-plugins')
// 使用 loadPlugins 自动安装插件
const plugins = loadPlugins()
// 需要安装 gulp-sass 插件
// cosnt sass = require('gulp-sass')
// 需要安装 gulp-babel 插件
// 同时需要安装 @babel/core @babel/preset-env
// cosnt babel = require('gulp-babel')
// 需要安装 gulp-swig 插件
// cosnt swig = require('gulp-swig')
// 需要安装 gulp-imagemin 插件
// cosnt imagemin = require('gulp-imagemin')
// 下面代码中用到插件的地方,前面都加上plugins即可,例如:
// 样式编译任务
const style = () => {
return src('src/assets/styles/*.scss', { base: 'src' })
.pipe(plugins.sass({ outputStyle: 'expanded' }))
.pipe(dest('dist'))
}
7、开发服务器
browser-sync 搭建开发服务器
// 需要安装 browser-sync 插件,支持热更新
const browserSync = require('browser-sync')
// 接收 browserSync创建的开发服务器
const bs = browserSync.create()
// 服务任务
const serve = () => {
bs.init({
notify: false, // browserSync 连接提示
port: 2080, // 端口
// open: false, // 自动打开页面
files: 'dist/**', // 监听哪些文件改变后需要更新浏览器
server: {
baseDir: 'dist',
routes: { // 优先于 baseDir
'/node_modules': 'node_modules'
}
}
})
}
// 导出任务成员
module.exports = {
compile,
build,
serve
}
8、监视变化及构建优化
watch 监听文件变化
// watch 监视文件变化,决定是否重新执行任务
const { src, dest, parallel, series, watch } = require('gulp')
// 服务任务
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, // browserSync 连接提示
port: 2080, // 端口
// open: false, // 自动打开页面
// files: 'dist/**', // 监听哪些文件改变后需要更新浏览器
server: {
baseDir: ['dist', 'src', 'public'],
routes: { // 优先于 baseDir
'/node_modules': 'node_modules'
}
}
})
}
// watch 中的files 监听可以转到每个任务中使用 bs.reload
// 样式编译任务
const style = () => {
return src('src/assets/styles/*.scss', { base: 'src' })
.pipe(plugins.sass({ outputStyle: 'expanded' }))
.pipe(dest('dist'))
.pipe(bs.reload({ stream: true }))
}
// 脚本编译任务
const script = () => {
return src('src/assets/scripts/*.js', { base: 'src' })
.pipe(plugins.babel({ presets: ['@babel/preset-env'] }))
.pipe(dest('dist'))
.pipe(bs.reload({ stream: true }))
}
// 页面模板编译任务
const page = () => {
return src('src/*.html', { base: 'src' })
.pipe(plugins.swig({ data, defaults: {cache: false} }))
.pipe(dest('dist'))
.pipe(bs.reload({ stream: true }))
}
// 开发环境下图片字体文件不需要压缩,提高执行效率
const compile = parallel(style, script, page)
// 上线之前需要执行的任务
const build = series(clean, parallel(compile, image, font, extra))
// 开发环境需要执行的任务
const dev = series(compile, serve)
// 导出任务成员
module.exports = {
compile,
clean,
build,
dev
}
9、useref 文件引用处理
gulp-useref 文件引用处理
// useref 插件可以自动处理 html 中的构建注释
// 把引入的第三方的样式文件都引入到一个css文件中
// 把引入的第三方的脚本文件都引入到一个js文件中
// 引入useref 插件
const useref = () => {
return src('dist/*.html', { base: 'dist' })
.pipe(plugins.useref({ searchPath: ['dist', '.'] }))
.pipe(dest('dist'))
}
// 导出任务成员
module.exports = {
compile,
clean,
build,
dev,
useref
}
10、文件压缩
gulp-htmlmin, gulp-uglify, gulp-clean-css, gulp-if 进行文件压缩
// 需下载 gulp-htmlmin gulp-uglify gulp-clean-css gulp-if
// 引入useref 插件
const useref = () => {
return src('dist/*.html', { base: 'dist' })
.pipe(plugins.useref({ searchPath: ['dist', '.'] }))
// 对 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('release'))
}
11、重新规划构建过程
首先将过渡的文件从放在dist改为放到temp,将真正上线的文件放到dist文件夹下
完整源码:
// 需要安装 del 插件,它是一个promise方法
const del = require('del')
// 需要安装 browser-sync 插件,支持热更新
const browserSync = require('browser-sync')
// 读取 src, 写入 dest, 组合任务[并行] parallel, 组合任务[串行] series
// watch 监视文件变化,决定是否重新执行任务
const { src, dest, parallel, series, watch } = require('gulp')
// 需要安装 gulp-load-plugins 插件
const loadPlugins = require('gulp-load-plugins')
// 使用 loadPlugins 自动安装插件
const plugins = loadPlugins()
// 接收创建的开发服务器
const bs = browserSync.create()
// 模拟模板页面中需要的动态数据
const data = {
menus: [
{
name: 'Home',
icon: 'aperture',
link: 'index.html'
},
{
name: 'About',
link: 'about.html'
},
{
name: 'Contact',
link: '#',
children: [
{
name: 'CSDN',
link: 'https://blog.csdn.net/gongye2019'
},
{
name: 'zhihu',
link: 'https://www.zhihu.com/people/gong-ye-18-46'
},
{
name: 'Gitee',
link: 'https://gitee.com/gongyexj'
},
{
name: 'Github',
link: 'https://github.com/gyxj'
}
]
}
],
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' })
// sass 在工作的时候,认为 _ 开头的样式文件是主文件依赖的文件
// 所以 sass 直接忽略掉这些 _ 开头的样式文件
// { outputStyle: 'expanded' } => 完全展开的格式生成
.pipe(plugins.sass({ outputStyle: 'expanded' }))
.pipe(dest('temp'))
.pipe(bs.reload({ stream: true }))
}
// 脚本编译任务
const script = () => {
return src('src/assets/scripts/*.js', { base: 'src' })
// babel 只是提供一个环境, presets 是 babel 插件的集合
// 不配置 { presets: ['@babel/preset-env'] } ,转换就不会生效
.pipe(plugins.babel({ presets: ['@babel/preset-env'] }))
.pipe(dest('temp'))
.pipe(bs.reload({ stream: true }))
}
// 页面模板编译任务
const page = () => {
return src('src/*.html', { base: 'src' })
// // 防止模板缓存导致页面不能及时更新
.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 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, // browserSync 连接提示
port: 2080, // 端口
// open: false, // 自动打开页面
// files: 'dist/**', // 监听哪些文件改变后需要更新浏览器
server: {
baseDir: ['temp', 'src', 'public'],
routes: { // 优先于 baseDir
'/node_modules': 'node_modules'
}
}
})
}
// 引入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, image, font)
// 开发环境下图片字体文件不需要压缩,提高执行效率
const compile = parallel(style, script, page)
// 通过 series 先执行 clean 任务,在同时执行其他任务
// 上线之前需要执行的任务
const build = series(
clean,
parallel(
series(compile, useref),
image,
font,
extra
)
)
// 开发环境需要执行的任务
const dev = series(compile, serve)
// 导出任务成员
module.exports = {
clean,
build,
serve,
dev
}
12、补充
将 gulpfile.js 中导出的任务,定义到 package.json 中的 scripts中,方便开发使用
"scripts": {
"clean": "gulp clean",
"build": "gulp build",
"serve": "gulp serve",
"dev": "gulp dev"
},