前端工程化之开发脚手架及封装自动化构建工作流

前端工程化概念

  1. 前端工程化指的是:遵循一定的标准和规范;通过工具去提高效率,降低成本的手段。
  2. 为什么要做前端工程化:1、想要使用ES6+新特性,但是兼容有问题 2、想要使用Less、Sass、PostCSS增强CSS的编程性,但是运行环境不能直接支持。3、想要使用模块化的方式提高项目的可维护性,但是运行环境不能直接支持。4、部署上线前需要时候东压缩代码及资源文件;部署过程需要手动上传代码到服务器。 5、多人协作开发,无法硬性统一大家的代码风格,从仓库中pull回来的代码质量无法保证。6、部分功能开发时需要等待后端服务接口提前完成。
  3. 主要解决的问题:传统语言或语法的弊端;无法使用模块化/组件化;重复的机械式工作;代码风格统一、质量保证;依赖后端服务接口支持;整体依赖后端项目。
  4. 工程化表现:一切以提高效率、降低成本、质量保证为目的的手段都属于工程化。
    • 脚手架工具开发
    • 自动化构建系统
    • 模块化打包
    • 项目代码规范化
    • 自动化部署

这里需要注意的是 工程化 不等于 某个工具

脚手架工具

  1. 概述

    • 脚手架工具:前端工程化的发起者
    • 脚手架的本质作用:创建项目基础结构、提供项目规范和约定
    • 在开发相同而项目时候会有一些相同的约定:相同的文件组织结构、相同的开发范式、相同的模块依赖、相同的工具配置、相同的基础代码。
  2. 常用的脚手架工具

    • Vue——vue-cli
    • React——create-react-app
    • Vue——vue-cli
    • Angular——angular-cli
    • Yeoman——通用
    • Plop—— 一个小而美的脚手架工具,可以创建同类型的目录文件
  3. Yeoman脚手架工具

// 初使用:yarn global add yo
// 我们要使用yeoman区创建项目要找到对应项目的generator: 安装 yarn global add generator-node  这里的generator是生成一个node项目的generator
// 然后使用 yo node就可以创建一个项目

// 使用Yeoman怎么来生成配置文件:使用Sub Generator
// 例如我们要用到cli应用所需要的文件:yo node:cli 当我们有了cli之后;就可以将安装的模块做一个全局的命令行模块使用了:yarn link (到全局范围) ;然后可以通过模块的名字来运行这个模块了 ; 使用yarn来安装依赖


// 使用yeoman步骤:
// 1、明确你的需求
// 2、找到合适的Generator
// 3、全局范围安装找到的Generator
// 4、通过Yo运行对象的Generator
// 5、通过命令行交互填写选项;
// 6、生成你所需要的项目结构

自定义Generator

index.js文件内容:
// 此文件作为Generator的核心入口
// 需要导出一个继承自Yeoman Generator的类型
// Yeoman Generator在工作时会自动调用我们在此类型中定义的一些生命周期方法
// 我们在这些方法中可以实现通过调用父类提供的一些工具方法实现一些功能,例如文件写入

const Generator = require('yeoman-generator')

module.exports = class extends Generator {
  // 对于模板中的动态数据:例如项目的标题、名称等;一般通过命令行交互的形式去询问我们的使用者;在generator中想要发起一个命令行交互的询问;可以使用Promting方法
  prompting () {
    // Yeoman 在询问用户环节会自动调用此方法
    // 在此方法中可以调用父类的prompt()方法发出对用户的命令行询问
    return this.prompt([
      {
        type: 'input',
        name: 'name',
        message: 'Your project',
        default: this.appname   // appname当前目录为项目生成目录名称
      }
    ]).then(answers => {
      // answers => {name:'user input value'}
      this.answers = answers
    })
  }


  writing () {
    // Yeoman 自动在生成文件阶段调用此方法
    // 我们这里尝试往项目中写入文件
    // this.fs.write(
    //   // 获取生成项目目录下对应的路径
    //   this.destinationPath('temp.txt'),
    //   Math.random().toString()  // 生成的文件内容用随机数代替
    // )
    //  this.fs.write(
    //   this.destinationPath('temp.txt'),
    //   Math.random().toString()
    // )

    // 通过模板方式写入文件到目标目录

    // // 模板文件路径
    // const tmpl = this.templatePath('foo.txt')
    // // 输出目标路径
    // const output = this.destinationPath('foo.txt')
    // // 模板数据上下文
    // const context = { title: 'hello sfy', success: false }
    // // 下面的会自动的把我们的模板文件生成到目录文件上
    // this.fs.copyTpl(tmpl, output, context)


    // 模板文件路径
    const tmpl = this.templatePath('bar.html')
    // 输出目标路径
    const output = this.destinationPath('bar.html')
    // 模板数据上下文
    const context = this.answers
    // 下面的会自动的把我们的模板文件生成到目录文件上
    this.fs.copyTpl(tmpl, output, context)


  }
}

最后使用yarn publish发布就行了;也可以使用npm publish发布

  1. Plop脚手架工具
// 基本使用 
// yarn add plop --dev
// 根目录下创建plopfile.js文件 plop的入口文件;需要导出一个函数;此函数接受一个plop对象;英语创建生成器任务
// plopfile.js
module.exports = plop => {
  plop.setGenerator('component', {
    description: 'create a compnent',
    prompts: [
      {
        type: 'input',
        name: 'name',
        message: 'component name',
        default: 'MyComponent'
      }
    ],
    actions: [
      {
        type: 'add',  // 代表添加文件
        path: 'src/components/{{name}}/{{name}}.js',
        templateFile: ''

      }
    ]
  })
}
总结:
• 将 plop 模块作为项目开发依赖安装
• 在项目根目录下创建一个 plopfile.js 文件
• 在 plopfile.js 文件中定义脚手架任务
• 编写用于生成特定类型文件的模板
• 通过 Plop 提供的cli运行脚手架任务

Node开发脚手架 node-cli

// package.json 文件添加 bin 字段,用于指定 cli 应用的入口文件 cli.js

// cli.js cli 入口文件
#!/user/bin/env node
// Node CLI 应用入口文件必须要有这样的文件头
// 脚手架的工作过程:
// 1.通过命令行交互询问用户问题 yarn add inquirer
// 2.根据用户回答的结果生成文件

const fs = require('fs')
const path = require('path')
const inquirer = require('inquirer')
const ejs = require('ejs') // yarn add ejs

inquirer.prompt([
  {
  	type: 'input',
    name: 'name',
    message: 'project name?'
  }
])
.then(anwsers => {
	// console.log(anwsers)
  // 根据用户回答的结果生成文件
  
  // 模板目录
  const tmplDir = path.join(_dirname, 'templates')
  // 目标目录
  const destDir = process.cwd()
  
  // 将模板下的文件全部转换到目标目录
  fs.readdir(tmplDir,(err, files) => {
  	if (err) throw err
    files.forEach(file => {
    	//通过模板引擎渲染文件
      ejs.renderFile(path.join(tmplDir, file), anwsers, (err, result) => {
      	if (err) throw err
        
        // 将结果写入目标文件路径
        fs.writeFileSync(path.join(destDir, file), result)
      })
    })
  })
})

自动化构建

  1. 概述
    开发阶段写出来的源代码自动化转换成生产环节可以运行的代码
    作用:脱离运行环境兼容带来的问题

  2. NPM Scripts自动化构建

<!-- 要把sass转化为css可以 通过命令yarn add sass --dev来安装sass ;然后通过.\node_modules\.bin\sass .\scss\main.css css/style.css来在css文件中生成对应的css文件-->


<!-- 需要解决在项目开发阶段这些要重复执行的终端命令;NPM Scripts(package.json中的)就是主要解决这问题的;可以在scripts里面加入一些常用命令;便于在项目开发中的使用 -->

<!-- NPM Scripts是实现自动化构建工作流的最简方式 -->
<!-- 如果通过NPM Scripts实现自动化构建:
1、首先安装:yarn add browser-sync --dev;用于启动一个测试服务器去运行我们的项目;
2package.json中的scripts中加入:"serve": "browser-sync ."
3、运行项目:yarn serve
4、 如果项目中没有要运行的css文件;我们可以在serve前生成;就是在scripts里面加入"preserve":'yarn build',位置放在serve之前;这样就可以在启动之前自动构建我们的sass文件;也可以在sass文件后面加上--watch,就可以监听sass文件的变化,但是这个时候就要同时执行多个任务;就可以借助npm-run-all 的文件 : yarn add npm-run-all --dev
5、在scripts中加入:"start": "run-p build serve" 运行start就可以运行build和serve同时运行了
6、也可以在browser-sync . 后面再加上--files \"css/*.css\" 就是监听files后面的文件的变化然后自动的去改变浏览器的内容

NPM Scripts确实能够构建一些任务;但是相对于一些复杂的任务,它就显得比价吃力
-->
  1. 常用的自动化构建工具: Grunt 、 Gulp 、 FIS
  2. Grunt
yarn add grunt
// 新建 gruntfile.js文件   code gruntfile.js

(1)基本使用

// Grunt的入口文件
// 用于定义一些需要Grunt 自动执行的任务
// 需要导出一个函数
// 此函数接受一个grunt 的形参,内部提供一些创建文任务时可以用到的API
const sass = require('sass')
const loadGrountasks = require('load-grunt-tasks')

module.exports = grunt => {
  // foo是任务名称;第二个参数是任务;运行任务:yarn grunt foo
  // grunt.registerTask('foo', () => {
  //   console.log('hello grunt~');
  // })
  // grunt.registerTask('bar', '任务描述', () => {
  //   console.log('other task~');
  // })
  // // 当任务名称为default时候;相当于grunt的默认任务;运行直接yarn grunt
  // // grunt.registerTask('default', '任务描述', () => {
  // //   console.log('other task~');
  // // })
  // // // 同时运行两任务
  // // grunt.registerTask('default', ['foo', 'bar'])

  // // grunt异步操作任务 
  // // grunt.registerTask('async-task', () => {
  // //   setTimeout(() => {
  // //     console.log('async task workding');  // 没有打印;因为grunt默认支持同步模式;如果要使用异步操作的话就需要使用this.async方法得到一个回调函数;当我们异步操作完成之后调用这个回调函数;标识我们的任务已经完成
  // //   }, 1000)
  // // })

  // grunt.registerTask('async-task', function () {
  //   const done = this.async()
  //   setTimeout(() => {
  //     console.log('async task workding');
  //     done()
  //   }, 1000)
  // })


  // // 如果我们在构建任务的逻辑代码中发生错误;就可以把这个任务标记为失败任务
  // grunt.registerTask('bad', () => {
  //   console.log('bad workding');
  //   return false
  // })

  // // 同时运行两任务  yarn grunt default --force;强制运行完所有任务;不管有没有错误
  // grunt.registerTask('default', ['foo', 'bad', 'bar'])

  // // 给异步操作任务标记为失败任务的时候就需要给this.async的回调函数传一个false就行了
  // grunt.registerTask('bad-async-task', function () {
  //   const done = this.async()
  //   setTimeout(() => {
  //     console.log('bad async task workding');
  //     done(false)
  //   }, 1000)
  // })




  // // grunt也提供了一个用于添加配置选项的api;  initConfig
  // // grunt.initConfig({
  // //   foo1: 'bar'
  // // })
  // grunt.initConfig({
  //   foo1: {
  //     bar: 123
  //   }
  // })

  // grunt.registerTask('foo1', () => {
  //   console.log(grunt.config('foo1.bar'));  // 123
  // })

  // // 多目标模式,可以让人物根据配置形成多个子任务
  // // 先配置目标
  // grunt.initConfig({
  //   build: {
  //     css: '1',
  //     js: '2',
  //     options: {  // 这里面放置配置选项
  //       foo: 'bar'
  //     },
  //     ai: {
  //       options: {
  //         foo: 'baz'  // 会覆盖掉上面的options;在执行ai子任务的时候就会输出baz
  //       }
  //     }
  //   }
  // })
  // grunt.registerMultiTask('build', function () {
  //   console.log('build task', `targent:${this.target},data: ${this.data}`); // 运行两遍;一次是css目标;一个是js目标   build task targent:css,data: 1    build task targent:js,data: 2
  //   console.log(this.options()); // 获取所有配置选项
  // })



  // // Grunt插件的使用 例如:grunt-contrib-clean  用于清除我们在开发项目中产生的临时文件
  // // 通过grunt.loadNpmTasks('grunt-contrib-clean')去加载插件中提供的任务
  // // 增加一些配置选项为这个插件
  // grunt.initConfig({
  //   clean: {
  //     temp: 'temp/app.js',
  //     temp1: 'temp/*.txt'  // 可以使用通配符*.txt来找到temp目录下所有的.txt文件   **代表temp目录下所有文件都会被找到
  //   }
  // })
  // grunt.loadNpmTasks('grunt-contrib-clean')   // 通过yarn grunt clean运行

  // Grunt常用的插件及总结
  // 1.  grunt-sass  yarn add grunt-sass sass --dev  需要用到sass官方提供的模块支持;所以命令式这个

  grunt.initConfig({
    sass: {
      options: {
        implementation: sass,
        sourceMap: true
      },
      main: {  // main 是目标
        files: {
          // 键是生成的目录用来存放;值是目标文件的路径(输入文件的路径)
          'dist/css/main.css': 'src/scss/main.scss'
        }
      }
    },

    babel: {
      options: {
        // babel是es6最新特性转换
        presets: ['@babel/preset-env'],   // 可以把es6中所有的新特性加载进来并转换
        sourceMap: true
      },
      // 为load-grunt-tasks提供配置
      main: {
        files: {
          'dist/js/app.js': 'src/js/app.js'
        }
      }
    },
    watch: {
      js: {
        files: ['src/js/*.js'],
        tasks: ['babel']  // 监听的内容:上面的babel配置名
      },
      css: {
        files: ['src/scss/*.scss'],
        tasks: ['sass'] // 监听的内容:上面的sass配置名
      }
    }
  })
  // grunt.loadNpmTasks('grunt-sass')

  // 2。 去编译es6的语法  可以使用babel  grunt-babel插件  yarn add grunt-babel @babel/core @babel/preset-env --dev

  // 3、当文件修改完之后我们需要自动的去编译  yarn add grunt-contrib-watch --dev
  // 
  grunt.registerTask('default', ['sass', 'babel', 'watch'])



  // 当我们有很多插件要是用的时候;可以使用load-grunt-tasks  :yarn add load-grunt-tasks --dev
  // const loadGrountasks = require('load-grunt-tasks')
  // // 这个时候就不需要使用loadNpmTasks来导入插件了
  loadGrountasks(grunt) // 会自动加载所有的grunt插件中的任务

}
  1. gulp
    基本使用
code gulpfile.js // gulp 入口文件
exports.foo = done => {
	done() // 标识任务完成
}
exports.default = done => {
	done() // 标识任务完成
}
// gulp 4.0 以前
const gulp = require('gulp')
gulp.task('bar', done => {
	done()
})

gulp中的组合任务

exports.foo = series(task1,task2, task3) // 串行
exports.bar = parallel(task1,task2, task3) // 并行

异步任务

exports.promise = () => {
  console.log('promise task~')
	return Promise.resolve()
}
const timeout = time => {
	return new promise(resolve => {
  	setTimeout(resolve, time)
  })
}
exports.async = async () => {
  await timeout(1000)
	return Promise.resolve()
}
exports.stream = () => {
	const readStream = fs.createReadStream('package.json')
  const writeSteam = fs.createWriteStream('temp.txt')
  readStream.pipe(writeStream)
  return readStream
}
exports.stream = () => {
	const readStream = fs.createReadStream('package.json')
  const writeSteam = fs.createWriteStream('temp.txt')
  readStream.pipe(writeStream)
  readStream.on('end', () => {
  	done()
  })
}

核心工作原理

const fs = require('fs')
const { Transform } = require('stream')
const { callback } = require('../01-gulp的基本使用/gulpfile')


exports.default = () => {
  // 创建文件读取流
  const read = fs.createReadStream('normalize.css')   // 参数就是读取文件的路径
  // 文件写入流
  const write = fs.createWriteStream('normalize.min.css')
  // 文件转换流
  const transform = new Transform({
    transform: (chunk, encoding, callback) => {
      // 核心转换过程实现
      // chunk  => 读取流中读取到的内容(Buffer)
      const input = chunk.toString()
      const output = input.replace(/\s+/g, '').replace(/\/\*.+?\*\//g, '')
      callback(null, output)
    }
  })

  // 把读取出来的文件流导入写入文件流
  read
    .pipe(transform)  // 转换
    .pipe(write)     // 写入
  return read

}

文件操作API


const { src, dest } = require('gulp')
const cleanCss = require('gulp-clean-css')
const rename = require('gulp-rename')

exports.default = () => {
  return src('src/*.css')  // 通过src()创建一个文件读取流  通配符匹配所有的css文件
    .pipe(cleanCss())  // 转换压缩文件流
    .pipe(rename({ extname: '.min.css' }))  // 重命名文件的扩展名为min.css
    .pipe(dest('dist'))  // dest(目标路径) 通过dest()创建一个文件写入流
}


// 要想要转换上面的文件流;可以使用gulp-clean-css 插件

// gulp-rename


例子:

const { src, dest, parallel, series, watch } = require('gulp')

const del = require('del')
// 提供一个开发服务器;自动化加载;支持热更新 yarn add browser-sync --dev
const browserSync = require('browser-sync')
// 同时加载多个插件不用一个一个的去加载了 yarn add gulp-load-plugins --dev
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'])  // 清除dist目录下的文件  yarn add del --dev
}

const style = () => {
  return src('src/assets/styles/*.scss', { base: 'src' })  // 设置base可以保证输出后的文件依然保持原来的目录结构 
    .pipe(plugins.sass({ outputStyle: 'expanded' }))   // yarn add gulp-sass插件outputStyle: 'expanded'可以设置css文件里面的结束的大括号放到空行
    .pipe(dest('temp'))
    .pipe(bs.reload({ stream: true }))  //以流的方式向浏览器里面推;让浏览器更新内容;使用了它之后就没必要在使用bs.init中的files: 属性了
}

const script = () => {
  return src('src/assets/scripts/*.js', { base: 'src' })
    .pipe(plugins.babel({ presets: ['@babel/preset-env'] })) // 转换js文件流插件:yarn add gulp-babel @babel/core @babel/preset --dev 
    .pipe(dest('temp'))
    .pipe(bs.reload({ stream: true }))
}

const page = () => {
  // 转换模板文件(HTML文件)的插件 yarn add gulp-swig -- dev
  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())  // yarn add gulp-imagemin --dev 转换图片
    .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,   // 关闭右上角的notify连接提示
    port: 2080,
    // open: false,
    // files: 'dist/**',  // 这个可以监听那些文件改变进行热更新
    server: {
      baseDir: ['temp', 'src', 'public'],  // 网站运行的根目录  加入src/public的原因:在开发阶段是没有必要监听图片、字体的改变,会降低构建的效率;放在原目录就行;我们只是对他进行了压缩
      routes: {  // 路由
        '/node_modules': 'node_modules'
      }
    }
  })
}
// usere能够自动的处理HTML中的构建注释(<build:css assets/styles/vendor.css> <endbuild>)  ;可以把构建注释中的所有引入都打包到一个文件中;就是上面所写的vendor.css文件中   yarn add gulp-useref --dev
const useref = () => {
  return src('temp/*.html', { base: 'temp' })
    .pipe(plugins.useref({ searchPath: ['temp', '.'] }))
    // 有三个类型的文件: html js css 进行压缩 需要安装对应的压缩插件 yarn add gulp-htmlmin gulp-uglify gulp-clean-css --dev
    // 因为需要判断是哪个文件类型;需要安装 yarn add gulp-if --dev
    .pipe(plugins.if(/\.js$/, plugins.uglify()))
    .pipe(plugins.if(/\.css$/, plugins.cleanCss()))
    .pipe(plugins.if(/\.html$/, plugins.htmlmin({
      collapseWhitespace: true, // 压缩HTML的选项;加了这三个就可以HTML的比较完整的压缩
      minifyCSS: true,
      minifyJS: true
    })))
    .pipe(dest('dist'))  // 把最终的结果放到dist里面
}

const compile = parallel(style, script, page)

// 上线之前执行的任务
const build = series(
  clean,  // 必须是先删除dist中的文件再生成
  parallel(
    series(compile, useref),  // useref需要在compile之后运行
    image,
    font,
    extra
  )
)

const develop = series(compile, serve)

module.exports = {
  clean,
  build,
  develop
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值