skip to content
OnionTalk

使用yeoman创建属于你自己的脚手架

现代前端开发,由于构建工具的大规模使用,项目的配置趋于日渐复杂。很多时候你只是需要快速编写一个简单的demo,但是你首先需要做的可能是花费不少的时间搭建一整套的前端工具链。为了解决这个问题,社区创造出了不少比较成熟的脚手架或者boilerplate来让开发者快速搭建项目。但是如果你想快速的定制化属于你自己的脚手架,今天要聊的yeoman可能是一个不错的选择。

什么是 yeoman generator

真正开始写一个 yeoman 脚手架之前,我们需要先搞清楚 generator 这个名词。

generator 是 yeoman 提供的对于脚手架的封装,每一个 generator 是一个单独的 npm package。不同的 generator 之间可以通过组合生成新的 generator(e.g. generator-generator 是对于 generator-node 的组合)

如果你想使用一个已经发布的 generator,你只需要


yarn global add yo generator-[generator-name] # e.g generator-node

yo [generator-name] # e.g yo node

如何编写一个 yeoman generator

上面也提到过 generator 其实是 yeoman 提供的一个对于脚手架抽象的封装,所以在 yeoman 对于 generator 的项目结构也做出了一些开发者必须要满足的要求。

在项目的 package.json 中

  • 项目必须依赖于 yeoman-generator,
  • 项目 keywords 必须有 yemoan-generator
  • package 名应该以 generator-[generator-name]的格式进行命名

项目结构

yeoman generator 支持两种不同的项目组织模式

模式 1

├───package.json
└───generators/
    ├───app/
    │   └───index.js
    └───router/
        └───index.js

模式 2

├───package.json
├───app/
│   └───index.js
└───router/
    └───index.js

需要注意的是, 第二种模式下必须要在 pacakge.json 里面显式的做出声明

{
  "files": ["app", "router"]
}

一个最简单的 generator

以下是一个最简单的 Generator

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

module.exports = class extends Generator {
  constructor(args, opts) {
    super(args, opts);

    this.option('babel'); //
  }

  method1() {
    this.log('method 1 just ran');
  }

  method2() {
    this.log('method 2 just ran');
  }
};

运行 npm link 将你本地的 generator link 到全局的 path 中, 然后运行 yo foo

console 将会打印出 method1 just ranmethod2 just ran

私有方法

yeoman 在运行时会把所有的 class 内部的 class 方法都进行调用,如果你只是想做一些逻辑的规整和抽离,并不想抽离出来的这些方法被调用,你可以使用 yeoman 提供的两种私有方法声明方式。


class extends Generator {
   constructor(args, opts) {
       super(args, opts);
   }

   // use instance method
   this.utilMethod = () => console.log("I won't be called");
}

or


class extends Generator {
   constructor(args, opts) {
       super(args, opts);
   }

   // use _ as a private method mark
   _utilMethod = () => console.log("I won't be called");
}

yeoman 还提供了通过 extends generator 的方法去创建私有方法,Extend a parent generator 但是笔者认为这个方式不够优雅,不太推荐大家使用。

Run Loop

yeoman 内部通过Grouped-queue 实现了一套定义好的任务队列,任务队列的顺序如下。

  1. initializing - 初始化脚手架,读取 yeoman 配置等。
  2. prompting - 通过 this.prompt()与用户进行交互。
  3. configuring - 编辑和配置项目的配置文件。
  4. default - 如果 generator 内部还有不符合任意一个任务队列任务名的方法,将会被放在 default 这个任务下进行运行。
  5. writing - 将预置好的模板进行填充
  6. conflicts - 处理冲突,比如文件名重复等(仅限内部使用)
  7. install - 进行依赖的安装(如 npm, bower)
  8. end - 最后一个任务,一般在这个任务里面会进行一些 clean up 之类的工作

每一个任务通过实现一个对应名字的 class method 来进行声明

class extends Generator {
   // initializing | prompting...
   [priorityName]() {
       // do some thing
   }
}

Prompt

yeoman 使用了 Inquirer 来做用户提示

Inquirer 支持以下几种类型的用户提示

  • input
  • confirm
  • list
  • rawlist
  • expand
  • checkbox
  • password
  • editor

yeoman 通过 prompt 任务中调用 this.prompt 来进行用户提示。


module.exports = class extends Generator {
   async prompting() {
       this.answers = await this.prompt([{
           type    : 'confirm',
           name    : 'cool',
           message : 'Would you like to enable the Cool feature?'
           // 作为下一次的default
           store: true
       }]);
   }

   writing() {
       this.log('cool feature', this.answers.cool); // user answer `cool` used
   }
};

需要注意的是,所有的 console 输出 yeoman 都推荐使用内建的 this.log() 而不是 console.log() 或者 =process.stdout.write()=进行输出。

Argument 和 options

作为一个 CLI 工具,如何得到用户输入的 argument 和 options 也是一个很需要考虑的问题。好消息是 yeoman 也帮我们做了封装。

Argument?

怎样得到 my-project?

yo webapp my-project

module.exports = class extends Generator {
  constructor(args, opts) {
    super(args, opts);
    this.argument('appname', { type: String, required: true });
    // this.options.appname 就是 my-project
    this.log(this.options.appname);
  }
};

argument 有以下 options 可以在定义一个 argument 的时候传入。

  • desc argument 描述
  • required 是否必须
  • type String,Number,Array 或者 function
  • default 这个 argument 的 default value

options

怎样得到 —foo? yo webapp --foo

module.exports = class extends Generator {
  constructor(args, opts) {
    super(args, opts);
    // 绑定--foo 这个option
    this.option('foo');
    this.log(this.options.foo);
  }
};

option 有以下几个 options 可以在定义一个 optins 的时候传入。

  • desc option 描述
  • alias option 的 short name, 比如 -h 可以定义为 --help 的 short name
  • type Boolean,String, Number 或者 function
  • default 这个 option 的 default value
  • hide 这个 option 是否要在—help 中隐藏

需要注意的是,不管是 option 还是 argument,都需要把定义声明在 constructor 中

文件命令

Yeoman 封装了一系列路径和文件操作的语法糖来很便利的提供使用者与文件系统的交互。

this.destinationRoot() 是一个对于 destination 位置的语法糖,支持传入文件或者目录名进行连接。

this.destinationRoot();
// returns '~/projects'

this.destinationRoot('index.js');
// returns '~/projects/index.js'

对于 generator 中包含的模板的地址,yeoman 也提供了一些语法糖来帮助我们取得其路径。

this.sourceRoot();
// returns './templates'

this.templatePath('index.js');
// returns './template/index.js'

以上配置都可以通过修改.yo - rc.json来进行修改;

修改一个模板

yeoman 提供了一个 fs.copyTpl 方法来进行模板的填充。fs.copyTpl 方法默认使用 ejs 模板。

<!-- index.html -->
<html>
  <head>
    <title><%= title %></title>
  </head>
</html>
class extends Generator {
 writing() {
   this.fs.copyTpl(
     this.templatePath('index.html'),
     this.destinationPath('public/index.html'),
     { title: 'Templating with Yeoman' }
   );
 }
}

渲染后的结果

<html>
  <head>
    <title>Templating with Yeoman</title>
  </head>
</html>

参考资料

create a generator