目录
前端工程化,指遵循一定的标准和规范,利用工具提高效率降低成本的一种手段,是对项目整体的规划和架构。
要解决的问题:
- 传统语法的弊端,兼容问题
- 无法使用组件化、模块化
- 重复性的工作(打包,上传等)
- 代码风格统一
- 依赖后端服务和接口
通过哪些方式:
- 脚手架
- 自动化构建
- 模块化打包
- 项目代码规范化
- 自动化部署
脚手架工具开发
脚手架作用:创建项目基础结构,提供项目规范和约定。
常用的脚手架工具
- create-react-app
- vue-cli
- angular-cli
- Yeoman 通用型
- Plop 创建特定类型(如组件/模块)的文件
Yeoman 使用步骤
- 安装Yeoman
yarn global add yo
- 安装对应的generator(如generator-node)
yarn global add generator-node
- 创建项目文件夹, 如my-module
- 在项目文件夹下用Yeoman运行generator
cd project-dir
yo node
- sub generator,用于补充生成项目中的文件,如果在已经存在的项目基础上要创建某些特定类型的文件,可以使用sub generator,如node下的cli:
yo node:cli
yarn link
my-module
自定义generator
demo:带有一定基础代码的Vue脚手架
generator基本结构:
|-generators/
|—app/--------------------默认生成器目录
|-----templates/
|-------template------------模版文件
|-----index.js
|—sub/--------------------sub generator目录,可以并列多个
|-----index.js
|-package.json
步骤:
- 创建生成器模块目录(generator-)
yarn init
创建package.jsonyarn add yeoman-generator
安装yeoman- generator- 按照项目基本结构创建文件
yarn link
- 创建项目目录
- 通过自定义generator创建项目
yo <name>
generators/app/index.js
generator核心入口,需要导出一个继承自Yeoman Generator的类
// generator工作时会自动调用此类型中定义的一些生命周期方法,
// 在这些生命周期方法中可以调用父类的一些方法实现一些功能
const Generator = require('yeoman-generator')
module.exports = class extends Generator {
// 用户命令行交互
prompting(){
// 调用父类的 prompt() 方法,该方法返回一个premise对象
return this.prompt([
{
type: 'input',
name: 'name',
message: 'Your project name',
default: this.appname //appname为项目名称
},
{
type: 'input',
name: 'success',
message: '',
default: false
}
])
.then(answers => {
this.answers = answers
//{name: user input value}
})
}
// 写入文件
writing(){
this.fs.write(
// 两个参数:1.文件绝对路径 2.文件内容
this.destinationPath('test.txt'),
'hello generator'
)
// 根据模版创建文件
const tmpl = this.templatePath('template.txt')
const output = this.destinationPath('templateFile.txt')
const content = {name: 'pxhh', success:true}
// 三个参数:1.模版文件路径 2.输出文件路径 3.文件内容
this.fs.copyTpl(tmpl, output, content)
const output2 = this.destinationPath('questions.txt')
const content2 = this.answers
this.fs.copyTpl(tmpl, output2, content2)
}
}
根据模版生成文件
- 模版文件放在 app/templates/ 目录下
- 模版文件中可以使用EJS模版标签 <%= name %>
- 通过
this.templatePath('')
获取模版文件路径 - 通过
this.destinationPath('')
指定输出文件路径 this.fs.copyTpl(tpl, output, content)
接收用户输入数据
在prompting()生命周期方法中调用父类的prompt([])方法,type类型:
- input 输入框
- confirm 确认提示框
- 。。。
发布generator
- 将generator模块托管到git上
- 发布模块
npm/yarn publish
修改镜像源yarn publish --registry=https://registry.yarnpkg.com
Plop使用步骤
在已有项目中创建特定类型的文件,一般不会用它去生成项目。
- 安装
yarn add plop --dev
- 在项目根目录下创建 plopfile.js
// 这是plop的入口文件,需要导出一个函数
// 该函数接收一个对象 plop 作为参数,用于创建生成器任务
module.exports = plop => {
//两个参数:1.生成器名字 2.配置选项
plop.setGenerator('component', {
description: 'create a component',
//命令行交互
prompts: [
{
type: 'input',
name: 'name',
message: 'component name',
default: 'MyComponent'
}
],
//命令行交互后执行的动作
actions: [
{
type: 'add', //添加文件
path: 'src/components/{{name}}.js',
templateFile: 'plop-templates/component.js'
}
]
})
}
- 在项目根目录下准备模版文件 plop-templates,模版文件中支持hbs语法
let title = '{{name}}'
- 执行
yarn plop <generator-name>
如:yarn plop component
脚手架开发
脚手架就是一个node cli应用。
- 创建脚手架项目目录 如 ‘my-scaffold’
yarn init
- 在 package.json 中添加 bin 字段,指定入口文件 cli.js
- 在根目录下创建 /cli.js
// 文件头,必须
// 如果是linux系统,需要执行 `chmod 755 cli.js` 修改此文件的读写权限
#!/usr/bin/env node
console.log('cli working!')
yarn link
my-scaffold
,执行脚手架名称命令确保脚手架生效yarn add inquirer
,安装 inquirer 模块,用于命令行交互- 准备模版文件 /templates/
模版文件中可以使用EJS模版标签 <%= name %> yarn add ejs
,安装模版引擎 ejs,用来渲染文件
#!/usr/bin/env node
const path = require('path')
const fs = require('fs')
const inquirer = require('inquirer')
const ejs = require('ejs')
inquirer.prompt([
{
type: 'input',
name: 'name',
message: 'Project name?'
}
])
.then(answers => {
//根据用户回答的结果生成文件
console.log(answers)
//模版目录
const templateDir = path.join(__dirname, 'templates')
//目标目录(生成项目目录)
const distDir = process.cwd()
//读取模版目录下所有文件
fs.readdir(templateDir, (err, files) => {
// console.log(files) //模版文件夹下的文件路径
if(err) throw err
files.forEach(file => {
//根据模版渲染文件
ejs.renderFile(path.join(templateDir, file), answers, (error, res) => {
if(err) throw error
//将渲染结果写入到目标文件
fs.writeFileSync(path.join(distDir, file), res)
})
})
})
})
- 创建项目目录 如my-project
- 在该项目下执行脚手架名称命令
自动化构建
将开发阶段的源代码,自动化地转化为能够在生产环境运行的代码。
这个自动转化的过程称之为自动化构建工作流。
Npm Scripts
实现自动化工作流最简单的方式。
package.json
"scripts": {
"build": "sass scss/main.scss css/main.css --watch",
"serve": "browser-sync . --files\"css/*.css\"",
"start": "run-p buile serve"
}
- build:监听并编译sass
- serve:启动一个web服务,并在css文件发生变化时刷新浏览器
- start:同时运行 build 和 serve 命令
常用的自动化构建工具
webpack是一个模块打包工具。
- Grunt
最早,生态完善,构建速度较慢 - Gulp
最流行,高效,易用 - FIS
高度集成
Grunt的基本使用
- 创建项目,
yarn init --yes
yarn add grunt --dev
安装- 根目录下创建grunt的入口文件 gruntfile.js
// 这是grunt的入口文件,需要导出一个函数
// 该函数接收一个对象 grunt 作为参数,用于定义一些需要Grunt自动执行的任务
module.exports = grunt => {
//两个必选参数:1.任务名称 [2.任务描述] 3.任务函数
grunt.registerTask('foo', '任务描述,在--help时显示', ()=>{
console.log('hello grunt')
})
//默认任务,可以同时执行多个任务
grunt.registerTask('default', ()=>{
console.log('default task')
})
grunt.registerTask('default', ['foo', 'bar'])
//异步任务需要使用this.async()返回一个函数,在异步任务完成后,调用这个函数
//异步任务不能使用箭头函数,因为需要使用 this
grunt.registerTask('async-task', function(){
const done = this.async()
setTimeout(()=>{
console.log('async task')
done()
}, 1000)
})
}
yarn grunt <task-name>
执行任务,
不指定任务名称时,执行默认任务
标记任务失败
grunt.registerTask('foo', ()=>{
consoloe.log('foo false')
return false
})
grunt.registerTask('async-task', function(){
const done = this.async()
setTimeout(()=>{
console.log('async task')
done(false)
}, 1000)
})
Grunt配置方法
module.exports = grunt => {
grunt.initConfig({
//接收一个对象参数,对象的key为任务名称,value可以是任意类型的值
// foo: 'abc'
foo: {
bar: 123
}
})
grunt.registerTask('foo', () => {
console.log(grunt.config('foo'))
})
}
多目标任务
通过配置方法为多目标任务指定目标
grunt.initConfig({
//options是任务的配置选项
//可以通过this.options()方法获取到配置选项
//目标中的options能够覆盖统一的options
options: {
foo: 'bar'
},
//key为任务名称,value是一个对象
build: {
//key为目标名称
css: '111',
js: {
options: {
foo: 'baz'
}
}
}
})
//可以通过this取到目标内容
grunt.registerMultiTask('build', function(){
console.log(this.options())
console.log(`target: ${this.target}, data: ${this.data}`)
})
Grunt插件
使用
- 安装插件 如:grunt-contrib-clean
- 在gruntfile.js中通过loadNpmTasks方法加载插件;
加载多个grunt插件时,使用load-grunt-tasks插件yarn add load-grunt-tasks
- 在配置方法中为插件任务添加配置选项
grunt.initConfig({
//clean是一个多目标任务
clean: {
temp: 'temp/*.vue'
}
})
grunt.loadNpmTasks('grunt-contrib-clean')
- 执行任务,如
yarn grunt clean
常用插件
- sass编译:grunt-sass (依赖于sass)
- ES6语法编译:grunt-babel(依赖于@babel/core 和 @babel/perset-env)
- 文件修改后自动编译:grunt-contrib-watch
const sass = require('sass')
const loadGruntTasks = require('load-grunt-tasks')
grunt.initConfig({
sass: {
options: {
implamentation: sass
},
//目标配置选项
main: {
files: {
//key为输出路径,值为输入路径
'dist/css/main.css': 'src/scss/main.scss'
}
}
},
babel: {
options: {
presets: ['@babel/preset-env']
},
main: {
files: {
'dist/js/app.js': 'src/js/app.js'
}
}
},
watch: {
js: {
files: ['src/js/*.js'],
tasks: ['babel']
},
css: {
files: ['src/scss/*.scss'],
tasks: ['sass']
}
}
})
//自动加载所有grunt的插件任务
loadGruntTasks(grunt)
grunt.registerTask('default', ['sass', 'babel', 'watch'])
Gulp的基本使用
- 创建项目,
yarn init --yes
或在已有项目中 yarn add gulp --dev
安装gulp- 根目录下创建gulp的入口文件 gulpfile.js
exports.foo = done => {
console.log('foo task')
done() //done是回调函数,标识任务已经完成
}
//默认任务
exports.default = done => {
console.log('default task')
done()
}
yarn gulp <task-name>
执行任务,
不指定任务名称时,执行默认任务
创建组合任务
const {series, parallel} = require('gulp')
const task1 = done => {
...
done()
}
const task2 = done => {
...
done()
}
const task3 = done => {
...
done()
}
//串行任务
exports.foo = series(task1, task2, task3)
//并行任务
exports.foo = parallel(task1, task2, task3)
异步任务的几种方式
- 回调函数
- promise
- async await
- stream
const fs = require('fs')
exports.callback = done => {
console.log('callback task')
// done()
done(new Eroor('err'))
}
exports.promise = () => {
console.log('promise task')
// return Promise.resolve()
return Promise.reject(new Eroor('err'))
}
const timer = time => {
return new Promise(resolve => {
setTimeout(resolve, time)
})
}
exports.async = async () => {
await timer(1000)
console.log('async task')
}
exports.stream = () => {
const readStream = fs.createReadStream('package.json')
const writeStream = fs.creatWriteStream('temp.txt')
readStream.pipe(writeStream)
return readStream //gulp会根据读取流的状态判断任务是否完成
}
构建过程核心原理
const fs = require('fs')
const {Transform} = require('stream')
exports.default = () => {
//文件读取流
const read = fs.creatReadStream('main.css')
//文件写入流
const write = fs.createWriteStream('main.min.css')
//文件转换流
const transform = new Transform({
transform: (chunk, encoding, callback)=>{
const input = chunk.toString()
const output = input.replace(/\s+/g, '').replace(/\/\*.+?\*\//g, '')
callback(null, output) //错误优先
}
})
read.pipe(transform).pipe(write)
return read
}
Gulp文件操作API
- src 读取流
- dest 写入流
- 转换流一般由插件提供
- 安装插件
- 载入插件
- 使用插件 读取流 -> 转换流 -> 写入流
const {src, dest} = require('gulp')
//载入插件
const cleanCss = require('gulp-clean-css')
const rename = require('gulp-rename')
exports.default = () => {
return src('src/*.css')
//插件一般提供一个函数,并返回一个转换流
.pipe(cleanCss())
.pipe(rename({extname: 'min.css'}))
.pipe(dest('dist'))
}
基础案例
- 样式编译:gulp-sass
- 脚本编译(ES6):gulp-babel
- 模版文件编译(Html):gulp-swig
- 图片和字体文件转换:gulp-imagemin
- 其他不需要编译的文件,如 public 文件夹,直接复制
- 文件清除:del,返回一个promise
- 自动加载gulp插件:gulp-load-plugins
- 热更新开发服务器:browser-sync
- 引入css和js文件(尤其是第三方的文件):gulp-useref
<!--build:css assets/style/vendor.css-->
<link ref=stylesheet href="node_modules/bootstrap/dist/css/bootstrap.css">
<!--endbuild-->
<!--build:js assets/js/vendor.js-->
<script src="node_modules/jquery/dist/jquery.js"></script>
<script src="node_modules/bootstrap/dist/bootstrap.js"></script>
<!--endbuild-->
- 压缩文件:gulp-htmlmin、gulp-clean-css、gulp-uglify
- 对文件类型进行判断,从而执行对应的任务:gulp-if
gulpfile.js
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 cwd = process.cwd() //当前项目目录
const data = {} //模版文件中使用到的数据
let config = {
//default config
}
try{
const loadConfig = requeire(`${cwd}/demo.config.js`)
config = Object.assign({}, config, loadConfig)
}catch(e){}
const clean = () => {
return del(['dist', 'result']) //中间目录也需要删除
}
const style = () => {
return src('src/assets/style/*.scss', {base: 'src'})
.pipe(plugins.sass({outputStyle: 'expanded'}))
.pipe(dest('dist'))
.pipe(bs.reload({stream: true}))
}
const script = () => {
return src('src/assets/js/*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))
.pipe(dest('dist'))
.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/style/*.scss', style)
watch('src/assets/js/*js', script)
watch('src/*.html', page)
// watch('src/assets/images/**', image)
// watch('public/**', extra)
//一般在开发阶段不对image、font和public进行构建,在server中添加多个基本路径,这类文件依旧访问源代码
watch([
'src/assets/images/**',
'public/**'
], bs.reload)
bs.init({
notify: false, //是否在浏览器中显示browser-sync链接成功
port: 9000,
open: true, //是否自动打开浏览器
// files: 'dist/**', //根据哪些文件改变自动更新,也可以在构建任务重通过bs.reload()实现
server: {
baseDir: ['dist', 'src', 'public']
}
})
}
const useref = () => {
// 会将引入的第三方文件(可能是多个)合并到指定文件中
// 对dist中的html进行处理,而不是src中
return src('dist/*.html', {base: 'dist'})
.pipe(plugins.useref({searchPath: ['dist', '.']}))
// 对不同类型文件(需要安装gulp-if插件)分别进行压缩
.pipe(plugins.if(/\.js$/, plugins.uglify()))
.pipe(plugins.if(/\.css$/, plugins.cleanCss()))
.pipe(plugins.if(/\.html$/, plugins.htmlmin({
collapseWhitespace:true,
minifyCSS:true,
minifyJS:true
})))
// dist即是读取流又是写入流,容易出错,需要有一个中间目录
// 一般把dist作为最终的输出目录
.pipe(dest('result'))
}
//组合任务
const compile = parallel(style, script, page)
const build = series(
clean,
parallel(
series(compile, useref),
image,
font,
extra
)
)
const dev = series(compile, serve)
module.exports = {
clean,
build,
dev
}
将构建任务添加到package.json
"scripts": {
"clean": "gulp clean",
"dev": "gulp dev",
"build": "gulp build"
}
封装自动化构建工作流
- 将 gulpfile.js 封装成一个模块发布到npm,方便复用。
使用时先安装,然后在项目的gulpfile.js中引入模块 - 将gulpfile.js中可能会变化的内容抽象出来,如data、文件路径等配置
- 也可以不在项目中创建gulpfile.js,但需要在运行gulp命令时指定node_modules中的gulpfile路径和项目的路径
yarn gulp build --gulpfile ./node_modules/zce-pages/lib/index.js --cwd .
- 为了解决3命令过于复杂的问题,在zce-pages模块中封装gulp cli:bin/zce-pages.js
FIS的基本使用
- 在项目中安装fis
yarn add fis3 --dev
- 在项目根目录下创建fis配置文件 fis-conf.js
- 执行fis中的构建命令