前端工程化-01-Yeoman-Grunt

没有前端工程化遇到的问题

  • 使用ES6+新特性,但是有兼容问题
  • 使用Less/Sass/PostCss增强CSS的变成性,运行环境不能直接支持
  • 使用或快画的方式提高项目的可维护性,运行环境不能直接支持
  • 部署上线前需要手动压缩代码及资源文件
  • 部署过程中需要手动上传代码到服务器
  • 多人协作无法硬性统一大家的代码风格,从仓库中pull回来的代码质量无法保证
  • 开发是需要等待后端服务接口提前完成

工程化表现

一切以提高效率、降低成本、质量保证为目的的手段都属于「工程化」

graph LR
创建项目-->编码
编码-->预览/测试
预览/测试-->提交
提交-->部署
部署-->编码

工程化不等于某个工具

工程化的核心是对项目整体的规划和架构,工具只是落地规划和架构的一种手段

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hYlfGtv4-1591460280651)(http://oss.ahh5.com/ahh5/md/202020200529221827.png)]

工程化包含

  • 脚手架工具开发
  • 自动化构建系统
  • 模块化打包
  • 项目代码规范
  • 自动化部署

脚手架工具概要

脚手架的本质就是自动的创建项目基础结构、提供项目规范和约定,脚手架工具可以快速的搭建特定项目的骨架。

约定

  • 相同的组织结构
  • 相同的开发范式
  • 相同的模块化依赖
  • 相同的攻击配置
  • 相同的基础代码

常用的脚手架工具

  • create-react-app
  • vue-cli
  • angular-cli

相同点

根据提供信息自动创建对应的项目基础结构,但一般适用于自身所服务框架的项目

Yeoman(老牌、强大、通用)

不针对于某一框架相对常用脚手架较为灵活。Yeoman可以搭配不同的generator创建不同的项目

缺点:过于通用不够专注

基础使用

安装

yarn global add yo

安装对应的generator ==> generator-node

yarn global add generator-node

通过yo运行generator

yo node 
# 输入项目名字
# 输入项目名字
# 输入项目主页
# 输入作者
# 输入邮箱
# 输入主页
# 输入关键词

Yeoman sub Generator

可以通过生成器的子集生成一些文件。例如eslint README

生成一个node cli

# 创建cli
yo node:cli  
# 安装新增加的依赖
yarn  
# 链接到全局
yarn link "yo-test-learn" 
# 或者
npm link yo-test-learn
# 输入下面命令即可看到
yo-test-learn --help 

Yeoman 使用步骤

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

自定义Generator

基于Yeoman 搭建自己的脚手架

Generator 基本结构

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-msZgVvsb-1591460280657)(http://oss.ahh5.com/ahh5/md/202020200530230529.png)]

提供多个 sub generator 需要在app同级目录下创建一个新的目录

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tjkNyYoP-1591460280660)(http://oss.ahh5.com/ahh5/md/202020200530230749.png)]

# 创建一个文件夹
mkidr generator-hello
# 进入目录
cd generator-hello
# 创建package.json
yarn init
# 安装 yeoman-generator 模块
yarn add yeoman-generator

创建 /generators/app/index.js

  • 此文件为generator的核心入口
  • 需要导出一个继承自 Yeoman Generator 的类型
  • Yeoman Generator 工作时会自动调用我们在此类型中定义的一些生命周期方法
  • 在这个文件中调用父类中的一些方法实现一些功能比如文件写入
const Generator = require('yeoman-generator')

module.exports = class extends Generator {
    // yeoman 自动在生成文件中调用此方法
    writing() {
        // 通过文件读写方式向目标目录写入文件
        this.log('hello')
        // this.destinationPath 目前目录路径
        this.fs.write(
            this.destinationPath('temp.txt'),
            Math.random().toString()
        )
    }
}

通过 npm link 将模块安装到全局

找一个空目录运行 yo hello

.
└── temp.txt

根据模板创建文件

在app目录下创建一个templates目录

这是一个模板文件,内部使用EJS模板标记输入数据

app/templates/foo.txt

<%= title %>
<% if(success){ %>

    成功才会看到我

<% } %>

app/index.js

const Generator = require('yeoman-generator')

module.exports = class extends Generator{
    // yeoman 自动在生成文件中调用此方法
    writing(){
        // 模板文件路径
        const tmpl = this.templatePath('foo.txt')
        // 输出目标路径
        const output = this.destinationPath('foo.txt')
        // 模板数据上下文
        const context = {
            title:"hello word",
            success : true
        }
        // 输出模板文件
        this.fs.copyTpl(tmpl,output,context)
    }
}

完成目录结构

├── generators
│   └── app
│       ├── index.js
│       └── templates
│           └── foo.txt
├── package-lock.json
├── package.json
└── yarn.lock

找一个空目录运行 yo hello 即可看到输出的 foo.txt

内容如下

hello word

    成功才会看到我

接收用户输入数据

app/index.js 暴露出的类添加如下方法

prompting() {
    // 在此方法可以调用父类的 prompt() 方法对用户命令行询问
    return this.prompt([{
            type: 'input',
            name: 'name',
            message: 'your project name',
            default: this.appname //当前生成目录文件夹的名字
        }])
        .then(answers => {
            // answers => { name : 'user input value' }
            this.answers = answers
        })
}

修改 writing

writing() {
    // 模板文件路径
    const tmpl = this.templatePath('foo.txt')
    // 输出目标路径
    const output = this.destinationPath('foo.txt')
    // 模板数据上下文
    const context = {
        // 此处title 接收用户输入的值
        title: this.answers.name,
        success: true
    }
    // 输出模板文件
    this.fs.copyTpl(tmpl, output, context)
}

执行 yo hello
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xWzvFQFE-1591460280662)(http://oss.ahh5.com/ahh5/md/202020200531112617.png)]

输出文件内容如下

My project

    成功才会看到我

自己实现一个 Vue Generator

基础vue 构建我选择的还是cli,基于上面在做一些自己的配置就好了

# 创建文件夹
mkdir generator-zzy-vue
# 进入文件夹
cd generator-zzy-vue  
# 初始化项目
yarn init   
# 添加yeoman依赖   
yarn add  yeoman-generator 

手动创建一些文件

基本结构如下

├── generators
│   └── app
│       ├── index.js
│       └── templates
├── package.json
└── yarn.lock

创建一个vue模板

vue create tmp-project
rm -rf node_modules
rm -rf .git
rm yarn.lock

目录结构如下

├── .browserslistrc
├── .editorconfig
├── .eslintrc.js
├── .gitignore
├── README.md
├── babel.config.js
├── package.json
├── public
│   ├── favicon.ico
│   └── index.html
└── src
    ├── App.vue
    ├── assets
    │   └── logo.png
    ├── components
    │   └── HelloWorld.vue
    ├── main.js
    ├── router
    │   └── index.js
    ├── store
    │   └── index.js
    └── views
        ├── About.vue
        └── Home.vue

修改README.md

# <%= name %>

修改 /public/index.html


<html lang="en">

<head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
    <link rel="icon" href="favicon.ico">
    <title><%= name %>title>
head>

<body>
    <noscript>
        <strong>We're sorry but <%= name %> doesn't work properly without JavaScript enabled. Please enable it to continue.strong>
    noscript>
    <div id="app">div>
    
body>

html>

复制到generators/app/templates中

app/index.js

const Generator = require('yeoman-generator')

module.exports = class extends Generator {
    // yeoman 在询问用户环节会自动调用此方法
    prompting() {
        return this.prompt([{
                type: "input",
                name: 'name',
                message: "Your project name",
                default: this.appname
            }])
            .then(answers => {
                this.answers = answers
            })
    }
    // yeoman 自动在生成文件中调用此方法
    writing() {
        const templates = [".browserslistrc", ".editorconfig", ".eslintrc.js", ".gitignore", "README.md", "babel.config.js", "package.json", "public/favicon.ico", "public/index.html", "src/App.vue", "src/assets/logo.png", "src/components/HelloWorld.vue", "src/main.js", "src/router/index.js", "src/store/index.js", "src/views/About.vue", "src/views/Home.vue"]
        templates.forEach(filePath => {
            this.fs.copyTpl(
                this.templatePath(filePath),
                this.destinationPath(filePath),
                this.answers
            )
        })
    }
}

关于templates文件名获取, 可以通过node脚本实现

const fs = require('fs');
const path = require('path');
const _filePath = path.resolve('./templates');
const outFilePath = path.resolve('./arr');
let filePathArr = []
getFileRecursively(_filePath)

function getFileRecursively(filePath) {
    const files = fs.readdirSync(filePath)
    files.forEach(filename => {
        const filedir = path.join(filePath, filename);
        const stats = fs.statSync(filedir)
        if (stats.isFile()) {
            filedir.includes('.DS_Store') || filePathArr.push(filedir.replace(_filePath + '/', ''))
        }
        if (stats.isDirectory()) {
            getFileRecursively(filedir);
        }
    })
}
fs.writeFileSync(outFilePath, JSON.stringify(filePathArr))

注意不要将此方法放入 app/index.js 而是通过该脚本获取并输出到一个新的文件中。后面在手动复制到app/index.js中。因为不能确定 yeoman 的执行环境,以及不需要用户每次执行都去重新递归遍历所有文件

发布一个自己的npm包

  • 建立一个公开仓库推荐github
  • 创建一个npm 用户
  • npm login
  • npm publish

plop

一个小而美的脚手架工具,一般不会独立去使用,而是集成到项目之中区创建同类型的文件使用

plop基本使用

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

自动化构建

自动化通过机器代替手工完成某些操作,

自动化构建就是将源代码自动化构建为生成代码或者程序。

常见的自动化构建工具

  • Grunt

    最早的自动化构建工具,但因为通过临时文件工作,所以工作效率较慢。

  • Gulp

    使用频率较高,通过操作内存实现自动化构建,相对于Grunt速度快了很多且默认支持多任务

  • Fls

    百度开源产品,继承项目常用自动化构建流程,例如资源加载、模块化开发、代码部署、性能优化,缺点不够灵活。

Grunt

基本使用

# 初始化项目
yarn init --yes   
# 添加grunt模块
yarn add grunt --dev

根目录下创建gruntfile.js文件

  • Grunt入口文件
  • 用于定义一些需要 Grunt 自动执行的任务
  • 需要导出一个函数
  • 函数需要接收一个grunt的形参,内部提供了一些创建任务是可以用到的 API
module.exports = grunt => {
    // 注册的任务  第一个参数为任务名称  第二个参数为任务描述(可以省略)  第三个是执行代码
    grunt.registerTask('foo', '任务描述', () => {
        console.log('hello word')
    })
    // 如果任务名称为default 那么yarn grunt 会默认执行
    grunt.registerTask('default', () => {
        console.log('grunt default task')
    })
}
# 运行grunt 不加参数执行默认任务
yarn grunt

# 指定任务执行
yarn grunt foo

# 查看任务描述
yarn grunt --help

default实际工作中是执行多个task的默认任务例如

module.exports = grunt => {
    grunt.registerTask('foo', 'foo任务描述', () => {
        console.log('hello foo')
    })
    grunt.registerTask('bar', 'bar任务描述', () => {
        console.log('hello bar')
    })
    grunt.registerTask('default', ['foo', 'bar'])
}

此时执行 yarn grunt 会执行 foo bar 两个任务

异步任务

module.exports = grunt => {
    grunt.registerTask('async-task', 'async-task任务描述', function() {
        // 需要定义一个done 值为this.async() 标记此任务为异步任务
        const done = this.async()
        setTimeout(() => {
            console.log('async-task workding')
            // 异步任务执行结束后需要通过 done() 触发程序截止
            done()
        }, 1000)
    })
}

标记任务失败

在函数内部 return false

module.exports = grunt => {
    grunt.registerTask('bad', 'bad任务描述', () => {
        console.log('This is a bad task')
        return false
    })
}

如果在任务列表中,则后续任务不会执行

module.exports = grunt => {
    grunt.registerTask('bad', 'bad任务描述', () => {
        console.log('This is a bad task')
        return false
    })
    grunt.registerTask('test', 'test任务描述', () => {
        console.log('This is a test task')
        return false
    })
    grunt.registerTask('default', ['bad', 'test'])
}
// 此时执行 yarn grunt 则会抛出 warnings 警告且 test不会执行

如果采用强制执行方式 --force 则所有的任务都会执行

yarn grunt --force

异步任务失败标记

module.exports = grunt => {
    grunt.registerTask('async-task', 'async-task任务描述', function() {
        const done = this.async()
        setTimeout(() => {
            console.log('async-task workding')
            // done 传入一个实参 false
            done(false)
        }, 1000)
    })
}

配置选项方法

基本用法

module.exports = grunt => {
    // 初始化config
    grunt.initConfig({
        foo: 'test'
    })
    grunt.registerTask('foo', 'foo任务描述', () => {
        // 获取config foo的键值
        const config_foo = grunt.config('foo')
        console.log( `hello ${config_foo}` )
        // 输出 hello test
    })
}

多目标模式

通过配置多个目标, 执行多个目标

module.exports = grunt => {
    grunt.initConfig({
        // 定义build
        build: {
            // 参数配置不会作为目标执行
            options: {
                test: false
            },
            // 定义两个目标
            css: '1',
            js: '2'
        }
    })
    // 通过 registerMultiTask 方法定义
    grunt.registerMultiTask('build', function() {
        // 拿出执行目标值与值
        const {
            target,
            data,
        } = this
        // 获取所有配置
        const { test } = this.options()
        console.log( `build ${target} ${data} ` )
        console.log(` test ${test} `)
    })
}

插件使用

安装一个文件清除的插件

yarn add grunt-contrib-clean -D 
module.exports = grunt => {
    grunt.initConfig({
        // 定义clean
        clean: {
            // 定义一个目标 清除temp 下的所有文件
            temp:'temp/**'
        }
    })
    // 清除指定文件任务 需要多个目标
    grunt.loadNpmTasks('grunt-contrib-clean')
}

常用插件

编译sass

# 安装 grunt-sass 及 sass 模块
yarn add grunt-sass sass -D 
// 引入sass 模块
const sass = require('sass')
module.exports = grunt => {
    grunt.initConfig({
        sass: {
            // 定义配置文件
            options:{
                // 定义处理方法
                implementation:sass,
                // 开启sourceMap文件
                sourceMap:true
            },
            // 定义个目标
            main: {
                // 定义文件
                files:{
                    // 输出路径   输入路径
                    'dist/css/main.css':'src/scss/main.scss'
                }
            }
        }
    })
    grunt.loadNpmTasks('grunt-sass')
}

npm grunt插件自动导出

# 安装插件
yarn add load-grunt-tasks -D 
// 引入loadGruntTasks
const loadGruntTasks = require('load-grunt-tasks')
module.exports = grunt => {
    // 将grunt传入loadGruntTasks 实现挂载所有的grunt模块
    loadGruntTasks(grunt)
}

编译es6语法

yarn add grunt-babel @babel/core @babel/preset-env -D 
const loadGruntTasks = require('load-grunt-tasks')
module.exports = grunt => {
    grunt.initConfig({
        babel: {
            // 定义配置文件
            options: {
                presets:['@babel/preset-env'],
                // 开启sourceMap文件
                sourceMap: true
            },
            // 定义个目标
            main: {
                // 定义文件
                files: {
                    // 输出路径   输入路径
                    'dist/js/main.js': 'src/js/main.js'
                }
            }
        }
    })
    loadGruntTasks(grunt)
}

执行 yarn babel

修改自动更新

yarn add grunt-contrib-watch -D 
const loadGruntTasks = require('load-grunt-tasks')
const sass = require('sass')
module.exports = grunt => {
    grunt.initConfig({
        sass: {
            // 定义配置文件
            options: {
                // 定义处理方法
                implementation: sass,
                // 开启sourceMap文件
                sourceMap: true
            },
            // 定义个目标
            main: {
                // 定义文件
                files: {
                    // 输出路径   输入路径
                    'dist/css/main.css': 'src/scss/main.scss'
                }
            }
        },
        babel: {
            // 定义配置文件
            options: {
                presets: ['@babel/preset-env'],
                // 开启sourceMap文件
                sourceMap: true
            },
            // 定义个目标
            main: {
                // 定义文件
                files: {
                    // 输出路径   输入路径
                    'dist/js/main.js': 'src/js/main.js'
                }
            }
        },
        watch: {
            js: {
                files: ['src/js/*.js'],
                tasks: ['babel']
            },
            css: {
                files: ['src/scss/*.scss'],
                tasks: ['sass']
            }

        },
    })
    loadGruntTasks(grunt)
    grunt.registerTask('default', ['sass', 'babel','watch'])
}

你可能感兴趣的:(前端工程化,node.js,npm,yarn)