前端工程化
工程化就是将一些重复繁琐的工作让计算机自动处理,以节省人力,我们今天来看看前端工程化是怎么一个过程。
# 之前存在哪些问题
前端经过多年发展,经过刀耕火种到现在,发展过程中也遇到很多问题,技术都是为了解决问题而催生的。
- 日常想要使用新语法,但是又不得不考虑兼容性问题
- 想要使用less/scss/postcss等css增强,但是运行环境一般都不支持
- 想要使用新的模块化,组件化的方式提高维护性,运行环境一般也不支持
- 压缩代码,上传部署等重复工作
- 编码风格及规范在多人协作时不容易统一
- 开发时,很多接口服务需要依赖后端接口完成后才可以进行
以上问题都可以通过工程化解决
# 工程化如何解决问题
- 创建项目时,我们可以通过脚手架工具初始化项目
- 编码时,我们可以格式化代码,规范风格,编译新特性,兼容性构建以及压缩打包
- 开发/测试时,mock数据,开发服务器,热更新,以及使用sourceMap方便调试
- 提交时,git hooks 检查,lint规范检查;持续集成
- 部署时,自动发布;
# 脚手架工具
脚手架工具主要作用就是创建项目基础结构,提供项目规范和约定。大致包含以下方面
- 相同的组织结构
- 相同的开发范式
- 相同的模块依赖
- 相同的工具配置
- 相同的基础代码
脚手架工具就是为我们省去这些重复过程,直接初始化这样一个起手势。
# 常用脚手架工具
不同框架会提供自己的脚手架工具,但功能都大同小异,均是根据信息创建对应但项目基础结构
- react - create-react-app
- vue - vue-cli
- angular - angular-cli
# 通用工具
Yeoman;一种是通用的工具,他可以根据模版生成项目对应结构;具体的他是根据不同的生成器(自定义生成器)生成任意的项目结构。
所以yeoman 脚手架使用分为以下步骤
- 明确需求
- 找到合适的generator,这里已有大量generator,查找 (opens new window)
- 全局安装对应生成器(generator) - 如: npm i generator-node
- 通过yo命令运行generator -如:yo node
- 命令行填写项目信息
填写完成就可以自动创建完成了;
脚手架工具其实就是接收一些预设,通过用户输入,结合模版生成项目结构。
yo yo node
? Module Name demo-yo
? Description demo-yo
? Project homepage url
? Author's Name jason
? Author's Email demo@mail.com
? Author's Homepage
? Package keywords (comma to split) demo
? Send coverage reports to coveralls No
? Enter Node versions (comma separated)
? GitHub username or organization
? Which license do you want to use? (Use arrow keys)
❯ Apache 2.0
MIT
Mozilla Public License 2.0
BSD 2-Clause (FreeBSD) License
BSD 3-Clause (NewBSD) License
Internet Systems Consortium (ISC) License
GNU AGPL 3.0
(Move up and down to reveal more choices)
我们还可以创建自己的脚手架
# 自动化构建
计算机擅长的事就是根据命令及程序,不知疲倦自动完成指令。这里,我们需要他帮我们自动化把开发代码转换为生成代码。 这让我们在开发阶段尽量不考虑生成环境,可以去使用一些高级的语法,新的规范标准,而不用过多考虑兼容性。
首先是npm script ,这个配置在package.json中的属性,可以配置自定义运行命令,并和项目一起维护。
"scripts": {
"dev": "vuepress dev docs",
"build": "vuepress build docs",
"deploy": "sh ./shell/deploy-test.sh",
"service": ""
}
# Grunt
最早的自动化工具,插件非常完善,官方号称可以完成任何想做的事。 然而Grunt有他的缺陷,他的工作过程是基于临时文件的,每一步工作流会有大量的磁盘读写,这就导致速度上会有劣势;
举个例子,如果我们需要使用他进行scss构建;先编译再添加属性前缀,最后再压缩代码。这三步Grunt每一步都会产生临时文件,磁盘读写 ,scss编译完后,它会将结果保存,然后在读取这个文件进行下一步,步数越多,磁盘读写越多,势必会影响整体构建速度。
// gruntfile.js
// Grunt 的入口文件
// 用于定义一些需要 Grunt 自动执行的任务
// 需要导出一个函数
// 此函数接收一个 grunt 的对象类型的形参
// grunt 对象中提供一些创建任务时会用到的 API
module.exports = grunt => {
grunt.registerTask('print', () => {
console.log('task print')
})
// 异步任务
// 由于函数体中需要使用 this,所以这里不能使用箭头函数
grunt.registerTask('async-task', function () {
const done = this.async()
setTimeout(() => {
console.log('async task working~')
done()
// done(false) 异步任务失败在这里返回
}, 1000)
})
// 任务失败,会导致串联的任务直接返回
grunt.registerTask('fail', () => {
console.log('task fail')
return false // 返回false即为失败
})
grunt.registerTask('default','描述:这个任务默认导出,直接运行 npm grunt,数组中依次执行',['print','fail'])
}
// 命令行运行 npm grunt print
> npm grunt print
Running "print" task
task print
Done.
# Gulp
相比Grunt ,Gulp解决了这个速度问题,他是基于内存实现每一步的产出的,这大大提高了运行速度。而且,gulp可以同时执行多个任务,这也大大提高了执行效率。 另外使用方式他也会简单许多,插件也很丰富,所以后来居上,Gulp比较流行。
下面展示了gulp常规工作流程,即读取流,转换流,输出流的三步。
// 读取js文件,替换掉空格,输出文件
const fs = require('fs')
const {Transform} = require('stream')
exports.zipJs = () => {
const read = fs.createReadStream('gulpfile.js')
const write = fs.createWriteStream('gulpfile.txt')
const transform = new Transform({
transform(chunk, encoding, cb) {
const input = chunk.toString()
const output = input.replace(/\s+/g, '')
cb(null, output) // 第一个参数为错误,没有则传null
}
})
read.pipe(transform).pipe(write)
return read
}
此外,gulp封装了更易于使用的流api
const {src,dest} = require('gulp')
const clean =require('gulp-clean-css')
exports.test = ()=>{
return src('style.css')
.pipe(clean()) // 压缩css
.pipe(dest('dist')) // 移动文件到此目录
}
同样的,gulp也有大量的插件 (opens new window) 下面是一个综合例子
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()
}
const clean = () => {
return del(['dist', 'temp'])
}
// 处理scss文件
const style = () => {
return src('src/assets/styles/*.scss', { base: 'src' })
.pipe(plugins.sass({ outputStyle: 'expanded' }))
.pipe(dest('temp'))
.pipe(bs.reload({ stream: true }))
}
// 转译js
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 }))
}
// html文件处理
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,
port: 2080,
// open: false,
// files: 'dist/**',
server: {
baseDir: ['temp', 'src', 'public'],
routes: {
'/node_modules': 'node_modules' // 引入文件映射
}
}
})
}
const useref = () => {
return src('temp/*.html', { base: 'temp' })
// 解析 HTML 文件中的构建块以替换对未优化脚本或样式表的引用。
.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
}
# 模块化打包
这方面就是webpack之类的打包工具的主场了,可以看之前文章
# 项目规范化
规范化也是前端工程化的一大部分; 为什么需要规范化,软件开发是多人协同的,不同的人有不一样的习惯,不规范会导致维护成本上升。
# 哪些需要规范
- 代码 - js,html,css
- 文档
- 提交日志 - git提交日志按类型区分
# 使用的工具
各种lint工具可以很好的很好的规范化我们遇到的问题,也可以很方便的自动化处理。
- ESLint - 最主流的JS/TS Lint工具
- StyleLint - css/less/sass/PostCss的Lint工具
- Prettier - 代码格式化工具
- Git Hooks - 提交时强制检查
// Husky 实现 git hooks
// package.json 文件中增加
{
"husky":{
"hooks":{
"pre-commit":"npm run lint" // 提交前运行此命令
}
}
}
这个时候 commit 就会先自动执行 npm run lint 了,然后才会 commit。
但是这样解决了以上的问题,当项目大的时候会遇到一些问题,比如每次 lint 是整个项目的文件,文件太多导致跑的时间过久,另外如果这个 lint 是在项目后期接入的话,可能 lint 命令会报很多错误,全量去改可能会有问题。
lint-staged
基于上面的痛点,lint-staged 就出现了,它的解决方案就是只检查本次提交所修改(指 git 暂存区里的东西)的问题,这样每次 lint 量就比较小,而且是符合我们的需求的。
// package.json 文件中增加
{
"lint-staged":{
"**/*.less": "stylelint --syntax less",
"**/*.{js,jsx,ts,tsx}": "eslint --ext .js,.jsx,.ts,.tsx",
"**/*.{js,jsx,tsx,ts,less,md,json}": [
"prettier --write",
"git add"
]
}
}
这些工具对于检查代码质量,规范风格,修复部分错误都不在话下。使用方式也大同小异,可以通过配置文件管理规则,再集成到构建工具中自动化就可以了。
# 部署自动化
一般主流开源使用的是jenkins,它可以根据git钩子触发自动构建发布;