前言
在上一篇中文章中,我们讲到如何开发一个简单的cli
工具(这里),在实际项目中,cli
工具可能会发挥更多的作用。
笔者曾在一个项目中参与实现了「使用yeoman工具生成一个前端脚手架」的功能,觉得挺有意思,特与大家分享。
什么是 yeoman
yeoman 是一个可以帮助我们生成任何类型 app 的脚手架工具,这一功能依赖于 yeoman 的各种 generators。每一个 generator 都是 yeoman 封装好的一个个 npm package,我们可以引用已经发布的 generators ,也可以自定义一个 generator。
在使用的时候只需要运行下面的命令即可进行安装。
npm install -g yo
npm install -g generator-[generator name] eg:generator-XYZ
运行下面的命令创建脚手架,
yo [generator name] eg:yo XYZ
创建一个简单的 generator
规则
- 文件夹必须命名为 generator-[generator name] 的形式
- 项目中须有 package.json 文件,且属性值须满足下列要求:
- name 属性值须是 "generator-name"
- keywords 属性值必须为 "yeoman-generator"
- files 属性值须为数组且包含所有 "generator" 的目录
配置package.json
- 按照上面的规则创建文件夹 "generator-myapp",运行
npm init
生成package.json
并修改相应属性或者拷贝以下内容至自己的package.json
文件中。
{
"name": "generator-myapp",
"version": "1.0.0",
"description": "",
"main": "index.js",
"keywords": [
"yeoman-generator"
],
"files": [
"generators"
],
"author": "",
"license": "ISC",
"dependencies": {
"yeoman-generator": "^4.0.1"
}
}
- 安装
yeoman-generator
依赖
npm install --save yeoman-generator
目录结构
yeoman 的功能强依赖于文件结构,每一个 sub-generator 都有着自己的结构。
yeoman 支持如下两种定义目录结构的方式:
和
如果使用的是第二种方式,需要在package.json
中显式声明
{
"files": [
"app",
"router"
]
}
可以运行yo [generator name]
命令执行,默认会执行app
目录下的 generator;
yeoman
还支持sub-generator
的概念,运行yo [generator name]:subcommand
执行,这要求项目中必须存在名为subcommand
的与app
同名的文件夹。
例如,上述结构中的router
目录就是一个sub-generator
,可以运行命令yo [generator name]:router
执行。
继承 Generator
yeoman
提供了一个Generator
基类,方便我们实现自己的功能。
在app/index.js
中实现一个简单的generator
.
const Generator = require('yeoman-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
}
// add your own methods
method1() {
console.log('I am a custom method');
}
};
在上面的例子中,我们在generator
中实现了一个简单的方法,可以在控制台中执行yo [generator name](这里则是yo myapp)
查看效果。
需要注意的是,因为我们的示例代码是在本地开发,并未发布到npm
上,因此yeoman
是无法识别我们的generator
的,因此需要执行npm link
将其链接到本地。
运行后可在控制台打出”I am a custom method“
The Run Loop
对于我们自定义的方法,yeoman
将按照队列顺序依次执行,同时yeoman
也内置了一些的预先定义好执行顺序的方法供我们使用(类似生命周期方法)。
- initializing -- 初始化方法(检查状态、获取配置等)
- prompting -- 获取用户交互数据(this.prompt())
- configuring -- 编辑和配置项目的配置文件
- default -- 如果generator内部还有不符合任意一个任务队列任务名的方法,将会被放在default这个任务下进行运行
- writing -- 填充预置模板
- conflicts -- 处理冲突(仅限内部使用)
- install -- 进行依赖的安装(eg:npm,bower)
- end -- 最后调用,做一些clean工作
交互
prompts
是yeoman
实现交互的主要方式,即上一部分提到的prompting
,它是基于inquirer.js实现,关于inquirer.js
我在上一篇如何开发一个简单的cli工具中有提到,感兴趣的欢迎围观。
prompt
是一个异步方法,因此在获取交互数据的时候需要做同步处理。
const Generator = require('yeoman-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
}
// add your own methods
prompting() {
const prompts = [
{
type: 'input',
name: 'name',
message: 'Project name:',
default: 'test-project'
},
{
type: 'list',
name: 'framework',
message: 'choose a framework',
choices: ['React', 'Vue', 'Angular'],
default: 'React'
}
];
this.prompt(prompts).then(answers => {
this.name = answers.name;
this.framework = answers.framework;
})
}
};
上述代码用于获取用户输入的项目名称和框架类型并保存。
根据模板生成脚手架
获取到了交互数据,接下来就该根据预定义的模板去生成脚手架了。
这一步本质上是文件的拷贝,所以需要考虑的有三个问题
- 如何读取源文件地址(模板)
- 如何读取目标地址
- 如何完成拷贝操作
不用担心,这些yeoman
已经帮我们完成了封装。
Destination context
目标目录即我们要生成脚手架的目录,在yeoman
中定义了两个目标目录的位置,当前工作目录和最相邻的一个包含.yo-rc.json
文件的父级目录。
.yo-rc.json
文件允许用户在子目录下运行yeoman
命令去操作工程,这样保证了用户操作的连贯性。
yeoman
提供了this.destinationRoot()
方法供我们获取目标路径。
调用时可以指定子目录。
// Given destination root is ~/projects
class extends Generator {
paths() {
this.destinationRoot();
// returns '~/projects'
this.destinationPath('index.js');
// returns '~/projects/index.js'
}
}
Template context
模板路径顾名思义就是模板存放的路径,默认存在当前路径下的templates
目录,可以调用this.sourceRoot()
来获取,也可以指定路径。
class extends Generator {
paths() {
this.sourceRoot();
// returns './templates'
this.templatePath('index.js');
// returns './templates/index.js'
}
};
拷贝文件
yeoman
提供了copyTpl方法来完成模板的拷贝,copyTpl
方法在this.fs
下且默认使用ejs
语法。
class extends Generator {
writing() {
this.fs.copyTpl(
this.templatePath(),
this.destinationPath(this.name)
);
}
}
运行yo [generator name]
可以看到生成了一个包含模板文件的脚手架。
结合自定义cli
我们在运行自定义generator
的时候通常需要借助yo
,yeoman
也提供了一种高级用法满足我们不想使用yo
的需求,那就是yeoman-environment
首先,我们需要有一个自定义cli
工具的工程框架(非常简单,步骤可参见这里)
引入yeoman-environment
,然后调用它的createEnv
方法。
var yeoman = require('yeoman-environment');
var env = yeoman.createEnv();
然后基于generator
的路径进行注册
env.register(require.resolve('../../generator-myapp/generators/app'), 'myapp');
添加执行代码
env.run('myapp');
最后,修改package.json
文件,添加自定义的cli
入口
{
"name": "my-cli",
"version": "1.0.0",
"description": "",
"main": "src/index.js",
"scripts": {
"eslint": "eslint ./.eslintrc.js"
},
"bin": {
"myapp": "src/index.js"
},
"author": "",
"license": "ISC",
"devDependencies": {
"eslint": "^6.1.0"
},
"dependencies": {
"inquirer": "^6.5.0",
"myapp": "0.0.0-1",
"yeoman-environment": "^2.4.0"
}
}
现在,再去运行myapp
命令就可以运行我们的generator
了。
写在最后
源码:
参考资料: