yeoman生成器超详细的教程解读,看完写不出脚手架生成器你找我!

2,151 阅读21分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第2天,点击查看活动详情

快速入门

温馨提示:学习此篇文章前,您可以先学习juejin.cn/post/717292… 了解一下yemon,并按照示例快速体验自己手写一个脚手架生成工具。

使用generators可以实现自己快速手写一个脚手架生成器。

generators是Yeoman 生态系统的基石。它们是由 yo 命令运行的一个脚本,用来生成用户规定好模板的文件。

现在,我们一遍学习生成器编写,一遍学习官方的一些api。

生成器创建

注:为了便于演示,我们的生成器命名为:gcshi

模块创建

生成器的核心是 Node.js 模块。

首先,创建一个文件夹,在其中编写生成器。此文件夹必须命名为 generator-XXX (其中 XXX 是生成器的名称,本示例中可以写为 generator-gcshi)。

必须严格安装此方式创建文件名,Yeoman依赖文件系统寻找可用的插件

创建一个 packe.json 文件。

{
  "name": "generator-gcshi",
  "version": "0.1.0",
  "description": "",
  "files": [
    "generators"
  ],
  "keywords": ["yeoman-generator"],
  "dependencies": {
    "yeoman-generator": "^1.0.0"
  }
}
  • name属性必须以 generator- 做为前缀
  • keywords属性必须包含“yeoman-generator
  • 仓库最好有一个描述,以便被生成器页面索引。

您应该确保将最新版本的yeoman-generator设置为依赖项。你可以通过运行下面的代码完成

npm install——save yeoman-generator

files属性必须是生成器使用的文件和目录数组。

files 配置是一个数组,用来描述当把 npm 包作为依赖包安装时需要说明的文件列表。当 npm 包发布时,files 指定的文件会被推送到 npm 服务器中,如果指定的是文件夹,那么该文件夹下面所有的文件都会被提交。

可以根据情况完成 packe.json 文件。

构建文件夹树

Yeoman的功能取决于你如何构建你的目录树。 每个子发生器都包含在自己的文件夹中。

当你调用 yo name 命令时(name是自定义生成器名),使用的是默认生成器,默认生成器必须位于 / app目录中。

当使用子生成器时,在app同级目录新建一个目录

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

然后,使用如下命令

yo gcshi:router

Yeoman允许两种不同的目录结构来注册可用的生成器, ./generators/ 的形式都是可用的。

上面的例子也可以写成如下:

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

如果您使用第二个目录结构,必须在package.json中的files属性内增加文件名。

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

上述结构配好后,就可以开始编写实际的生成器了。

编写生成器

yeoman提供了一个基类(yeoman-generator)用来帮助用户快速实现生成器,您可以扩展它来实现自己的行为。

在生成器的 index.js 文件中,下面是扩展基生成器的方法:

let Generator = require('yeoman-generator');

module.exports = class extends Generator {
  
};

重写构造函数

有些生成器方法只能在构造函数内部调用。这些特殊的方法可能会做一些事情,比如设置重要的状态控件,并且不能在构造函数之外运行。

若要重写生成器构造函数,请像下面这样添加构造函数方法:

module.exports = class extends Generator {
  // The name `constructor` is important here
  constructor(args, opts) {
    // 调用super构造函数非常重要,这样才能正确设置生成器
    // args, opts是父类接受的两个参数
    super(args, opts);

    // 接下来,添加自定义代码
    this.option('babel'); // 这个方法增加了对“——babel”标志的支持
  }
};

添加自己的功能

每个添加到原型中的方法都在调用生成器之后运行——通常是按顺序运行。但是,正如我们将在下一节中看到的,一些特殊的方法名将触发特定的运行顺序

让我们添加一些方法:

let Generator = require('yeoman-generator');

module.exports = class extends Generator {
  method1() {
    // this.log是对console.log()的封装,为了解决一些异常
    this.log('哈哈哈哈,我是第一');
  }

  method2() {
    this.log('哈哈哈哈,我是第二');
  }
};

运行生成器时,您将看到这些记录运行的时机。

运行生成器

因为是在本地开发生成器,所以它还不能作为全局 npm 模块使用。可以使用 npm 创建全局模块并将其与本地模块进行软链接。这样就可以运行项目了。

在命令行上,从生成器项目的根目录(generator-gcshi/文件夹中)键入:

npm link

此时,项目被全局安装,可以使用 yo gcshi 命令运行项目

注意:必须先安装yo命令,项目里必须安装yeoman-generator依赖

可以看见,Generator子类的原型上的方法确实是被逐个执行了。

查找项目根目录

在运行生成器时,yeoman将尝试根据它所运行的文件夹的上下文解决一些问题。

要注意的是,Yeoman会在目录树中搜索.yo-rc.json文件。如果找到,它会将文件的位置视为项目的根。在幕后,Yeoman将把当前目录更改为.yo-rc.Json文件的位置,并在那里运行所请求的生成器。

运行环境

在编写 Generator 时最重要的是掌握方法运行的时机和运行环境。

作为任务执行的原型方法

每个直接附加到生成器原型的方法都被认为是一项任务。 每个任务按照 Yeoman 环境运行循环顺序运行。

换句话说,Object.getPrototypeOf (Generator) 返回的对象上的每个函数都将自动运行。

Object.getPrototypeOf() 方法返回指定对象的原型(内部[[Prototype]]属性的值)。

语法

Object.getPrototypeOf(object)

参数obj

要返回其原型的对象。

返回值

给定对象的原型。如果没有继承属性,则返回 null

定义私有方法

现在你知道原型方法被认为是一个任务,如何定义不会被默认执行的私有任务呢?实现这个目标有三种不同的方法。

将方法以 下划线 为前缀命名:

示例:_method

let Generator = require('yeoman-generator');
module.exports = class extends Generator {
  method1() {
    // this.log是对console.log()的封装,为了解决一些异常
    this.log('哈哈哈哈,我是第一');
  }
  _method2() {
    this.log('哈哈哈哈,我是第二');
  }
};

作为实例方法使用:

let Generator = require("yeoman-generator");

module.exports = class extends Generator {
  constructor(args, opts) {
    super(args, opts)
    this.method2 = function () {
      this.log("哈哈哈哈,我是第二");
    };
  }
  method1() {
    // this.log是对console.log()的封装,为了解决一些异常
    this.log("哈哈哈哈,我是第一");
  }
};

扩展父发生器:

   class MyBase extends Generator {
     helper() {
       console.log('methods on the parent generator won't be called automatically');
     }
   }

   module.exports = class extends MyBase {
     exec() {
       this.helper();
     }
   };

运行循环

如果只有个一个生成器,按顺序运行任务是可以的。 但一旦开始将生成器组合在一起,这是不行的,因此,Yeoman 使用一个运行循环

运行循环是一个具有优先支持的队列系统,使用分组队列模块来处理循环。

优先级在代码中被定义为特殊的原型方法名称。 当方法名与优先级名称相同时,运行循环将该方法推送到此特殊队列中。 如果方法名与优先级不匹配,则在default默认组中被推送。

这些特殊定义的原型方法名称如下(按照执行顺序):

方法名释义
initializing初始化方法(检查当前项目状态、获取配置等)
prompting当您提示用户选择时(当您调用this.prompt())
configuring保存配置并配置项目( 创建editorconfig文件及其他元数据文件)
default如果方法名称与优先级不匹配,它将被推送到这个组
writing你写生成器特定的文件(路由器,控制器等)
conflicts处理冲突的地方(内部使用)
install运行设备的地方(npm,bower)安装队列
end函数的最后一步,比如说goodbye

看一个示例

let Generator = require("yeoman-generator");

module.exports = class extends Generator {
  constructor(args, opts) {
    super(args, opts);
    this.log("1我是构造函数内的东西");
  }
  initializing() {
    this.log("2我是initializing任务");
  }
  prompting() {
    this.log("3我是prompting任务");
  }
  configuring() {
    this.log("4我是configuring任务");
  }
  default() {
    this.log("5我是default任务");
  }
  writing() {
    this.log("6我是writing任务");
  }
  conflicts() {
    this.log("7我是conflicts任务");
  }
  install() {
    this.log("8我是install任务");
  }
  end() {
    this.log("9我是end任务");
  }
  customMethod1() {
    this.log("10我是自定义方法1");
  }
  customMethod2() {
    this.log("11我是自定义方法2");
  }
};

运行结果

通过运行结果,我们可以得到一下结论

  • 子类内的方法(被定义在这个类的原型上)都会被默认执行。
  • 这些方法会按照一定顺序执行,首先执行构造函数内的方法。
  • 方法的执行顺序和命名有关,如果命名和上面表格任务队列方法名称一致,安装表格顺序执行,如果不一致,方法会被分配到default任务队列中,以此执行。(参考红框内容)
  • tip:下划线开头的方法名不会被默认执行

异步任务

有多种方法可以暂停运行循环,直到任务完成异步工作。

最简单的方法就是使用promise方法。 一旦承诺解除,循环将继续进行,否则会引发异常,如果失败就停止。

如果你所依赖的异步 API 不支持promise,那么您可以依赖this.async ()方式。 调用这个.async ()将返回一个函数,一旦任务完成就会调用。

具体请参考:yeoman.io/authoring/r…

与用户互动

脚手架工具会根据用户输入的不同内容生成个性化的模板文件。Yeoman 在终端上运行,内置了这些交互方法,我们只需要调用即可。

为了保证yeoman的交互功能正常运行,永远不要使用 console.log ()或 process.stdout.write ()来输出内容。 使用它们可以隐藏不使用终端的用户的输出。 相反,请始终依赖用户界面通用的 this.log ()方法,这是当前生成器的上下文。

提示-prompt()方法

提示符是生成器与用户交互的主要方式。 提示模块由 Inquirer.js 提供,您应当参考其 API,以获得可用的提示选项列表。

提示方法通过this.prompt() 方法调用,这个方法是异步的,并且返回用户输入交互的相应结果。

let Generator = require("yeoman-generator");

module.exports = class extends Generator {
  constructor(args, opts) {
    super(args, opts)
  }
  async prompting() {
    const answers = await this.prompt([
      { 
        // 提示类型 input需要用户输入自定义内容
        type: "input",
        // 自定义属性名
        name: "projectName",
        // 提示语
        message: "请输入项目名",
        // 默认值
        default: this.appname // 默认是当前的文件夹名
      },
      {
        //提示类型 confirm返回用户的选择是true还是false
        type: "confirm",
        name: "tip",
        message: "你是否大帅哥?"
      }
    ]);

    this.log("app name", answers.projectName);
    this.log("cool feature", answers.tip);
  }
};

prompt方法接受一个数组,数组的每一个对象是和用户进行交互的配置,这个数组的结构在其他方法中可以拿到,做相应的逻辑处理。

请注意,我们使用提示队列 来请求用户的反馈。 提示队列的方法是 ****prompting

使用用户答案

一个非常常见的场景是在后面的阶段使用用户回答,例如在写writing队列时。

let Generator = require("yeoman-generator");

module.exports = class extends Generator {
  constructor(args, opts) {
    super(args, opts);
  }
  async prompting() {
    this.answers = await this.prompt([
      {
        // 提示类型 input需要用户输入自定义内容
        type: "input",
        // 自定义属性名
        name: "projectName",
        // 提示语
        message: "请输入项目名",
        // 默认值
        default: this.appname, // 默认是当前的文件夹名
      },
      {
        //提示类型 confirm返回用户的选择是true还是false
        type: "confirm",
        name: "tip",
        message: "你是否大帅哥?",
      },
    ]);
  }
  writing() {
    this.log("cool feature", this.answers.tip); 
  }
};

记住用户首选项

用户每次运行你的生成器时都可能对某些问题给出相同的输入。 对于这些问题,您可能想要记住用户以前回答了什么,并把这个答案用作新的默认值。

通过添加一个存储属性来提问对象,延伸了 Inquirer.js API。 这个属性允许您指定用户提供的答案应该作为未来的默认答案。 可以这样做:

this.prompt({
  type: "input",
  name: "username",
  message: "What's your GitHub username",
  store: true
});

注意: 提供默认值将防止用户返回任何空答案。

通过命令行获取参数

在使用命令行时,可以自定义参数,根据这些参数我们可以完成一些个性化设置。

命令行传递参数

yo gcshi cusArg

如上,在命令行内键入一个字符,就可以作为我们插件中一个可以被解析的参数。这个参数的配置,需要在构造函数中调用this.argument() 这个方法。

参数配置

let Generator = require("yeoman-generator");
module.exports = class extends Generator {
  constructor(args, opts) {
    super(args, opts);
    this.argument("cusArg", { type: String, required: true });
  }
};

通过this.argument方法,我们定义了一个cusArg参数,设置类型为字符串,而且用户必须输入

不输入,则会报错

this.argument方法必须定义在构造函数内,接受两个参数,第一参数为一个名称(字符串),第二个参数是一个配置对象。

这个名称实际是一个形参名,这意味着我们命令行输入的值实际是这个形参的值。

可选配置对象属性名可选值释义
desc对论点的描述
requiredturefasle是否需要
typeString, Number, Array(也可以是接收原始字符串值并解析它的自定义函数)数据类型。注:Array 类型的参数将包含传递给生成器的所有剩余参数。
default参数的默认值

参数值接收

我们使用this.options[自定义参数名称] 可以获取这个参数值

本示例中通过this.options.cusArg可以获取对应的值:

  constructor(args, opts) {
    super(args, opts);
    this.argument("cusArg", { type: String, required: true });
    this.log(this.options.cusArg);
  }

多个参数

如果需要多个参数,那就多写几个 this.argument()

 constructor(args, opts) {
    super(args, opts);
    this.argument("cusArg", { type: String, required: true });
    this.argument("study", { type: String, required: true });
  }

选择(命令行标志)

选项看起来很像参数,但它们被写成命令行标志。

yo webapp --coffee

要想使这个指令生效,我们需要使用this.option ()方法。 此方法接受一个名称(字符串)和一个配置对象。

  constructor(args, opts) {
    super(args, opts);
        this.option("coffee");
        // 这个参数值同样可以被options接受
        this.log(this.options.coffee)
  }

可选配置对象属性名可选值释义
desc选项的说明
alias指令的简称
typeBoolean、 String 或 Number数据类型。(也可以是接收原始字符串值并对其进行解析的自定义函数)
default默认值
hideBoolean whether to hide from help

信息输出

输出信息由 this.log 模块处理。

生成器组合

依赖安装

注:官网的示例文档这里木有更新,原有方法不能使用了!!!

当运行生成器时,我们往往需要在生成的模板文件内安装好依赖。我们可以在writing任务队列中执行安装方法。

let Generator = require("yeoman-generator");
module.exports = class extends Generator {
  constructor(args, opts) {
    super(args, opts);
  }
  writing() {
    this.addDependencies({
      qs:"6.10.3",
      axios:"0.6.0"
    })
    this.addDevDependencies({
      "vue":"2.6.3"
    })
  }
};

相当于在命令行执行

npm install qs -save & npm install vue -dev

如果想动态的往package.json中再增加一点内容呢?

  writing() {
    this.addDependencies({
      qs:"6.10.3",
      axios:"0.6.0"
    })
    this.addDevDependencies({
      "vue":"2.6.3"
    })
    this.packageJson.merge({
        script:{test:true}
    })
  }

注:通过this.env.options.nodePackageManager = "npm"可以设置安装方式

依赖的安装

Package.json 存储解析为 this.destinationPath('package.json') .

Environment 监视 package.json 的变化 this.env.cwd ,如果它已提交到磁盘,则触发包管理器安装。如果 package.json 位于不同的文件夹中,例如更改的生成器根目录,请将其传播到环境中,例如 this.env.cwd = this.destinationPath()

demo

//设置根目录为projectName
this.destinationRoot("projectName");
//设置Package.json解析目录为projectName/package.json
this.env.cwd = this.destinationPath();
// 触发控制台依赖安装
this.fs.copyTpl(this.templatePath("_package.json"), this.destinationPath("package.json"));

依赖安装的时机

 writing任务队列的优先级高于install,Environment 监视 package.json 的变化 this.env.cwd ,如果它已提交到磁盘,则触发包管理器安装。

也就是说,writing中就已经触发了依赖安装,writing内的函数全执行完毕时,执行依赖安装,依赖安装完毕,才会进入install队列。

writing(){
  // 函数执行
  this.fs.copyTpl(this.templatePath("_package.json"), this.destinationPath("package.json"));
}
// npm依赖安装
// 进入install队列
install() {
  // 执行函数
}

所以,this.packageJson.merge,this.addDependencies需要写在writing队列,写在install队列会引起冲突。

文件系统

将模板文件复制到目标文件夹

将生成器内置的模板文件复制到用户指定的目标文件夹是脚手架的一个核心功能。要使用这个功能,我们必须知道目标文件夹和模板文件夹的地址,首先需要了解Yeoman两个重要的上下文概念。

目标上下文

目标文件夹是通过脚手架上生成的项目文件夹。如通过 vue create test 生成的test项目文件夹。

我们可以通过this.destinationRoot()获取目标上下 文路径(在哪里执行命令,就是哪个路径)

  constructor(args, opts) {
    super(args, opts);
    this.log(this.destinationRoot())  
  }

在项目内执行

在桌面执行

可以发现,在哪里执行命令行命令时,this.destinationRoot()就会获取当前执行命令的路径。

模板上下文

模板文件夹是生成器app (本示例中的router文件夹也可以) 文件夹下的templates文件夹,默认情况下,这个文件夹下的内容将来会输出为用户可用的模板。

我们先创建一个模板文件夹

增加一些生成逻辑:

let Generator = require("yeoman-generator");
module.exports = class extends Generator {
  constructor(args, opts) {
    super(args, opts); 
  }
  writing() {
    // 获取模板目录
    let srcDir = this.sourceRoot();
    // 获取目标目录(在哪里执行命令就是哪里)
    let destDir = this.destinationRoot();
    // 将目标目录内容生成到模板目录内
    this.fs.copyTpl(srcDir, destDir,this.answers);
  }
};

我们找一个空白文件夹,执行以下yo gcshi命令。文件夹内的文件就生成好了(和tempalte内完全一致)。

通过上述示例,可以知道,模板的生成主要靠this.fs.copyTpl()函数

这个函数接受三个参数:模板路径目标路径一个配置对象(下一小节展开)

目标路径的更改

假设我们的模板系统内只有一个index.html文件,通过下面的配置

  writing() {
    // 获取模板目录
    let srcDir = this.sourceRoot();
    // 获取目标目录(在哪里执行命令就是哪里)
    let destDir = this.destinationRoot();
    // 将目标目录内容生成到模板目录内
    this.fs.copyTpl(srcDir, destDir,this.answers);
  }

当我们在桌面使用 yo gcshi 命令时,桌面会生成index.html文件。

现在,我们期望的是,根据用户输入的项目名,模板内的文件都能放入这个项目文件夹中。

我们可以通过 this.destinationPath('自定义文件夹名') ,来更改 目标文件夹。

let Generator = require("yeoman-generator");

module.exports = class extends Generator {
  constructor(args, opts) {
    super(args, opts);
  }
  async prompting() {
    this.answers = await this.prompt([
      {
        // 提示类型 input需要用户输入自定义内容
        type: "input",
        // 自定义属性名
        name: "projectName",
        // 提示语
        message: "请输入项目名",
        // 默认值
        default: this.appname, // 默认是当前的文件夹名
      },
    ]);
  }
  writing() {
    // 获取模板目录
    let srcDir = this.sourceRoot();
    // 获取目标目录(在哪里执行命令就是哪里)
    let destDir = this.destinationPath(this.answers.projectName);
    // 将目标目录内容生成到模板目录内
    this.fs.copyTpl(srcDir, destDir, {});
  }
};

在桌面执行yo gcshi后,我们可以发现,桌面增加了一个用户自定义的文件夹,而且里面有我们的模板文件。

this.destinationRoot('自定义文件夹名')的方法会更改源目标文件夹,影响destinationPath

模板路径的修改

假设,我们的模板文件夹下现在有vue2和vue3两个目录(两个目录中均有文件),我们只想使用其中一个目录下的文件该怎么做呢?

使用this.templatePath('vue2')这个方法即可

结合inquirer.js我们完善下prompt()方法,并来个示例

let Generator = require("yeoman-generator");

module.exports = class extends Generator {
  constructor(args, opts) {
    super(args, opts);
  }
  async prompting() {
    this.answers = await this.prompt([
      {
        type: "list",
        name: "version",
        message: "请选择Vue版本",
        default: "vue2", // 默认是当前的文件夹名
        choices: ["vue2", "vue3"],
      },
    ]);
  }
  writing() {
    // 获取模板目录
    let srcDir = this.templatePath(this.answers.version);
    // 获取目标目录(在哪里执行命令就是哪里)
    let destDir = this.destinationRoot();
    // 将目标目录内容生成到模板目录内
    this.fs.copyTpl(srcDir, destDir, {});
  }
};

注:模板文件夹根目录中的任何空文件夹不会被拷贝出来。。。。

使用this.sourceRoot('new/template/path')可以更改模板文件夹的源位置

文件操作示例

Yeoman在覆盖用户文件时非常小心。基本上,对已存在的文件上写入时,都先解决冲突。 解决冲突时要求用户验证每个文件的(覆盖)写入。

这样,可防止出现意外情况并限制发生错误的风险。 另一方面,这意味着每个文件都异步写入磁盘。

由于异步API难以使用,因此Yeoman提供了一个同步文件系统API,其中每个文件都被写入内存文件系统,并且只有在Yeoman完成运行后才被写入磁盘。

复制模板文件

这里有一个例子,我们想要复制和处理模板文件。

templates 文件夹内的index.html :

<html>
  <head>
    <title><%= title %></title>
  </head>
</html>

我们将使用 fs.copyTpl 方法复制文件,同时copyTpl 的第三个参数用来替换index.html文件内的占位符。

class extends Generator {
  writing() {
    this.fs.copyTpl(
      this.templatePath('index.html'),
      this.destinationPath('public/index.html'),
      { title: 'Templating with Yeoman' }
    );
  }
}

一旦生成器运行完成,public / index.html 将包含:

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

这里展位方式我们使用ejs模板语法

  • <% ... %>包含js代码
  • <%= ... %>输出变量 变量若包含 '<' '>' '&'等字符 会被转义
  • <%- ... %>输出变量 不转义
  • <%- include('user/show') %>引入其他模板 包含 ./user/show.ejs
  • <%# some comments %>注释标签,不会输出,也不会执行
  • <%%转义为 '<%'
  • <% ... -%>删除新的空白行模式
  • <%_ ... _%>删除前后空白符模式
  • <% var ejsVar = '' %>ejs变量声明

一个非常常见的场景是在提示阶段存储用户答案并将其用于模板:

class extends Generator {
  async prompting() {
    this.answers = await this.prompt([{
      type    : 'input',
      name    : 'title',
      message : 'Your project title',
    }]);
  }

  writing() {
    this.fs.copyTpl(
      this.templatePath('index.html'),
      this.destinationPath('public/index.html'),
      { title: this.answers.title } // user answer `title` used
    );
  }

通过流转换输出文件

generator系统允许每次写入文件时应用自定义过滤器。 完全可以自动美化文件,规范空格等。

每个Yeoman进程一次,我们会将每个修改过的文件写入磁盘。 任何生成器作者都可以注册transformStream来修改文件路径和内容。

通过registerTransformStream() 方法注册新的修饰符。 这是一个例子:

var beautify = require("gulp-beautify");
this.registerTransformStream(beautify({ indent_size: 2 }));

请注意,全部文件都将通过此流传递。 确保任何转换流都会通过不支持的文件。 诸如gulp-ifgulp-filter之类的工具将有助于过滤无效类型并将其传递。

基本上,可以在Yeoman转换流中使用任何gulp插件,以在编写阶段处理生成的文件。

更新现有文件的内容

更新预先存在的文件并不容易。 最可靠的方法是解析文件AST(抽象语法树) 并对其进行编辑。 这种解决方案的主要问题在于,编辑AST可能会很冗长且难以掌握。

一些流行的AST解析器是:

  • Cheerio 用于解析HTML
  • Esprima 用于解析JavaScript - 可能对 AST-Query 感兴趣,该查询提供了用于编辑Esprima语法树的较低级别的API。
  • 对于Json文件,可以使用原生的 JSON object methods.
  • Gruntfile Editor 用于动态修改Gruntfile。

使用RegEx解析代码文件很危险,在这样做之前,您应该阅读 CS anthropological answers并掌握RegEx解析的缺陷。 如果确实选择使用RegEx而不是AST树来编辑现有文件,请小心并提供完整的单元测试。 注意:请不要破坏用户的代码。

用户配置储存

存储用户配置选项并在子发生器之间共享是一个常见的任务。这些配置可以通过Yeoman Storage API存储在 .yo-rc.json文件中。 可通过generator.config对象访问此API。

方法

this.config.save()

此方法会将配置写入.yo-rc.json文件。 如果文件不存在,则save方法将创建它。

.yo-rc.json文件还确定了项目的根目录。 最佳实践:即使不使用存储空间,最好也要在app genrator中调用save。

注意,每次设置配置选项时,都会自动调用save方法。 因此,您通常无需显式调用它。

this.config.set()

set既可以采用键和关联值,也可以采用多个键/值的对象哈希。

请注意,值必须是JSON可序列化的(字符串,数字或非递归对象)。

this.config.get()

get将String键作为参数并返回关联的值。

this.config.getAll()

返回具有完整可用配置的对象。

返回的对象按值而不是引用传递。 即:如果要更改值,需要使用Set方法。

this.config.delete()

删除一个键

this.config.defaults()

接受选项的哈希值以用作默认值。 如果键/值对已经存在,则该值将保持不变。 如果缺少密钥,则添加。

结构

.yo-rc.json文件是一个JSON文件,其中存储了来自多个Generator的配置对象。 每个Generator配置都具有名称空间,以确保Generator之间不会发生命名冲突。

这也意味着每个Generator配置都是沙盒化的,并且只能在子Generator之间共享。 无法使用存储API在不同Generator之间共享配置。 调用期间使用选项和参数在不同生成器之间共享数据。

.yo-rc.json文件如下:

{
  "generator-backbone": {
    "requirejs": true,
    "coffee": true
  },
  "generator-gruntfile": {
    "compass": false
  }
}

对于最终用户,该结构非常全面。 这意味着,可以在该文件中存储高级配置,没有必要对每个选项都使用提示,高级用户可直接编辑该文件。