Lucius's Blog

脚手架的略知一二

什么是脚手架?

当我们建造房子的时候,工地上会先搭个架子,然后工人们会在这个的基础上添砖加瓦,直到房屋成型。那么我们程序也是这样的道理,会在我们开发之前有一套相对成熟且适用的架子(配置项、技术栈),然后我们会在这基础上直接产出需求功能,而需求重复的造架子。脚手架在我们开发项目中也是极其重要,能够提高我们的开发。

那么我们开发中有用到哪些脚手架呢?有vue-clicreate-react-appyeoman等。

Yeoman

yeoman较真来讲更应该称为脚手架框架,因为它不能直接创建项目文件,而是提供了一套完整的开发API,可以通过这些API来灵活地来制造符合你需求的脚手架方案。

开始

我们先来创建一个yeoman脚手架生成器,注意文件夹的名字得为generator-开头,如generator-vuecli、generator-createapp

  • npm install --global yo
  • mkdir generator-testcli && cd generator-testcli
  • npm init,生成package.json需要主要几点:
    • name 属性必须带有前缀 generator-
    • keywords 属性必须含有 yeoman-generator
  • npm install --save yeoman-generator

yeoman-generator:yeoman 会根据用户选择的 generator 下载的本地,其中 generator 指的是一套具有模板的项目。
在官方例子中generator-fountain-webapp就是其中的一个generator。当然你也可以选择你要的generator,如输入命令行yo,会有选项Install a generator让你选择你想要安装的generator

提示:假如安装过程报Error: EACCES, permission denied错误,请使用sudo npm进行安装。

目录结构

yeoman支持两种不同的目录结构:./ 和 ./generators。即:

1
2
3
4
5
6
7
8
9
10
11
12
|---- package.json
|---- generators/
|---- app/
|---- index.js
|---- router/
|---- index.js
############################################
|---- package.json
|---- app/
|---- index.js
|---- router/
|---- index.js

重写构造函数

某些生成器方法只能在构造函数内部调用。这些特殊方法可能会执行诸如设置重要状态控件之类的操作,并且可能无法在构造函数之外运行。

1
2
3
4
5
6
7
8
9
10
11
12
var 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
}
};

添加功能函数

1
2
3
4
5
6
7
method1() {
this.log('method 1 just ran');
}

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

完整代码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var 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
}

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

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

npm绑定全局

使用 npm 创建一个node全局模块,并且连接到本地。在脚手架项目根目录下generator-name/,输入npm link

执行

yo testcli,会出现如下图信息。

到这里一个小小的官方demo已经执行完成了,更多的可以查看Writing Your Own Yeoman Generator | Yeoman

自定义前端脚手架

刚才在上问已经说了,yeoman实际上更像是一个脚手架框架,可以通过它来打造各种利器(脚手架)。那我们现在就从头开始学习下怎么打造一个利器。

我们现在就来简单的做一个小栗子,执行init时会拉取GitHub上面的代码。上面已经说过了,实际上就是相当于clone一个基础项目到本地。

起步

我们把这个项目称为messi吧。

  • mkdir messi && cd messi
  • npm init(此处的package.json就没有yeoman那样需要name跟key有前缀的要求了);
  • npm install --save chalk co co-prompt commander
    • chalk:能在terminal中显示自定义字符串颜色的插件;
    • co:异步流程控制工具;
    • co-prompt:不但可以提供提示信息,还可以分步骤输入你想要的参数和选项;
    • commander:有效的组织terminal的输入;

入口文件

我们在根目录创建/bin文件夹,然后在该文件夹创建的文件messi.js。即bin/messi.js为我们这个项目的入口文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// bin/messi.js
// 执行环境能够支持ES6
#!/usr/bin/env node --harmony
'use strict'

const program = require('commander');
const pkg = require('../package');

// 定义当前版本
program.version(pkg.version);

// 定义使用方法
// Usage: messi [options] [command] ->
// Usage: messi <command>
// 区别就是可选到必选
program
.usage('<command>');

program.parse(process.argv);

if (!program.args.length) {
program.help();
}

挂载全局

其实这样就可以看到效果了。我们现在在package.json添加以下配置,表示全局钩子messi的加载文件是bin/messi.js

例如:

1
2
>>> /usr/local/bin/messi -> /usr/local/lib/node_modules/messi/bin/messi.js
>>> /usr/local/lib/node_modules/messi -> /Users/xxx/Documents/study/messi

package.json

1
2
3
"bin": {
"messi": "bin/messi.js"
}

然后在根目录执行npm linksudo npm link。打开另一个terminal标签执行messi命令。

添加init模版

我们在根目录创建command文件夹,然后在其文件夹创建我们的主角init.js。目的是想通过init从远端仓库拉取一个项目模板。

我们需要在messi.js添加了相对应的命令行。

1
2
3
4
5
6
7
8
// messi.js
const init = require('../init');

program
.command('init')
.description('创建新项目')
.alias('i')
.action(() => { init(); });

然后我们需要在init.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
// init.js
const co = require('co');
const chalk = require('chalk')
const prompt = require('co-prompt');
const exec = require('child_process').exec;

module.exports = () => {
console.log(chalk.white('init...'));

co(function *() {
let pull = yield prompt('你是否想要拉取scaffold-cli项目? y/n ');
pull = pull.toLowerCase();
if (pull === 'n') {
console.log(chalk.red('已经成功退出!'));
process.exit()
} else if (pull === 'y') {
const projectName = yield prompt('项目名称:');
const gitCmd = `git clone git@github.com:Lucius0/scaffold-cli.git ${projectName} && cd ${projectName} && git checkout master`;

console.log(chalk.white('开始拉取...'));

exec(gitCmd, (error, stdout, stderr) => {
if (error) {
console.log(error);
process.exit();
}
console.log(chalk.green('拉取成功!'));
process.exit()
});
}
});
};

到此刻为止,我们基本上的一个小demo已经完成了。关于exec方法,可以阅览exec

目录结构

1
2
3
4
5
6
|__ bin
|__ messi.js
|__ command
|__ init.js
|__ node_modules
|__ package.json

emmm。是不是感觉有些缺少什么?是的,我们如何能做到跟vue-cli一样优秀,可以自主选择自己想要的选择(有点拗口)。

inquirer

常用交互式命令行用户界面的集合。

具体的官方文档可以看 -> inquirer,我们来简单的展示一下使用方法。首先npm install --save inquirer,然后在messi.js添加我们跟init.js功能一样的指令init2以及在init2.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
// messi.js
const init2 = require('../command/init2');
program
.command('init2')
.description('创建新项目')
.alias('i2')
.action(() => {
init2();
});

// init2.js
const co = require('co');
const chalk = require('chalk')
const inquirer = require('inquirer');
const exec = require('child_process').exec;

module.exports = () => {
console.log(chalk.white('init...'));

co(function *() {
var promps = [];

promps.push({
type: 'list',
name: 'pullProject',
message: '你是否想要拉取scaffold-cli项目?',
choices: [{
name: 'Yes',
value: 'y'
}, {
name: 'No',
value: 'n'
}]
});

inquirer.prompt(promps).then((answers) => {
console.log(answers);
const wanna = answers['pullProject'].toLowerCase();
if (wanna === 'n') {
console.log(chalk.red('已经成功退出!'));
process.exit()
} else if (wanna === 'y') {
promps = [];

promps.push({
type: 'input',
name: 'nameProject',
message: '项目名称:'
});

inquirer.prompt(promps).then((answers) => {
const projectName = answers['nameProject'];
const gitCmd = `git clone git@github.com:Lucius0/scaffold-cli.git ${projectName} && cd ${projectName} && git checkout master`;
console.log(chalk.white('开始拉取...'));
exec(gitCmd, (error, stdout, stderr) => {
if (error) {
console.log(error);
process.exit();
}
console.log(chalk.green('拉取成功!'));
console.log(`cd ${projectName}`);
exec(`cd ${projectName}`, () => {
process.exit();
});
});
});
}
});
});
};

接下来一步就是构建项目npm link,然后执行messi ini2

好了。。。。。。睡觉。。。。。。。

我只是试下能不能被赞赏😳