脚手架工具

281 阅读6分钟

概要

在对前端工程化的整体有了初步的认识之后,我们就顺着一个项目的开发过程先从脚手架开始去探讨前端工程化在项目创建环节当中的表现。

脚手架的本质作用

脚手架可以简单地理解为就是用来自动地去帮我们创建项目基础结构的一个工具,那看似很普通的一个需求背后,却包含一定的哲学。除了创建文件,更重要的是提供给开发者一些约定和规范。

通常在去开发相同类型的项目时都会有一些相同的约定,这其中包括:

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

这样一来我们在搭建新项目的时候有大量的重复工作要做。脚手架工具呢,就是用来去解决这样一类问题的。我们可以通过脚手架工具快速地搭建特定类型的项目骨架,然后基于这个骨架进行后续的开发工作。

举个例子

IDE 创建项目的过程就是一个脚手架的工作流程。 以Android Studio为例:

从点击创建新项目开始就进入了Android Studio脚手架工作流程,我们可以先去选择类型,再去填写项目相关的一些属性和配置,最后就可以得到一个安卓项目最基础的骨架,这里面默认的一些代码和结构都会自动去帮我们去生成。

前端脚手架

前端项目创建过程当中,由于前端技术选型比较多样,另外又没有一个统一的标准,所以前端方向的脚手架一般不会集中在某一个IDE当中。 它都是一个独立的工具存在,从而显得相对会复杂一些,但是本质上脚手架的目标都是一样的,因为他们都是为了解决我们在创建项目过程当中那些复杂的工作。

常用的脚手架工具

有很多成熟的脚手架工具,但是大多数是为了特定类型的项目服务的。

以上的实现方式都大同小异无,外乎都是根据你提供的一些信息自动去生成一些项目所需要的特定文件以及相关哪些配置,但它们一般只是用于自身所服务的那个框架的项目。

还有一类就是像Yeoman为代表的通用性脚手架工具。它们可以根据一套模板生成一个对应的项目结构,这种类型的脚手架的一般都很灵活,而且很容易扩展。

除了以上说的创建项目时才会用到的脚手架工具,还有一类脚手架也非常有用,代表性的工具叫做Plop。它们在项目开发过程当中用于去创建一些特定类型的文件,例如我们要想在一个组件化的项目当中去创建一个新的组件或者是模块化的项目当中去创建新的模块,但这些模块和组件一般是由特定的几个文件组成的,而且每一个文件的都有些基本的代码结构,那相对于我们手动一个一个去创建的话,脚手架会提供更为便捷,更稳定的一种操作方式。

Yeoman

简介

官方定义,Yeoman是一款用于创造现代化Web应用的脚手架工具,但不同于vue-cli这样的工具,Yeoman更像是一个脚手架的运行平台,我们可以通过Yeoman搭配不同的generator去创建任何类型的项目。也就是说我们可以通过创建自己的generator从而去定制属于我们自己的前端脚手架。Yeoman的优点同样也是它的缺点,在很多专注基于框架开发的人眼中Yeoman过于通用不够专注,所以他们更愿意使用像vue-cli这类的脚手架,这也是像vue-cli这类的工具现在变得这么成功的原因。

基础使用

  • 在全局范围安装 yo

$ npm install yo --global # or yarn global add yo

  • 安装对应的 generator

$ npm install generator-node --global # or yarn global add generator-node

  • 通过 yo 运行generator
$ mkdir my-module
$ cd my-module
$ yo node

Sub Generator

有时候我们并不需要去创建完整的项目结构可能我们只是需要在已有的项目基础之上去创建一些特定类型的文件内容。例如给一个已经存在的项目去创建README,又或者说在一个原有的项目基础之上去添加某些类型的配置文件,比如说Eslint或是Babel的配置文件,这些文件它们都有些基础代码,如果手动去写很容易出错,我们可以通过生成器自动地去生成,这样可以提高效率。

常规使用

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

自定义 Generator(基于 Yeoman 搭建自己的脚手架)

通过以上的介绍我们发现不同的Generator可以用来生产不同项目,也就是说,我们可以通过创造自己的Generator去帮我们生成自定义的项目结构。即便是市面上已经有了很多的Generator,我们还是有创造自己的Generator的必要,因为市面上的Generator都是通用的,而我们在实际开发过程当中,会出现一部分基础代码,甚至业务代码在相同类型项目时是会重复的,那我们就可以把公共的部分都放的脚手架当中去生成,让脚手架工具发挥更大的价值。例如,我们在创建vue.js项目的时候,官方默认的脚手架工具只会去创建一个最基础的项目骨架,并不包含我们经常要用到的一些模块,例如axios、router、vuex等,我们需要在每次创建完项目之后再去手动的引入这些模块,并且去编写一些基础的使用代码。那试想一下,如果我们把这些也放到脚手架当中那么就不存在我们刚说的这样一个问题了。那么自定义Generator该如何去实现?接下来通过自定义一个带有一定基础代码的vue.js项目脚手架来跟大家具体介绍。

创建Generator模块(Generator本质上就是一个NPM模块)

Generator基本结构 提供Sub Generator 除了特定结构还有一个与普通的NPM不同的是Yeoman的Generator模块名称必须是generator-<name>的格式,如果说你在具体开发的时候没有去使用这样一个格式的名称,那Yeoman在后续工作的时候就没有办法找到你所提供的Generator模块。

$ mkdir generator-sample
$ cd generator-sample
$ yarn init
$ yarn add yeoman-generator # 提供了生成器的一个基类,它当中有一些工具函数,在创建生成器的时候可以使用

根据基本目录结构,创建 generators/app/index.js 文件

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

const Generator = require('yeoman-generator')

module.exports = class extends Generator {
  writing () {
    // Yeoman 自动在生成文件阶段调用此方法
    
    // 我们这里尝试往项目目录中写入文件
    this.fs.write(
      this.destinationPath('temp.txt'),
      Math.random().toString()
    )
  }
}

以上一个简单的Generator就完成了

$ yarn link # 把这个模块链接到全局范围,使之成为一个全局模块包

接下来就可以通过Yeoman来运行这个生成器

$ mkdir my-project
$ cd my-project
$ yo sample

运行过后 my-project 文件夹下就生成了 temp.txt 文件

根据模板创建文件

有时候需要创建的文件很多而且文件的内容也相对复杂,在这样的情况下就可以使用模板去创建文件提高效率。 创建 generators/app/templates/foo.txt 文件

这是一个模板文件
内部可以使用 EJS 模板标记输出数据
例如:<%= title %>

其他的 EJS 语法也支持

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

generators/app/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)
  }
}

接下来就可以再通过Yeoman来运行这个生成器

$ cd my-project
$ yo sample

运行过后 my-project 文件夹下就生成了 foo.txt 文件

这是一个模板文件
内部可以使用 EJS 模板标记输出数据
例如:Hello zce~

其他的 EJS 语法也支持
接收用户输入数据

对于模板当中的一些动态数据,例如像项目的名称、标题等,一般通过命令行交互的方式去询问使用者得到。在Generator当中想要发起一个命令行交互的询问可以通过实现prompting方法。

generators/app/index.js

// 此文件作为 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
    })
  }
  / 模板文件路径
    const tmpl = this.templatePath('bar.html')
    // 输出目标路径
    const output = this.destinationPath('bar.html')
    // 模板数据上下文
    const context = this.answers

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

接下来就可以再通过Yeoman来运行这个生成器,就会有用户输入的相应提示

$ cd my-project
$ yo sample
my-project yo sample
? Your project name (my project)
Vue Generator案例
$ mkdir generator-xsq-vue
$ cd generator-xsq-vue
$ yarn init
$ yarn add yeoman-generator # 提供了生成器的一个基类,它当中有一些工具函数,在创建生成器的时候可以使用

然后在 generators/app/templates 下创建理想的项目与结构,通过EJS 语法在项目中接受用户自定义输入

generators/app/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
      )
    })
  }
}
$ yarn link # 把这个模块链接到全局范围,使之成为一个全局模块包

接下来就可以通过Yeoman来运行这个生成器

$ mkdir my-vue-project
$ cd my-project
$ yo xsq-vue

运行过后 my-vue-project 文件夹下就生成了自定义的vue项目结构

发布 Generator

一般会将这个项目的源代码托管到一个公开的源代码仓库,首先创建一个本地的仓库,在创建本地仓库之前,先去创建.gitignore

$ cd generator-xsq-vue
$ echo node_modules > .gitignore
$ git init # 初始化本地的一个空仓库
$ git add .
$ git commit -m"feat: initial commit"

然后到github创建一个远端仓库

$ git remove add origin 仓库地址
$ git push -u origin master
$ yarn publish --registry=https://registry.yarnpkg.com # 发布模块

发布过后就可以通过npm 或 yarn 安装使用了。

Plop

简介

是一款主要用于去创建项目中特定类型的文件一个小工具,他有点Yeoman当中的SubGenerator,不过一般不会独立去使用,我们会把它集成到项目当中,用来自动化的去创建同类型的项目文件。例如一个组件需要.css .js等的文件,且文件内容都有相同的一些基础代码,通过plop就可以自动地帮我们创建该组件需要的文件。

基本使用

接下来在React项目当中去加入Plop的集成去了解该如何具体使用。

$ cd my-react-project
$ yarn add plop --dev

安装成功后在根目录下新建propfile.js

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

module.exports = plop => {
  // component为生成器名称
  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-templates
——component.hbs
——component.css .hbs
——component.test.hbs
$ yarn plop component

命令执行过后src/components下就自动地根据我们的模板创建了MyComponent组件。

使用总结

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

脚手架的工作原理

通过前面对一些脚手架工具的介绍不难发现大部分脚手架的工作原理都很简单,无外乎是启动过后主动的去询问一些预设的问题,然后将你回答的结果结合一些模板文件生成一个项目的结构。

接下来就通过Node.js去开发一个小型的脚手架工具去深入体会一下脚手架工具的工作过程。脚手架实际上就是一个Node CLI应用。

$ mkdir sample-scaffolding
$ cd sample-scaffolding
$ yarn init

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

"bin": "cli.js",

在cli.js 中 console测试一下

console.log('cli working')
$ yarn link
$ sample-scaffolding
 => cli working 

接下来去实现脚手架的工作过程

#!/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)
      })
    })
  })
})