没有前端工程化遇到的问题
- 使用ES6+新特性,但是有兼容问题
- 使用Less/Sass/PostCss增强CSS的变成性,运行环境不能直接支持
- 使用或快画的方式提高项目的可维护性,运行环境不能直接支持
- 部署上线前需要手动压缩代码及资源文件
- 部署过程中需要手动上传代码到服务器
- 多人协作无法硬性统一大家的代码风格,从仓库中pull回来的代码质量无法保证
- 开发是需要等待后端服务接口提前完成
- …
工程化表现
一切以提高效率、降低成本、质量保证为目的的手段都属于「工程化」
graph LR
创建项目-->编码
编码-->预览/测试
预览/测试-->提交
提交-->部署
部署-->编码
工程化不等于某个工具
工程化的核心是对项目整体的规划和架构,工具只是落地规划和架构的一种手段
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hYlfGtv4-1591460280651)(http://oss.ahh5.com/ahh5/md/202020200529221827.png)]
工程化包含
- 脚手架工具开发
- 自动化构建系统
- 模块化打包
- 项目代码规范
- 自动化部署
脚手架工具概要
脚手架的本质就是自动的创建项目基础结构、提供项目规范和约定,脚手架工具可以快速的搭建特定项目的骨架。
约定
- 相同的组织结构
- 相同的开发范式
- 相同的模块化依赖
- 相同的攻击配置
- 相同的基础代码
常用的脚手架工具
- create-react-app
- vue-cli
- angular-cli
相同点
根据提供信息自动创建对应的项目基础结构,但一般适用于自身所服务框架的项目
Yeoman(老牌、强大、通用)
不针对于某一框架相对常用脚手架较为灵活。Yeoman可以搭配不同的generator创建不同的项目
缺点:过于通用不够专注
基础使用
安装
yarn global add yo
安装对应的generator ==> generator-node
yarn global add generator-node
通过yo运行generator
yo node
# 输入项目名字
# 输入项目名字
# 输入项目主页
# 输入作者
# 输入邮箱
# 输入主页
# 输入关键词
Yeoman sub Generator
可以通过生成器的子集生成一些文件。例如eslint README
生成一个node cli
# 创建cli
yo node:cli
# 安装新增加的依赖
yarn
# 链接到全局
yarn link "yo-test-learn"
# 或者
npm link yo-test-learn
# 输入下面命令即可看到
yo-test-learn --help
Yeoman 使用步骤
- 明确需求
- 找到合适的 Generator
- 全局范围安装找到的 Generator
- 通过运行 Yo 运行对应的 Generator
- 通过交互命令交互填写选项
- 生成你所需要的项目结构
自定义Generator
基于Yeoman 搭建自己的脚手架
Generator 基本结构
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-msZgVvsb-1591460280657)(http://oss.ahh5.com/ahh5/md/202020200530230529.png)]
提供多个 sub generator 需要在app同级目录下创建一个新的目录
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tjkNyYoP-1591460280660)(http://oss.ahh5.com/ahh5/md/202020200530230749.png)]
# 创建一个文件夹
mkidr generator-hello
# 进入目录
cd generator-hello
# 创建package.json
yarn init
# 安装 yeoman-generator 模块
yarn add yeoman-generator
创建 /generators/app/index.js
- 此文件为generator的核心入口
- 需要导出一个继承自 Yeoman Generator 的类型
- Yeoman Generator 工作时会自动调用我们在此类型中定义的一些生命周期方法
- 在这个文件中调用父类中的一些方法实现一些功能比如文件写入
const Generator = require('yeoman-generator')
module.exports = class extends Generator {
// yeoman 自动在生成文件中调用此方法
writing() {
// 通过文件读写方式向目标目录写入文件
this.log('hello')
// this.destinationPath 目前目录路径
this.fs.write(
this.destinationPath('temp.txt'),
Math.random().toString()
)
}
}
通过 npm link 将模块安装到全局
找一个空目录运行 yo hello
.
└── temp.txt
根据模板创建文件
在app目录下创建一个templates目录
这是一个模板文件,内部使用EJS模板标记输入数据
app/templates/foo.txt
<%= title %>
<% if(success){ %>
成功才会看到我
<% } %>
app/index.js
const Generator = require('yeoman-generator')
module.exports = class extends Generator{
// yeoman 自动在生成文件中调用此方法
writing(){
// 模板文件路径
const tmpl = this.templatePath('foo.txt')
// 输出目标路径
const output = this.destinationPath('foo.txt')
// 模板数据上下文
const context = {
title:"hello word",
success : true
}
// 输出模板文件
this.fs.copyTpl(tmpl,output,context)
}
}
完成目录结构
├── generators
│ └── app
│ ├── index.js
│ └── templates
│ └── foo.txt
├── package-lock.json
├── package.json
└── yarn.lock
找一个空目录运行 yo hello 即可看到输出的 foo.txt
内容如下
hello word
成功才会看到我
接收用户输入数据
app/index.js 暴露出的类添加如下方法
prompting() {
// 在此方法可以调用父类的 prompt() 方法对用户命令行询问
return this.prompt([{
type: 'input',
name: 'name',
message: 'your project name',
default: this.appname //当前生成目录文件夹的名字
}])
.then(answers => {
// answers => { name : 'user input value' }
this.answers = answers
})
}
修改 writing
writing() {
// 模板文件路径
const tmpl = this.templatePath('foo.txt')
// 输出目标路径
const output = this.destinationPath('foo.txt')
// 模板数据上下文
const context = {
// 此处title 接收用户输入的值
title: this.answers.name,
success: true
}
// 输出模板文件
this.fs.copyTpl(tmpl, output, context)
}
执行 yo hello
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xWzvFQFE-1591460280662)(http://oss.ahh5.com/ahh5/md/202020200531112617.png)]
输出文件内容如下
My project
成功才会看到我
自己实现一个 Vue Generator
基础vue 构建我选择的还是cli,基于上面在做一些自己的配置就好了
# 创建文件夹
mkdir generator-zzy-vue
# 进入文件夹
cd generator-zzy-vue
# 初始化项目
yarn init
# 添加yeoman依赖
yarn add yeoman-generator
手动创建一些文件
基本结构如下
├── generators
│ └── app
│ ├── index.js
│ └── templates
├── package.json
└── yarn.lock
创建一个vue模板
vue create tmp-project
rm -rf node_modules
rm -rf .git
rm yarn.lock
目录结构如下
├── .browserslistrc
├── .editorconfig
├── .eslintrc.js
├── .gitignore
├── README.md
├── babel.config.js
├── package.json
├── public
│ ├── favicon.ico
│ └── index.html
└── src
├── App.vue
├── assets
│ └── logo.png
├── components
│ └── HelloWorld.vue
├── main.js
├── router
│ └── index.js
├── store
│ └── index.js
└── views
├── About.vue
└── Home.vue
修改README.md
# <%= name %>
修改 /public/index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="favicon.ico">
<title><%= name %></title>
</head>
<body>
<noscript>
<strong>We're sorry but <%= name %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>
复制到generators/app/templates中
app/index.js
const Generator = require('yeoman-generator')
module.exports = class extends Generator {
// yeoman 在询问用户环节会自动调用此方法
prompting() {
return this.prompt([{
type: "input",
name: 'name',
message: "Your project name",
default: this.appname
}])
.then(answers => {
this.answers = answers
})
}
// yeoman 自动在生成文件中调用此方法
writing() {
const templates = [".browserslistrc", ".editorconfig", ".eslintrc.js", ".gitignore", "README.md", "babel.config.js", "package.json", "public/favicon.ico", "public/index.html", "src/App.vue", "src/assets/logo.png", "src/components/HelloWorld.vue", "src/main.js", "src/router/index.js", "src/store/index.js", "src/views/About.vue", "src/views/Home.vue"]
templates.forEach(filePath => {
this.fs.copyTpl(
this.templatePath(filePath),
this.destinationPath(filePath),
this.answers
)
})
}
}
关于templates文件名获取, 可以通过node脚本实现
const fs = require('fs');
const path = require('path');
const _filePath = path.resolve('./templates');
const outFilePath = path.resolve('./arr');
let filePathArr = []
getFileRecursively(_filePath)
function getFileRecursively(filePath) {
const files = fs.readdirSync(filePath)
files.forEach(filename => {
const filedir = path.join(filePath, filename);
const stats = fs.statSync(filedir)
if (stats.isFile()) {
filedir.includes('.DS_Store') || filePathArr.push(filedir.replace(_filePath + '/', ''))
}
if (stats.isDirectory()) {
getFileRecursively(filedir);
}
})
}
fs.writeFileSync(outFilePath, JSON.stringify(filePathArr))
注意不要将此方法放入 app/index.js 而是通过该脚本获取并输出到一个新的文件中。后面在手动复制到app/index.js中。因为不能确定 yeoman 的执行环境,以及不需要用户每次执行都去重新递归遍历所有文件
发布一个自己的npm包
- 建立一个公开仓库推荐github
- 创建一个npm 用户
- npm login
- npm publish
plop
一个小而美的脚手架工具,一般不会独立去使用,而是集成到项目之中区创建同类型的文件使用
plop基本使用
- 将plop模块作为项目开发依赖安装
- 在项目根目录中创建一个plopfile.js文件
- 在plopfile.js文件中定义脚手架任务
- 编写用于生成特定类型文件的模板
- 通过Plop提供的Cli运行脚手架任务
自动化构建
自动化通过机器代替手工完成某些操作,
自动化构建就是将源代码自动化构建为生成代码或者程序。
常见的自动化构建工具
-
Grunt
最早的自动化构建工具,但因为通过临时文件工作,所以工作效率较慢。
-
Gulp
使用频率较高,通过操作内存实现自动化构建,相对于Grunt速度快了很多且默认支持多任务
-
Fls
百度开源产品,继承项目常用自动化构建流程,例如资源加载、模块化开发、代码部署、性能优化,缺点不够灵活。
Grunt
基本使用
# 初始化项目
yarn init --yes
# 添加grunt模块
yarn add grunt --dev
根目录下创建gruntfile.js文件
- Grunt入口文件
- 用于定义一些需要 Grunt 自动执行的任务
- 需要导出一个函数
- 函数需要接收一个grunt的形参,内部提供了一些创建任务是可以用到的 API
module.exports = grunt => {
// 注册的任务 第一个参数为任务名称 第二个参数为任务描述(可以省略) 第三个是执行代码
grunt.registerTask('foo', '任务描述', () => {
console.log('hello word')
})
// 如果任务名称为default 那么yarn grunt 会默认执行
grunt.registerTask('default', () => {
console.log('grunt default task')
})
}
# 运行grunt 不加参数执行默认任务
yarn grunt
# 指定任务执行
yarn grunt foo
# 查看任务描述
yarn grunt --help
default实际工作中是执行多个task的默认任务例如
module.exports = grunt => {
grunt.registerTask('foo', 'foo任务描述', () => {
console.log('hello foo')
})
grunt.registerTask('bar', 'bar任务描述', () => {
console.log('hello bar')
})
grunt.registerTask('default', ['foo', 'bar'])
}
此时执行 yarn grunt 会执行 foo bar 两个任务
异步任务
module.exports = grunt => {
grunt.registerTask('async-task', 'async-task任务描述', function() {
// 需要定义一个done 值为this.async() 标记此任务为异步任务
const done = this.async()
setTimeout(() => {
console.log('async-task workding')
// 异步任务执行结束后需要通过 done() 触发程序截止
done()
}, 1000)
})
}
标记任务失败
在函数内部 return false
module.exports = grunt => {
grunt.registerTask('bad', 'bad任务描述', () => {
console.log('This is a bad task')
return false
})
}
如果在任务列表中,则后续任务不会执行
module.exports = grunt => {
grunt.registerTask('bad', 'bad任务描述', () => {
console.log('This is a bad task')
return false
})
grunt.registerTask('test', 'test任务描述', () => {
console.log('This is a test task')
return false
})
grunt.registerTask('default', ['bad', 'test'])
}
// 此时执行 yarn grunt 则会抛出 warnings 警告且 test不会执行
如果采用强制执行方式 --force 则所有的任务都会执行
yarn grunt --force
异步任务失败标记
module.exports = grunt => {
grunt.registerTask('async-task', 'async-task任务描述', function() {
const done = this.async()
setTimeout(() => {
console.log('async-task workding')
// done 传入一个实参 false
done(false)
}, 1000)
})
}
配置选项方法
基本用法
module.exports = grunt => {
// 初始化config
grunt.initConfig({
foo: 'test'
})
grunt.registerTask('foo', 'foo任务描述', () => {
// 获取config foo的键值
const config_foo = grunt.config('foo')
console.log( `hello ${config_foo}` )
// 输出 hello test
})
}
多目标模式
通过配置多个目标, 执行多个目标
module.exports = grunt => {
grunt.initConfig({
// 定义build
build: {
// 参数配置不会作为目标执行
options: {
test: false
},
// 定义两个目标
css: '1',
js: '2'
}
})
// 通过 registerMultiTask 方法定义
grunt.registerMultiTask('build', function() {
// 拿出执行目标值与值
const {
target,
data,
} = this
// 获取所有配置
const { test } = this.options()
console.log( `build ${target} ${data} ` )
console.log(` test ${test} `)
})
}
插件使用
安装一个文件清除的插件
yarn add grunt-contrib-clean -D
module.exports = grunt => {
grunt.initConfig({
// 定义clean
clean: {
// 定义一个目标 清除temp 下的所有文件
temp:'temp/**'
}
})
// 清除指定文件任务 需要多个目标
grunt.loadNpmTasks('grunt-contrib-clean')
}
常用插件
编译sass
# 安装 grunt-sass 及 sass 模块
yarn add grunt-sass sass -D
// 引入sass 模块
const sass = require('sass')
module.exports = grunt => {
grunt.initConfig({
sass: {
// 定义配置文件
options:{
// 定义处理方法
implementation:sass,
// 开启sourceMap文件
sourceMap:true
},
// 定义个目标
main: {
// 定义文件
files:{
// 输出路径 输入路径
'dist/css/main.css':'src/scss/main.scss'
}
}
}
})
grunt.loadNpmTasks('grunt-sass')
}
npm grunt插件自动导出
# 安装插件
yarn add load-grunt-tasks -D
// 引入loadGruntTasks
const loadGruntTasks = require('load-grunt-tasks')
module.exports = grunt => {
// 将grunt传入loadGruntTasks 实现挂载所有的grunt模块
loadGruntTasks(grunt)
}
编译es6语法
yarn add grunt-babel @babel/core @babel/preset-env -D
const loadGruntTasks = require('load-grunt-tasks')
module.exports = grunt => {
grunt.initConfig({
babel: {
// 定义配置文件
options: {
presets:['@babel/preset-env'],
// 开启sourceMap文件
sourceMap: true
},
// 定义个目标
main: {
// 定义文件
files: {
// 输出路径 输入路径
'dist/js/main.js': 'src/js/main.js'
}
}
}
})
loadGruntTasks(grunt)
}
执行 yarn babel
修改自动更新
yarn add grunt-contrib-watch -D
const loadGruntTasks = require('load-grunt-tasks')
const sass = require('sass')
module.exports = grunt => {
grunt.initConfig({
sass: {
// 定义配置文件
options: {
// 定义处理方法
implementation: sass,
// 开启sourceMap文件
sourceMap: true
},
// 定义个目标
main: {
// 定义文件
files: {
// 输出路径 输入路径
'dist/css/main.css': 'src/scss/main.scss'
}
}
},
babel: {
// 定义配置文件
options: {
presets: ['@babel/preset-env'],
// 开启sourceMap文件
sourceMap: true
},
// 定义个目标
main: {
// 定义文件
files: {
// 输出路径 输入路径
'dist/js/main.js': 'src/js/main.js'
}
}
},
watch: {
js: {
files: ['src/js/*.js'],
tasks: ['babel']
},
css: {
files: ['src/scss/*.scss'],
tasks: ['sass']
}
},
})
loadGruntTasks(grunt)
grunt.registerTask('default', ['sass', 'babel','watch'])
}