前端工程化之自动化构建

在前端快速发展的今天,如果不能时刻保持学习就会很快被淘汰。分享一下前端工程化中有关于自动化构建的相关知识,文章有点长,希望对大家有所帮助。每天进步一点点。

一、自动化构建简介

自动化构建就是把我们开发阶段写出来的源代码,自动化的转换成生产环境中可以运行的代码或者程序,这个自动化转换的过程称为自动化构建工作流。

它的作用是尽量脱离运行环境兼容带来的问题,在开发阶段使用一些提高效率的语法,规范和标准,例如,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"
 },
上一篇:Gulp自动化构建案例---图片和字体文件转换


下一篇:Gulp安装使用教程