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

285 阅读12分钟
  • 工程化:整体设计架构
  • 实现工程化出发
    1. 模块化
    2. 组件化
    3. 规范化
    4. 自动化
  • 解决了:
    1. 传统语言或语法的舞弊
    2. 大量手动重复、机械工作
    3. 代码不统一
    4. 依赖后端接口的支持

脚手架工具

作用

  • 创建项目基础结构、提供项目规范和约定

  • 相同的组织结构、开发范式、模块依赖、工具配置、项目的基础代码

    例:IDE创建项目的过程

通用脚手架工具剖析

React : create-react-app

Vue.js : vue-cli

Angular : angular-cli

作用:根据信息创建对应的项目于基础结构

Yeoman

  • 一个通用的脚手架工具

  • Yeoman本质上就是一个Node.js 的CLI程序

  • Yeoman需要配合特定的Generator模块才能够生成对应的项目骨架

使用

  • 全局安装yo

    npm install yo --global
    
  • 安装对应的generator,要搭配特定的generator

    npm install generator-node --global
    
  • 通过yo运行generator

    cd path/to/project-dir
    mkdir my-module
    yo node
    

Sub Generator

  • 用于补充生成项目中的文件

  • 添加cli结构

    yo mode:cli
    
  • 把模块链接到全局范围,使之成功一个全局模块包

    yarn link
    
  • 步骤

  1. 明确你的需求
  2. 找到合适的Generator
  3. 全局范围安装找的Generator
  4. 通过Yo运行对应的Generator
  5. 通过命令行交互填写选项
  6. 生成你所需要的项目结构

开发脚手架 / 开发Generator

自定义Generator

  • Generator本质上就是一个NPM模块
  • 一般通过Yeoman实现自定义脚手架实际上就是开发一个Generator

步骤

  • 创建一个package.json

    yarn init
    
  • 安装一个提供工具函数的生成器模块

    yarn add yeoman-generator
    
  • 创建generators\app\index.js,作为Generator的核心入口,需要导出一个继承自Yeoman Generator的类型。Yeoman Generator在工作时会自动调用我们在此类型中定义的一些生命周期方法,我们在这些方法中可以通过调用父类提供的一些工具方法实现一些功能:文件写入

    image-20200902103417662

根据模板创建文件

  • 新建一个模板文件,内部可以使用EJS模板标记输出数据,例如:<%= title%>,其它的EJS语法也支持
  • 这样就可以不借助于fs.write写入文件
//通过模板方式写入文件到目标目录

//模板文件路径
const tmpl = this.templatePath('foo.txt')
//输出目标路径
const output = this.destinationPath('foo.txt')
//模板数据上下文
const context = { title:'hello' , success:false }
//将模板文件自动映射到输出文件上
this.fs.copyTpl(tmpl, output, context)
  • 作用:相对于手动创建每一个文件,模板的方式大大提高了效率

动态接收用户输入

module.exports = class extends Generator {
    prompting() {
        //Yeoman 在询问用户环境会自动调用此类方法
      	//在此方法中可以调用父类的prompt()方法发出对用户的命令行询问,它是一个promise方法
        return this.prompt([
            {
                type: 'input',
                name: 'name',
                message: 'Your project name',
                default: this.appanem//appname为项目生成目录名称
            }
        ])
        。then(answers => {
            //answers => {name:'user input value'}
            this.answers = answers
        })
    }
}
yo sample

Vue Generator案例(略)

发布Generator(略)

淘宝npm镜像源是只读的,不能发布

Plop

集成到项目之中,创建同类型的项目文件,提高每次我们在项目创建文件时的效率

  • 一般是创建项目中的文件
yarn add plop --dev
yarn plop component

示例:根据模板创建新的文件

yarn add plop --dev

定义脚手架任务:

//./src/plopfile.js

//plop入口文件,需要导出一个文件
//函数接收一个plop对象,用于创建生成器任务
module.exports = plop => {
    //生成器
    plop.setGenerator('component' , {
        description: 'create',
        //发起命令行交互询问
        prompts:[
            {
                type: 'input',
                name: 'name',
                message: 'component name',
                default: 'MyComponent'
            }
        ],
        //动作对象
        actions:[
            {
                type:'add',//添加一个全新的文件
                path:'src/components/{{name}}/{{name}}.js',
                //模板文件
                templateFile:'plop-templates/component.hbs'
            }
        ]
    })
}

模板文件书写:

//.\plop-templates\component.hbs

import React from 'react';

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

命令行:

yarn plop component

总结

  1. 将plop模块作为项目开发依赖安装
  2. 在项目根目录下创建一个plopfile.js文件
  3. 在plopfile.js文件中定义脚手架任务
  4. 编写用于生成特定类型文件的模板
  5. 通过Plop提供的CLI运行脚手架任务

脚手架工作原理(自定义一个小型脚手架)

脚手架的工作过程:

  • 通过命令行交互询问用户问题

  • 根据用户回答的结果生成文件

步骤:

  1. 创建一个package.json

    yarn init
    
  2. 安装项目所需要的依赖

    • inquirer:用户询问模块
    • ejs:模板引擎
    yarn add inquirer
    yarn add ejs
    
  3. 指定cli入口文件:通过package.json中的bin字段指定

    //./package.json
    {
        "bin": "cli.js"
    }
    
  4. cli.js

    #!/usr/bin/env node
    // Node CLI 应用入口文件必须要有这样的文件头
    // 如果是 Linux 或者 macOS 系统下还需要修改此文件的读写权限为 755
    // 具体就是通过 chmod 755 cli.js 实现修改
    
    const fs = require('fs')
    const path = require('path')
    const inquirer = require('inquirer')
    const ejs = require('ejs')
    
    inquirer.prompt([//inquirer.prompt:发起一个命令行的询问,数组中每个命令行成员就是发起的一个问题
      {
        type: 'input',//问题输入方式
        name: 'name',//问题返回值的键值
        message: 'Project name?'//给用户的提示
      }
    ])
    .then(anwsers => {
      console.log(anwsers)//问题接收到的用户的答案
      // 根据用户回答的结果生成文件 
    
      // 模板目录
      const tmplDir = path.join(__dirname, 'templates')
      // 目标目录
      const destDir = process.cwd()// destDir是node的方法
    
     
      fs.readdir(tmplDir, (err, files) => { // readdir自动扫描tmplDir下的文件
        if (err) throw err
        files.forEach(file => {// file得到的是相对路径,通过模板引擎渲染文件
          ejs.renderFile(path.join(tmplDir, file), anwsers, (err, result) => {
            if (err) throw err
    
            // 将结果写入目标文件路径
           
     	console.log(result)
        fs.writeFileSync(path.join(destDir, file), result)
          })
        })
      })
    })
    
  5. 模板文件:

    • templates/index.html

      <!DOCTYPE html>
      <html lang="en">
      <head>
          <meta charset="UTF-8">
          <meta name="viewport" content="width=device-width, initial-scale=1.0">
          <title><%= name %></name></title>
          <!-- 通过<%=%>获得询问环境得到的用户询问答案 -->
      </head>
      <body>
      </body>
      </html>
      
    • templates/style.css

      body{
          height: auto;
      }
      
  6. 命令行创建一个空文件夹test进行测试

    mkdir test
    cd test
    define-scaffold
    

自动化构建系统

  • 自动化构建工作流
  • 作用:使用提高效率的语法、规范和标准

工具:

  • ECMAScript Next:提高编码效率和质量
  • Sass:提高可编程性
  • 模板引擎:抽象页面中重复的HTML

自动化构建工具:构建转换那些不被支持的**“特性”**

初体验

graph LR
A[你]--> |编写| B[scss]
B[scss] --> |构建| C[css]
C[css] --> |运行| D[浏览器]
  1. sass

    步骤:

yarn add sass --dev
.\node_modules\.bin\sass sass/main.scss css/style.css
//输出路径 输入路径

//也可以在package.json里添加字段
scripts:{
	"build": "sass sass/main.scss css/style.css"
}

yarn build
  1. NPM Scripts:实现自动化构建工作流最简方式

    yarn add browser-sync --dev
    //启动测试服务器
    
    //package.json
    scripts:{
    	"build": "sass sass/main.scss css/style.css --watch".
    	//“preserve”: "yarn build",
    	//完成启动web服务器之前自动构建sass文件
    	"serve": "browser-sync . --files \"css/*.css\“",
    	//监听最新的文件变化
    	"start": "run-p build serve"
    }
    
    yarn server
    //会自动启动一个web服务器,并浏览当前网页
    
    yarn add npm-run-all --dev
    //执行多个任务,以免堵塞
    yarn start
    //build任务和preserve任务同时被执行
    

常用的自动化构建工具

  • Grunt 构建速度较慢,有磁盘读写操作
  • Gulp 基于内存实现,速度块,默认执行多个任务,生态完善
  • FIS 已集成

Grunt

  1. default命名任务可以自动调用

  2. 默认支持同步模式,使用异步模式必须要使用this.async()得到一个回调函数

    module.exports = grunt => {
        grunt.registerTask('nihao', () => {
            console.log('hi')
        })
        //注册一个任务
    
        grunt.registerTask('xi', 'ren' => {
            console.log('hello')
        })
        //注册字符串
    
        grunt.registerTask('default', ['nihao','xi'])
    
        grunt.registerTask('async-task', function() {
            const done = this.async()
            setTimeout(() => {
                console.log('hiiiii')
                done()
            }, 1000)
        })
        //默认支持同步模式,使用异步模式必须要使用this.async()得到一个回调函数
    }
    
  3. 标记任务失败

    • return false
    • 异步 done(false)
  4. 配置任务:可以通过initConfig配置属性,任务通过config获取所设属性

        grunt.initConfig({
            name: {
                a:'1'
            }
        })
        //配置属性
        grunt.registerTask('name', () => {
            console.log(grunt.config('name.a'))
            //1
        })
    
  5. 多任务模式:让任务根据配置形成多个任务

    	grunt.initConfig({
            build:{
                options: {
                    hi:'3'
                },
                //作为任务的配置选项出现
                css: {
                    options:{
                        css:'1'
                    }
                },
                js: '2'
            }
        })
    
        grunt.registerMultiTask('build', function(){
            console.log(this.options())
            console.log(`target: ${this.target}, data: ${this.data}`)
        })
        //同时执行两个目标 yarn grunt build:css执行指定目标
    
  6. Grunt插件

    yarn add grunt-contrib-clean
    
    //gruntfile.js
        //删除插件
        grunt.initConfig({
            clean: {
                temp: 'temp/**'
            }
        })
    
        grunt.loadNpmTasks('grunt-contrib-clean')
    

实现常见的构建任务

  1. 初始化项目 + 添加项目依赖
yarn init
yarn add grunt --dev

yarn add grunt-sass sass --dev	//将css文件通过编译成sass
yarn add grunt-babel @babel/core @babel/preset-env --dev	//使用babel转换特性
yarn add grunt-contrib-watch --dev	//自动编译的监视文件
yarn add grunt-contrib-clean 	//自动清除指定文件
yarn add load-grunt-tasks --dev	//自动加载所有的插件
  1. 编写Grunt的入口文件:用于定义一些需要Grunt自动执行的任务,需要导出一个函数,接收一个grunt的参数
/** gruntfile.js : 用来配置或定义任务(task)并加载Grunt插件
 * 1.wrapper函数
 * 2.项目与任务配置
 * 3.加载grunt插件和任务
 * 4.自定义任务
 **/

const sass = require('sass')
const loadGruntTasks = require('load-grunt-tasks')


module.exports = grunt => { //wrapper函数
    grunt.initConfig({ //项目与任务配置
        sass: {
            options: {
                sourceMap: true,//生成对应的sourceMap文件
                implementation: sass
            },
            main: {
                files: {
                    'dist/css/main.css': 'src/scss/main.scss'
                }
            }
        },
        babel: {//转换特性
            options: {
                sourceMap: true,
                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']
            },
            html: {
                files: ['src/*.html'],
                tasks: ['web_swig', 'bs-reload']
            }
        },
        clean: {
            //所要清除的文件路径
            files: 'dist/**'
        },
        //模板文件
        cptpl: {
            test: {
                options: {
                    banner: '/*BANNER*/\n',
                    engine: 'dot'
                },
                files: {
                    'tmp/': ['test/html/abc.html']
                }
            }
        }
    })

    loadGruntTasks(grunt)//自动加载所有的插件

    grunt.registerTask('default', ['clean', 'sass', 'babel', 'watch'])
    //为使用watch去监视sass和babel,使用registerTask做一个映射,启动时先编译,再做监听

}
  1. 可以在package.json中对命令进行封装,方便使用

    {
        "scripts": {
        "clean": "grunt clean"
      },
    }
    
  2. 此时命令行运行,便可以自动构建文件

    yarn grunt
    

Gulp

  1. 取消了同步代码模式,每个任务都必须是异步的任务,当任务结束之后需要调用回调函数或者其它方式

    exports.default = done => {
        console.log('2')
        done()
    }
    
  2. 创建组合任务:并行parallel、串行series

    const {
        series,
        parallel
    } = require('gulp')
    
    //导出成员的方式
    const hello = done => {
        setTimeout(() => {
            console.log('1')
            done() //标识任务完成
        }, 1000)
    }
    //取消了同步代码模式,每个任务都必须是异步的任务,当任务结束之后需要调用回调函数或者其它方式
    
    const hi = done => {
        setTimeout(() => {
            console.log('2')
            done()
        }, 1000)
    }
    
    const nihao = done => {
        setTimeout(() => {
            console.log('3')
            done()
        }, 1000)
    }
    
    
    exports.series = series(hello, hi, nihao)
    exports.parallel = parallel(hello, hi, nihao)
    
    //同时运行css和js的任务互不扰,可以使用并行
    //部署先编译,串行
    
    yarn glup hi
    
  3. 三种异步任务

    • callback
    • promise
    • async / await ES2017
    • stream
    exports.callback = done => {
        console.log('1')
        done()
    }
    
    exports.callback_error = done => {
        console.log('2')
        done(new Error('hi'))
    }
    
    ===========================================
    
    exports.promise = () => {
        console.log('3')
        return Promise.resolve()
    }
    
    exports.promise_error = () => {
        console.log('5')
        return Promise.reject(new Error('hi~'))
    }
    
    ===========================================
    
    //async / await
    const timeout = time => {
        return new Promise(resolve => {
            setTimeout(resolve, time)
        })
        //返回一个promise对象
    }
    
    exports.async = async () => {
        await timeout(1000)
        console.log('666')
    }
    
    ===========================================
    
    //Gulp支持
    exports.stream = () => {
        const readStream = fs.createRreadStream('package.json')
        const writeStream = fs.createRreadStream('temp.txt')
        readStream.pipe(writeStream)
        // return readStream//可以正常结束,glup中只是注册了这个事件,里面有done()
        readStream.on(`end` , () => {
            done()
        })
    }
    

核心工作原理

输入:读取流 => //src

加工:转换流 => //dest

输出:写入流

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

export.default = () => {
    return src('src/*.css')//创建文件读取流
        .pipe(cleanCss())//转换流
        .pipe(dest('dist'))//写入流:dest写入目标任务
}

案例1:基于Glup写一个自动化构建项目

github.com/zce/zce-gul…

文档框架:

├── public ······························ 不需要被加工,最终要拷贝到文件夹目录中的内容
│   ├── favicon.ico ················· 站点图标
├── src ····························· 都会被构建,被转换
│   ├── assets ················· 编写样式
│   └── layouts ···················· 
│   └── partials ···················· .html文件,通过模板编写
├── gulpfile.js ······················· 脚手架
样式 / 脚本 / html编译任务

① 样式编译任务

const sass = require('gulp-sass')

const style = () => {
    return src('src/assets/styles/*.scss',{base:'src'})
    .pipe(sass())
    .pipe(dest('dist'))
}

module.exports = {
    style
}

② 脚本编译任务

const babel = require('gulp-babel')

const script = () => {
    return src('src/assets/scripts/*.js', { base: 'src'})
    .pipe(babel({ presets: ['@babel/preset-env'] }))
    //babel是 的转换平台 / presets最新特性整体打包
    .pipe(dest('dist'))
}

//导出
module.exports = {
    script
}

③ html编译任务

const data = {
    menus:[...],
	pkg:require('./package.json')
    date:new Date()
}

const page = () => {
    return src('src/*.html', { base: 'src'})
    .pipe(swig({ data }))//模板引擎的插件转换
    .pipe(dest('dist'))
}

④ 三个任务同时执行

const { parallel } = require('gulp')

const compile = parallel(style, script, page)

module.exports = {
    //只导出compile任务
    compile
}
字体 / 图片文件转换
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'))
}

module.exports = {
    image,
    font
}
其它文件 / 文件清除
const extra = () => {
    return src('public/**', { base:'public' })
    .pipe(dest('dist'))
}
//避免产生混淆
const build = parallel(compile, extra)

module.exports = {
    build
}
const {series} = require('gulp')//需要先删除dist文件,再去生成dist

const del = require('del')

const clean = () => {
	return del(['dist'])//放文件路径
}

const build = series(clean, parallel(compile, extra))

module.exports = {
    build
}
自动加载插件

使用load-plugins加载插件

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

const plugins = loadPlugins
开发服务器 / 监视变化
yarn add browser-sync --dev
const { watch } = require('gulp')

const browserSync = require('browser-sync')
const bs = browserSync.create()//自动创建一个开发服务器,单独定义到一个任务启动

const serve = () => {
  watch(['src/assets/images/**',
    'src/assets/fonts/**',
    'public/**'
  ], bs.reload) //bs.reload是一个任务
  watch('src/assets/scripts/*.js', script)
  watch('src/*.html', page)
  watch('src/assets/styles/*.scss', style) //监视样式文件、脚本等
  bs.init({
    notify: false, //提示
    port: 2080,
    // files: 'dist/**', //自动更新浏览器
    server: {
      baseDir: ['temp', 'src', 'public'],
      routes: {
        '/node_modules': 'node_modules'
      }
    }
  })
}

module.exports = {
    serve
}
构建优化

※ 如果页面不刷新,是因为swig模板引擎缓存的机制导致页面不会变化,此时需要额外将swig选项中的cashe设置为false

const page = () => {
    ...
    .pipe(plugins.swig({ data, defaults: { cache: false } })) // 防止模板缓存导致页面不能及时更新
}
useref文件引用处理

合并构建注释内容到一个js文件中

yarn add gulp-useref --dev
/** useref插件处理html内的构建注释,线上无法显示
 */
const useref = () => {
    return src('dist/*.html', {base:'dist'})
    .pipe(plugins.useref({ searchPath: ['dist', '.']}))
    .pipe(dest('dist'))
}

构建注释全部去掉,把构建注释里的内容包含到一个文件中

在useref中进行文件压缩(html / js / css)
yarn add gulp-htmlmin gulp-uglify gulp-clean-css --dev

需要判断什么文件

yarn add gulp-if --dev
重新规划构建过程

build:上线之前执行的任务,最终构建的文件放到dist中

develop:开发过程执行的任务,临时文件放在temp中

//package.json
"scripts": {
    "clean": "gulp clean",
    "build": "gulp build",
    "develop": "gulp develop",
  },
//.gitignore

/temp
/dist

案例2:封装工作流 / 构建多个项目

即,提取一个可复用的工作流

  • glupfile + gulp = 构建工作流

  • gulpfile + gulp CLI = my-pages


本文首发于我的GitHub博客,其它博客同步更新。