笔记六:开发脚手架与自动化构建工作流封装

2020-08-12  本文已影响0人  油菜又矮吹

工程化概述

前端工程化事指遵循一定的标准和规范,通过工具去提高效率、降低成本和质量保证的一种手段

前端开发中遇到的问题

主要解决的问题

工程化表现

工程化不等于某个具体工具

工具并不是工程化的核心,工程化的核心事对项目的整体规划或架构,工具只是落地和实现工程化的一个手段
**常见的一些成熟的工程化集成

工程化与Node.js

工程化工具都是Node.js开发的

脚手架工具

脚手架的本质作用就是创建项目基础结构、提供项目规范和约定

脚手架工具的作用

因为在前端工程中,可以存在有:

常用的脚手架工具

通用脚手架工具剖析

1.Yeoman + Generator

Yeoman是最老牌、最强大、最通用的脚手架工具,是创建现代化应用的脚手架工具,不同于vue-cli,Yeoman更像是脚手架运行平台,我们可以通过Yeoman搭配不同的Generator去创建任何类型的项目,我们可以创建我们自己的Generator,从而去创建我们自己的前端脚手架。缺点是,在框架开发的项目中,Yeoman过于通用不够专注。

如果使用Yeoman

2.SubGenerator

有时候我们可能不需要创建一个完成的项目结构,而是在已有项目的基础上,创建一些项目文件,如README.md,或者是创建一些特定类型的文件,如ESLint、Babel配置文件

3.Plop

Plop是一个小而美的脚手架工具,通常用于创建项目中特定类型文件的小工具,一般是把Plop集成到项目中,用来自动化创建同类型的项目文件。

如何使用Polp创建文件:

4.脚手架工作原理

脚手架的工作原理就是在启动脚手架之后,回自动地区询问一些预设问题,通过回答的结构并结合一些模板文件,生成项目的结构,使用NodeJS开发一个小型的脚手架工具

{
  "name": "sample-scaffolding",
  "version": "0.1.0",
  "main": "index.js",
  "bin": "cli.js",
  "author": "zce <w@zce.me> (https://zce.me)",
  "license": "MIT",
  "dependencies": {
    "ejs": "^2.6.2",
    "inquirer": "^7.0.0"
  }
}
#!/usr/bin/env node

// Node CLI 应用入口文件必须要有这样的文件头
// 如果是 Linux 或者 macOS 系统下还需要修改此文件的读写权限为 755
// 具体就是通过 chmod 755 cli.js 实现修改

// 脚手架的工作过程:
// 1. 通过命令行交互询问用户问题
// 2. 根据用户回答的结果生成文件

const fs = require('fs')
const path = require('path')
const inquirer = require('inquirer')
const ejs = require('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)
      })
    })
  })
})
5.自定义Generator开发脚手架

注意:Yeoman的生成器名称必须是generator-<name>,安装生成器的时候,就执行yo <name>

创建Generator生成器的步骤

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

const Generator = require('yeoman-generator')

module.exports = class extends Generator {
  prompting () {
    // Yeoman 在询问用户环节会自动调用此方法
    // 在此方法中可以调用父类的 prompt() 方法发出对用户的命令行询问
    return this.prompt([
      {
        type: 'input',
        name: 'name',
        message: 'Your project name',
        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()
    // )

    // -------------------------------------------------------

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

    // // 模板文件路径
    // const tmpl = this.templatePath('foo.txt')
    // // 输出目标路径
    // const output = this.destinationPath('foo.txt')
    // // 模板数据上下文
    // const context = { title: 'Hello zce~', 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)
  }
}
这是一个模板文件
内部可以使用 EJS 模板标记输出数据
例如:<%= title %>

其他的 EJS 语法也支持

<% if (success) { %>
哈哈哈
<% }%>
5.Plop

yarn add plop
plopfile.js

// Plop 入口文件,需要导出一个函数
// 此函数接收一个 plop 对象,用于创建生成器任务

module.exports = plop => {
  plop.setGenerator('component', {
    description: 'create a component',
    prompts: [
      {
        type: 'input',
        name: 'name',
        message: 'component name',
        default: 'MyComponent'
      }
    ],
    actions: [
      {
        type: 'add', // 代表添加文件
        path: 'src/components/{{name}}/{{name}}.js',
        templateFile: 'plop-templates/component.hbs'
      },
      {
        type: 'add', // 代表添加文件
        path: 'src/components/{{name}}/{{name}}.css',
        templateFile: 'plop-templates/component.css.hbs'
      },
      {
        type: 'add', // 代表添加文件
        path: 'src/components/{{name}}/{{name}}.test.js',
        templateFile: 'plop-templates/component.test.hbs'
      }
    ]
  })
}

编写模板:
component.hbs:

import React from 'react';

export default () => (
  <div className="{{name}}">
    <h1>{{name}} Component</h1>
  </div>
)

Component.css.hbs:

import React from 'react';
import ReactDOM from 'react-dom';
import {{name}} from './{{name}}';

it('renders without crashing', () => {
  const div = document.createElement('div');
  ReactDOM.render(<{{name}} />, div);
  ReactDOM.unmountComponentAtNode(div);
});

执行命令:yarn plop component

自动化构建

源代码自动化构建成生产代码,也成为自动化构建工作流
使用提高效率的语法、规范和标准,如:EXMAScript Next、Sass、模板引擎,这些用法大都不被浏览器直接支持,自动化工具就是解决这些问题的,构建转换那些不被支持的特性

1.NPM Scripts

在package.json中增加一个scripts对象,如:

{
  "scripts": {
    "build": "sass scss/main.scss css/style.css"
  }
}

scripts可以自动发现node_modules里面的命令,所以不需要写完整的路径,直接写命令的名称就可以。然后可以通过npm或yarn运行scripts下面的命令名称,npm用run启动:npm run build ,或使用yarn启动:yarn build
**NPM Scripts是实现自动化构建工作流的最简单方式

{
  "scripts": {
    "build": "sass scss/main.scss css/style.css",
    "preserve": "yarn build",
    "serve": "browser-sync ."
  }
}

preserve是一个钩子,保证在执行serve之前,回先执行build,使样式先处理,然后再执行serve
通过--watch可以监听sass文件的变化自动编译,但是此时sass命令在工作时,命令行会阻塞,去等待文件的变化,导致了后面的serve无法去工作,此时就需要同时执行多个任务,要安装npm-run-all这个模块

{
  "scripts": {
    "build": "sass scss/main.scss css/style.css --watch",
    "serve": "browser-sync .",
    "start": "run-p build serve"
  }
}

运行npm run start命令,build和serve就回被同时执行

2.Grunt

Grunt时最早的前端构建系统,它的插件生态非常完善,它的插件可以帮你完成任何你想做的事情。由于Grunt工作过程时基于临时文件去实现的,所以会比较慢
如何使用Grunt:

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

module.exports = grunt => {
  grunt.registerTask('foo', () => {// 第一个参数是任务名字,第二个参数接受一个回调函数,是指定任务的执行内容,执行命令是yarn grunt foo
    console.log('hello grunt ~')
  })

  grunt.registerTask('bar', '任务描述', () => { // 如果第二个参数是字符串,则是任务描述,执行命令是yarn grunt bar
    console.log('other task~')
  })

  grunt.registerTask('default', () => { // 如果任务名称是'default',则为默认任务,grunt在运行时不需要执行任务名称,自动执行默认任务,执行命令是yarn grunt
    console.log('default task')
  })

  grunt.registerTask('default', ['foo', 'bad', 'bar']) // 一般用default映射其他任务,第二个参数传入一个数组,数组中指定任务的名字,grunt执行默认任务,则会依次执行数组中的任务,执行命令是yarn grunt

  // grunt.registerTask('async-task', () => {
  //   setTimeout(() => {
  //     console.log('async task working')
  //   }, 1000);
  // })

  // 异步任务,done()表示结束
  grunt.registerTask('async-task', function () { // grunt代码默认支持同步模式,如果需要异步操作,则需要通过this.async()得到一个回调函数,在你的异步操作完成过后,去调用这个回调函数,标记这个任务已经被完成。知道done()被执行,grunt才会结束这个任务的执行。执行命令是yarn grunt async-task
    const done = this.async()
    setTimeout(() => {
      console.log('async task working..')
      done()
    }, 1000);
  })

  // 失败任务
  grunt.registerTask('bad', () => { // 通过return false标志这个任务执行失败,执行命令是yarn grunt bad。如果是在任务列表中,这个任务的失败会导致后序所有任务不再被执行,执行命令是yarn grunt。可以通过--force参数强制执行所有的任务,,执行命令是yarn grunt default --force
    console.log('bad working...')
    return false
  })

  // 异步失败任务,done(false)表示任务失败,执行命令是yarn grunt bad-async-task
  grunt.registerTask('bad-async-task', function () {
    const done = this.async()
    setTimeout(() => {
      console.log('bad async task working..')
      done(false)
    }, 1000);
  })
}
module.exports = grunt => {

  grunt.initConfig({
    // 对象的属性名一般与任务名保持一致。
    // foo: 'bar'
    foo: {
      bar: 123
    }
  })

  grunt.registerTask('foo', () => {
    // console.log(grunt.config('foo')) // bar
    console.log(grunt.config('foo.bar')) // 123.grunt的config支持通过foo.bar的形式获取属性值,也可以通过获取foo对象,然后取属性
  })
}
module.exports = grunt => {

  grunt.initConfig({
    // 与任务名称同名
    build: {
      options: { // 是配置选项,不会作为任务
        foo: 'bar'
      },
      // 每一个对象属性都是一个任务
      css: {
        options: { // 会覆盖上层的options
          foo: 'baz'
        }
      },
      // 每一个对象属性都是一个任务
      js: '2'
    }
  })
  
  // 多目标任务,可以让任务根据配置形成多个子任务,registerMultiTask方法,第一个参数是任务名,第二个参数是任务的回调函数
  grunt.registerMultiTask('build', function () {
    console.log(this.options())
    console.log(`build task: ${this.target}, data: ${this.data}`)
  })
}

执行命令:yarn grunt build,输出结果

Running "build:css" (build) task
{ foo: 'baz' }
build task: css, data: [object Object]

Running "build:js" (build) task
{ foo: 'bar' }
build task: js, data: 2
module.exports = grunt => {
// 多目标任务需要通过initConfig配置目标
  grunt.initConfig({
    clean: {
      temp: 'temp/**' // ** 表示temp下的子目录以及子目录下的文件
    }
  })

  grunt.loadNpmTasks('grunt-contrib-clean')
}

执行:yarn grunt clean,就回删除temp文件

const sass = require('sass')
const loadGruntTasks = require('load-grunt-tasks')
module.exports = grunt => {
  grunt.initConfig({
    sass: {
      options: {
        sourceMap: true,
        implementation: sass, // implementation指定在grunt-sass中使用哪个模块对sass进行编译,我们使用npm中的sass
      },
      main: {
        files: {
          'dist/css/main.css': 'src/scss/main.scss'
        }
      }
    },
    babel: {
      options: {
        presets: ['@babel/preset-env'],
        sourceMap: true
      },
      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.loadNpmTasks('grunt-sass')
  loadGruntTasks(grunt) // 自动加载所有的grunt插件中的任务

  grunt.registerTask('default', ['sass', 'babel', 'watch'])
3.Gulp

Gulp时目前世界上最流行的前端构建系统,其核心特点就是高效、易用。她很好的解决了Grunt中读写磁盘慢的问题,Gulp时基于内存操作的,Gulp支持同时执行多个任务,效率自然大大提高,而且它的使用方式相对于Grunt更加易懂,而且Gulp的生态也非常完善,所以后来居上,更受欢迎

// gulp的入口文件
exports.foo = done => {
  console.log('foo task working...')
  done() // 使用done()标识任务完成
}

exports.default = done => {
  console.log('default task working...')
  done()
}

执行命令:yarn gulp foo执行foo任务,或者yarn gulo执行默认任务default
gulp4.0之前的任务写法:

const gulp = require('gulp')

gulp.task('bar', done => {
  console.log('bar working...')
  done()
})

执行命令yarn gulp bar可以运行bar任务,gulp4.0之后也保留了这个API,但是不推荐使用了

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

// gulp的入口文件
exports.foo = done => {
  console.log('foo task working...')

  done() // 标识任务完成
}

exports.default = done => {
  console.log('default task working...')
  done()
}

const task1 = done => {
  setTimeout(() => {
    console.log('task1 working...')
    done()
  }, 1000);
}

const task2 = done => {
  setTimeout(() => {
    console.log('task2 working...')
    done()
  }, 1000);
}

const task3 = done => {
  setTimeout(() => {
    console.log('task3 working...')
    done()
  }, 1000);
}

// series 串行执行
// exports.bar = series(task1, task2, task3)

// parallel 并行执行
exports.bar = parallel(task1, task2, task3)
const fs = require('fs')

exports.callback = done => {
  console.log('callback task...')
  done() // 通过使用done()标志异步任务执行结束
}

exports.callback_error = done => {
  console.log('callback task...')
  done(new Error('task failed!')) // done函数也是错误优先回调函数。如果这个任务失败了,后序任务也不会工作了
}

exports.promise = () => {
  console.log('promise task...')
  return Promise.resolve() // resolve执行的时候,表示异步任务执行结束了。resolve不需要参数,因为gulp会忽略它的参数
}

exports.promise_error = () => {
  console.log('promise task...')
  return Promise.reject(new Error('task failed')) // reject标志这是一个失败的任务,后序的任务也会不再执行
}

const timeout = time => {
  return new Promise(resolve => {
    setTimeout(resolve, time);
  })
}
exports.async = async() => {
  await timeout(1000) // 在node8以上可以使用async和await,await的就是一个Promise对象
  console.log('async task...')
}

exports.stream = (done) => { // 最常用的就是基于stream的异步任务
  const readStream = fs.createReadStream('package.json')
  const writeSteam = fs.createWriteStream('temp.txt')
  readStream.pipe(writeSteam)
  return readStream  // 相当于下面的写法
  // readStream.on('end', () => {
  //    done()
  // })
}
const fs = require('fs')
const {Transform} = require('stream')

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
}
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'))
}
// 实现这个项目的构建任务
const {src, dest, parallel, series, watch} = require('gulp')

const del = require('del')
const browserSync = require('browser-sync')

const bs = browserSync.create()

const loadPlugins = require('gulp-load-plugins')
const plugins = loadPlugins()

const {sass, babel, swig, imagemin} = plugins

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'])
}

const style = () => {
  return src('src/assets/styles/*.scss', { base: 'src' })
  .pipe(sass({ outputStyle: 'expanded' }))
  .pipe(dest('temp'))
  .pipe(bs.reload({stream: true}))
}

const script = () => {
  return src('src/assets/scripts/*.js', { base: 'src' })
  .pipe(babel({ presets: ['@babel/preset-env'] }))
  .pipe(dest('temp'))
  .pipe(bs.reload({stream: true}))
}

const page = () => {
  return src('src/**/*.html', {base: 'src'})
  .pipe(swig(data))
  .pipe(dest('temp'))
  .pipe(bs.reload({stream: true}))
}

const image = () => {
  return src('src/assets/images/**', {base: 'src'})
  .pipe(imagemin())
  .pipe(dest('dist'))
}

const font = () => {
  return src('src/assets/fonts/**', {base: 'src'})
  .pipe(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/**',
    'src/assets/fonts/**',
    'public/**'
  ], bs.reload)

  bs.init({
    notify: false,
    port: 2080,
    open: false,
    // files: 'temp/**',
    server: {
      baseDir: ['temp', 'src', 'public'], // 按顺序查找
      routes: {
        '/node_modules': 'node_modules'
      }
    }
  })
}

const useref = () => {
  return src('temp/*.html', { base: 'temp' })
  .pipe(plugins.useref({ searchPath: ['temp', '.'] }))
  .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, image, font)
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,
  compile,
  build,
  develop,
}
4.FIS

FIS是百度的前端团队推出的构建系统,FI是相对于前两种微内核的特点,它更像是一种捆绑套餐,它把我们需求都尽可能的集成在内部,例如资源加载、模块开发、代码部署、甚至是性能优化。正是因为FIS的大而全,所以在国内流行,更适合初学者使用

上一篇下一篇

猜你喜欢

热点阅读