开发脚手架及封装自动化构建工作流
本人前端小白一枚,只是整理一下自己的学习过程,第一次在掘金发表自己的文章,有问题欢迎大家及时指出
前端工程化的定义
前端工程化是指遵循一定的标准和规范通过工具提高效率的一种手段
解决的问题:
传统语言或语法的弊端
无法使用模块化/组件化
重复的机械式工作
代码风格统一、质量保证
依赖后端服务接口支持
整体依赖后端项目
工程化
一切以提高效率、降低成本、质量保证为目的的手段都属于工程化
工程化≠工具
工程化主要包含以下内容:
·脚手架工具开发
·自动化构建系统
·模块化打包
·项目代码规范化
·自动化部署
脚手架
解决创建前端项目时的一些复杂工作
相同的组织结构
相同的开发范式
相同的模块依赖
相同的工具配置
相同的基础代码
脚手架作用
常用的脚手架工具
常用的脚手架工具都是以框架为基础的,比如vue项目的vue-cli
Yeoman:通用脚手架工具
Yeoman的使用方法
plop: 在开发过程中创建一些特定类型的文件
plop的使用方法
通用脚手架工具剖析
脚手架的工作流程:
1.通过命令行交互询问用户问题
2.根据用户反馈的结果生成文件
3.根据用户回答的结果生成文件
4.将模板下的文件全部输出到目标目录
5.通过模板引擎渲染文件
6.将结果写入目标文件路径
自动构建工具
Grunt
最早的自动化构建工具,目前使用较少
单目标任务
grunt的入口配置文件为gruntfile.js,任务通过创建registerTask来执行,默认任务名为default,可捆绑执行,若有任务失败,return false,会阻碍之后的任务执行,可以通过yarn grunt --force执行 异步任务通过done(false)强制执行
//Grunt的入口文件
//用于定义一些需要Grunt自动执行的任务
//需要导出一个函数,接收一个grunt的参数
module.exports = grunt => {
// 创建任务,任务执行方式通过npm run/yarn grunt foo 执行
grunt.registerTask('foo',() => {
console.log("hello grunt")
// 标记任务失败,通过return false来实现,这种失败会阻碍后续组合任务的执行
// 可以通过 --force忽略失败,不影响后续组合任务执行
return false
})
// 默认任务
grunt.registerTask('default',"任务描述",() => {
console.log("hello default grunt")
})
// 组合任务
grunt.registerTask("default",["foo","bar"])
// 异步任务
grunt.registerTask("async-task",function(){
const done = this.async()
setTimeout(()=>{
console.log("async...")
// 标记任务失败,通过调用done方法,传入false来实现
done(false)
},1000)
})
}
多目标任务
多目标任务需要使用registerMultiTask进行任务的注册,并且通过grunt.initConfig对多目标任务进行配置
module.exports = grunt => {
grunt.initConfig({
build: {
//options作为任务的配置选项
options:{
foo:"bar"
},
css: {
options:{
foo:"baz" //会覆盖任务中的options
}
},
js: "2"
}
})
grunt.registerMultiTask('build', function () {
console.log(this.options())
console.log(`target:${this.target},data:${this.data}`)
})
}
上述是通过grunt自定义任务执行,在grunt官网中,官方为我们提供了一些常用的grunt插件 www.gruntjs.net/plugins
此处用grunt-sass来举例(grunt-sass是依赖sass进行工作的,所以在安装依赖时,需要同时安装grunt-sass和sass)
//引入插件
const sass = require("sass")
//使用
module.exports = grunt => {
grunt.initConfig({
//sass转化为css功能
sass: {
options: {
sourceMap: true,
implementation: sass
},
main: {
files: {
'dist/css/main.css': 'src/scss/main.scss'
}
}
}
})
}
// grunt 的官方API中的方法,通过loadNpmTasks的方式载入grunt封装的插件
// 通过该方法,不需要通过registerTask的方式去新建任务直接可以执行
grunt.loadNpmTasks('grunt-sass')
// 执行
yarn grunt sass
当然,这种loadNpmTasks的方式需要对每一个插件进行单独引用,我们可以通过grunt的插件对所有的插件进行引用,再对需要使用的插件进行单独的initConfig配置
//安裝
yarn add load-grunt-tasks --dev
//使用
const loadGruntTasks = require("load-grunt-tasks")
//自动加载所有的grunt插件
loadGruntTasks(grunt)
gulp
基于流的构建方式
gulp与grunt的区别
易于使用:采用代码优于配置策略,Gulp让简单的事情继续简单,复杂的任务变得可管理。
高效:通过利用Node.js强大的流,不需要往磁盘写中间文件,可以更快地完成构建。
高质量:Gulp严格的插件指导方针,确保插件简单并且按你期望的方式工作。
易于学习:通过把API降到最少,你能在很短的时间内学会Gulp。构建工作就像你设想的一样:是一系列流管道。
基本任务
// 箭头函数中的传参是回调函数,用来区分同步和异步任务
// 传参说明异步任务,需要手动回调,同样可以在done中传new Error('task_error')标记错误
exports.foo = done => {
console.log("foo task")
done() //标识任务完成
done(new Error('task_error')) // 标识任务失败
}
// 同样可以通过task创建任务,不推荐使用
const gulp = require('gulp')
gulp.task('bar', done => {
console.log("bar task")
done()
})
组合任务:串行任务 series;并行任务 parallel
// 并行任务和串行任务的区别在于并行任务是任务和任务之间不影响,可以同时进行构建的一种提高构建效率的方式
// 串行任务是需要前一个任务执行完再去执行后面的任务,依次执行
const { series, parallel, task } = require("gulp")
exports.task1 = series(foo, bar) //串行
exports.task2 = parallel(foo, bar) //并行
//测试
yarn gulp task1
yarn gulp task2
这些是gulp的一些基本概念,这里简单总结一下
转换流:Transform
通过stream,将每一个人物通过.pipe方法依次执行
文件操作,通过gulp中的src获取需要操作的文件路径,随后通过dest将流返回到对应路径中
下面是自己创建的demo,配合注释可以查看学习
Demo
// gulp基本的常用API
// src:需要被构建的文件路径
// dest:构建成功后的文件路径
// parallel:并行任务
// series:串行任务
// watch:监听任务
const { src, dest, parallel, series, watch } = require('gulp')
// 清除目标文件
const del = require('del')
// 生成浏览器服务器插件
const browserSync = require('browser-sync')
// gulp插件加载库,类似于load-grunt-tasks
const loadPlugins = require('gulp-load-plugins')
const plugins = loadPlugins()
// 创建浏览器服务器
const bs = browserSync.create()
// 目标文件根路径
const cwd = process.cwd()
let 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/**'
}
}
}
// 上面通过引入gulp-load-plugins这个插件,在这里就不需要再次引入了
// 这里的sass == plugins.sass,所以下面执行任务时替换成plugins.sass,以此类推
// const sass = require('gulp-sass')
// const babel = require('gulp-babel')
// const swig = require('gulp-swig')
// const imagemin = require('gulp-imagemin')
// 定义的变量
const data = {
menus: [],
pkg: require('./package.json'),
data: new Date()
}
// clean任务,清除目标路径下的文件
const clean = () => {
return del([config.build.dist, config.build.temp])
}
// sass转换成css
const style = () => {
return src(config.build.paths.styles, { base: config.build.src, cwd: config.build.src })
// outputStyle这个属性是默认构建后css格式为完全展开的格式,不会将结尾大括号放在最后一个属性末尾
.pipe(plugins.sass({ outputStyle: 'expanded' }))
.pipe(dest(config.build.temp))
.pipe(bs.reload({ stream: true }))
}
// js编译
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 }))
}
// html编译
const page = () => {
return src(config.build.paths.pages, { base: config.build.src, cwd: config.build.src })
.pipe(plugins.swig({ data }))
.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 = () => {
// 监听对应的文件修改,监听过后执行后面的task
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)
// bs.reload是监听对应文件修改之后,自动刷新浏览器
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: 8000, // 设置端口,默认3000
// open: false, // 是否自动打开浏览器
// files: 'dist/**',
server: {
// 作为数组,在构建过程中,image font page只是作为压缩体积的作用
// 对于构建测试阶段没有作用,只会降低构建效率
// 所以在这里设置为数组,当dist文件夹中没有对应文件,则向下src寻找,提高构建效率
// 当上线时再进行build
baseDir: [config.build.temp, config.build.src, config.build.public],
routes: {
'/node_modules' : 'node_modules'
}
}
})
}
// 文件压缩
const useref = () => {
return src('temp/*.html', { base: 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('dist'))
}
// 组合任务
const compile = parallel(style, script, page)
const build = series(clean, parallel(series(compile, useref), image, font, extra))
const develop = series(build, serve)
// 导出需要执行的任务
module.exports = {
clean,
develop,
compile,
build
}
以上就是我对于自动化构建的基本理解,若有错误的地方希望大家提出,共同学习共同进步