按:在Yeoman脚手架使用入门Yeoman脚手架核心机制这两篇文章中已经对Yeoman脚手架工具的基本使用以及去核心运转机制进行了深入的介绍,这篇文章将以实例的方式来教会如何从零开始创建属于我们自己的generator。

点击获取本文示例的generator

generator创建准备

这里我们一切从零开始,在创建自己的generator之前需要做一些准备工作,比如准备好yo命令行工具,比如对生成器生成的项目结构和目录文件有清晰的规划等。

yo命令行工具

在安装了NodeJS和npm的前提下,可以通过下面的命令来安装yo命令行工具,并检查安装是否成功。

$ npm install -g yo
$ yo --version

generator-generator的安装

创建generator可以完全从零开始,也可以使用Yeoman官方提供的generator引导,这里我们选择使用Yeoman官方推荐的方式来处理。
$ mkdir YeomanTest && cd YeomanTest/ 创建新的目录并进入
$ npm install -g generator-generator 安装Yeoman引导generator

列出具体的执行情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
wendingding$ mkdir YeomanTest
wendingding$ cd YeomanTest/
wendingding$ pwd
/Users/文顶顶/Desktop/Yeoman/YeomanTest
wendingding$ npm install -g generator-generator + generator-generator@4.0.2

updated 1 package in 117.639s

╭─────────────────────────────────────╮
│ │
│ Update available 5.5.1 → 6.1.0 │
│ Run npm i -g npm to update │
│ │
╰─────────────────────────────────────╯

执行Yeoman官方的引导generator,并处理交互式配置部分,下面列出具体的执行情况。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
wendingding$ yo generator
? Your generator name generator-wendingding
Your generator must be inside a folder named generator-wendingding
I'll automatically create this folder.
? Description 博客文章测试创建生成器
? Project homepage url http://www.wendingding.com
? Author's Name 文顶顶
? Author's Email 18681537032@163.com
? Author's Homepage http://www.wendingding.com
? Package keywords (comma to split) wendingding
? Send coverage reports to coveralls Yes
? GitHub username or organization flowerField
? Which license do you want to use? Apache 2.0
create package.json
create README.md
create .editorconfig
create .gitattributes
create .gitignore
create generators/app/index.js
create generators/app/templates/dummyfile.txt
create __tests__/app.js
create .travis.yml
create .eslintignore
create LICENSE
I'm all done. Running npm install for you to install the required dependencies.
If this fails, try running the command yourself.

在执行generator-generator这个生成器的过程中,会询问项目名称、作者、使用协议、主页地址等等信息,依次选择填空即可。

注意:按照约定,Yeoman generator的名字必须以“generator-”的前缀开头,这是因为所有的generator其实都是全局安装的node模块,所以Yeoman其实是完全依靠文件系统来对这些生成器进行查找操作的。

当上面的命令执行完毕后,会发现在当前的路径下面生成了generator-wendingding目录,进入到generator-wendingding目录,使用tree命令查看当前目录结构,显示如下:

1
2
3
4
5
6
7
8
9
10
11
12
.
├── LICENSE
├── README.md
├── __tests__
├── generators
├── app
│ ├── index.js
│ └── templates
│ └── dummyfile.txt
├── node_modules
├── package-lock.json
└── package.json

上面目录结构中虽然有很多文件,但我们真正需要关注的应该是generators路径下面的app/index.js文件以及templates目录,其中index文件对应是generators的组装指令部分,templates路径用于存放项目所有的模板文件。

项目模板文件准备

上面这些准备工作完成之后,接下来我们开始着手分析目标项目的文件结构,即我们使用自己创建的这个脚手架来搭建项目,其结构目录应该是怎样的?需要包含哪些文件等等。任何时候,明确知道你的目标,知道自己正在做什么至关重要。

下面试着给出目标项目的文件结构。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
.
├── Gruntfile.js
├── bower.json
├── build
├── dist
├── package.json
└── src
├── css
│ └── style.css
├── index.html
├── js
│ └── index.js
├── libs
│ └── jquery
└── template

我们可以看到该项目应该包含bulidsrc以及dist三个目录,其中src目录中需要创建名为cssjslibstemplate的文件夹,分别用来保存样式文件、脚本文件、依赖的框架以及模板文件等。

除了这些必要的文件外,假设目标项目需要使用bower来进行依赖管理,使用Grunt来进行自动化构建,所以自然还应该拥有Gruntfile.js、bower.json以及package.json文件。

假设目标项目中一定会使用到jQuery框架,可能会使用到bootstrap框架。

现在我们可以开始分析生成器中应该包含项目模板文件了,也就是在generators/templates路径中应该包含哪些文件。

固定文件

index.jsstyle.css创建空文件即可。
Gruntfile.js文件因为内容固定不变,所以选择直接从旧项目中拷贝。
.jshintrc文件用于js文件语法检查,内容也是固定不变的。
.bowerrc文件用于重置Bower下载包的安装路径,内容为{“directory”: “src/libs/“}

灵活文件

package.json文件中项目名称、作者以及开源协议等需要用户配置
bower.json文件的项目名称、作者、开源协议以及依赖框架等需要用户配置

可选文件

bootstrap框架相关的部分为可选文件,需要根据用户配置进行处理。

依赖文件

jQuery框架相关的部分为依赖文件,在组装指令部分通过在代码中调用方法来下载和安装。

根据上面的分析,我们在generators/templates准备了多个模板文件,下面列出文件结构以及主要文件的具体内容:

1
2
3
4
5
6
7
8
9
10
11
12
.
└── app
├── index.js
└── templates
├── Gruntfile.js
├── bower.json
├── css
│ └── style.css
├── index.html
├── js
│ └── index.js
└── package.json

package.json文件内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
"name": "<%= appName %>",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "<%= appAuthor %>",
"license": "<%= appLicense %>",
"devDependencies": {
"grunt": "^1.0.2",
"grunt-contrib-concat": "^1.0.1",
"grunt-contrib-cssmin": "^2.2.1",
"grunt-contrib-jshint": "^1.1.0",
"grunt-contrib-uglify": "^3.3.0",
"grunt-contrib-watch": "^1.0.0"
}
}

bower.json文件内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
{
"name": "<%= appName %>",
"description": "\"测试使用\"",
"main": "js/index.js",
"authors": [
"<%= appAuthor %>"
],
"license": "<%= appLicense %>",
"keywords": [
"generator-wendingding",
"yeoman-generator"
],
"homepage": "https://github.com/flowerField/generator-wen",
"ignore": [
"**/.*",
"node_modules",
"bower_components",
"test",
"tests"
],
"dependencies": {
"jquery": "^3.3.1"<% if(isIncludeBootstrap) { %>,
"bootstrap": "^4.1.1" <% } %>
}
}

index.html文件内容

1
2
3
4
5
6
7
8
9
10
11
12
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="utf-8">
<title><%= appName %></title>
<link rel="stylesheet" href="css/style.css">
<script type="text/javascript" src="js/index.js"></script>
</head>
<body>

</body>
</html>

Gruntfile.js文件内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
//包装函数
module.exports = function (grunt) {
// 项目配置信息
grunt.config.init({
pkg:grunt.file.readJSON("package.json"),
//代码合并
concat:{
options:{
stripBanners:true,
banner:'/*项目名称:<%=pkg.name%> 项目版本:<%=pkg.version%> 项目的作者:<%=pkg.author%> 更新时间:<%=grunt.template.today("yyyy-mm-dd")%>*/\n'
},
target:{
src:["src/js/*.js"],
dest:'build/js/index.js'
}
},
//js代码压缩
uglify:{
target:{
src:"build/js/index.js",
dest:"build/js/index.min.js"
}
},
//css代码压缩
cssmin:{
target:{
src:"src/css/style.css",
dest:"build/css/style.min.css"
}
},
//js语法检查
jshint:{
target:['Gruntfile.js',"dist/js/index.js"],
},
//监听 自动构建
watch:{
target:{
files:["src/js/*.js","src/css/*.css"],
//只要指定路径的文件(js和css)发生了变化,就自动执行tasks中列出的任务
tasks:["concat","jshint","uglify","cssmin"]
}
}
});
//通过命令行安装插件(省略...)
//从node_modules路径加载插件
grunt.loadNpmTasks("grunt-contrib-concat");
grunt.loadNpmTasks("grunt-contrib-uglify");
grunt.loadNpmTasks("grunt-contrib-cssmin");
grunt.loadNpmTasks("grunt-contrib-jshint");
grunt.loadNpmTasks("grunt-contrib-watch");
//注册任务:在执行$ grunt命令的时候依次执行代码的合并|检查|压缩等任务并开启监听
grunt.registerTask("default",["concat","jshint","uglify","cssmin","watch"]);
};

注意:上面部分文件中很多地方使用模板语法来传递参数,Yeoman所用的模板语言是EJS,具体用法请参考EJS官网

组装指令

处理完上面这些工作之后,接下来就是最最核心的部分了,我们需要在app/index.js文件中编写组装指令,这部分代码控制着这个生成器应该怎么执行,包括交互式配置的具体内容、如何复制文件以及框架依赖和Node模块下载等内容。

下面列出该示例中的index.js文件内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
'use strict';
const Generator = require('yeoman-generator');
const chalk = require('chalk');
const yosay = require('yosay');
const mkdirp = require('mkdirp');

module.exports = class extends Generator {
prompting() {
this.log(
// yosay(`Welcome to the transcendent ${chalk.red('generator-wen')} generator!`)
yosay(`欢迎使用\n${chalk.red('generator-wen')} !\n Author:文顶顶`)
);

const prompts = [
{
type : 'input',
name : 'appName',
message : '请输入项目名称:',
default : this.appname //appname是内置对象,代表工程名,这里就是ys
},
{
type : 'input',
name : 'appAuthor',
message : '请输入作者姓名:',
default : '文顶顶'
},
{
type: 'list',
name: 'appLicense',
message: '请选择使用的license:',
choices: ['MIT', 'ISC', 'Apache-2.0', 'AGPL-3.0']
},
{
type : 'confirm',
name : 'isIncludeBootstrap',
message : '是否需要使用bootStrap框架?',
default : false
},

];

return this.prompt(prompts).then(props => {
// To access props later use this.props.someAnswer;
this.props = props;
});
}

writing() {
mkdirp("build");
mkdirp("dist");
mkdirp("src/template");

this.fs.copyTpl(
this.templatePath('index.html'),
this.destinationPath('src/index.html'),
{appName: this.props.appName}
);

this.fs.copy(
this.templatePath('css/style.css'),
this.destinationPath('src/css/style.css')
);

this.fs.copy(
this.templatePath('js/index.js'),
this.destinationPath('src/js/index.js')
);

this.fs.copy(
this.templatePath('.bowerrc'),
this.destinationPath('.bowerrc')
);

this.fs.copy(
this.templatePath('Gruntfile.js'),
this.destinationPath('Gruntfile.js')
);

this.fs.copy(
this.templatePath('.jshintrc'),
this.destinationPath('.jshintrc')
);

this.fs.copyTpl(
this.templatePath('package.json'),
this.destinationPath('package.json'),
{appName: this.props.appName,appAuthor:this.props.appAuthor,appLicense:this.props.appLicense}
);

this.fs.copyTpl(
this.templatePath('bower.json'),
this.destinationPath('bower.json'),
{appName: this.props.appName,appAuthor:this.props.appAuthor,appLicense:this.props.appLicense,isIncludeBootstrap:this.props.isIncludeBootstrap}
);
}

install() {
//this.installDependencies();
this.bowerInstall();
}
};

上面的代码大概由三部分组成,第一部分为prompting函数用来处理安装提示,第二部分为writing函数用来设置模板文件的复制操作,第三部分为install函数用来处理框架依赖和node包的安装。

generator的发布和测试

项目模板文件和组装指令都准备好了后,我们就可以发布自己的generator了,可以先通过$ npm link命令以软连接的方式生成一个全局的npm包,测试使用。
具体的执行细节如下

1
2
3
4
wendingding:generator-wendingding wendingding$ npm link
up to date in 3.897s
/usr/local/lib/node_modules/generator-wendingding -> /Users/文顶顶/Desktop/Yeoman/YeomanTest/generator-wendingding
wendingding:generator-wendingding wendingding$

测试·使用自己创建的generator来生成初始化项目

随便找个目录新建文件夹,使用$ yo wendingding命令即可完成项目的初始化工作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
wendingding:YeomanTest wendingding$ mkdir Demo
wendingding:YeomanTest wendingding$ cd Demo/
wendingding:Demo wendingding$ yo wendingding

_-----_ ╭──────────────────────────╮
| | │ 欢迎使用 │
|--(o)--| │ generator-wen ! │
`---------´ │ Author:文顶顶 │
( _´U`_ ) ╰──────────────────────────╯
/___A___\ /
| ~ |
__'.___.'__
´ ` |° ´ Y `

? 请输入项目名称: Demo
? 请输入作者姓名: 文顶顶
? 请选择使用的license: Apache-2.0
? 是否需要使用bootStrap框架? Yes
create bower.json
create package.json
create src/index.html
create src/css/style.css
create src/js/index.js
create .bowerrc
create Gruntfile.js
create .jshintrc
bower invalid-meta for:/Users/文顶顶/Desktop/Yeoman/YeomanTest/Demo/bower.json
bower invalid-meta The "name" is recommended to be lowercase, can contain digits, dots, dashes
bower cached https://github.com/jquery/jquery-dist.git#3.3.1
bower validate 3.3.1 against https://github.com/jquery/jquery-dist.git#^3.3.1
bower cached https://github.com/twbs/bootstrap.git#4.1.1
bower validate 4.1.1 against https://github.com/twbs/bootstrap.git#^4.1.1
bower install jquery#3.3.1
bower install bootstrap#4.1.1

jquery#3.3.1 src/libs/jquery

bootstrap#4.1.1 src/libs/bootstrap
wendingding:Demo wendingding$ tree -L 3
.
├── Gruntfile.js
├── bower.json
├── build
├── dist
├── package.json
└── src
├── css
│ └── style.css
├── index.html
├── js
│ └── index.js
├── libs
│ ├── bootstrap
│ └── jquery
└── template

9 directories, 6 files

如果需要把这个生成器发布到社区,可以参考官网的说明。