# 从零搭建脚手架之准备工作
# 整体结构
脚手架架构图如下所示:
根据以上架构图可知,脚手架主要分为以下四个模块
- 核心模块:core
- 命令模块:commands
- 初始化
- 发布
- 缓存清除
- 模型模块:models
- Command 命令
- Project 项目
- Component 组件
- Npm 模块
- Git 仓库
- 支撑模块:utils
- Git 操作
- 云构建
- API 请求
# 准备过程
脚手架准备阶段主要分为以下步骤:
- 检测脚手架版本号
- 检测 node 版本
- 检查 root 账户及降级权限
- 检查用户主目录
- 入参检查及 Debug 模式开发
- 检查环境变量
- 检查新版本及提示更新
# 自定义 log 方法
首先我们在新建文件夹下通过 npm 和 lerna 初始化之后,修改 lerna.json 文件中的相关字段
"packages": [
"core/*",
"utils/*",
"models/*",
"commands/*"
],
2
3
4
5
6
之后我们首先通过 lerna create log utils
命令在 utils 文件夹下新增辅助 log 模块,之后通过 lerna add npmlog utils/log
安装 npmlog 包,经过相关自定义之后,导出即可,主要代码如下所示:
'use strict';
const log = require('npmlog')
// 设置提示等级
log.level = process.env.LOG_LEVEL ? rocess.env.LOG_LEVEL : 'info'
// log信息自定义前缀
log.heading = 'test-cli'
// 自定义heading 样式 fg 字体样式 bg 背景颜色
log.headingStyle = { fg: 'black', bg: 'white' }
// 添加自定义success 命令
log.addLevel('success', 2000, { fg: 'green', bold: true })
module.exports = {
log
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 脚手架执行文件处理
我们首先通过 lerna create cli core
在 core 目录增加 cli 模块,之后在其 package.json 文件中增加 bin 字段并引入自定义 log 模块
"bin": {
"test-cli": "bin/index.js"
},
"dependencies": {
"@test-cli/log": "file:../../utils/log"
},
2
3
4
5
6
之后在该目录下执行 npm install
和 npm link
将引入的仓库安装,并将 test-cli
命令注册为全局命令,之后我们在命令的入口文件判断如果全局 node_modules 和本地 node_modules 都存在该包,则优先使用本地包,并进行提示。
// bin/index.js
#!/usr/bin/env node
const importLocal = require('import-local')
if (importLocal(__filename)) {
require('@test-cli/log').info('cli', 'using local version of test-cli')
} else {
require('../lib')(process.argv.slice(2))
}
2
3
4
5
6
7
8
9
10
# 检测脚手架版本号
之后我们在 lib/inde.js 文件中检测脚手架版本号,如下所示:
'use strict';
const { log } = require('@test-cli/log')
const pkg = require('../package.json')
function core () {
checkVersion()
}
function checkVersion () {
log.notice('cli', pkg.version)
}
module.exports = core
2
3
4
5
6
7
8
9
10
11
12
13
14
# 检测 node 版本号
通过 process.version 可以获取当前 node 版本,我们可以在 lib/const.js 中自定义脚手架所需 node 的最低版本,并通过 semver 包来比较两个版本号并给出相应的提示,首先我们通过 npm install chalk
来安装 chalk 包,用于终端输出美化。
注:由于当前 lerna 问题,通过 lerna add pkg [loc]
的方式安装之后,还需进入该模块下执行 npm install
才可正常引入该包,因此我们直接在该模块下通过 npm install pkg
的形式来引入。
//lib/const.js
const LOWEST_NODE_VERSION = 'v16.0.0'
module.exports = {
LOWEST_NODE_VERSION
}
//lib/index.js
'use strict';
const chalk = require('chalk')
const semver = require('semver')
const { LOWEST_NODE_VERSION } = require('./const')
function core () {
try {
...
checkNodeVersion()
} catch (e) {
log.error(e.message)
}
}
function checkNodeVersion () {
const currentVersion = process.version
if (!semver.gte(currentVersion, LOWEST_NODE_VERSION)) {
throw new Error(chalk.red(`current node version is ${currentVersion}, the lowest required node version is ${LOWEST_NODE_VERSION}, please ungrade node`))
}
}
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
执行 test-cli
命令,我们可以得到以下提示信息:
# 检查 root 账户及降级权限
在创建项目的时候,我们需要避免使用 root 账户取创建,如果使用 root 账户创建,那么将自动将账户进行降级,因为使用 root 账户创建的文件,其他账号将无法进行更改。
我们可以通过 process.getuid()
来获取账号权限,在 Mac 下其值为 501,通过 sudo 执行的命令则为 0,这里我们通过 root-check包来实现用户权限的降级。
// 经过 root-check 处理后,在使用 sudo 执行,结果就是 501
// 原理:通过 process.setuid 、 process.setgid 来动态修改了用户及其分组的权限
function checkRoot() {
const rootCheck = require('root-check');
rootCheck()
console.log(process.geteuid()) // 501
}
2
3
4
5
6
7
# 检查用户主目录
获取用户的主目录,并检测该目录是否存在
const { homedir } = require('os')
const fs = require('fs')
function checkUserHome () {
const homeDirName = homedir()
if (!homeDirName || !fs.existsSync(homeDirName)) {
throw new Error('The current user home directory does not exist')
}
}
2
3
4
5
6
7
8
# 获取入参及开启 debug 模式
我们可以通过 minimist
来将入参转换为对象形式,具体可查看文档 (opens new window)
function checkInputArgs () {
const argv = require("minimist")(process.argv.slice(2));
console.log(argv)
log.level = argv.debug ? 'verbose' : 'info'
}
2
3
4
5
这时我们运行命令的时候,添加 --debug
即可开启调试模式。
# 检查环境变量
我们可以通过在环境变量中储存用户信息,配置信息等数据。
- 通过 dotenv 库来实现获取配置文件(默认
path.resolve(process.cwd(), '.env')
)信息 - 文件中配置信息为 key=value 的形式,获取配置信息后回挂载到 process.env 上
function checkEnv () {
// 读取 .env 文件中的数据,配置到环境变量中
const dotenvPath = path.resolve(process.cwd(), '../../.env');
console.log(dotenvPath)
if (fs.existsSync(dotenvPath)) {
require("dotenv").config({ path: dotenvPath });
}
createDefaultConfig();
log.verbose('环境变量缓存文件路径', process.env.CLI_HOME)
}
// 创建环境变量缓存环境
function createDefaultConfig () {
const cliConfig = {}
const { CLI_HOME } = process.env
cliConfig.cliHomePath = CLI_HOME || DEFAULT_CLI_HOME;
process.env.CLI_HOME = cliConfig.cliHomePath;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 检查是否需要更新版本
- 获取当前版本号和版本名
- 通过 npm API,获取所有版本号
- 提取所有版本号,对比哪些版本号大于当前版本号
- 获取最新的版本号,提示用户更新到该版本
// core/cli/index.js
async function checkGlobalUpdate () {
const { getNpmSemverVersion } = require('@test-cli/get-npm-info')
const latestVersion = await getNpmSemverVersion({ npmName: pkg.name, baseVersion: pkg.version })
if (latestVersion && semver.gt(latestVersion, pkg.version)) {
log.warn('upgrade info',
chalk.yellow(
`please upgrade ${pkg.name} version,the latest version is ${latestVersion}, use npm i ${pkg.name} -g to upgrade`
)
);
}
}
// core/cli/package.json
"dependencies": {
"@test-cli/get-npm-info": "file:../../utils/get-npm-info",
"@test-cli/log": "file:../../utils/log",
}
// utils/get-npm-info/lib/index.js
'use strict';
const semver = require('semver')
const axios = require('axios')
const urlJoin = require("url-join");
// 优先调用淘宝源
function getRegistry ({ isOriginal = false } = {}) {
return isOriginal ? 'https://registry.npmjs.org/' : 'https://registry.npm.taobao.org/';
}
async function getNpmInfo ({ npmName, registry } = {}) {
if (!npmName) return null
const url = registry || urlJoin(getRegistry(), npmName);
let res = {}
try {
res = await axios.get(url)
} catch (e) {
return e
}
return res.status === 200 ? res.data : {}
}
async function getNpmVersions ({ npmName, registry } = {}) {
const res = await getNpmInfo({ npmName, registry })
return res.versions ? Object.keys(res.versions) : []
}
function getSemverVersions ({ baseVersion, versions }) {
return versions
.filter(version => semver.satisfies(version, `>${baseVersion}`))
.sort((a, b) => semver.gt(b, a) ? 1 : -1)
}
async function getNpmSemverVersion ({ baseVersion, npmName, registry }) {
const versions = await getNpmVersions({ npmName, registry })
const latestVersions = getSemverVersions({ baseVersion, versions })
return latestVersions[0]
}
module.exports = {
getNpmInfo,
getNpmVersions,
getSemverVersions,
getNpmSemverVersion,
};
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
# Commander 实现自定义 option 和 comamnd 功能
#!/usr/bin/env node
const pkg = require('../package.json')
const { Command } = require('commander')
const program = new Command()
program
.name(Object.keys(pkg.bin)[0])
.usage('<command> [options]')
.version(pkg.version)
.option('-d, --debug', '开启调试模式', false)
.option('-e, --env <env>', '获取环境变量名称')
// 使用opts方法来获取选项的值
// console.log(program.opts())
// 输出帮助信息
// program.outputHelp()
// command 注册命令。返回的是命令对象而不是program, <>表示必填参数 []表示可选参数
const clone = program.command('clone <source> [destination]')
clone
.description("clone a repository into a newly created directory")
.option("-f, --force", "是否强制克隆", false)
.action((source, destination, optObj) => {
console.log(source, destination, optObj);
});
// addCommand 注册命令
const service = new Command('service')
service.command('start <port>')
.description('service start at some port')
.action(port => {
console.log('service start at ', port)
})
service.command('stop')
.description('stop service')
.action(() => console.log('stop service'))
program.addCommand(service)
/**
* .command()带有描述参数时,就意味着使用独立的可执行文件作为子命令。
* Commander 将会执行一个新的命令。新的命令为: 脚手架命令-注册的命令,
* 如执行 test-cli install 则相当于执行 test-cli-install
* 配置选项 executableFile 可以自定义名字 如 executableFile:'recovery-test', test-cli install => recovery-test
* isDefault: true, 则代表默认执行该命令
* hidden 是否在帮助文档中隐藏该命令
*/
program.command('install [name]', 'install package', {
executableFile: 'recovery-test',
// isDefault: true, // test-cli => test-cli-install
hidden: true
}).alias('i')
/**
* arguments 可以为最顶层命令指定命令参数。如下例:表示配置一个必选命令 username 和一个可选命令 password。
* arguments 指定的命令参数是泛指,只要不是 command 和 addCommand 注册的命令都会被捕获到。
* 如 test-cli aa 则 username 就是 aa
* 可以向.description()方法传递第二个参数,从而在帮助中展示命令参数的信息。该参数是一个包含了 “命令参数名称:命令参数描述” 键值对的对象
*/
program
.arguments('<username> [password]')
.description('test command', {
username: 'user to login',
password: 'password for user, if required'
})
.action((username, password) => {
console.log(username, password)
})
// 高级定制,自定义help信息
/**
* 重写 helpInformation,返回值 是啥 help 信息就是啥
* helpInformation 返回空字符串,用 on 方法监听 --help, 从而输出信息
* 调用 addHelpText 方法
*/
// program.helpInformation = () => {
// return 'this is help information\n'
// return ''
// }
// program.on('--help', () => {
// console.log('custom help information')
// })
// program.addHelpText(
// "after",
// `
// Example call:
// $ custom-help --help`
// );
/**
* 可以监听 option 输入
* --debug 或者 option:debug
*/
program.on('option:debug', () => {
console.log('custom debug:', program.opts().debug)
process.env.LOG_LEVEL = 'verbose'
})
// 监听未知命令
program.on('command:*', obj => {
console.log(obj)
console.log('未知命令:', obj[0])
// 获取所有已知命令
const availableCommands = program.commands.map(cmd => cmd.name())
console.log('可用命令', availableCommands.join(','))
})
program.parse()
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
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117