前端工程化概述

前端工程化,指遵循一定的标准和规范,利用工具提高效率降低成本的一种手段,是对项目整体的规划和架构。

要解决的问题:

  • 传统语法的弊端,兼容问题
  • 无法使用组件化、模块化
  • 重复性的工作(打包,上传等)
  • 代码风格统一
  • 依赖后端服务和接口

通过哪些方式:

  • 脚手架
  • 自动化构建
  • 模块化打包
  • 项目代码规范化
  • 自动化部署

脚手架工具开发

脚手架作用:创建项目基础结构,提供项目规范和约定。

常用的脚手架工具

  • create-react-app
  • vue-cli
  • angular-cli
  • Yeoman 通用型
  • Plop 创建特定类型(如组件/模块)的文件

Yeoman 使用步骤

  1. 安装Yeoman
    yarn global add yo
  2. 安装对应的generator(如generator-node)
    yarn global add generator-node
  3. 创建项目文件夹, 如my-module
  4. 在项目文件夹下用Yeoman运行generator
    cd project-dir
    yo node
  5. sub generator,用于补充生成项目中的文件,如果在已经存在的项目基础上要创建某些特定类型的文件,可以使用sub generator,如node下的cli:
    yo node:cli
  6. yarn link
  7. my-module

自定义generator

demo:带有一定基础代码的Vue脚手架

generator基本结构:
|-generators/
|—app/--------------------默认生成器目录
|-----templates/
|-------template------------模版文件
|-----index.js
|—sub/--------------------sub generator目录,可以并列多个
|-----index.js
|-package.json

步骤:

  1. 创建生成器模块目录(generator-)
  2. yarn init 创建package.json
  3. yarn add yeoman-generator 安装yeoman- generator
  4. 按照项目基本结构创建文件
  5. yarn link
  6. 创建项目目录
  7. 通过自定义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

  1. 将generator模块托管到git上
  2. 发布模块 npm/yarn publish
    修改镜像源 yarn publish --registry=https://registry.yarnpkg.com

Plop使用步骤

在已有项目中创建特定类型的文件,一般不会用它去生成项目。

  1. 安装 yarn add plop --dev
  2. 在项目根目录下创建 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'
            }
        ]
    })
}
  1. 在项目根目录下准备模版文件 plop-templates,模版文件中支持hbs语法
let title = '{{name}}'
  1. 执行 yarn plop <generator-name>
    如:yarn plop component

脚手架开发

脚手架就是一个node cli应用。

  1. 创建脚手架项目目录 如 ‘my-scaffold’
  2. yarn init
  3. 在 package.json 中添加 bin 字段,指定入口文件 cli.js
  4. 在根目录下创建 /cli.js
// 文件头,必须
// 如果是linux系统,需要执行 `chmod 755 cli.js` 修改此文件的读写权限

#!/usr/bin/env node
console.log('cli working!')
  1. yarn link
  2. my-scaffold,执行脚手架名称命令确保脚手架生效
  3. yarn add inquirer,安装 inquirer 模块,用于命令行交互
  4. 准备模版文件 /templates/
    模版文件中可以使用EJS模版标签 <%= name %>
  5. 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)
      })
    })
  })
})
  1. 创建项目目录 如my-project
  2. 在该项目下执行脚手架名称命令

自动化构建

将开发阶段的源代码,自动化地转化为能够在生产环境运行的代码。
这个自动转化的过程称之为自动化构建工作流。

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的基本使用

  1. 创建项目,yarn init --yes
  2. yarn add grunt --dev 安装
  3. 根目录下创建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)
  })
}
  1. 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插件

使用
  1. 安装插件 如:grunt-contrib-clean
  2. 在gruntfile.js中通过loadNpmTasks方法加载插件;
    加载多个grunt插件时,使用load-grunt-tasks插件 yarn add load-grunt-tasks
  3. 在配置方法中为插件任务添加配置选项
grunt.initConfig({
    //clean是一个多目标任务
    clean: {
        temp: 'temp/*.vue'
    }
})
grunt.loadNpmTasks('grunt-contrib-clean')
  1. 执行任务,如 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的基本使用

  1. 创建项目,yarn init --yes 或在已有项目中
  2. yarn add gulp --dev 安装gulp
  3. 根目录下创建gulp的入口文件 gulpfile.js
exports.foo = done => {
    console.log('foo task')
    done() //done是回调函数,标识任务已经完成
}

//默认任务
exports.default = done => {
    console.log('default task')
    done()
}
  1. 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 写入流
  • 转换流一般由插件提供
    1. 安装插件
    2. 载入插件
    3. 使用插件 读取流 -> 转换流 -> 写入流
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"
}

封装自动化构建工作流

  1. 将 gulpfile.js 封装成一个模块发布到npm,方便复用。
    使用时先安装,然后在项目的gulpfile.js中引入模块
  2. 将gulpfile.js中可能会变化的内容抽象出来,如data、文件路径等配置
  3. 也可以不在项目中创建gulpfile.js,但需要在运行gulp命令时指定node_modules中的gulpfile路径和项目的路径
    yarn gulp build --gulpfile ./node_modules/zce-pages/lib/index.js --cwd .
  4. 为了解决3命令过于复杂的问题,在zce-pages模块中封装gulp cli:bin/zce-pages.js

FIS的基本使用

  1. 在项目中安装fis yarn add fis3 --dev
  2. 在项目根目录下创建fis配置文件 fis-conf.js
  3. 执行fis中的构建命令
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值