Yeoman 脚手架应用工具
Yeoman 是一个通用的脚手架系统,可以用来创建任何类型的应用程序。Yeoman和语言无关所以可以用来生成任何语言的项目。
Yeoman 本身没有任何流程控制,所有流程都是由Yeoman的插件生成器( Generators)来控制的。
npm install -g yo
只是单独安装Yeoman,是没有用的,需要同时安装需要的生成器( Generators)。生成器都是命名为generator-XYZ
的npm包,都可以通过npm来安装。
例如
npm install -g generator-webapp
安装之后需要搭建新项目,就可以运行对应的命令调用指定的生成器即可。
yo webapp
大多数生成器,都会询问一系列的问题用来配置工程,如果想要查看有哪些选项,可以使用help
命令。
yo webapp --help
大多数生成器依赖构建系统(Grunt 或者Gulp)或者包管理器(npm),如果需要了解如何使用这个生成器,可以使用home 命令访问生成器的主页。
npm home generator-webapp
一部分复杂的生成器可能会提供额外的生成器用来生成项目的较小的部分。这些生成器通常被称为子生成器,可以通过generator:sub-generator
来访问。
例如:
yo angular:controller MyNewController
yo --help
访问帮助信息yo --generators
列出已经安装的生成器yo --version
查看版本yo doctor
命令可以诊断并提供解决常见问题的步骤生成器是Yeoman生态系统的基石。它们是用来最终为用户生成文件的插件。
生成器是一个npm包,所以和创建普通的npm一样,不过,生成器的名字格式要设置为generator-xxxx
的形式。Yeoman依靠这个在文件系统中查找可用的生成器。
{
"name": "generator-name",
"version": "0.1.0",
"description": "",
"files": [
"generators"
],
"keywords": ["yeoman-generator"],
"dependencies": {
"yeoman-generator": "^1.0.0"
}
}
Yeoman的功能取决于如何和构建目录树,每个子生成器都包含在自己的文件夹中。我们使用yo name
调用的默认生成器在app文件夹中。子生成器则存储在与sub命令相同的文件夹中。
例如:
├───package.json
└───generators/
├───app/
│ └───index.js
└───router/
└───index.js
这个生成器,将会暴露 yo name
和 yo name:router
两个命令。
Yeoman 也支持两个不同的文件夹结构。它将会搜索 ./
和 generators/
两个文件夹下的内容。上面的例子也可以写作:
├───package.json
├───app/
│ └───index.js
└───router/
└───index.js
如果使用了第二种目录结构,需要确保 package.json
中的 files
属性指向对应的文件夹
{
"files": [
"app",
"router"
]
}
Yeoman 提供了一个基础的生成器,我们可以扩展它来实现我们的功能,这个生成器将会简化大多数任务,
// generator/app/index.js
var Generator = require('yeoman-generator');
module.exports = class extends Generator {};
某些生成器方法只能在构造函数内部使用,这些方法可能会执行例如设置状态之类的操作,并且在构造函数之外无法运行。
可以通过下面的方法来覆盖构造函数。
module.exports = class extends Generator {
// The name `constructor` is important here
constructor(args, opts) {
// Calling the super constructor is important so our generator is correctly set up
super(args, opts);
// Next, add your custom code
this.option('babel'); // This method adds support for a `--babel` flag
}
};
添加到原型中的每个方法都会在调用生成器后运行,并且通常是按顺序运行的。但是,正如我们将在下一节中看到的,一些特殊的方法名称将触发特定的运行顺序。
module.exports = class extends Generator {
method1() {
this.log('method 1 just ran');
}
method2() {
this.log('method 2 just ran');
}
};
这个时候我们就有了一个生成器,如果是本地开发的生成器,还没有发布为npm模块,我们可以使用npm创建全局模块并使用npm link 链接到本地,(pnpm 的多包管理也不错)
npm link
这个命令将会安装,当前的项目到全局模块,然后就可以 使用yo name
来调用了。
Yeoman 将会沿着目录树向上搜索, 如果找到.yo-rc.json
文件,它将会把这个文件所在的目录作项目根目录。Yeoman将会切换工作目录到yo-rc.json
文件所在的目录中。
直接附加到生成器原型的方法都被视为一个任务,有Yeoman按顺序循环运行,简单来说Object.getPrototypeOf(Generator)
返回的每一个函数都会自动运行。
想要让Yeoman不把原型上的方法视为任务,可以通过下面三个方法
class extends Generator {
method1() {
console.log('hey 1');
}
_private_method() {
console.log('private hey');
}
}
```
class extends Generator {
constructor(args, opts) {
// Calling the super constructor is important so our generator is correctly set up
super(args, opts)
this.helperMethod = function () {
console.log('won\'t be called automatically');
};
}
}
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使用循环的原因。
Yeoman 运行循环(The run loop) 是具有优先级支持的系统,使用分组队列来处理循环。
优先级为在代码中定义为特殊的原型方法的名称、当方法名与优先级名称相同的时候,运行循环会将该方法推送到特殊队列中,如果不匹配则会推送到default中
class extends Generator {
priorityName() {}
}
也可在同一个队列中添加多个方法:
Generator.extend({
priorityName: {
method() {},
method2() {}
}
});
注意:这个写法不支持js 的class语法
可用的优先级(按运行顺序):
initializing
- 初始化方法(检查当前项目状态,获取配置等)prompting
- 提示用户输入选项的位置(调用的位置this.prompt())configuring
- 保存配置和配置项目(创建文件和其他元数据文件).editorconfigdefault
- 如果方法名称与优先级不匹配,则会将其推送到此组。writing
- 写入生成器特定文件(路由、控制器等)的位置conflicts
- 处理冲突的地方(内部使用)install
- 运行安装的地方(npm,bower)end
- 叫最后,清理,说再见等遵循这些优先级指南,您的生成器将与其他生成器配合得很好。
有多种方法暂停 运行循环 直到异步任务完成。最简单的方法是,返回一个Promise 。循环将会一直等到Promise 返回。
如果依赖的API不支持Promise,也可以使用this.async()
方法。
asyncTask() {
var done = this.async();
getUserEmail(function (err, name) {
done(err);
});
}
如果 done
方法的入参是一个Error 循环将会终止并挂起。
提示是生成器和用户的主要交互方式,由Inquirer.js 提供。
prompt
是一个异步的方法返回一个Promise,所以你需要返回一个promise 等待对应的操作完成。然后再继续。
module.exports = class extends Generator {
async prompting() {
const answers = await this.prompt([
{
type: "input",
name: "name",
message: "Your project name",
default: this.appname // Default to current folder name
},
{
type: "confirm",
name: "cool",
message: "Would you like to enable the Cool feature?"
}
]);
this.log("app name", answers.name);
this.log("cool feature", answers.cool);
}
};
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?"
}
]);
}
writing() {
this.log("cool feature", this.answers.cool); // user answer `cool` used
}
};
用户在使用生成器的时候对于某些问题总是提供相同的输入,对于这些问题,我们可以记住用户得选择,并将该答案作为新的默认值。
Yeoman 通过扩展 Inquirer.js 的API 给question 对象新增 store
属性。 此属性允许您指定将来应将用户提供的答案用作默认答案。这可以按如下方式完成
this.prompt({
type: "input",
name: "username",
message: "What's your GitHub username",
store: true
});
注意:提供默认值,将会阻止用户返回空回答
参数可以直接通过 命令行传递:
yo webapp my-project
为了通知系统我们期望一个参数,我们使用this.argument()
方法。此方法接受name参数 (string) 或者一个对象。
name 参数可以通过 this.options[name]
访问到:
这个选项接受的对象的属性有
desc
参数描述required
Boolean 是否必填type
String, Number, Array 类型,也可以是一个自定义函数来解析default
参数默认值this.argument()
这个方法,只能在 构造函数constructor
中调用。否则当用户使用help
的时候,Yeoman将无法输出帮助信息。
module.exports = class extends Generator {
// note: arguments and options should be defined in the constructor.
constructor(args, opts) {
super(args, opts);
// This makes `appname` a required argument.
this.argument("appname", { type: String, required: true });
// And you can then access it later; e.g.
this.log(this.options.appname);
}
};
选项和参数有点类似,但是它们是作为命令行标志编写的
yo webapp --coffee
为了让系统知道我们接受一个选项,可以使用this.option()
方法。这个方法接受一个 name
(String) 参数,
name 的值可以通过 this.options[name]
来访问,
选项可以有第二个参数,
desc
选项描述alias
选项别名type
类型 Boolean, String or Number 也可以自定义函数default
默认值hide
Boolean 是否在help信息中隐藏module.exports = class extends Generator {
// note: arguments and options should be defined in the constructor.
constructor(args, opts) {
super(args, opts);
// This method adds support for a `--coffee` flag
this.option("coffee");
// And you can then access it later; e.g.
this.scriptSuffix = this.options.coffee ? ".coffee" : ".js";
}
};
Yeoman 输出信息通过 this.log
模块.
你可以直接使用this.log。功能基本和console.log相同。
module.exports = class extends Generator {
myAction() {
this.log("Something has gone wrong!");
}
};
完整API文档
将较小的部分组合成为一个大的东西。
该方法允许生成器与另一个生成器(或子生成器)并行运行。这样,它就可以使用其他生成器的功能,而不必自己完成所有操作。
generatorPath
想要组合的生成器路径(通常使用 require.resolve())options
要传给生成器的选项例如:
this.composeWith(require.resolve('generator-bootstrap/generators/app'), {preprocessor: 'sass'});
require.resolve()返回 Node.js 将加载提供的模块的路径。
尽管不鼓励这样做,但您也可以将生成器命名空间传递给 。在这种情况下,Yeoman 将尝试查找作为peerDependencies
或全局安装在用户系统上的生成器
this.composeWith('backbone:route', {rjs: true});
第一个参数也可以是一个对象,对象应该包含下面的属性:
Generator
- 生成器的calsspath
- 生成器文件地址// Import generator-node's main generator
const NodeGenerator = require('generator-node/generators/app/index.js');
// Compose with it
this.composeWith({
Generator: NodeGenerator,
path: require.resolve('generator-node/generators/app')
});
例子:
// In my-generator/generators/turbo/index.js
module.exports = class extends Generator {
prompting() {
this.log('prompting - turbo');
}
writing() {
this.log('writing - turbo');
}
};
// In my-generator/generators/electric/index.js
module.exports = class extends Generator {
prompting() {
this.log('prompting - zap');
}
writing() {
this.log('writing - zap');
}
};
// In my-generator/generators/app/index.js
module.exports = class extends Generator {
initializing() {
this.composeWith(require.resolve('../turbo'));
this.composeWith(require.resolve('../electric'));
}
};
执行结果
prompting - turbo
prompting - zap
writing - turbo
writing - zap
npm允许三种类型的依赖
├───generator-backbone/
└───generator-gruntfile/
使用peerDependencies的时候不要指定特定版本,或者范围很窄的版本。
{
"peerDependencies": {
"generator-gruntfile": "*",
"generator-bootstrap": ">=1.0.0"
}
}
Yeoman 提供安装助手用于安装依赖。
this.npmInstall()
方法用来安装npm 依赖
class extends Generator {
installingLodash() {
this.npmInstall(['lodash'], { 'save-dev': true });
}
}
相当于调用 npm install lodash --save-dev
class extends Generator {
writing() {
const pkgJson = {
devDependencies: {
eslint: '^3.15.0'
},
dependencies: {
react: '^16.2.0'
}
};
// Extend or create package.json file in destination path
this.fs.extendJSON(this.destinationPath('package.json'), pkgJson);
}
install() {
this.npmInstall();
}
};
this.yarnInstall()
this.bowerInstall()
this.installDependencies()
,默认执行 npm 和bower
generators.Base.extend({
install: function () {
this.installDependencies({
npm: false,
bower: true,
yarn: true
});
}
});
Yeoman 允许用户使用 spawn
来执行任意的命令行命令。
class extends Generator {
install() {
this.spawnCommand('composer', ['install']);
}
}
目标上下文是Yeoman将在其中搭建新的应用程序的文件夹,当前工作目录或者最近的包含.yo-rc.json
的父级目录。.yo-rc.json
定义了Yeoman工程的根目录。
可以通过this.destinationRoot()
方法获取目标地址。或者通过this.destinationPath('sub/path').
来获取地址链接。
// Given destination root is ~/projects
class extends Generator {
paths() {
this.destinationRoot();
// returns '~/projects'
this.destinationPath('index.js');
// returns '~/projects/index.js'
}
}
可以使用this.destinationRoot('new/path')
来设置默认的根目录,但是为了保持一致,你不应该修改这个默认的目标目录。如果你想要知道用户是在哪里运行yo
命令,可以使用this.contextRoot
来获取对应的路径,
模板上下文是指存储模板文件的地址,通常会从其中读取和赋值文件,模板上下文默认为./templates/
,你可以通过this.sourceRoot('new/template/path')
来修改它,this.sourceRoot()
可以用来获取这个路径,或者this.templatePath('app/index.js')
来拼接路径。
class extends Generator {
paths() {
this.sourceRoot();
// returns './templates'
this.templatePath('index.js');
// returns './templates/index.js'
}
};
由于写入文件
异步写入API很难用,Yeoman提供了一个同步的文件写入系统API,将每一个文件写入 内存中的文件系统,在Yeoman运行完成的时候一次性写入硬盘。内存文件系统在所有的生成器之间共享。
生成器通过内存文件系统编辑器的实例thsi.fs
暴露出所有的文件操作方法。内存文件编辑器文档.
<html>
<head>
<title><%= title %>title>
head>
html>
copyTpl
使用 EJS模板语法
class extends Generator {
writing() {
this.fs.copyTpl(
this.templatePath('index.html'),
this.destinationPath('public/index.html'),
{ title: 'Templating with Yeoman' }
);
}
}
例子:接受用户参数并输出到模板
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
);
}
}
Yeoman 的生成器系统允许我们自定义过滤器,在文件写入的时候对文件进行格式化等操作,
一旦Yeoman处理结束,Yeoman将会把所有修改的文件写入硬盘。这个过程将会通过 vinyl 对象流,任何生成器作者都可以注册一个transformStream
方法来修改,文件的路径或者内容。
var beautify = require("gulp-beautify");
this.registerTransformStream(beautify({ indent_size: 2 }));
注意 任何类型的文件都将通过此流传递,所以需要使用gul-if
或者gulp=filter
这类的工具有助于过滤无效的文件。
更新现有的文件,是最可靠的方法是通过解析文件的AST。
存储用户配置并在子生成器之间共享是一个比较常见的任务。这些配置可以通过Yeoman存储 在.yo-rc.json
文件之中。可以通过generator.config
对象暴露API来访问。
this.config.save()
这个方法将会把配置写入.yo-rc.json
。如果不存在将会自动创建。.yo-rc.json
文件也决定了工程的根目录,save
方法将会在你每次调用set
方法之后自动调用,所以通常并不需要显式调用。
this.config.set()
set
方法接受 key 和一个关联的值,或者一个对象,值必须是JSON序列化的。
this.config.get()
get
接受一个String参数返回关联的值。
this.config.getAll()
返回所有的可用配置,返回的对象是值传递。
this.config.delete()
删除数据
this.config.defaults()
设置默认值,如果该值已存在则保持不变,如果不存在则添加。
.yo-rc.json
结构{
"generator-backbone": {
"requirejs": true,
"coffee": true
},
"generator-gruntfile": {
"compass": false
}