基本功能
- 通过
zr create <name>
命令启动项目 - 询问用户需要下载的模板
- 远程拉取模板文件
搭建步骤拆解
- 创建项目
- 创建脚手架启动命令(使用
commander
) - 询问用户问题, 获取创建所需信息(使用
inqurer
) - 下载远程模板(用
download-git-repo
) - 发布项目
创建项目
1. npm init
初始化项目
2. 在package.json
中添加如下内容:
"bin": {
"zr": "./bin/cli.js"
},
zr 和后面的值分别表示命令名和启动文件路径
3. 在./bin/cli.js
中添加 helloworld 信息:
#! /usr/bin/env node
// 上面这一行在 js 可执行文件中必须有, 否则控制台不识别
console.log('zhurong-cli working ~');
4. 执行npm link
为了方便调试,将该项目链接至全局(如果是 MacOS 环境, 需要在命令前加 sudo);
- 假如有两个项目, 一个
npm-link-module
, 是我们要开发的 npm 模块; 另一个是npm-link-example
, 使我们要运行 npm 模块的项目. 我们首先进入npm-link-module
.- 执行
npm link
后,npm-link-module
会根据package.json
上的配置, 被链接到全局, 路径是{prefix}/lib/node_modules/<package>
(实际上会在该路径下, 新建一个链接至npm-link-module
的快捷方式).- 可用
npm config get prefix
命令获取到prefix
的值(/usr/local
)- 然后进入
npm-link-example
项目, 执行npm link npm-link-module
npm-link-module
会被链接到npm-link-exapmle/node_modules
下面(实际上是快捷方式)
5. 测试结果
随便打开一个终端, 路径不限, 输入命令zr
即可, 然后控制台就会打印.bin/cli.js
的执行结果:
qiuyue@B-K4WTQ05P-0033 > ~/learning/zhurong-cli > zr
zhurong-cli working ~
6. 加第一条功能: 限制用户启动脚手架的 node.js 版本
semver 是一个对
npm 语义版本
的识别工具, 可用于识别当前 node.js 版本, 它的功能相当于对运算符>
重载.
npm install semver
在./bin/cli.js
中添加如下内容:
// ./bin/cli.js
const semver = require('semver');
function checkNodeVersion() {
if (!semver.satisfies(process.version, '>=8.0.0')) {
console.error('Please use node.js >= 8.0.0');
process.exit(1);
}
}
checkNodeVersion();
至此, 项目的第一部分: 创建项目 就此完成.
创建脚手架启动命令
1. 安装依赖
npm install commander
2. 用commander
的不同方法在./bin/cli.js
中创建命令
首先:
const program = require('commander');
然后用法如下:
.version(require('../package.json').version)
- 设置当前版本号
- , 默认为
-V, --version
- , 用法和
option
基本一致.
.usage('<command> [option]')
用于自定义帮助信息.command('create <app-name>')
- 定义命令和参数
- 用户在输入命令时, 根据命令的语义, 也可能会输入参数, 如上面的
app-name
command()
的第一个参数为命令名称, 参数也可以写进命令名称里, 也可以用.arguments()
单独指定. 尖括号表示必选参数, 方括号表示可选参数- 第二个参数表示对命令的说明
.description('create a new project')
对一条命令或一个选项的描述, 在用户值输入zr
启动脚手架, 但不输入任何命令与选项, 或者用户调用脚手架的说明文档时会有体现.option('-f, --force', 'overwrite target directory if it exist')
- option 用于定义选项, 具体用法点这里查看
- 任意一个选项的本质就是一个 label-value 对, 其中 value 可为 true/false, 也可为字符串. 选项有其简称(如
-f
), 有其对应的全称(如--force
), 全称也就是 label-value 对中的 label. 选项还有其用法说明字符串, 还有默认值. - 如果 label 为一个中间带横线的名字, 则转到本地 js 处理时, 自动将其转为驼峰命名的变量. 如: label 为
--a-bc-de
, 则本地 js 脚本处理时, 其 label 被转为aBcDe
.action((name, action) => { // doSomething... })
接下来就是确定
action
中的函数内容. 创建lib/create.js
, 并将 action 中的内容写进该文件中. 实践: 将./bin/cli.js
中的内容改为如下内容:
#! /usr/bin/env node
const program = require('commander');
program
.command('create <name>')
.description('create a new project')
.option('-f, --force', 'overwrite target directory if it exist')
.action((name, options) => {
require('../lib/create')(name, options);
});
program
.version(require('../package.json').version)
.usage('<command> [option]');
program.parse(process.argv); // 解析用户执行命令传入参数
然后, 在./lib/create.js
中写如下内容:
module.exports = async (name, options) => {
console.log(`>>> create name: ${name}, options: ${JSON.stringify(options)}`);
}
然后, 在控制台上分别输入zr
, zr create
, zr create myFirstProject
, zr create myFirstProject -f
, 结果如下:
qiuyue@B-K4WTQ05P-0033 > ~/learning/zhurong-cli > zr
Usage: zr <command> [option]
Options:
-V, --version output the version number
-h, --help display help for command
Commands:
create [options] <name> create a new project
help [command] display help for command
qiuyue@B-K4WTQ05P-0033 > ~/learning/zhurong-cli > zr create
error: missing required argument 'name'
qiuyue@B-K4WTQ05P-0033 > ~/learning/zhurong-cli > zr create myFirstProject
>>> create name: myFirstProject, options: {}
qiuyue@B-K4WTQ05P-0033 > ~/learning/zhurong-cli > zr create myFirstProject -f
>>> create name: myFirstProject, options: {"force":true}
接下来, 将
./lib/create.js
的逻辑补充完整:
在创建一个项目的时候, 需要考虑这样一种情况: 项目是否已经存在?
- 不存在: 直接创建即可;
- 存在:
- 当
force === true
时, 移除原来的项目(目录), 直接创建; - 当
force === false
时, 询问用户是否要覆盖原来的项目?- 是, 要覆盖, 移除原来的项目(目录), 直接创建;
- 否, 退出
- 当
为实现上述逻辑, 这里用到了 fs 的扩展工具
fs-extra
, 其特点是支持 Promise 来处理异步的文件管理逻辑npm install fs-extra
然后, 改写 create.js 逻辑如下:
const path = require('path');
const fs = require('fs-extra');
module.exports = async (name, options) => {
console.log(`>>> create name: ${name}, options: ${JSON.stringify(options)}`);
// 获取当前终端所处的目录, 如 /Users/wanhaosheng/learning/zhurong-cli
const cwd = process.cwd();
// 需要创建的目录地址; 以用户的使用习惯来说, 用户在哪个目录开启终端, 就在哪个目录新建项目
const targetAir = path.join(cwd, name);
// 目录是否已经存在?
if (fs.existsSync(targetAir)) {
if (options.force) {
// 是, 且用户要求强制创建
await fs.remove(targetAir);
} else {
// 否, 先询问用户是否要强制覆盖
// doSomething... 询问逻辑留到下面再处理
}
}
// doSomething... 创建新项目的逻辑
}
3. 创建更多命令
总结具体的步骤:
- 在
./bin/cli.js
中配置各种命令 - 在
./lib
下写各种命令的处理逻辑
// bin/cli.js
// 配置 config 命令
program
.command('config [value]')
.description('inspect and modify the config')
.option('-g, --get <path>', 'get value from option')
.option('-s, --set <path> <value>')
.option('-d, --delete <path>', 'delete option from config')
.action((value, options) => {
console.log(value, options);
});
// 配置 ui 命令
program
.command('ui')
.description('start add open roc-cli ui')
.option('-p, --port <port>', 'Port used for the UI Server')
.action((option) => {
console.log(option);
});
遗留问题: 像
.option('-s, --set <path> <value>')
这样, 一个 option 带两个值的情况很少见, 官方文档没有提及, 经本人亲手实验, 第二个值是会被忽略的
4. 完善帮助信息
现在当输入zr --help
时, 结尾缺少说明信息, 且说明性信息应为绿色更合适. 因此这里引入chalk
处理.
npm install chalk@4.0.0
Note: chalk 从 5.0.0 版只支持 ESModule. 因此请安装 4.0.0 版本.
然后补充如下内容:
// ./bin/cli.js
const chalk = require('chalk');
// 监听 --help 执行, 也同样能监听到 -h
program.on('--help', () => {
// 新增说明信息
console.log(`\r\nRun ${chalk.cyan(`zr <command> --help`)} for detailed usage of given command\r\n`);
});
5. 再给脚手架加一个 logo
npm install figlet
代码如下:
console.log('\r\n' + figlet.textSync('ZR', {
font: 'Ghost',
horizontalLayout: 'default',
verticalLayout: 'default',
width: 80,
whitespaceBreak: true
}));
询问用户问题, 获取创建所需信息
npm install inquirer
接下来的步骤只需要看官方文档说明即可
在./lib/create.js
中增加如下内容:
const { action } = await inquirer.prompt([
{
name: 'action', // 存储当前问题回答的变量
type: 'list', // 表示提问的类型
message: 'Target directory already exists Pick an action:', // 问题的描述
default: true, // 默认值
choices: [ // 可选的选项
{ name: 'Overwrite', value: true },
{ name: 'Cancel', value: false }
]
}
]);
if (!action) {
// 如果用户不想强制重新初始化项目, 则直接跳出即可
return;
} else {
// 移除已经存在的目录, 然后跳出, 继续初始化项目
console.log('\r\nRemoving...');
await fs.remove(targetAir);
console.log(`has removed ${targetAir}`);
}
// 初始化项目...
生成项目文件的核心逻辑
1. 利用 Metalsmith 生成项目
npm install metalsmith
后续补充...
2. 利用 shelljs 使任务执行终端命令
npm install shelljs
目标: 在刚刚 init 的项目的目录中调用npm install
, 来给新项目安装好所有依赖包;
由于这个安装过程很长, 因此控制台需要显示一个 loading 图标, 这一点用 ora 来实现:
npm install ora
const shell = require('shelljs');
const ora = require('ora');
const spinner = ora().start();
shell.exec('npm install', function(code, _stdout, stderr) {
if (code !== 0) {
console.log(chalk.redBright('Npm executed error:')(;
spinner.fail(stderr);
} else {
spinner.succeed('Done.');
}
});