前端工程化

590 阅读7分钟

工程化

无标题.png

定义

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

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

主要解决的问题

  • 传统语言或者语法的弊端
  • 无法使用模块化/组件化
  • 重复的机械式工作
  • 代码风格统一、质量保证
  • 依赖后端服务接口支持
  • 整体依赖后端项目

工程化≠某个工具

在现阶段有一部分工具过于庞大,例如webpack,导致有些新手就误认为工程化就是指webpack,只要用了webpack就有了工程化,其实不是这样的。工具并不是工程化的核心,工程化的核心是对项目整体的规划或者架构,工具在这个过程中只是用来落地去实现这种规划或架构的一种手段。

脚手架工具

脚手架工具概要

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

相同的约定:

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

常用的脚手架工具

react项目-->create-react-app、vue项目-->vue-cli、angular项目-->angular-cli, 以上三种脚手架都是根据信息创建对应的项目基础结构。
Yeoman是通用型项目脚手架工具,它可以根据一套模板生成对应的项目结构,这种比较灵活,容易扩展。
Plop用于在项目开发过程中创建特定类型的文件,例如创建一个组件/模块所需要的文件。

Yeoman

基本使用

安装
全局范围安装yonpm i yo --global
安装对应的generator,这里用node作示例npm i generator-node --global
通过yo运行generatoryo node
sub generator
在已有的项目基础上创建特定类型的文件,例如yo node:cli

使用步骤总结

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

自定义Generator

自定义Generator就是基于Yeoman搭建自己的脚手架。
创建Generator

Generator本质上就是一个npm模块

Generator基本结构

image.png 命令
generator-<name>

示例

//创建generator-sample目录
mkdir generator-sample
cd generator-sample\
//初始化package.json
npm init
//安装yeoman依赖
npm i yeoman-generator

接着进入generator-sample文件夹创建generators/app/index.js文件

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

const Generator = require('yeoman-generator')
//导出一个类型,让这个类型继承自Generator
module.exports = class extends Generator {
  writing () {
    // Yeoman 自动在生成文件阶段调用此方法
    // 我们这里尝试往项目目录中写入文件
    this.fs.write(
        //文件绝对路径
        this.destinationPath('temp.txt'),
        Math.random().toString()
    )
  }
}

接着通过npm link方式链接到全局范围,使之成为全局模块包,这样yeoman在工作的时候就可以找到我们写的generator-sample。然后就可以通过yo sample运行这个生成器创建一个temp.txt。

  • 根据模板创建文件

很多时候需要自动创建的文件很多,而且文件的内容也相对复杂,这种情况下可以使用模板创建文件。 首先创建generators/app/templates/foo.txt文件

// 这是一个模板文件foo.txt
// 内部可以使用 EJS 模板标记输出数据
// 例如:<%= title %>
// 其他的 EJS 语法也支持

<% if (success) { %>
哈哈哈
<% }%>

有了模板过后,在生成文件时就可以通过模板方式写入文件到目标目录

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

const Generator = require('yeoman-generator')

module.exports = class extends Generator {
  writing () {
    // Yeoman 自动在生成文件阶段调用此方法

    // 通过模板方式写入文件到目标目录
    // 模板文件路径
    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)
  }
}
  • 接收用户输入数据
// index.js
// 此文件作为 Generator 的核心入口
// 需要导出一个继承自 Yeoman Generator 的类型
// Yeoman Generator 在工作时会自动调用我们在此类型中定义的一些生命周期方法
// 我们在这些方法中可以通过调用父类提供的一些工具方法实现一些功能,例如文件写入

const Generator = require('yeoman-generator')

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

    // 模板文件路径
    const tmpl = this.templatePath('bar.html')
    // 输出目标路径
    const output = this.destinationPath('bar.html')
    // 模板数据上下文
    const context = this.answers

    this.fs.copyTpl(tmpl, output, context)
  }
}

Vue Generator案例

//创建目录
mkdir generator-jc-vue
cd generator-jc-vue\
//初始化package.json
npm init
//安装yeoman依赖
npm i yeoman-generator

新建generator主目录文件generators/app/index.js
创建generators/app/templates目录,把项目原有的结构拷贝进来作为模板,并将相应的项目名称改写成模板引擎语法<%= name %> image.png

// index.js
const Generator = require('yeoman-generator')

module.exports = class extends Generator {
  prompting () {
    return this.prompt([
      {
        type: 'input',
        name: 'name',
        message: 'Your project name',
        default: this.appname
      }
    ])
    .then(answers => {
      this.answers = answers
    })
  }

  writing () {
    // 把每一个文件都通过模板转换到目标路径

    const templates = [
      '.browserslistrc',
      '.editorconfig',
      '.env.development',
      '.env.production',
      '.eslintrc.js',
      '.gitignore',
      'babel.config.js',
      'package.json',
      'postcss.config.js',
      'README.md',
      'public/favicon.ico',
      'public/index.html',
      'src/App.vue',
      'src/main.js',
      'src/router.js',
      'src/assets/logo.png',
      'src/components/HelloWorld.vue',
      'src/store/actions.js',
      'src/store/getters.js',
      'src/store/index.js',
      'src/store/mutations.js',
      'src/store/state.js',
      'src/utils/request.js',
      'src/views/About.vue',
      'src/views/Home.vue'
    ]
    // 循环遍历生成对应的目标文件
    templates.forEach(item => {
      // item => 每个文件路径
      this.fs.copyTpl(
        this.templatePath(item),
        this.destinationPath(item),
        this.answers
      )
    })
  }
}

通过yo jc-vue命令生成项目

发布Generator

因为Generator实际上就是一个npm模块,所以发布Generator就是发布一个npm模块,我们只需要将自己写好的Generator模块通过npm publish命令发布成一个公开的模块。

Plop

Plop是一个主要去创建项目中特定类型文件的工具,一般会集成到项目中用来自动化的创建同类型的文件,不独立使用。

// 安装依赖
npm i plop --dev

在项目根目录下新建一个plopfile.js文件

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

module.exports = plop => {
  // setGenerator(生成器名字,生成器配置选项)
  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'
      }
    ]
  })
}
// 执行plop
npm plop component

脚手架的工作原理

大部分脚手架的工作原理就是启动之后去询问你一些预设的问题,根据你的回答结合模板文件生成一个项目的结构。

mkdir sample-scaffolding
cd sample-scaffolding\
npm init

在package.json文件中添加bin字段,用于指定cli项目的入口文件

// package.json
{
  "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"
  }
}

添加cli.js文件

#!/usr/bin/env node
// Node CLI 应用入口文件必须要有这样的文件头
// 如果是 Linux 或者 macOS 系统下还需要修改此文件的读写权限为 755,这样这个文件才可以作为一个cli入口
// 具体就是通过 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 => {
  // 根据用户回答的结果生成文件

  // 模板目录
  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)
      })
    })
  })
})

执行npm link把这个模块link到全局 执行sample-scaffolding生成文件