《开发一款简单的脚手架cli》
# 0. 基本思路
该图引用自大佬文章,侵删
核心流程
- 命令行交互实现
- 获取命令行交互/全局配置有关内容
- 下载模板
- 根据交互进行模板渲染
- 项目初始化成功
本文仅讲解核心流程,没有将所有源码贴出来,各位小伙伴可以下载源码对照着看。cli-simple
# 1. 核心依赖
一个 cli 的实现必须要依赖以下几个工具,建议先了解基本用法再进行后面的阅读。
核心
其他
# 2. 项目结构
本次 demo 只实现两个命令,一个是create、一个是config。按照项目目录建立文件后,就可以进入开发了。
# 3. 添加命令
- 新建
/bin/cute.js
文件
const program = require('commander');
const package = require('../package.json');
// 查看版本
program
.version(package.version, '-v --version', '获取当前版本')
.helpOption('-h, --help', '输出帮助文档')
.usage('<command> [options]');
// 解析命令行参数
program.parse(process.argv);
1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
- 在
package.json
中设置bin
字段 。key值cute
, 就是cli的关键字。
{
"name": "cli-simple",
"version": "1.0.0",
"description": "",
"main": "index.js",
"bin": {
"cute": "bin/cute"
}
}
1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
package.json提供一个映射到本地本地文件名的bin字段,一旦被引入后,npm将软链接这个文件到prefix/bin里面,以便于全局引入,或者在./node_modules/.bin/目录里
- 在终端中输入以下命令可以看到输出
cute -v
1.0.0
1
2
2
# 4. 添加 create 命令
到这里我们已经可以学会了如何给 cute 添加命令。接下来进入关键流程--拉取模板渲染。create 命令是一个cli最基本的功能。
create 实现流程
- 使用
inquirer
编写跟用户的交互
- 获取第一步的answer
- 拉取准备好的模板,此时模板是已经根据预设值进行了处理
- 根据第二步的answer跟拉取下来的模板进行渲染
- 完成
- 在
/bin/cute.js
中新增
// 添加create命令
program
.command('create <app-name>')
.description('创建一个新的项目')
.action((name, cmd) => {
const options = cleanArgs(cmd);
require('../lib/create')(name, options);
});
1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
- 新增
/lib/create.js
const fs = require('fs-extra');
const path = require('path');
const inquirer = require('inquirer');
const chalk = require('chalk');
const globby = require('globby');
const ejs = require('ejs')
const util = require('./util')
const getPrompts = require('./util/prompts')
// 读取 tpl 模板信息
async function generateProject (projectName, tpl, answer) {
await fs.copy(tpl, projectName, {
filter: function (src) {
// todo 偷个懒,在这里过滤交互命令里的内容,可以改成更高级的写法
if (!answer.router) {
return !/tpl(\\|\/)node_modules/.test(src) && !/tpl(\\|\/)router/.test(src)
}
if (!answer.vuex) {
return !/tpl(\\|\/)node_modules/.test(src) && !/tpl(\\|\/)vuex/.test(src)
}
return !/tpl(\\|\/)node_modules/.test(src)
}
})
}
// 渲染 ejs 文件
async function renderEjs (files, context) {
for(let tpl of files) {
await render(tpl)
}
util.logGreen('项目初始化完成')
async function render(tpl) {
let content = await ejs.renderFile(tpl, context, {async: true})
await fs.writeFile(tpl.replace('.ejs', ''), content, 'utf8')
await fs.remove(tpl)
}
}
// 进入create流程
async function create(projectName, options = {}) {
const cwd = options.cwd || process.cwd();
const targetDir = path.resolve(cwd, projectName);
const tplDir = path.resolve(__dirname, '../', 'tpl')
// 目标目录是否存在
if (fs.existsSync(targetDir)) {
console.log(`\n 当前目录下${chalk.cyan(projectName)} 已存在,请换一个项目名...`);
return;
}
// 进入命令交互
const featurePrompt = inquirer.createPromptModule()
let answer = await featurePrompt(getPrompts(projectName))
console.log('answer>>>>>', answer)
// 拉取 & 渲染模板
await generateProject(projectName, tplDir, answer)
const projectEjs = globby.sync('**/*.ejs', {cwd: projectName, absolute: true, dot: true})
await renderEjs(projectEjs, answer)
};
// 记得export出去的时候是一个函数哦
module.exports = (...args) => {
return create(...args).catch(e => {
console.error(e);
});
};
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
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
- 在终端执行create命令
vue create <app-name>
1
# 4.1. 实现用户交互
前面也有说过,实现用户交互的关键是inquirer
。
const getPrompts = require('./util/prompts')
// 进入命令交互
const featurePrompt = inquirer.createPromptModule()
let answer = await featurePrompt(getPrompts(projectName))
console.log('answer>>>>>', answer)
1
2
3
4
5
6
7
2
3
4
5
6
7
answer打印出来的结果。
{
"name": "qw",
"description": "",
"page": "single",
"router": true,
"routerMode": "hash",
"vuex": true
}
1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
# 4.2 模板准备
demo使用的是ejs进行渲染,所有需要逻辑判断都可以改为.ejs后缀,并且可以在文件中使用 4.1 获取到的 answer。
- 查看
lib/util/prompts.js
中对应的关键字page
,如
...
prompts.push({
type: 'list',
name: 'page',
message: '请选择应用类型',
choices: [
{ name: '单页应用', value: 'single', checked: true },
{ name: '多页应用', value: 'multi' }
]
});
...
1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
- 在
tpl/index.vue.ejs
是使用关键字page
<template>
<div class="app">
<h2>这是个ejs模板,可以使用获取到交互命令有关的内容</h2>
<div>
<% if (page == 'single') { %>
<p>这是一个单页应用</p>
<% } else { %>
<p>这是一个多页应用</p>
<% } %>
</div>
<div>
看看package.json,也可以使用交互的参数哦。
</div>
</div>
</template>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
为了方便演示,本次demo的模板是放在当前目录下的,想要更改为远程仓库只需要调整模板下载的方式即可。
# 4.3 模板渲染
模板准备好后,就可以进入渲染。ejs.renderFile()
方法会将拉取下来的模板根据 ejs 的语法进行渲染。
// 渲染 ejs 文件
async function renderEjs (files, context) {
for(let tpl of files) {
await render(tpl)
}
util.logGreen('项目初始化完成')
async function render(tpl) {
let content = await ejs.renderFile(tpl, context, {async: true})
await fs.writeFile(tpl.replace('.ejs', ''), content, 'utf8')
await fs.remove(tpl)
}
}
1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
# 5. 添加 config 命令
config 命令是用来进行全局变量的配置。
config 实现流程
- 预设
.cute-config.json
- 将
.cute-config.json
放到$HOME/
下
- 获取
.cute-config.json
内容
- 对
.cute-config.json
进行操作,set/get/remove
- 在
/bin/cute.js
中新增
// 添加config命令
program
.command('config')
.arguments('[key] [val]', '设置或者读取全局配置如`token`、`env`、`host`等')
.option('-r, --rm', '移除配置')
.action((key, value, cmd) => {
require('../lib/config')(key, value, cmd);
});
1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
- 新增
/lib/config.js
...
// 进入config流程
async function config (key, val, cmd) {
let configKey = key
let configVal = val
let rm = cmd['rm']
let config
if(!configKey) {
config = await getConfig()
return util.log(config)
}
if (configKey) {
if (rm) {
// remove key
await removeConfig(configKey)
config = await getConfig()
return util.log(config)
} else if (!configVal) {
config = await getConfig(configKey)
return util.log(config)
}
}
// set key
if (configKey && configVal) {
config = await setConfig(configKey, configVal)
util.log(config)
}
}
module.exports = (...args) => {
return config(...args).catch(e => {
console.error(e);
});
};
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
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
- 在终端执行config命令
查看配置信息:
cute config
{
"token": "",
"registry": "https://github.com"
}
1
2
3
4
5
6
7
2
3
4
5
6
7
设置env
变量
cute config env 123
{
"token": "",
"registry": "https://github.com",
"env": "123"
}
1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
移除token
变量
cute config -r token
{
"registry": "https://github.com",
"env": "123"
}
1
2
3
4
5
6
7
2
3
4
5
6
7
# 5.1 将配置文件添加到环境变量
查看/lib/config.js
const fs = require('fs-extra');
const os = require('os');
const path = require('path');
let CONFIG_FILE = path.resolve(os.homedir(), '.cute-config.json')
let OPTIONS = ['env', 'token', 'registry']
// 读取 config
async function readConfig () {
let config
if (!config) {
try {
config = await fs.readJson(CONFIG_FILE)
} catch (e) {
// todo 偷个懒,配置文件放着,可以将它提取出来,如存放在.rc文件
config = {token: '', registry: 'https://github.com'}
// 查看$HOME/.cute-config.json 可以看到配置文件已存在
await fs.writeJson(CONFIG_FILE, config)
}
}
return config
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
在$HOME/
下查看,发现已存在.cute-config.json
文件
# 6. 本地调试
cli-simple下载安装后,在项目下执行npm link
就可以在本地调试了。
# 最后
这只是一个简单的demo,欢迎提出意见和建议。